Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb439787a4 | ||
|
|
8fa368f473 | ||
|
|
7fb46cdfc4 | ||
|
|
52985e5ddc | ||
|
|
e880935dc3 | ||
|
|
cc22b1bca5 | ||
|
|
49a5128918 | ||
|
|
fedc7dcb44 | ||
|
|
cd32b20215 | ||
|
|
15cd9832c4 | ||
|
|
f25d4e4553 | ||
|
|
12d1c82b63 | ||
|
|
aebe855078 | ||
|
|
3416a71ebd | ||
|
|
94b3fea528 | ||
|
|
ad1a9ecca1 | ||
|
|
715accfb8a | ||
|
|
a8e03c6138 | ||
|
|
f69446b648 | ||
|
|
eedfbe5846 | ||
|
|
153351cc9f | ||
|
|
1b1eec40a7 | ||
|
|
763877541a | ||
|
|
1fad7d72a2 | ||
|
|
51ea2ea879 | ||
|
|
d77a478bf0 | ||
|
|
e413c0264a | ||
|
|
f88e7f898c | ||
|
|
d07bd4a6db | ||
|
|
fb34c099d5 | ||
|
|
1d2ee56a15 | ||
|
|
86665f7f09 | ||
|
|
0d2b4af986 | ||
|
|
dc2b2eeb9f | ||
|
|
e5dbb66d53 | ||
|
|
3474b1c471 | ||
|
|
3886de5b7c | ||
|
|
2b3cec06b3 | ||
|
|
8536754d14 | ||
|
|
1f36235801 | ||
|
|
a4194b14f9 | ||
|
|
2dcc629d9d | ||
|
|
98ddadc6bc | ||
|
|
f6e47b7383 | ||
|
|
f073ddc906 | ||
|
|
3e00631925 | ||
|
|
9b7ac58562 | ||
|
|
f242ddd801 | ||
|
|
c129886fe2 | ||
|
|
f577e814cf | ||
|
|
c860a0cedd | ||
|
|
ae7e28e492 | ||
|
|
90a63234ad | ||
|
|
14bca52e8f | ||
|
|
2f3c3361cf | ||
|
|
4034134055 | ||
|
|
c04f94cb7b | ||
|
|
fd1bbc7925 | ||
|
|
ff69bed394 | ||
|
|
d6e8c5146f | ||
|
|
9a04cf99d7 | ||
|
|
86e7c11e71 | ||
|
|
361cc08faa | ||
|
|
70dc771052 | ||
|
|
c14873a799 | ||
|
|
bba5abd74b | ||
|
|
a224e79c1f | ||
|
|
c305d98186 | ||
|
|
7c5a473e71 | ||
|
|
5e0f5d1eed | ||
|
|
238b269bc4 | ||
|
|
0ad121b9d2 | ||
|
|
7088acd9fd | ||
|
|
e0a900d4b6 | ||
|
|
a0fe2f0c7d | ||
|
|
d5b9bc2f26 | ||
|
|
584254e6ca | ||
|
|
a2963ed7bb | ||
|
|
2a3c2e133d | ||
|
|
3e7dcb2755 | ||
|
|
faeec00b39 | ||
|
|
eeed81392f | ||
|
|
95dce9e992 | ||
|
|
502bd2a191 | ||
|
|
17ac92a9d0 | ||
|
|
ba028cde0c | ||
|
|
6e751e7a9b | ||
|
|
948b56d0e6 | ||
|
|
4bf2dc9ece | ||
|
|
125823f8ab | ||
|
|
24d33397e9 | ||
|
|
2c553825f4 | ||
|
|
198c485e9a | ||
|
|
0138505507 | ||
|
|
5d50dcc600 | ||
|
|
7bdd8c4626 | ||
|
|
fc82c35f0c | ||
|
|
426ebad300 | ||
|
|
1afe61c593 | ||
|
|
c20751829b | ||
|
|
a3b8ee8392 | ||
|
|
156c0fe7f6 | ||
|
|
216f7a38cf | ||
|
|
fd04dc10d4 | ||
|
|
d39bdce926 | ||
|
|
c6e01245b0 | ||
|
|
c168ee7ba4 | ||
|
|
7575253000 | ||
|
|
c28c1efbb1 | ||
|
|
e6aa2c3b78 | ||
|
|
ab7c481f83 | ||
|
|
84ad1c352d | ||
|
|
e9aad39ac9 | ||
|
|
c3444a87bc | ||
|
|
67b224b340 | ||
|
|
bded14d36b | ||
|
|
73fa0b6631 | ||
|
|
2f07337588 | ||
|
|
da163d44e7 | ||
|
|
56fbf8ae0c | ||
|
|
327eb4b39b | ||
|
|
ae7873a7e3 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
.cache
|
||||
**/*.env
|
||||
**/env
|
||||
**/node_modules
|
||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: wh1te909
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-10 17:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0008_user_dark_mode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='show_community_scripts',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -8,6 +8,7 @@ class User(AbstractUser, BaseAuditModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
totp_key = models.CharField(max_length=50, null=True, blank=True)
|
||||
dark_mode = models.BooleanField(default=True)
|
||||
show_community_scripts = models.BooleanField(default=True)
|
||||
|
||||
agent = models.OneToOneField(
|
||||
"agents.Agent",
|
||||
|
||||
@@ -108,6 +108,13 @@ class GetUpdateDeleteUser(APIView):
|
||||
def put(self, request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
if (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
@@ -115,7 +122,15 @@ class GetUpdateDeleteUser(APIView):
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(User, pk=pk).delete()
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
if (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
):
|
||||
return notify_error("The root user cannot be deleted from the UI")
|
||||
|
||||
user.delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -124,8 +139,14 @@ class UserActions(APIView):
|
||||
|
||||
# reset password
|
||||
def post(self, request):
|
||||
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
if (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.set_password(request.data["password"])
|
||||
user.save()
|
||||
|
||||
@@ -133,8 +154,14 @@ class UserActions(APIView):
|
||||
|
||||
# reset two factor token
|
||||
def put(self, request):
|
||||
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
if (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.totp_key = ""
|
||||
user.save()
|
||||
|
||||
@@ -161,6 +188,13 @@ class TOTPSetup(APIView):
|
||||
class UserUI(APIView):
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
user.dark_mode = request.data["dark_mode"]
|
||||
user.save(update_fields=["dark_mode"])
|
||||
|
||||
if "dark_mode" in request.data:
|
||||
user.dark_mode = request.data["dark_mode"]
|
||||
user.save(update_fields=["dark_mode"])
|
||||
|
||||
if "show_community_scripts" in request.data:
|
||||
user.show_community_scripts = request.data["show_community_scripts"]
|
||||
user.save(update_fields=["show_community_scripts"])
|
||||
|
||||
return Response("ok")
|
||||
@@ -26,7 +26,7 @@ def get_wmi_data():
|
||||
agent = Recipe(
|
||||
Agent,
|
||||
hostname="DESKTOP-TEST123",
|
||||
version="1.1.0",
|
||||
version="1.1.1",
|
||||
monitoring_type=cycle(["workstation", "server"]),
|
||||
salt_id=generate_agent_id("DESKTOP-TEST123"),
|
||||
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import requests
|
||||
import datetime as dt
|
||||
import time
|
||||
import base64
|
||||
from Crypto.Cipher import AES
|
||||
@@ -8,9 +7,7 @@ from Crypto.Hash import SHA3_384
|
||||
from Crypto.Util.Padding import pad
|
||||
import validators
|
||||
import msgpack
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from collections import Counter
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
@@ -89,6 +86,10 @@ class Agent(BaseAuditModel):
|
||||
def has_nats(self):
|
||||
return pyver.parse(self.version) >= pyver.parse("1.1.0")
|
||||
|
||||
@property
|
||||
def has_gotasks(self):
|
||||
return pyver.parse(self.version) >= pyver.parse("1.1.1")
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
# return the default timezone unless the timezone is explicity set per agent
|
||||
@@ -163,13 +164,11 @@ class Agent(BaseAuditModel):
|
||||
elif i.status == "failing":
|
||||
failing += 1
|
||||
|
||||
has_failing_checks = True if failing > 0 else False
|
||||
|
||||
ret = {
|
||||
"total": total,
|
||||
"passing": passing,
|
||||
"failing": failing,
|
||||
"has_failing_checks": has_failing_checks,
|
||||
"has_failing_checks": failing > 0,
|
||||
}
|
||||
return ret
|
||||
|
||||
@@ -545,6 +544,7 @@ class Agent(BaseAuditModel):
|
||||
|
||||
ret = AgentEditSerializer(agent).data
|
||||
del ret["all_timezones"]
|
||||
del ret["client"]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
@@ -573,61 +573,6 @@ class Agent(BaseAuditModel):
|
||||
|
||||
return resp
|
||||
|
||||
def schedule_reboot(self, obj):
|
||||
|
||||
start_date = dt.datetime.strftime(obj, "%Y-%m-%d")
|
||||
start_time = dt.datetime.strftime(obj, "%H:%M")
|
||||
|
||||
# let windows task scheduler automatically delete the task after it runs
|
||||
end_obj = obj + dt.timedelta(minutes=15)
|
||||
end_date = dt.datetime.strftime(end_obj, "%Y-%m-%d")
|
||||
end_time = dt.datetime.strftime(end_obj, "%H:%M")
|
||||
|
||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
||||
random.choice(string.ascii_letters) for _ in range(10)
|
||||
)
|
||||
|
||||
r = self.salt_api_cmd(
|
||||
timeout=15,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Windows\\System32\\shutdown.exe"',
|
||||
'arguments="/r /t 5 /f"',
|
||||
"trigger_type=Once",
|
||||
f'start_date="{start_date}"',
|
||||
f'start_time="{start_time}"',
|
||||
f'end_date="{end_date}"',
|
||||
f'end_time="{end_time}"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
"delete_after=Immediately",
|
||||
],
|
||||
)
|
||||
|
||||
if r == "error" or (isinstance(r, bool) and not r):
|
||||
return "failed"
|
||||
elif r == "timeout":
|
||||
return "timeout"
|
||||
elif isinstance(r, bool) and r:
|
||||
from logs.models import PendingAction
|
||||
|
||||
details = {
|
||||
"taskname": task_name,
|
||||
"time": str(obj),
|
||||
}
|
||||
PendingAction(agent=self, action_type="schedreboot", details=details).save()
|
||||
|
||||
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
||||
return {"msg": {"time": nice_time, "agent": self.hostname}}
|
||||
else:
|
||||
return "failed"
|
||||
|
||||
def not_supported(self, version_added):
|
||||
return pyver.parse(self.version) < pyver.parse(version_added)
|
||||
|
||||
def delete_superseded_updates(self):
|
||||
try:
|
||||
pks = [] # list of pks to delete
|
||||
|
||||
@@ -36,12 +36,16 @@ class AgentSerializer(serializers.ModelSerializer):
|
||||
|
||||
class AgentTableSerializer(serializers.ModelSerializer):
|
||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
||||
pending_actions = serializers.SerializerMethodField()
|
||||
status = serializers.ReadOnlyField()
|
||||
checks = serializers.ReadOnlyField()
|
||||
last_seen = serializers.SerializerMethodField()
|
||||
client_name = serializers.ReadOnlyField(source="client.name")
|
||||
site_name = serializers.ReadOnlyField(source="site.name")
|
||||
|
||||
def get_pending_actions(self, obj):
|
||||
return obj.pendingactions.filter(status="pending").count()
|
||||
|
||||
def get_last_seen(self, obj):
|
||||
if obj.time_zone is not None:
|
||||
agent_tz = pytz.timezone(obj.time_zone)
|
||||
@@ -62,6 +66,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"description",
|
||||
"needs_reboot",
|
||||
"patches_pending",
|
||||
"pending_actions",
|
||||
"status",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
|
||||
@@ -3,12 +3,12 @@ from loguru import logger
|
||||
from time import sleep
|
||||
import random
|
||||
import requests
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from packaging import version as pyver
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
from agents.models import Agent, AgentOutage
|
||||
from core.models import CoreSettings
|
||||
@@ -16,174 +16,123 @@ from logs.models import PendingAction
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
OLD_64_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2.exe"
|
||||
OLD_32_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2-x86.exe"
|
||||
|
||||
def _check_agent_service(pk: int) -> None:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||
if r == "pong":
|
||||
logger.info(
|
||||
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery"
|
||||
)
|
||||
data = {"func": "recover", "payload": {"mode": "tacagent"}}
|
||||
asyncio.run(agent.nats_cmd(data, wait=False))
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks, version):
|
||||
assert isinstance(pks, list)
|
||||
def monitor_agents_task() -> None:
|
||||
q = Agent.objects.all()
|
||||
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
|
||||
with ThreadPoolExecutor(max_workers=15) as executor:
|
||||
executor.map(_check_agent_service, agents)
|
||||
|
||||
|
||||
def agent_update(pk: int) -> str:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
# skip if we can't determine the arch
|
||||
if agent.arch is None:
|
||||
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
|
||||
return "noarch"
|
||||
|
||||
# force an update to 1.1.5 since 1.1.6 needs agent to be on 1.1.5 first
|
||||
if pyver.parse(agent.version) < pyver.parse("1.1.5"):
|
||||
version = "1.1.5"
|
||||
if agent.arch == "64":
|
||||
url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5.exe"
|
||||
inno = "winagent-v1.1.5.exe"
|
||||
elif agent.arch == "32":
|
||||
url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5-x86.exe"
|
||||
inno = "winagent-v1.1.5-x86.exe"
|
||||
else:
|
||||
return "nover"
|
||||
else:
|
||||
version = settings.LATEST_AGENT_VER
|
||||
url = agent.winagent_dl
|
||||
inno = agent.win_inno_exe
|
||||
|
||||
if agent.has_nats:
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
action = agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).last()
|
||||
if pyver.parse(action.details["version"]) < pyver.parse(version):
|
||||
action.delete()
|
||||
else:
|
||||
return "pending"
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
return "created"
|
||||
# TODO
|
||||
# Salt is deprecated, remove this once salt is gone
|
||||
else:
|
||||
agent.salt_api_async(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": inno,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
return "salt"
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks: List[int], version: str) -> None:
|
||||
q = Agent.objects.filter(pk__in=pks)
|
||||
agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)]
|
||||
agents: List[int] = [
|
||||
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
|
||||
]
|
||||
|
||||
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
|
||||
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
# skip if we can't determine the arch
|
||||
if agent.arch is None:
|
||||
logger.warning(
|
||||
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
# golang agent only backwards compatible with py agent 0.11.2
|
||||
# force an upgrade to the latest python agent if version < 0.11.2
|
||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
||||
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
|
||||
inno = (
|
||||
"winagent-v0.11.2.exe"
|
||||
if agent.arch == "64"
|
||||
else "winagent-v0.11.2-x86.exe"
|
||||
)
|
||||
else:
|
||||
url = agent.winagent_dl
|
||||
inno = agent.win_inno_exe
|
||||
logger.info(
|
||||
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
|
||||
)
|
||||
|
||||
if agent.has_nats:
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
continue
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": agent.winagent_dl,
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": agent.win_inno_exe,
|
||||
},
|
||||
)
|
||||
# TODO
|
||||
# Salt is deprecated, remove this once salt is gone
|
||||
else:
|
||||
r = agent.salt_api_async(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": inno,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
sleep(10)
|
||||
for pk in agents:
|
||||
agent_update(pk)
|
||||
|
||||
|
||||
@app.task
|
||||
def auto_self_agent_update_task():
|
||||
def auto_self_agent_update_task() -> None:
|
||||
core = CoreSettings.objects.first()
|
||||
if not core.agent_auto_update:
|
||||
logger.info("Agent auto update is disabled. Skipping.")
|
||||
return
|
||||
|
||||
q = Agent.objects.only("pk", "version")
|
||||
agents = [
|
||||
pks: List[int] = [
|
||||
i.pk
|
||||
for i in q
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
logger.info(f"Updating {len(agents)}")
|
||||
|
||||
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
|
||||
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
# skip if we can't determine the arch
|
||||
if agent.arch is None:
|
||||
logger.warning(
|
||||
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
# golang agent only backwards compatible with py agent 0.11.2
|
||||
# force an upgrade to the latest python agent if version < 0.11.2
|
||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
||||
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
|
||||
inno = (
|
||||
"winagent-v0.11.2.exe"
|
||||
if agent.arch == "64"
|
||||
else "winagent-v0.11.2-x86.exe"
|
||||
)
|
||||
else:
|
||||
url = agent.winagent_dl
|
||||
inno = agent.win_inno_exe
|
||||
logger.info(
|
||||
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
|
||||
)
|
||||
|
||||
if agent.has_nats:
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
continue
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": agent.winagent_dl,
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": agent.win_inno_exe,
|
||||
},
|
||||
)
|
||||
# TODO
|
||||
# Salt is deprecated, remove this once salt is gone
|
||||
else:
|
||||
r = agent.salt_api_async(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": inno,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
sleep(10)
|
||||
for pk in pks:
|
||||
agent_update(pk)
|
||||
|
||||
|
||||
@app.task
|
||||
def update_salt_minion_task():
|
||||
q = Agent.objects.all()
|
||||
agents = [
|
||||
i.pk
|
||||
for i in q
|
||||
if pyver.parse(i.version) >= pyver.parse("0.11.0")
|
||||
and pyver.parse(i.salt_ver) < pyver.parse(settings.LATEST_SALT_VER)
|
||||
def sync_sysinfo_task():
|
||||
agents = Agent.objects.all()
|
||||
online = [
|
||||
i
|
||||
for i in agents
|
||||
if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online"
|
||||
]
|
||||
|
||||
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
|
||||
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
r = agent.salt_api_async(func="win_agent.update_salt")
|
||||
sleep(20)
|
||||
|
||||
|
||||
@app.task
|
||||
def get_wmi_detail_task(pk):
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
if agent.has_nats:
|
||||
asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False))
|
||||
else:
|
||||
agent.salt_api_async(timeout=30, func="win_agent.local_sys_info")
|
||||
|
||||
return "ok"
|
||||
for agent in online:
|
||||
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -209,25 +158,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 +261,22 @@ def agent_recovery_sms_task(pk):
|
||||
|
||||
@app.task
|
||||
def agent_outages_task():
|
||||
agents = Agent.objects.only("pk")
|
||||
agents = Agent.objects.only(
|
||||
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
if agent.status == "overdue":
|
||||
outages = AgentOutage.objects.filter(agent=agent)
|
||||
if outages and outages.last().is_active:
|
||||
continue
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
outages = AgentOutage.objects.filter(agent=agent)
|
||||
if outages and outages.last().is_active:
|
||||
continue
|
||||
|
||||
outage = AgentOutage(agent=agent)
|
||||
outage.save()
|
||||
outage = AgentOutage(agent=agent)
|
||||
outage.save()
|
||||
|
||||
if agent.overdue_email_alert and not agent.maintenance_mode:
|
||||
agent_outage_email_task.delay(pk=outage.pk)
|
||||
if agent.overdue_email_alert and not agent.maintenance_mode:
|
||||
agent_outage_email_task.delay(pk=outage.pk)
|
||||
|
||||
if agent.overdue_text_alert and not agent.maintenance_mode:
|
||||
agent_outage_sms_task.delay(pk=outage.pk)
|
||||
if agent.overdue_text_alert and not agent.maintenance_mode:
|
||||
agent_outage_sms_task.delay(pk=outage.pk)
|
||||
|
||||
@@ -5,22 +5,20 @@ from unittest.mock import patch
|
||||
from model_bakery import baker
|
||||
from itertools import cycle
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import PendingAction
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from .serializers import AgentSerializer
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from .models import Agent
|
||||
from .tasks import (
|
||||
agent_recovery_sms_task,
|
||||
auto_self_agent_update_task,
|
||||
update_salt_minion_task,
|
||||
get_wmi_detail_task,
|
||||
sync_salt_modules_task,
|
||||
batch_sync_modules_task,
|
||||
batch_sysinfo_task,
|
||||
OLD_64_PY_AGENT,
|
||||
OLD_32_PY_AGENT,
|
||||
)
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
@@ -33,7 +31,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
client = baker.make("clients.Client", name="Google")
|
||||
site = baker.make("clients.Site", client=client, name="LA Office")
|
||||
self.agent = baker.make_recipe(
|
||||
"agents.online_agent", site=site, version="1.1.0"
|
||||
"agents.online_agent", site=site, version="1.1.1"
|
||||
)
|
||||
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
|
||||
|
||||
@@ -186,10 +184,10 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_power_action(self, nats_cmd):
|
||||
url = f"/agents/poweraction/"
|
||||
def test_reboot_now(self, nats_cmd):
|
||||
url = f"/agents/reboot/"
|
||||
|
||||
data = {"pk": self.agent.pk, "action": "rebootnow"}
|
||||
data = {"pk": self.agent.pk}
|
||||
nats_cmd.return_value = "ok"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
@@ -222,30 +220,37 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("agents.models.Agent.salt_api_cmd")
|
||||
def test_reboot_later(self, mock_ret):
|
||||
url = f"/agents/rebootlater/"
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_reboot_later(self, nats_cmd):
|
||||
url = f"/agents/reboot/"
|
||||
|
||||
data = {
|
||||
"pk": self.agent.pk,
|
||||
"datetime": "2025-08-29 18:41",
|
||||
}
|
||||
|
||||
mock_ret.return_value = True
|
||||
r = self.client.post(url, data, format="json")
|
||||
nats_cmd.return_value = "ok"
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
|
||||
self.assertEqual(r.data["agent"], self.agent.hostname)
|
||||
|
||||
mock_ret.return_value = "failed"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "schedreboot",
|
||||
"trigger": "once",
|
||||
"name": r.data["task_name"],
|
||||
"year": 2025,
|
||||
"month": "August",
|
||||
"day": 29,
|
||||
"hour": 18,
|
||||
"min": 41,
|
||||
},
|
||||
}
|
||||
nats_cmd.assert_called_with(nats_data, timeout=10)
|
||||
|
||||
mock_ret.return_value = "timeout"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
mock_ret.return_value = False
|
||||
nats_cmd.return_value = "error creating task"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@@ -253,12 +258,12 @@ class TestAgentViews(TacticalTestCase):
|
||||
"pk": self.agent.pk,
|
||||
"datetime": "rm -rf /",
|
||||
}
|
||||
r = self.client.post(url, data_invalid, format="json")
|
||||
r = self.client.patch(url, data_invalid, format="json")
|
||||
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "Invalid date")
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@patch("os.path.exists")
|
||||
@patch("subprocess.run")
|
||||
@@ -428,7 +433,14 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.assertIn("&viewmode=13", r.data["file"])
|
||||
self.assertIn("&viewmode=12", r.data["terminal"])
|
||||
self.assertIn("&viewmode=11", r.data["control"])
|
||||
self.assertIn("mstsc.html?login=", r.data["webrdp"])
|
||||
|
||||
self.assertIn("&gotonode=", r.data["file"])
|
||||
self.assertIn("&gotonode=", r.data["terminal"])
|
||||
self.assertIn("&gotonode=", r.data["control"])
|
||||
|
||||
self.assertIn("?login=", r.data["file"])
|
||||
self.assertIn("?login=", r.data["terminal"])
|
||||
self.assertIn("?login=", r.data["control"])
|
||||
|
||||
self.assertEqual(self.agent.hostname, r.data["hostname"])
|
||||
self.assertEqual(self.agent.client.name, r.data["client"])
|
||||
@@ -538,6 +550,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "command",
|
||||
"monType": "all",
|
||||
"target": "agents",
|
||||
"client": None,
|
||||
"site": None,
|
||||
@@ -555,6 +568,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "command",
|
||||
"monType": "servers",
|
||||
"target": "agents",
|
||||
"client": None,
|
||||
"site": None,
|
||||
@@ -569,6 +583,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "command",
|
||||
"monType": "workstations",
|
||||
"target": "client",
|
||||
"client": self.agent.client.id,
|
||||
"site": None,
|
||||
@@ -586,6 +601,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "command",
|
||||
"monType": "all",
|
||||
"target": "client",
|
||||
"client": self.agent.client.id,
|
||||
"site": self.agent.site.id,
|
||||
@@ -603,6 +619,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "scan",
|
||||
"monType": "all",
|
||||
"target": "agents",
|
||||
"client": None,
|
||||
"site": None,
|
||||
@@ -616,6 +633,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
payload = {
|
||||
"mode": "install",
|
||||
"monType": "all",
|
||||
"target": "client",
|
||||
"client": self.agent.client.id,
|
||||
"site": None,
|
||||
@@ -739,19 +757,6 @@ class TestAgentTasks(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
@patch("agents.models.Agent.salt_api_async", return_value=None)
|
||||
def test_get_wmi_detail_task(self, salt_api_async, nats_cmd):
|
||||
self.agent_salt = baker.make_recipe("agents.agent", version="1.0.2")
|
||||
ret = get_wmi_detail_task.s(self.agent_salt.pk).apply()
|
||||
salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info")
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
self.agent_nats = baker.make_recipe("agents.agent", version="1.1.0")
|
||||
ret = get_wmi_detail_task.s(self.agent_nats.pk).apply()
|
||||
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@patch("agents.models.Agent.salt_api_cmd")
|
||||
def test_sync_salt_modules_task(self, salt_api_cmd):
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
@@ -787,84 +792,71 @@ class TestAgentTasks(TacticalTestCase):
|
||||
self.assertEqual(salt_batch_async.call_count, 4)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
@patch("agents.models.Agent.salt_batch_async", return_value=None)
|
||||
@patch("agents.tasks.sleep", return_value=None)
|
||||
def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async, nats_cmd):
|
||||
|
||||
self.agents_nats = baker.make_recipe(
|
||||
"agents.agent", version="1.1.0", _quantity=20
|
||||
)
|
||||
# test nats
|
||||
ret = batch_sysinfo_task.s().apply()
|
||||
self.assertEqual(nats_cmd.call_count, 20)
|
||||
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
self.agents_salt = baker.make_recipe(
|
||||
"agents.agent", version="1.0.2", _quantity=70
|
||||
)
|
||||
|
||||
minions = [i.salt_id for i in self.agents_salt]
|
||||
|
||||
ret = batch_sysinfo_task.s().apply()
|
||||
self.assertEqual(salt_batch_async.call_count, 1)
|
||||
salt_batch_async.assert_called_with(
|
||||
minions=minions, func="win_agent.local_sys_info"
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
salt_batch_async.reset_mock()
|
||||
[i.delete() for i in self.agents_salt]
|
||||
|
||||
# test old agents, should not run
|
||||
self.agents_old = baker.make_recipe(
|
||||
"agents.agent", version="0.10.2", _quantity=70
|
||||
)
|
||||
ret = batch_sysinfo_task.s().apply()
|
||||
salt_batch_async.assert_not_called()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@patch("agents.models.Agent.salt_api_async", return_value=None)
|
||||
@patch("agents.tasks.sleep", return_value=None)
|
||||
def test_update_salt_minion_task(self, mock_sleep, salt_api_async):
|
||||
# test agents that need salt update
|
||||
self.agents = baker.make_recipe(
|
||||
"agents.agent",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
salt_ver="1.0.3",
|
||||
_quantity=53,
|
||||
)
|
||||
ret = update_salt_minion_task.s().apply()
|
||||
self.assertEqual(salt_api_async.call_count, 53)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
[i.delete() for i in self.agents]
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test agents that need salt update but agent version too low
|
||||
self.agents = baker.make_recipe(
|
||||
"agents.agent",
|
||||
version="0.10.2",
|
||||
salt_ver="1.0.3",
|
||||
_quantity=53,
|
||||
)
|
||||
ret = update_salt_minion_task.s().apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
salt_api_async.assert_not_called()
|
||||
[i.delete() for i in self.agents]
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test agents already on latest salt ver
|
||||
self.agents = baker.make_recipe(
|
||||
"agents.agent",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
salt_ver=settings.LATEST_SALT_VER,
|
||||
_quantity=53,
|
||||
)
|
||||
ret = update_salt_minion_task.s().apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
salt_api_async.assert_not_called()
|
||||
|
||||
@patch("agents.models.Agent.salt_api_async")
|
||||
def test_agent_update(self, salt_api_async):
|
||||
from agents.tasks import agent_update
|
||||
|
||||
agent_noarch = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Error getting OS",
|
||||
version="1.1.0",
|
||||
)
|
||||
r = agent_update(agent_noarch.pk)
|
||||
self.assertEqual(r, "noarch")
|
||||
self.assertEqual(
|
||||
PendingAction.objects.filter(
|
||||
agent=agent_noarch, action_type="agentupdate"
|
||||
).count(),
|
||||
0,
|
||||
)
|
||||
|
||||
agent64_nats = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.1.0",
|
||||
)
|
||||
|
||||
r = agent_update(agent64_nats.pk)
|
||||
self.assertEqual(r, "created")
|
||||
action = PendingAction.objects.get(agent__pk=agent64_nats.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
self.assertEqual(action.details["url"], settings.DL_64)
|
||||
self.assertEqual(
|
||||
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
)
|
||||
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
|
||||
|
||||
agent64_salt = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.0.0",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
r = agent_update(agent64_salt.pk)
|
||||
self.assertEqual(r, "salt")
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
"url": settings.DL_64,
|
||||
},
|
||||
)
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
agent32_nats = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 7 Professional, 32 bit (build 7601.23964)",
|
||||
version="1.1.0",
|
||||
)
|
||||
|
||||
agent32_salt = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 7 Professional, 32 bit (build 7601.23964)",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
""" @patch("agents.models.Agent.salt_api_async")
|
||||
@patch("agents.tasks.sleep", return_value=None)
|
||||
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
|
||||
# test 64bit golang agent
|
||||
@@ -967,4 +959,4 @@ class TestAgentTasks(TacticalTestCase):
|
||||
"url": OLD_32_PY_AGENT,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.assertEqual(ret.status, "SUCCESS") """
|
||||
@@ -12,7 +12,6 @@ urlpatterns = [
|
||||
path("<pk>/agentdetail/", views.agent_detail),
|
||||
path("<int:pk>/meshcentral/", views.meshcentral),
|
||||
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
|
||||
path("poweraction/", views.power_action),
|
||||
path("uninstall/", views.uninstall),
|
||||
path("editagent/", views.edit_agent),
|
||||
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
|
||||
@@ -20,7 +19,7 @@ urlpatterns = [
|
||||
path("updateagents/", views.update_agents),
|
||||
path("<pk>/getprocs/", views.get_processes),
|
||||
path("<pk>/<pid>/killproc/", views.kill_proc),
|
||||
path("rebootlater/", views.reboot_later),
|
||||
path("reboot/", views.Reboot.as_view()),
|
||||
path("installagent/", views.install_agent),
|
||||
path("<int:pk>/ping/", views.ping),
|
||||
path("recover/", views.recover),
|
||||
@@ -31,4 +30,5 @@ urlpatterns = [
|
||||
path("bulk/", views.bulk),
|
||||
path("agent_counts/", views.agent_counts),
|
||||
path("maintenance/", views.agent_maintenance),
|
||||
path("<int:pk>/wmi/", views.WMI.as_view()),
|
||||
]
|
||||
|
||||
@@ -3,6 +3,8 @@ from loguru import logger
|
||||
import os
|
||||
import subprocess
|
||||
import pytz
|
||||
import random
|
||||
import string
|
||||
import datetime as dt
|
||||
from packaging import version as pyver
|
||||
|
||||
@@ -18,7 +20,7 @@ from rest_framework import status, generics
|
||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
||||
from core.models import CoreSettings
|
||||
from scripts.models import Script
|
||||
from logs.models import AuditLog
|
||||
from logs.models import AuditLog, PendingAction
|
||||
|
||||
from .serializers import (
|
||||
AgentSerializer,
|
||||
@@ -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"])
|
||||
@@ -798,6 +825,11 @@ def bulk(request):
|
||||
else:
|
||||
return notify_error("Something went wrong")
|
||||
|
||||
if request.data["monType"] == "servers":
|
||||
q = q.filter(monitoring_type="server")
|
||||
elif request.data["monType"] == "workstations":
|
||||
q = q.filter(monitoring_type="workstation")
|
||||
|
||||
minions = [agent.salt_id for agent in q]
|
||||
agents = [agent.pk for agent in q]
|
||||
|
||||
@@ -871,3 +903,15 @@ def agent_maintenance(request):
|
||||
return notify_error("Invalid data")
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class WMI(APIView):
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if pyver.parse(agent.version) < pyver.parse("1.1.2"):
|
||||
return notify_error("Requires agent version 1.1.2 or greater")
|
||||
|
||||
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response("ok")
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = "api"
|
||||
@@ -1,11 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from apiv3 import views as v3_views
|
||||
|
||||
urlpatterns = [
|
||||
path("triggerpatchscan/", views.trigger_patch_scan),
|
||||
path("<int:pk>/checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<int:pk>/taskrunner/", views.TaskRunner.as_view()),
|
||||
path("<int:pk>/saltinfo/", views.SaltInfo.as_view()),
|
||||
path("<int:pk>/meshinfo/", v3_views.MeshInfo.as_view()),
|
||||
]
|
||||
@@ -1,149 +0,0 @@
|
||||
from loguru import logger
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
authentication_classes,
|
||||
permission_classes,
|
||||
)
|
||||
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
from winupdate.tasks import check_for_updates_task
|
||||
|
||||
from autotasks.serializers import TaskRunnerGetSerializer, TaskRunnerPatchSerializer
|
||||
from checks.serializers import CheckRunnerGetSerializer, CheckResultsSerializer
|
||||
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@api_view(["PATCH"])
|
||||
@authentication_classes((TokenAuthentication,))
|
||||
@permission_classes((IsAuthenticated,))
|
||||
def trigger_patch_scan(request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
reboot_policy = agent.get_patch_policy().reboot_after_install
|
||||
reboot = False
|
||||
|
||||
if reboot_policy == "always":
|
||||
reboot = True
|
||||
|
||||
if request.data["reboot"]:
|
||||
if reboot_policy == "required":
|
||||
reboot = True
|
||||
elif reboot_policy == "never":
|
||||
agent.needs_reboot = True
|
||||
agent.save(update_fields=["needs_reboot"])
|
||||
|
||||
if reboot:
|
||||
r = agent.salt_api_cmd(
|
||||
timeout=15,
|
||||
func="system.reboot",
|
||||
arg=7,
|
||||
kwargs={"in_seconds": True},
|
||||
)
|
||||
|
||||
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
|
||||
check_for_updates_task.apply_async(
|
||||
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
|
||||
)
|
||||
else:
|
||||
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
|
||||
else:
|
||||
check_for_updates_task.apply_async(
|
||||
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class CheckRunner(APIView):
|
||||
"""
|
||||
For windows agent
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False)
|
||||
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_interval,
|
||||
"checks": CheckRunnerGetSerializer(checks, many=True).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
if check.check_type != "cpuload" and check.check_type != "memory":
|
||||
serializer = CheckResultsSerializer(
|
||||
instance=check, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_run=djangotime.now())
|
||||
|
||||
else:
|
||||
check.last_run = djangotime.now()
|
||||
check.save(update_fields=["last_run"])
|
||||
|
||||
check.handle_check(request.data)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class TaskRunner(APIView):
|
||||
"""
|
||||
For the windows python agent
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
return Response(TaskRunnerGetSerializer(task).data)
|
||||
|
||||
def patch(self, request, pk):
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
serializer = TaskRunnerPatchSerializer(
|
||||
instance=task, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_run=djangotime.now())
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SaltInfo(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
ret = {
|
||||
"latestVer": settings.LATEST_SALT_VER,
|
||||
"currentVer": agent.salt_ver,
|
||||
"salt_id": agent.salt_id,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.salt_ver = request.data["ver"]
|
||||
agent.save(update_fields=["salt_ver"])
|
||||
return Response("ok")
|
||||
@@ -45,15 +45,11 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
def test_get_mesh_info(self):
|
||||
url = f"/api/v3/{self.agent.pk}/meshinfo/"
|
||||
url2 = f"/api/v1/{self.agent.pk}/meshinfo/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
r = self.client.get(url2)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
self.check_not_authenticated("get", url2)
|
||||
|
||||
def test_get_winupdater(self):
|
||||
url = f"/api/v3/{self.agent.agent_id}/winupdater/"
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("checkin/", views.CheckIn.as_view()),
|
||||
path("hello/", views.Hello.as_view()),
|
||||
path("checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
||||
@@ -14,4 +15,6 @@ urlpatterns = [
|
||||
path("newagent/", views.NewAgent.as_view()),
|
||||
path("winupdater/", views.WinUpdater.as_view()),
|
||||
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
|
||||
path("software/", views.Software.as_view()),
|
||||
path("installer/", views.Installer.as_view()),
|
||||
]
|
||||
|
||||
@@ -2,12 +2,12 @@ import asyncio
|
||||
import os
|
||||
import requests
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -20,6 +20,7 @@ from checks.models import Check
|
||||
from autotasks.models import AutomatedTask
|
||||
from accounts.models import User
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from software.models import InstalledSoftware
|
||||
from checks.serializers import CheckRunnerGetSerializerV3
|
||||
from agents.serializers import WinAgentSerializer
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
|
||||
@@ -28,18 +29,122 @@ from winupdate.serializers import ApprovedUpdateSerializer
|
||||
from agents.tasks import (
|
||||
agent_recovery_email_task,
|
||||
agent_recovery_sms_task,
|
||||
get_wmi_detail_task,
|
||||
sync_salt_modules_task,
|
||||
)
|
||||
from winupdate.tasks import check_for_updates_task
|
||||
from software.tasks import get_installed_software, install_chocolatey
|
||||
from software.tasks import install_chocolatey
|
||||
from checks.utils import bytes2human
|
||||
from tacticalrmm.utils import notify_error, reload_nats
|
||||
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class CheckIn(APIView):
|
||||
"""
|
||||
The agent's checkin endpoint
|
||||
patch: called every 45 to 110 seconds, handles agent updates and recovery
|
||||
put: called every 5 to 10 minutes, handles basic system info
|
||||
post: called once on windows service startup
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent.version = request.data["version"]
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["version", "last_seen"])
|
||||
|
||||
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
|
||||
last_outage = agent.agentoutages.last()
|
||||
last_outage.recovery_time = djangotime.now()
|
||||
last_outage.save(update_fields=["recovery_time"])
|
||||
|
||||
if agent.overdue_email_alert:
|
||||
agent_recovery_email_task.delay(pk=last_outage.pk)
|
||||
if agent.overdue_text_alert:
|
||||
agent_recovery_sms_task.delay(pk=last_outage.pk)
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last()
|
||||
if recovery is not None:
|
||||
recovery.last_run = djangotime.now()
|
||||
recovery.save(update_fields=["last_run"])
|
||||
return Response(recovery.send())
|
||||
|
||||
# handle agent update
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
update = agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).last()
|
||||
update.status = "completed"
|
||||
update.save(update_fields=["status"])
|
||||
return Response(update.details)
|
||||
|
||||
# get any pending actions
|
||||
if agent.pendingactions.filter(status="pending").exists():
|
||||
agent.handle_pending_actions()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def put(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if "disks" in request.data.keys():
|
||||
disks = request.data["disks"]
|
||||
new = []
|
||||
# python agent
|
||||
if isinstance(disks, dict):
|
||||
for k, v in disks.items():
|
||||
new.append(v)
|
||||
else:
|
||||
# golang agent
|
||||
for disk in disks:
|
||||
tmp = {}
|
||||
for k, v in disk.items():
|
||||
tmp["device"] = disk["device"]
|
||||
tmp["fstype"] = disk["fstype"]
|
||||
tmp["total"] = bytes2human(disk["total"])
|
||||
tmp["used"] = bytes2human(disk["used"])
|
||||
tmp["free"] = bytes2human(disk["free"])
|
||||
tmp["percent"] = int(disk["percent"])
|
||||
new.append(tmp)
|
||||
|
||||
serializer.save(disks=new)
|
||||
return Response("ok")
|
||||
|
||||
if "logged_in_username" in request.data.keys():
|
||||
if request.data["logged_in_username"] != "None":
|
||||
serializer.save(last_logged_in_user=request.data["logged_in_username"])
|
||||
return Response("ok")
|
||||
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_seen=djangotime.now())
|
||||
|
||||
sync_salt_modules_task.delay(agent.pk)
|
||||
check_for_updates_task.apply_async(
|
||||
queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
|
||||
)
|
||||
|
||||
if not agent.choco_installed:
|
||||
install_chocolatey.delay(agent.pk, wait=True)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class Hello(APIView):
|
||||
#### DEPRECATED, for agents <= 1.1.9 ####
|
||||
"""
|
||||
The agent's checkin endpoint
|
||||
patch: called every 30 to 120 seconds
|
||||
@@ -123,8 +228,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 +489,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 +587,42 @@ class NewAgent(APIView):
|
||||
"token": token.key,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Software(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
raw: SoftwareList = request.data["software"]
|
||||
if not isinstance(raw, list):
|
||||
return notify_error("err")
|
||||
|
||||
sw = filter_software(raw)
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
else:
|
||||
s = agent.installedsoftware_set.first()
|
||||
s.software = sw
|
||||
s.save(update_fields=["software"])
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class Installer(APIView):
|
||||
def get(self, request):
|
||||
# used to check if token is valid. will return 401 if not
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
if "version" not in request.data:
|
||||
return notify_error("Invalid data")
|
||||
|
||||
ver = request.data["version"]
|
||||
if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER):
|
||||
return notify_error(
|
||||
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -1051,10 +1051,13 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
for task in tasks:
|
||||
run_win_task.assert_any_call(task.id)
|
||||
|
||||
def test_update_policy_tasks(self):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_update_policy_tasks(self, nats_cmd):
|
||||
from .tasks import update_policy_task_fields_task
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-29 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0008_auto_20201030_1515'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='run_time_bit_weekdays',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
from django.db import migrations
|
||||
from tacticalrmm.utils import get_bit_days
|
||||
|
||||
DAYS_OF_WEEK = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
}
|
||||
|
||||
|
||||
def migrate_days(apps, schema_editor):
|
||||
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
|
||||
for task in AutomatedTask.objects.exclude(run_time_days__isnull=True).exclude(
|
||||
run_time_days=[]
|
||||
):
|
||||
run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days]
|
||||
task.run_time_bit_weekdays = get_bit_days(run_days)
|
||||
task.save(update_fields=["run_time_bit_weekdays"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("autotasks", "0009_automatedtask_run_time_bit_weekdays"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_days),
|
||||
]
|
||||
@@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.fields import DateTimeField
|
||||
from automation.models import Policy
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.utils import bitdays_to_string
|
||||
|
||||
RUN_TIME_DAY_CHOICES = [
|
||||
(0, "Monday"),
|
||||
@@ -69,6 +70,8 @@ class AutomatedTask(BaseAuditModel):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
run_time_bit_weekdays = models.IntegerField(null=True, blank=True)
|
||||
# run_time_days is deprecated, use bit weekdays
|
||||
run_time_days = ArrayField(
|
||||
models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True),
|
||||
null=True,
|
||||
@@ -107,21 +110,12 @@ class AutomatedTask(BaseAuditModel):
|
||||
elif self.task_type == "runonce":
|
||||
return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}'
|
||||
elif self.task_type == "scheduled":
|
||||
ret = []
|
||||
for i in self.run_time_days:
|
||||
for j in RUN_TIME_DAY_CHOICES:
|
||||
if i in j:
|
||||
ret.append(j[1][0:3])
|
||||
|
||||
run_time_nice = dt.datetime.strptime(
|
||||
self.run_time_minute, "%H:%M"
|
||||
).strftime("%I:%M %p")
|
||||
|
||||
if len(ret) == 7:
|
||||
return f"Every day at {run_time_nice}"
|
||||
else:
|
||||
days = ",".join(ret)
|
||||
return f"{days} at {run_time_nice}"
|
||||
days = bitdays_to_string(self.run_time_bit_weekdays)
|
||||
return f"{days} at {run_time_nice}"
|
||||
|
||||
@property
|
||||
def last_run_as_timezone(self):
|
||||
@@ -169,6 +163,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
name=self.name,
|
||||
run_time_days=self.run_time_days,
|
||||
run_time_minute=self.run_time_minute,
|
||||
run_time_bit_weekdays=self.run_time_bit_weekdays,
|
||||
run_time_date=self.run_time_date,
|
||||
task_type=self.task_type,
|
||||
win_task_name=self.win_task_name,
|
||||
|
||||
@@ -1,52 +1,37 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
from loguru import logger
|
||||
from tacticalrmm.celery import app
|
||||
from django.conf import settings
|
||||
import pytz
|
||||
from django.utils import timezone as djangotime
|
||||
from packaging import version as pyver
|
||||
|
||||
from .models import AutomatedTask
|
||||
from logs.models import PendingAction
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
DAYS_OF_WEEK = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
}
|
||||
|
||||
|
||||
@app.task
|
||||
def create_win_task_schedule(pk, pending_action=False):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if task.task_type == "scheduled":
|
||||
run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days]
|
||||
|
||||
r = task.agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task.win_task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {task.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Weekly",
|
||||
f'start_time="{task.run_time_minute}"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
],
|
||||
kwargs={"days_of_week": run_days},
|
||||
)
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "weekly",
|
||||
"weekdays": task.run_time_bit_weekdays,
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
"hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
|
||||
"min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
|
||||
},
|
||||
}
|
||||
|
||||
elif task.task_type == "runonce":
|
||||
|
||||
# check if scheduled time is in the past
|
||||
agent_tz = pytz.timezone(task.agent.timezone)
|
||||
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
|
||||
@@ -57,45 +42,41 @@ def create_win_task_schedule(pk, pending_action=False):
|
||||
) + djangotime.timedelta(minutes=5)
|
||||
task.save()
|
||||
|
||||
r = task.agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task.win_task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {task.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
f'start_date="{task.run_time_date.strftime("%Y-%m-%d")}"',
|
||||
f'start_time="{task.run_time_date.strftime("%H:%M")}"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
"start_when_available=True",
|
||||
],
|
||||
)
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "once",
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
"year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
|
||||
"month": dt.datetime.strftime(task.run_time_date, "%B"),
|
||||
"day": int(dt.datetime.strftime(task.run_time_date, "%d")),
|
||||
"hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
|
||||
"min": int(dt.datetime.strftime(task.run_time_date, "%M")),
|
||||
},
|
||||
}
|
||||
|
||||
if task.remove_if_not_scheduled and pyver.parse(
|
||||
task.agent.version
|
||||
) >= pyver.parse("1.1.2"):
|
||||
nats_data["schedtaskpayload"]["deleteafter"] = True
|
||||
|
||||
elif task.task_type == "checkfailure" or task.task_type == "manual":
|
||||
r = task.agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task.win_task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {task.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
'start_date="1975-01-01"',
|
||||
'start_time="01:00"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
],
|
||||
)
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "manual",
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return "error"
|
||||
|
||||
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
|
||||
if r != "ok":
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
PendingAction(
|
||||
@@ -129,13 +110,16 @@ def create_win_task_schedule(pk, pending_action=False):
|
||||
def enable_or_disable_win_task(pk, action, pending_action=False):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
r = task.agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.edit_task",
|
||||
arg=[f"name={task.win_task_name}", f"enabled={action}"],
|
||||
)
|
||||
nats_data = {
|
||||
"func": "enableschedtask",
|
||||
"schedtaskpayload": {
|
||||
"name": task.win_task_name,
|
||||
"enabled": action,
|
||||
},
|
||||
}
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data))
|
||||
|
||||
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
|
||||
if r != "ok":
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
PendingAction(
|
||||
@@ -150,9 +134,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
|
||||
task.sync_status = "notsynced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
logger.error(
|
||||
f"Unable to update the scheduled task {task.win_task_name} on {task.agent.hostname}. It will be updated when the agent checks in."
|
||||
)
|
||||
return
|
||||
|
||||
# clear pending action since it was successful
|
||||
@@ -163,7 +144,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
|
||||
|
||||
task.sync_status = "synced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
logger.info(f"{task.agent.hostname} task {task.name} was edited.")
|
||||
return "ok"
|
||||
|
||||
|
||||
@@ -171,13 +151,13 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
|
||||
def delete_win_task_schedule(pk, pending_action=False):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
r = task.agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.delete_task",
|
||||
arg=[f"name={task.win_task_name}"],
|
||||
)
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": task.win_task_name},
|
||||
}
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
|
||||
|
||||
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
|
||||
if r != "ok":
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
PendingAction(
|
||||
@@ -188,9 +168,6 @@ def delete_win_task_schedule(pk, pending_action=False):
|
||||
task.sync_status = "pendingdeletion"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
logger.error(
|
||||
f"Unable to delete scheduled task {task.win_task_name} on {task.agent.hostname}. It was marked pending deletion and will be removed when the agent checks in."
|
||||
)
|
||||
return
|
||||
|
||||
# complete pending action since it was successful
|
||||
@@ -200,15 +177,13 @@ def delete_win_task_schedule(pk, pending_action=False):
|
||||
pendingaction.save(update_fields=["status"])
|
||||
|
||||
task.delete()
|
||||
logger.info(f"{task.agent.hostname} task {task.name} was deleted.")
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def run_win_task(pk):
|
||||
# TODO deprecated, remove this function once salt gone
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
r = task.agent.salt_api_async(func="task.run", arg=[f"name={task.win_task_name}"])
|
||||
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
||||
return "ok"
|
||||
|
||||
|
||||
@@ -220,18 +195,9 @@ def remove_orphaned_win_tasks(agentpk):
|
||||
|
||||
logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.")
|
||||
|
||||
r = agent.salt_api_cmd(
|
||||
timeout=15,
|
||||
func="task.list_tasks",
|
||||
)
|
||||
r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
|
||||
|
||||
if r == "timeout" or r == "error":
|
||||
logger.error(
|
||||
f"Unable to clean up scheduled tasks on {agent.hostname}. Agent might be offline"
|
||||
)
|
||||
return "errtimeout"
|
||||
|
||||
if not isinstance(r, list):
|
||||
if not isinstance(r, list) and not r: # empty list
|
||||
logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}")
|
||||
return "notlist"
|
||||
|
||||
@@ -240,7 +206,8 @@ def remove_orphaned_win_tasks(agentpk):
|
||||
exclude_tasks = (
|
||||
"TacticalRMM_fixmesh",
|
||||
"TacticalRMM_SchedReboot",
|
||||
"TacticalRMM_saltwatchdog", # will be implemented in future
|
||||
"TacticalRMM_sync",
|
||||
"TacticalRMM_agentupdate",
|
||||
)
|
||||
|
||||
for task in r:
|
||||
@@ -250,16 +217,16 @@ def remove_orphaned_win_tasks(agentpk):
|
||||
|
||||
if task.startswith("TacticalRMM_") and task not in agent_task_names:
|
||||
# delete task since it doesn't exist in UI
|
||||
ret = agent.salt_api_cmd(
|
||||
timeout=20,
|
||||
func="task.delete_task",
|
||||
arg=[f"name={task}"],
|
||||
)
|
||||
if isinstance(ret, bool) and ret is True:
|
||||
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
|
||||
else:
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": task},
|
||||
}
|
||||
ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
|
||||
if ret != "ok":
|
||||
logger.error(
|
||||
f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}"
|
||||
)
|
||||
else:
|
||||
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
|
||||
|
||||
logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import datetime as dt
|
||||
from unittest.mock import patch, call
|
||||
from model_bakery import baker
|
||||
from django.utils import timezone as djangotime
|
||||
@@ -25,9 +26,9 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
# setup data
|
||||
script = baker.make_recipe("scripts.script")
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent_old = baker.make_recipe("agents.agent", version="0.9.0")
|
||||
policy = baker.make("automation.Policy")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
old_agent = baker.make_recipe("agents.agent", version="1.1.0")
|
||||
|
||||
# test script set to invalid pk
|
||||
data = {"autotask": {"script": 500}}
|
||||
@@ -50,10 +51,10 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test invalid agent version
|
||||
# test old agent version
|
||||
data = {
|
||||
"autotask": {"script": script.id, "script_args": ["args"]},
|
||||
"agent": agent_old.id,
|
||||
"autotask": {"script": script.id},
|
||||
"agent": old_agent.id,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
@@ -63,7 +64,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
data = {
|
||||
"autotask": {
|
||||
"name": "Test Task Scheduled with Assigned Check",
|
||||
"run_time_days": [0, 1, 2],
|
||||
"run_time_days": ["Sunday", "Monday", "Friday"],
|
||||
"run_time_minute": "10:00",
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
@@ -84,6 +85,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
data = {
|
||||
"autotask": {
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
@@ -213,8 +215,8 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.models.Agent.salt_api_cmd")
|
||||
def test_remove_orphaned_win_task(self, salt_api_cmd):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_remove_orphaned_win_task(self, nats_cmd):
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
self.task1 = AutomatedTask.objects.create(
|
||||
agent=self.agent,
|
||||
@@ -222,20 +224,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
win_task_name=AutomatedTask.generate_task_name(),
|
||||
)
|
||||
|
||||
salt_api_cmd.return_value = "timeout"
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
self.assertEqual(ret.result, "errtimeout")
|
||||
|
||||
salt_api_cmd.return_value = "error"
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
self.assertEqual(ret.result, "errtimeout")
|
||||
|
||||
salt_api_cmd.return_value = "task not found in"
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
self.assertEqual(ret.result, "notlist")
|
||||
|
||||
salt_api_cmd.reset_mock()
|
||||
|
||||
# test removing an orphaned task
|
||||
win_tasks = [
|
||||
"Adobe Acrobat Update Task",
|
||||
@@ -250,50 +238,54 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
]
|
||||
|
||||
self.calls = [
|
||||
call(timeout=15, func="task.list_tasks"),
|
||||
call({"func": "listschedtasks"}, timeout=10),
|
||||
call(
|
||||
timeout=20,
|
||||
func="task.delete_task",
|
||||
arg=["name=TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"],
|
||||
{
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {
|
||||
"name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
),
|
||||
]
|
||||
|
||||
salt_api_cmd.side_effect = [win_tasks, True]
|
||||
nats_cmd.side_effect = [win_tasks, "ok"]
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
self.assertEqual(salt_api_cmd.call_count, 2)
|
||||
salt_api_cmd.assert_has_calls(self.calls)
|
||||
self.assertEqual(nats_cmd.call_count, 2)
|
||||
nats_cmd.assert_has_calls(self.calls)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
# test salt delete_task fail
|
||||
salt_api_cmd.reset_mock()
|
||||
salt_api_cmd.side_effect = [win_tasks, False]
|
||||
# test nats delete task fail
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.side_effect = [win_tasks, "error deleting task"]
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
salt_api_cmd.assert_has_calls(self.calls)
|
||||
self.assertEqual(salt_api_cmd.call_count, 2)
|
||||
nats_cmd.assert_has_calls(self.calls)
|
||||
self.assertEqual(nats_cmd.call_count, 2)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
# no orphaned tasks
|
||||
salt_api_cmd.reset_mock()
|
||||
nats_cmd.reset_mock()
|
||||
win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb")
|
||||
salt_api_cmd.side_effect = [win_tasks, True]
|
||||
nats_cmd.side_effect = [win_tasks, "ok"]
|
||||
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
|
||||
self.assertEqual(salt_api_cmd.call_count, 1)
|
||||
self.assertEqual(nats_cmd.call_count, 1)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@patch("agents.models.Agent.salt_api_async")
|
||||
def test_run_win_task(self, salt_api_async):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_win_task(self, nats_cmd):
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
self.task1 = AutomatedTask.objects.create(
|
||||
agent=self.agent,
|
||||
name="test task 1",
|
||||
win_task_name=AutomatedTask.generate_task_name(),
|
||||
)
|
||||
salt_api_async.return_value = "Response 200"
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = run_win_task.s(self.task1.pk).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@patch("agents.models.Agent.salt_api_cmd")
|
||||
def test_create_win_task_schedule(self, salt_api_cmd):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_create_win_task_schedule(self, nats_cmd):
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
|
||||
task_name = AutomatedTask.generate_task_name()
|
||||
@@ -303,46 +295,32 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
name="test task 1",
|
||||
win_task_name=task_name,
|
||||
task_type="scheduled",
|
||||
run_time_days=[0, 1, 6],
|
||||
run_time_bit_weekdays=127,
|
||||
run_time_minute="21:55",
|
||||
)
|
||||
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
self.assertEqual(salt_api_cmd.call_count, 1)
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {self.task1.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Weekly",
|
||||
'start_time="21:55"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
],
|
||||
kwargs={"days_of_week": ["Monday", "Tuesday", "Sunday"]},
|
||||
self.assertEqual(nats_cmd.call_count, 1)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "weekly",
|
||||
"weekdays": 127,
|
||||
"pk": self.task1.pk,
|
||||
"name": task_name,
|
||||
"hour": 21,
|
||||
"min": 55,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
self.assertEqual(self.task1.sync_status, "synced")
|
||||
|
||||
salt_api_cmd.return_value = "timeout"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||
|
||||
salt_api_cmd.return_value = "error"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||
|
||||
salt_api_cmd.return_value = False
|
||||
nats_cmd.return_value = "timeout"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
@@ -353,7 +331,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
agent=self.agent, action_type="taskaction"
|
||||
)
|
||||
self.assertEqual(self.pending_action.status, "pending")
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(
|
||||
pk=self.task1.pk, pending_action=self.pending_action.pk
|
||||
).apply()
|
||||
@@ -362,7 +340,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
self.assertEqual(self.pending_action.status, "completed")
|
||||
|
||||
# test runonce with future date
|
||||
salt_api_cmd.reset_mock()
|
||||
nats_cmd.reset_mock()
|
||||
task_name = AutomatedTask.generate_task_name()
|
||||
run_time_date = djangotime.now() + djangotime.timedelta(hours=22)
|
||||
self.task2 = AutomatedTask.objects.create(
|
||||
@@ -372,30 +350,29 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
task_type="runonce",
|
||||
run_time_date=run_time_date,
|
||||
)
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {self.task2.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
f'start_date="{run_time_date.strftime("%Y-%m-%d")}"',
|
||||
f'start_time="{run_time_date.strftime("%H:%M")}"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
"start_when_available=True",
|
||||
],
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "once",
|
||||
"pk": self.task2.pk,
|
||||
"name": task_name,
|
||||
"year": int(dt.datetime.strftime(self.task2.run_time_date, "%Y")),
|
||||
"month": dt.datetime.strftime(self.task2.run_time_date, "%B"),
|
||||
"day": int(dt.datetime.strftime(self.task2.run_time_date, "%d")),
|
||||
"hour": int(dt.datetime.strftime(self.task2.run_time_date, "%H")),
|
||||
"min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
# test runonce with date in the past
|
||||
salt_api_cmd.reset_mock()
|
||||
nats_cmd.reset_mock()
|
||||
task_name = AutomatedTask.generate_task_name()
|
||||
run_time_date = djangotime.now() - djangotime.timedelta(days=13)
|
||||
self.task3 = AutomatedTask.objects.create(
|
||||
@@ -405,31 +382,13 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
task_type="runonce",
|
||||
run_time_date=run_time_date,
|
||||
)
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
|
||||
self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {self.task3.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
f'start_date="{self.task3.run_time_date.strftime("%Y-%m-%d")}"',
|
||||
f'start_time="{self.task3.run_time_date.strftime("%H:%M")}"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
"start_when_available=True",
|
||||
],
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
# test checkfailure
|
||||
salt_api_cmd.reset_mock()
|
||||
nats_cmd.reset_mock()
|
||||
self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent)
|
||||
task_name = AutomatedTask.generate_task_name()
|
||||
self.task4 = AutomatedTask.objects.create(
|
||||
@@ -439,29 +398,24 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
task_type="checkfailure",
|
||||
assigned_check=self.check,
|
||||
)
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {self.task4.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
'start_date="1975-01-01"',
|
||||
'start_time="01:00"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
],
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "manual",
|
||||
"pk": self.task4.pk,
|
||||
"name": task_name,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
# test manual
|
||||
salt_api_cmd.reset_mock()
|
||||
nats_cmd.reset_mock()
|
||||
task_name = AutomatedTask.generate_task_name()
|
||||
self.task5 = AutomatedTask.objects.create(
|
||||
agent=self.agent,
|
||||
@@ -469,23 +423,18 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
win_task_name=task_name,
|
||||
task_type="manual",
|
||||
)
|
||||
salt_api_cmd.return_value = True
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=20,
|
||||
func="task.create_task",
|
||||
arg=[
|
||||
f"name={task_name}",
|
||||
"force=True",
|
||||
"action_type=Execute",
|
||||
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
|
||||
f'arguments="-m taskrunner -p {self.task5.pk}"',
|
||||
"start_in=C:\\Program Files\\TacticalAgent",
|
||||
"trigger_type=Once",
|
||||
'start_date="1975-01-01"',
|
||||
'start_time="01:00"',
|
||||
"ac_only=False",
|
||||
"stop_if_on_batteries=False",
|
||||
],
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "manual",
|
||||
"pk": self.task5.pk,
|
||||
"name": task_name,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@@ -20,7 +20,7 @@ from .tasks import (
|
||||
delete_win_task_schedule,
|
||||
enable_or_disable_win_task,
|
||||
)
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.utils import notify_error, get_bit_days
|
||||
|
||||
|
||||
class AddAutoTask(APIView):
|
||||
@@ -38,17 +38,20 @@ class AddAutoTask(APIView):
|
||||
parent = {"policy": policy}
|
||||
else:
|
||||
agent = get_object_or_404(Agent, pk=data["agent"])
|
||||
if not agent.has_gotasks:
|
||||
return notify_error("Requires agent version 1.1.1 or greater")
|
||||
|
||||
parent = {"agent": agent}
|
||||
added = "0.11.0"
|
||||
if data["autotask"]["script_args"] and agent.not_supported(added):
|
||||
return notify_error(
|
||||
f"Script arguments only available in agent {added} or greater"
|
||||
)
|
||||
|
||||
check = None
|
||||
if data["autotask"]["assigned_check"]:
|
||||
check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
|
||||
|
||||
bit_weekdays = None
|
||||
if data["autotask"]["run_time_days"]:
|
||||
bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
|
||||
|
||||
del data["autotask"]["run_time_days"]
|
||||
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save(
|
||||
@@ -56,6 +59,7 @@ class AddAutoTask(APIView):
|
||||
script=script,
|
||||
win_task_name=AutomatedTask.generate_task_name(),
|
||||
assigned_check=check,
|
||||
run_time_bit_weekdays=bit_weekdays,
|
||||
)
|
||||
|
||||
if not "policy" in data:
|
||||
|
||||
@@ -36,17 +36,6 @@ class AddCheck(APIView):
|
||||
else:
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
parent = {"agent": agent}
|
||||
added = "0.11.0"
|
||||
if (
|
||||
request.data["check"]["check_type"] == "script"
|
||||
and request.data["check"]["script_args"]
|
||||
and agent.not_supported(version_added=added)
|
||||
):
|
||||
return notify_error(
|
||||
{
|
||||
"non_field_errors": f"Script arguments only available in agent {added} or greater"
|
||||
}
|
||||
)
|
||||
|
||||
script = None
|
||||
if "script" in request.data["check"]:
|
||||
@@ -58,13 +47,6 @@ class AddCheck(APIView):
|
||||
request.data["check"]["check_type"] == "eventlog"
|
||||
and request.data["check"]["event_id_is_wildcard"]
|
||||
):
|
||||
if agent and agent.not_supported(version_added="0.10.2"):
|
||||
return notify_error(
|
||||
{
|
||||
"non_field_errors": "Wildcard is only available in agent 0.10.2 or greater"
|
||||
}
|
||||
)
|
||||
|
||||
request.data["check"]["event_id"] = 0
|
||||
|
||||
serializer = CheckSerializer(
|
||||
@@ -116,31 +98,8 @@ class GetUpdateDeleteCheck(APIView):
|
||||
pass
|
||||
else:
|
||||
if request.data["event_id_is_wildcard"]:
|
||||
if check.agent.not_supported(version_added="0.10.2"):
|
||||
return notify_error(
|
||||
{
|
||||
"non_field_errors": "Wildcard is only available in agent 0.10.2 or greater"
|
||||
}
|
||||
)
|
||||
|
||||
request.data["event_id"] = 0
|
||||
|
||||
elif check.check_type == "script":
|
||||
added = "0.11.0"
|
||||
try:
|
||||
request.data["script_args"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if request.data["script_args"] and check.agent.not_supported(
|
||||
version_added=added
|
||||
):
|
||||
return notify_error(
|
||||
{
|
||||
"non_field_errors": f"Script arguments only available in agent {added} or greater"
|
||||
}
|
||||
)
|
||||
|
||||
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
||||
@@ -38,7 +38,6 @@ class Client(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def has_failing_checks(self):
|
||||
|
||||
agents = (
|
||||
Agent.objects.only(
|
||||
"pk",
|
||||
@@ -50,14 +49,17 @@ class Client(BaseAuditModel):
|
||||
.filter(site__client=self)
|
||||
.prefetch_related("agentchecks")
|
||||
)
|
||||
|
||||
failing = 0
|
||||
for agent in agents:
|
||||
if agent.checks["has_failing_checks"]:
|
||||
return True
|
||||
failing += 1
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
return agent.status == "overdue"
|
||||
if agent.status == "overdue":
|
||||
failing += 1
|
||||
|
||||
return False
|
||||
return failing > 0
|
||||
|
||||
@staticmethod
|
||||
def serialize(client):
|
||||
@@ -98,7 +100,6 @@ class Site(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def has_failing_checks(self):
|
||||
|
||||
agents = (
|
||||
Agent.objects.only(
|
||||
"pk",
|
||||
@@ -110,14 +111,17 @@ class Site(BaseAuditModel):
|
||||
.filter(site=self)
|
||||
.prefetch_related("agentchecks")
|
||||
)
|
||||
|
||||
failing = 0
|
||||
for agent in agents:
|
||||
if agent.checks["has_failing_checks"]:
|
||||
return True
|
||||
failing += 1
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
return agent.status == "overdue"
|
||||
if agent.status == "overdue":
|
||||
failing += 1
|
||||
|
||||
return False
|
||||
return failing > 0
|
||||
|
||||
@staticmethod
|
||||
def serialize(site):
|
||||
|
||||
@@ -56,8 +56,9 @@ func downloadAgent(filepath string) (err error) {
|
||||
func main() {
|
||||
|
||||
debugLog := flag.String("log", "", "Verbose output")
|
||||
localSalt := flag.String("local-salt", "", "Use local salt minion")
|
||||
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
|
||||
noSalt := flag.Bool("nosalt", false, "Does not install salt")
|
||||
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
|
||||
cert := flag.String("cert", "", "Path to ca.pem")
|
||||
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
|
||||
flag.Parse()
|
||||
@@ -78,35 +79,39 @@ func main() {
|
||||
}
|
||||
|
||||
if debug {
|
||||
cmdArgs = append(cmdArgs, "--log", "DEBUG")
|
||||
cmdArgs = append(cmdArgs, "-log", "debug")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*localSalt)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "--local-salt", *localSalt)
|
||||
if *silent {
|
||||
cmdArgs = append(cmdArgs, "-silent")
|
||||
}
|
||||
|
||||
if *noSalt {
|
||||
cmdArgs = append(cmdArgs, "-nosalt")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*localMesh)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "--local-mesh", *localMesh)
|
||||
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*cert)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "--cert", *cert)
|
||||
cmdArgs = append(cmdArgs, "-cert", *cert)
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*timeout)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "--timeout", *timeout)
|
||||
cmdArgs = append(cmdArgs, "-timeout", *timeout)
|
||||
}
|
||||
|
||||
if Rdp == "1" {
|
||||
cmdArgs = append(cmdArgs, "--rdp")
|
||||
cmdArgs = append(cmdArgs, "-rdp")
|
||||
}
|
||||
|
||||
if Ping == "1" {
|
||||
cmdArgs = append(cmdArgs, "--ping")
|
||||
cmdArgs = append(cmdArgs, "-ping")
|
||||
}
|
||||
|
||||
if Power == "1" {
|
||||
cmdArgs = append(cmdArgs, "--power")
|
||||
cmdArgs = append(cmdArgs, "-power")
|
||||
}
|
||||
|
||||
if debug {
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -69,7 +69,11 @@ def version(request):
|
||||
@api_view()
|
||||
def dashboard_info(request):
|
||||
return Response(
|
||||
{"trmm_version": settings.TRMM_VERSION, "dark_mode": request.user.dark_mode}
|
||||
{
|
||||
"trmm_version": settings.TRMM_VERSION,
|
||||
"dark_mode": request.user.dark_mode,
|
||||
"show_community_scripts": request.user.show_community_scripts,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -84,3 +88,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
|
||||
|
||||
@@ -6,8 +6,5 @@ script = Recipe(
|
||||
name="Test Script",
|
||||
description="Test Desc",
|
||||
shell="cmd",
|
||||
filename="test.bat",
|
||||
script_type="userdefined",
|
||||
)
|
||||
|
||||
builtin_script = script.extend(script_type="builtin")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-07 15:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0003_auto_20200922_1344'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='script',
|
||||
name='category',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='script',
|
||||
name='favorite',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='script',
|
||||
name='script_base64',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-07 16:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0004_auto_20201207_1558'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='script',
|
||||
old_name='script_base64',
|
||||
new_name='code_base64',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-10 21:45
|
||||
|
||||
from django.db import migrations
|
||||
from django.conf import settings
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def move_scripts_to_db(apps, schema_editor):
|
||||
print("")
|
||||
Script = apps.get_model("scripts", "Script")
|
||||
for script in Script.objects.all():
|
||||
if not script.script_type == "builtin":
|
||||
|
||||
filepath = f"{settings.SCRIPTS_DIR}/userdefined/{script.filename}"
|
||||
|
||||
# test if file exists
|
||||
if os.path.exists(filepath):
|
||||
print(f"Found script {script.name}. Importing code.")
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
script_bytes = f.read().decode("utf-8").encode("ascii", "ignore")
|
||||
script.code_base64 = base64.b64encode(script_bytes).decode("ascii")
|
||||
script.save(update_fields=["code_base64"])
|
||||
else:
|
||||
print(
|
||||
f"Script file {script.name} was not found on the disk. You will need to edit the script in the UI"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scripts", "0005_auto_20201207_1606"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(move_scripts_to_db, migrations.RunPython.noop)]
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from django.conf import settings
|
||||
@@ -17,41 +18,27 @@ SCRIPT_TYPES = [
|
||||
class Script(BaseAuditModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
filename = models.CharField(max_length=255)
|
||||
filename = models.CharField(max_length=255) # deprecated
|
||||
shell = models.CharField(
|
||||
max_length=100, choices=SCRIPT_SHELLS, default="powershell"
|
||||
)
|
||||
script_type = models.CharField(
|
||||
max_length=100, choices=SCRIPT_TYPES, default="userdefined"
|
||||
)
|
||||
favorite = models.BooleanField(default=False)
|
||||
category = models.CharField(max_length=100, null=True, blank=True)
|
||||
code_base64 = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.filename
|
||||
|
||||
@property
|
||||
def filepath(self):
|
||||
# for the windows agent when using 'salt-call'
|
||||
if self.script_type == "userdefined":
|
||||
return f"salt://scripts//userdefined//{self.filename}"
|
||||
else:
|
||||
return f"salt://scripts//{self.filename}"
|
||||
|
||||
@property
|
||||
def file(self):
|
||||
if self.script_type == "userdefined":
|
||||
return f"{settings.SCRIPTS_DIR}/userdefined/{self.filename}"
|
||||
else:
|
||||
return f"{settings.SCRIPTS_DIR}/{self.filename}"
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
try:
|
||||
with open(self.file, "r") as f:
|
||||
text = f.read()
|
||||
except:
|
||||
text = "n/a"
|
||||
|
||||
return text
|
||||
if self.code_base64:
|
||||
base64_bytes = self.code_base64.encode("ascii", "ignore")
|
||||
return base64.b64decode(base64_bytes).decode("ascii", "ignore")
|
||||
else:
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def load_community_scripts(cls):
|
||||
@@ -79,22 +66,41 @@ class Script(BaseAuditModel):
|
||||
for script in info:
|
||||
if os.path.exists(os.path.join(scripts_dir, script["filename"])):
|
||||
s = cls.objects.filter(script_type="builtin").filter(
|
||||
filename=script["filename"]
|
||||
name=script["name"]
|
||||
)
|
||||
if s.exists():
|
||||
i = s.first()
|
||||
i.name = script["name"]
|
||||
i.description = script["description"]
|
||||
i.save(update_fields=["name", "description"])
|
||||
i.category = "Community"
|
||||
|
||||
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
|
||||
script_bytes = (
|
||||
f.read().decode("utf-8").encode("ascii", "ignore")
|
||||
)
|
||||
i.code_base64 = base64.b64encode(script_bytes).decode("ascii")
|
||||
|
||||
i.save(
|
||||
update_fields=["name", "description", "category", "code_base64"]
|
||||
)
|
||||
else:
|
||||
print(f"Adding new community script: {script['name']}")
|
||||
cls(
|
||||
name=script["name"],
|
||||
description=script["description"],
|
||||
filename=script["filename"],
|
||||
shell=script["shell"],
|
||||
script_type="builtin",
|
||||
).save()
|
||||
|
||||
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
|
||||
script_bytes = (
|
||||
f.read().decode("utf-8").encode("ascii", "ignore")
|
||||
)
|
||||
code_base64 = base64.b64encode(script_bytes).decode("ascii")
|
||||
|
||||
cls(
|
||||
code_base64=code_base64,
|
||||
name=script["name"],
|
||||
description=script["description"],
|
||||
filename=script["filename"],
|
||||
shell=script["shell"],
|
||||
script_type="builtin",
|
||||
category="Community",
|
||||
).save()
|
||||
|
||||
@staticmethod
|
||||
def serialize(script):
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError, ReadOnlyField
|
||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
|
||||
from .models import Script
|
||||
|
||||
|
||||
class ScriptSerializer(ModelSerializer):
|
||||
|
||||
code = ReadOnlyField()
|
||||
filepath = ReadOnlyField()
|
||||
|
||||
class ScriptTableSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Script
|
||||
fields = "__all__"
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"script_type",
|
||||
"shell",
|
||||
"category",
|
||||
"favorite",
|
||||
]
|
||||
|
||||
def validate(self, val):
|
||||
if "filename" in val:
|
||||
# validate the filename
|
||||
if (
|
||||
not val["filename"].endswith(".py")
|
||||
and not val["filename"].endswith(".ps1")
|
||||
and not val["filename"].endswith(".bat")
|
||||
):
|
||||
raise ValidationError("File types supported are .py, .ps1 and .bat")
|
||||
|
||||
# make sure file doesn't already exist on server
|
||||
# but only if adding, not if editing since will overwrite if edit
|
||||
if not self.instance:
|
||||
script_path = os.path.join(
|
||||
f"{settings.SCRIPTS_DIR}/userdefined", val["filename"]
|
||||
)
|
||||
if os.path.exists(script_path):
|
||||
raise ValidationError(
|
||||
f"{val['filename']} already exists. Delete or edit the existing script first."
|
||||
)
|
||||
|
||||
return val
|
||||
class ScriptSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Script
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"shell",
|
||||
"category",
|
||||
"favorite",
|
||||
"code_base64",
|
||||
]
|
||||
|
||||
|
||||
class ScriptCheckSerializer(ModelSerializer):
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from pathlib import Path
|
||||
from django.conf import settings
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from model_bakery import baker
|
||||
from .serializers import ScriptSerializer
|
||||
from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
from .models import Script
|
||||
|
||||
|
||||
@@ -16,16 +17,50 @@ class TestScriptViews(TacticalTestCase):
|
||||
url = "/scripts/scripts/"
|
||||
scripts = baker.make("scripts.Script", _quantity=3)
|
||||
|
||||
serializer = ScriptSerializer(scripts, many=True)
|
||||
serializer = ScriptTableSerializer(scripts, many=True)
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(serializer.data, resp.data)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
# TODO Need to test file uploads and saves
|
||||
def test_add_script(self):
|
||||
pass
|
||||
url = f"/scripts/scripts/"
|
||||
|
||||
data = {
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"shell": "powershell",
|
||||
"category": "New",
|
||||
"code": "Some Test Code\nnew Line",
|
||||
}
|
||||
|
||||
# test without file upload
|
||||
resp = self.client.post(url, data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(Script.objects.filter(name="Name").exists())
|
||||
self.assertEqual(Script.objects.get(name="Name").code, data["code"])
|
||||
|
||||
# test with file upload
|
||||
# file with 'Test' as content
|
||||
file = SimpleUploadedFile(
|
||||
"test_script.bat", b"\x54\x65\x73\x74", content_type="text/plain"
|
||||
)
|
||||
data = {
|
||||
"name": "New Name",
|
||||
"description": "Description",
|
||||
"shell": "cmd",
|
||||
"category": "New",
|
||||
"filename": file,
|
||||
}
|
||||
|
||||
# test with file upload
|
||||
resp = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
script = Script.objects.filter(name="New Name").first()
|
||||
self.assertEquals(script.code, "Test")
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_modify_script(self):
|
||||
# test a call where script doesn't exist
|
||||
@@ -40,23 +75,39 @@ class TestScriptViews(TacticalTestCase):
|
||||
"name": script.name,
|
||||
"description": "Description Change",
|
||||
"shell": script.shell,
|
||||
"code": "Test Code\nAnother Line",
|
||||
}
|
||||
|
||||
# test edit a userdefined script
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEquals(
|
||||
Script.objects.get(pk=script.pk).description, "Description Change"
|
||||
)
|
||||
script = Script.objects.get(pk=script.pk)
|
||||
self.assertEquals(script.description, "Description Change")
|
||||
self.assertEquals(script.code, "Test Code\nAnother Line")
|
||||
|
||||
# test edit a builtin script
|
||||
builtin_script = baker.make_recipe("scripts.builtin_script")
|
||||
|
||||
data = {"name": "New Name", "description": "New Desc", "code": "Some New Code"}
|
||||
builtin_script = baker.make_recipe("scripts.script", script_type="builtin")
|
||||
|
||||
resp = self.client.put(
|
||||
f"/scripts/{builtin_script.pk}/script/", data, format="json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# TODO Test changing script file
|
||||
data = {
|
||||
"name": script.name,
|
||||
"description": "Description Change",
|
||||
"shell": script.shell,
|
||||
"favorite": True,
|
||||
"code": "Test Code\nAnother Line",
|
||||
}
|
||||
# test marking a builtin script as favorite
|
||||
resp = self.client.put(
|
||||
f"/scripts/{builtin_script.pk}/script/", data, format="json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(Script.objects.get(pk=builtin_script.pk).favorite)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@@ -79,6 +130,7 @@ class TestScriptViews(TacticalTestCase):
|
||||
resp = self.client.delete("/scripts/500/script/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test delete script
|
||||
script = baker.make_recipe("scripts.script")
|
||||
url = f"/scripts/{script.pk}/script/"
|
||||
resp = self.client.delete(url, format="json")
|
||||
@@ -86,13 +138,50 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
self.assertFalse(Script.objects.filter(pk=script.pk).exists())
|
||||
|
||||
# test delete community script
|
||||
script = baker.make_recipe("scripts.script", script_type="builtin")
|
||||
url = f"/scripts/{script.pk}/script/"
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
# TODO Need to mock file open
|
||||
def test_download_script(self):
|
||||
pass
|
||||
# test a call where script doesn't exist
|
||||
resp = self.client.get("/scripts/500/download/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_load_community_scripts(self):
|
||||
# return script code property should be "Test"
|
||||
|
||||
# test powershell file
|
||||
script = baker.make(
|
||||
"scripts.Script", code_base64="VGVzdA==", shell="powershell"
|
||||
)
|
||||
url = f"/scripts/{script.pk}/download/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test"})
|
||||
|
||||
# test batch file
|
||||
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd")
|
||||
url = f"/scripts/{script.pk}/download/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test"})
|
||||
|
||||
# test python file
|
||||
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python")
|
||||
url = f"/scripts/{script.pk}/download/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test"})
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_community_script_json_file(self):
|
||||
valid_shells = ["powershell", "python", "cmd"]
|
||||
|
||||
if not settings.DOCKER_BUILD:
|
||||
@@ -113,5 +202,19 @@ class TestScriptViews(TacticalTestCase):
|
||||
self.assertTrue(script["name"])
|
||||
self.assertTrue(script["description"])
|
||||
self.assertTrue(script["shell"])
|
||||
self.assertTrue(script["description"])
|
||||
self.assertIn(script["shell"], valid_shells)
|
||||
|
||||
def test_load_community_scripts(self):
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
|
||||
) as f:
|
||||
info = json.load(f)
|
||||
|
||||
Script.load_community_scripts()
|
||||
|
||||
community_scripts = Script.objects.filter(script_type="builtin").count()
|
||||
self.assertEqual(len(info), community_scripts)
|
||||
|
||||
# test updating already added community scripts
|
||||
Script.load_community_scripts()
|
||||
self.assertEqual(len(info), community_scripts)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
import base64
|
||||
from loguru import logger
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -11,9 +11,10 @@ from rest_framework.response import Response
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
|
||||
from .models import Script
|
||||
from .serializers import ScriptSerializer
|
||||
from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@@ -22,74 +23,65 @@ class GetAddScripts(APIView):
|
||||
|
||||
def get(self, request):
|
||||
scripts = Script.objects.all()
|
||||
return Response(ScriptSerializer(scripts, many=True).data)
|
||||
return Response(ScriptTableSerializer(scripts, many=True).data)
|
||||
|
||||
def put(self, request, format=None):
|
||||
def post(self, request, format=None):
|
||||
|
||||
file_obj = request.data["filename"] # the actual file in_memory object
|
||||
|
||||
# need to manually create the serialized data
|
||||
# since javascript formData doesn't support JSON
|
||||
filename = str(file_obj)
|
||||
data = {
|
||||
"name": request.data["name"],
|
||||
"filename": filename,
|
||||
"category": request.data["category"],
|
||||
"description": request.data["description"],
|
||||
"shell": request.data["shell"],
|
||||
"script_type": "userdefined", # force all uploads to be userdefined. built in scripts cannot be edited by user
|
||||
}
|
||||
|
||||
if "favorite" in request.data:
|
||||
data["favorite"] = request.data["favorite"]
|
||||
|
||||
if "filename" in request.data:
|
||||
message_bytes = request.data["filename"].read()
|
||||
data["code_base64"] = base64.b64encode(message_bytes).decode(
|
||||
"ascii", "ignore"
|
||||
)
|
||||
|
||||
elif "code" in request.data:
|
||||
message_bytes = request.data["code"].encode("ascii", "ignore")
|
||||
data["code_base64"] = base64.b64encode(message_bytes).decode("ascii")
|
||||
|
||||
serializer = ScriptSerializer(data=data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
||||
with open(obj.file, "wb+") as f:
|
||||
for chunk in file_obj.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
return Response(f"{obj.name} was added!")
|
||||
|
||||
|
||||
class GetUpdateDeleteScript(APIView):
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def get(self, request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
return Response(ScriptSerializer(script).data)
|
||||
|
||||
def put(self, request, pk, format=None):
|
||||
def put(self, request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
|
||||
# this will never trigger but check anyway
|
||||
data = request.data
|
||||
|
||||
if script.script_type == "builtin":
|
||||
return notify_error("Built in scripts cannot be edited")
|
||||
# allow only favoriting builtin scripts
|
||||
if "favorite" in data:
|
||||
# overwrite request data
|
||||
data = {"favorite": data["favorite"]}
|
||||
else:
|
||||
return notify_error("Community scripts cannot be edited.")
|
||||
|
||||
data = {
|
||||
"name": request.data["name"],
|
||||
"description": request.data["description"],
|
||||
"shell": request.data["shell"],
|
||||
}
|
||||
|
||||
# if uploading a new version of the script
|
||||
if "filename" in request.data:
|
||||
file_obj = request.data["filename"]
|
||||
data["filename"] = str(file_obj)
|
||||
elif "code" in data:
|
||||
message_bytes = data["code"].encode("ascii")
|
||||
data["code_base64"] = base64.b64encode(message_bytes).decode("ascii")
|
||||
data.pop("code")
|
||||
|
||||
serializer = ScriptSerializer(data=data, instance=script, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
||||
if "filename" in request.data:
|
||||
|
||||
try:
|
||||
os.remove(obj.file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
with open(obj.file, "wb+") as f:
|
||||
for chunk in file_obj.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
return Response(f"{obj.name} was edited!")
|
||||
|
||||
def delete(self, request, pk):
|
||||
@@ -97,12 +89,7 @@ class GetUpdateDeleteScript(APIView):
|
||||
|
||||
# this will never trigger but check anyway
|
||||
if script.script_type == "builtin":
|
||||
return notify_error("Built in scripts cannot be deleted")
|
||||
|
||||
try:
|
||||
os.remove(script.file)
|
||||
except OSError:
|
||||
pass
|
||||
return notify_error("Community scripts cannot be deleted")
|
||||
|
||||
script.delete()
|
||||
return Response(f"{script.name} was deleted!")
|
||||
@@ -111,33 +98,12 @@ class GetUpdateDeleteScript(APIView):
|
||||
@api_view()
|
||||
def download(request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
use_nginx = False
|
||||
conf = "/etc/nginx/sites-available/rmm.conf"
|
||||
|
||||
if os.path.exists(conf):
|
||||
try:
|
||||
with open(conf) as f:
|
||||
for line in f.readlines():
|
||||
if "location" and "builtin" in line:
|
||||
use_nginx = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
if script.shell == "powershell":
|
||||
filename = f"{script.name}.ps1"
|
||||
elif script.shell == "cmd":
|
||||
filename = f"{script.name}.bat"
|
||||
else:
|
||||
use_nginx = True
|
||||
filename = f"{script.name}.py"
|
||||
|
||||
if settings.DEBUG or not use_nginx:
|
||||
with open(script.file, "rb") as f:
|
||||
response = HttpResponse(f.read(), content_type="text/plain")
|
||||
response["Content-Disposition"] = f"attachment; filename={script.filename}"
|
||||
return response
|
||||
else:
|
||||
response = HttpResponse()
|
||||
response["Content-Disposition"] = f"attachment; filename={script.filename}"
|
||||
|
||||
response["X-Accel-Redirect"] = (
|
||||
f"/saltscripts/{script.filename}"
|
||||
if script.script_type == "userdefined"
|
||||
else f"/builtin/{script.filename}"
|
||||
)
|
||||
return response
|
||||
return Response({"filename": filename, "code": script.code})
|
||||
|
||||
@@ -9,21 +9,6 @@ class TestServiceViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
|
||||
def test_get_services(self):
|
||||
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.get("/services/500/services/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
url = f"/services/{agent.pk}/services/"
|
||||
serializer = ServicesSerializer(agent)
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(serializer.data, resp.data)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_default_services(self):
|
||||
url = "/services/defaultservices/"
|
||||
resp = self.client.get(url, format="json")
|
||||
@@ -33,13 +18,13 @@ class TestServiceViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_get_refreshed_services(self, nats_cmd):
|
||||
def test_get_services(self, nats_cmd):
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.get("/services/500/refreshedservices/", format="json")
|
||||
resp = self.client.get("/services/500/services/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
url = f"/services/{agent.pk}/refreshedservices/"
|
||||
url = f"/services/{agent.pk}/services/"
|
||||
|
||||
nats_return = [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("<int:pk>/services/", views.get_services),
|
||||
path("defaultservices/", views.default_services),
|
||||
path("<int:pk>/refreshedservices/", views.get_refreshed_services),
|
||||
path("serviceaction/", views.service_action),
|
||||
path("<int:pk>/<svcname>/servicedetail/", views.service_detail),
|
||||
path("editservice/", views.edit_service),
|
||||
|
||||
@@ -19,17 +19,6 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@api_view()
|
||||
def get_services(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
return Response(ServicesSerializer(agent).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def default_services(request):
|
||||
return Response(Check.load_default_services())
|
||||
|
||||
|
||||
@api_view()
|
||||
def get_refreshed_services(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
@@ -43,6 +32,11 @@ def get_refreshed_services(request, pk):
|
||||
return Response(ServicesSerializer(agent).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def default_services(request):
|
||||
return Response(Check.load_default_services())
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def service_action(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import string
|
||||
from time import sleep
|
||||
from loguru import logger
|
||||
from tacticalrmm.celery import app
|
||||
@@ -8,6 +7,7 @@ from django.utils import timezone as djangotime
|
||||
|
||||
from agents.models import Agent
|
||||
from .models import ChocoSoftware, ChocoLog, InstalledSoftware
|
||||
from tacticalrmm.utils import filter_software
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@@ -87,44 +87,6 @@ def update_chocos():
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def get_installed_software(pk):
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
if not agent.has_nats:
|
||||
logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0")
|
||||
return
|
||||
|
||||
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=20))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
logger.error(f"{agent.salt_id} {r}")
|
||||
return
|
||||
|
||||
printable = set(string.printable)
|
||||
sw = []
|
||||
for s in r:
|
||||
sw.append(
|
||||
{
|
||||
"name": "".join(filter(lambda x: x in printable, s["name"])),
|
||||
"version": "".join(filter(lambda x: x in printable, s["version"])),
|
||||
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
|
||||
"install_date": s["install_date"],
|
||||
"size": s["size"],
|
||||
"source": s["source"],
|
||||
"location": s["location"],
|
||||
"uninstall": s["uninstall"],
|
||||
}
|
||||
)
|
||||
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
else:
|
||||
s = agent.installedsoftware_set.first()
|
||||
s.software = sw
|
||||
s.save(update_fields=["software"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def install_program(pk, name, version):
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
@@ -169,6 +131,4 @@ def install_program(pk, name, version):
|
||||
agent=agent, name=name, version=version, message=output, installed=installed
|
||||
).save()
|
||||
|
||||
get_installed_software.delay(agent.pk)
|
||||
|
||||
return "ok"
|
||||
|
||||
@@ -120,61 +120,8 @@ class TestSoftwareTasks(TacticalTestCase):
|
||||
salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list")
|
||||
self.assertEquals(salt_api_cmd.call_count, 2)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_get_installed_software(self, nats_cmd):
|
||||
from .tasks import get_installed_software
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
|
||||
nats_return = [
|
||||
{
|
||||
"name": "Mozilla Maintenance Service",
|
||||
"size": "336.9 kB",
|
||||
"source": "",
|
||||
"version": "73.0.1",
|
||||
"location": "",
|
||||
"publisher": "Mozilla",
|
||||
"uninstall": '"C:\\Program Files (x86)\\Mozilla Maintenance Service\\uninstall.exe"',
|
||||
"install_date": "0001-01-01 00:00:00 +0000 UTC",
|
||||
},
|
||||
{
|
||||
"name": "OpenVPN 2.4.9-I601-Win10 ",
|
||||
"size": "8.7 MB",
|
||||
"source": "",
|
||||
"version": "2.4.9-I601-Win10",
|
||||
"location": "C:\\Program Files\\OpenVPN\\",
|
||||
"publisher": "OpenVPN Technologies, Inc.",
|
||||
"uninstall": "C:\\Program Files\\OpenVPN\\Uninstall.exe",
|
||||
"install_date": "0001-01-01 00:00:00 +0000 UTC",
|
||||
},
|
||||
{
|
||||
"name": "Microsoft Office Professional Plus 2019 - en-us",
|
||||
"size": "0 B",
|
||||
"source": "",
|
||||
"version": "16.0.10368.20035",
|
||||
"location": "C:\\Program Files\\Microsoft Office",
|
||||
"publisher": "Microsoft Corporation",
|
||||
"uninstall": '"C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeClickToRun.exe" scenario=install scenariosubtype=ARP sourcetype=None productstoremove=ProPlus2019Volume.16_en-us_x-none culture=en-us version.16=16.0',
|
||||
"install_date": "0001-01-01 00:00:00 +0000 UTC",
|
||||
},
|
||||
]
|
||||
|
||||
# test failed attempt
|
||||
nats_cmd.return_value = "timeout"
|
||||
ret = get_installed_software(agent.pk)
|
||||
self.assertFalse(ret)
|
||||
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test successful attempt
|
||||
nats_cmd.return_value = nats_return
|
||||
ret = get_installed_software(agent.pk)
|
||||
self.assertTrue(ret)
|
||||
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
|
||||
|
||||
@patch("agents.models.Agent.salt_api_cmd")
|
||||
@patch("software.tasks.get_installed_software.delay")
|
||||
def test_install_program(self, get_installed_software, salt_api_cmd):
|
||||
def test_install_program(self, salt_api_cmd):
|
||||
from .tasks import install_program
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -195,6 +142,5 @@ class TestSoftwareTasks(TacticalTestCase):
|
||||
salt_api_cmd.assert_called_with(
|
||||
timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"]
|
||||
)
|
||||
get_installed_software.assert_called_with(agent.pk)
|
||||
|
||||
self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
@@ -10,7 +10,7 @@ from agents.models import Agent
|
||||
from .models import ChocoSoftware, InstalledSoftware
|
||||
from .serializers import InstalledSoftwareSerializer
|
||||
from .tasks import install_program
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.utils import notify_error, filter_software
|
||||
|
||||
|
||||
@api_view()
|
||||
@@ -45,25 +45,11 @@ def refresh_installed(request, pk):
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
|
||||
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
|
||||
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
printable = set(string.printable)
|
||||
sw = []
|
||||
for s in r:
|
||||
sw.append(
|
||||
{
|
||||
"name": "".join(filter(lambda x: x in printable, s["name"])),
|
||||
"version": "".join(filter(lambda x: x in printable, s["version"])),
|
||||
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
|
||||
"install_date": s["install_date"],
|
||||
"size": s["size"],
|
||||
"source": s["source"],
|
||||
"location": s["location"],
|
||||
"uninstall": s["uninstall"],
|
||||
}
|
||||
)
|
||||
sw = filter_software(r)
|
||||
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
|
||||
@@ -37,18 +37,18 @@ app.conf.beat_schedule = {
|
||||
"task": "agents.tasks.batch_sync_modules_task",
|
||||
"schedule": crontab(minute=25, hour="*/4"),
|
||||
},
|
||||
"sys-info": {
|
||||
"task": "agents.tasks.batch_sysinfo_task",
|
||||
"schedule": crontab(minute=15, hour="*/2"),
|
||||
},
|
||||
"update-salt": {
|
||||
"task": "agents.tasks.update_salt_minion_task",
|
||||
"schedule": crontab(minute=20, hour="*/6"),
|
||||
},
|
||||
"agent-auto-update": {
|
||||
"task": "agents.tasks.auto_self_agent_update_task",
|
||||
"schedule": crontab(minute=35, hour="*"),
|
||||
},
|
||||
"agents-sync": {
|
||||
"task": "agents.tasks.sync_sysinfo_task",
|
||||
"schedule": crontab(minute=55, hour="*"),
|
||||
},
|
||||
"check-agentservice": {
|
||||
"task": "agents.tasks.monitor_agents_task",
|
||||
"schedule": crontab(minute="*/15"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,16 +16,15 @@ def get_debug_info():
|
||||
EXCLUDE_PATHS = (
|
||||
"/api/v3",
|
||||
"/api/v2",
|
||||
"/api/v1",
|
||||
"/logs/auditlogs",
|
||||
"/winupdate/winupdater",
|
||||
"/winupdate/results",
|
||||
f"/{settings.ADMIN_URL}",
|
||||
"/logout",
|
||||
"/agents/installagent",
|
||||
"/logs/downloadlog",
|
||||
)
|
||||
|
||||
ENDS_WITH = "/services/"
|
||||
|
||||
|
||||
class AuditMiddleware:
|
||||
def __init__(self, get_response):
|
||||
@@ -37,7 +36,9 @@ class AuditMiddleware:
|
||||
return response
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
if not request.path.startswith(EXCLUDE_PATHS):
|
||||
if not request.path.startswith(EXCLUDE_PATHS) and not request.path.endswith(
|
||||
ENDS_WITH
|
||||
):
|
||||
# https://stackoverflow.com/questions/26240832/django-and-middleware-which-uses-request-user-is-always-anonymous
|
||||
try:
|
||||
# DRF saves the class of the view function as the .cls property
|
||||
|
||||
@@ -15,25 +15,25 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.2.1"
|
||||
TRMM_VERSION = "0.2.17"
|
||||
|
||||
# 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.100"
|
||||
|
||||
# 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.11"
|
||||
|
||||
MESH_VER = "0.6.84"
|
||||
MESH_VER = "0.7.24"
|
||||
|
||||
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 = "4"
|
||||
|
||||
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,24 +27,19 @@ 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: |
|
||||
rm -rf /myagent/_work/1/s/web/node_modules
|
||||
cd /myagent/_work/1/s/web
|
||||
npm install
|
||||
displayName: "Install Frontend"
|
||||
|
||||
- script: |
|
||||
cd /myagent/_work/1/s/web
|
||||
npm run test:unit
|
||||
displayName: "Run Vue Tests"
|
||||
|
||||
16
backup.sh
16
backup.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="3"
|
||||
SCRIPT_VERSION="5"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -50,6 +50,11 @@ if [ -d /meshcentral/meshcentral-coredumps ]; then
|
||||
rm -f /meshcentral/meshcentral-coredumps/*
|
||||
fi
|
||||
|
||||
printf >&2 "${GREEN}Running postgres vacuum${NC}\n"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage"
|
||||
|
||||
dt_now=$(date '+%Y_%m_%d__%H_%M_%S')
|
||||
tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX)
|
||||
sysd="/etc/systemd/system"
|
||||
@@ -72,18 +77,13 @@ mongodump --gzip --out=${tmp_dir}/meshcentral/mongo
|
||||
sudo tar -czvf ${tmp_dir}/salt/etc-salt.tar.gz -C /etc/salt .
|
||||
tar -czvf ${tmp_dir}/salt/srv-salt.tar.gz -C /srv/salt .
|
||||
|
||||
if [ -d "/certs" ]; then
|
||||
sudo tar -czvf ${tmp_dir}/certs/certs.tar.gz -C /certs .
|
||||
else
|
||||
sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt .
|
||||
fi
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt .
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
|
||||
|
||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${tmp_dir}/systemd/
|
||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
|
||||
|
||||
cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz
|
||||
cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/
|
||||
|
||||
@@ -19,7 +19,7 @@ FROM nginx:stable-alpine
|
||||
ENV PUBLIC_DIR /usr/share/nginx/html
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR}
|
||||
|
||||
|
||||
@@ -6,9 +6,12 @@ ENV TACTICAL_DIR /opt/tactical
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN npm install meshcentral@0.6.62
|
||||
COPY api/tacticalrmm/tacticalrmm/settings.py /tmp/settings.py
|
||||
|
||||
RUN grep -o 'MESH_VER.*' /tmp/settings.py | cut -d'"' -f 2 > /tmp/MESH_VER && \
|
||||
npm install meshcentral@$(cat /tmp/MESH_VER)
|
||||
|
||||
COPY docker/containers/tactical-meshcentral/entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -62,5 +62,11 @@ if [ ! -f "${TACTICAL_DIR}/tmp/mesh_token" ]; then
|
||||
node node_modules/meshcentral --logintokenkey > ${TACTICAL_DIR}/tmp/mesh_token
|
||||
fi
|
||||
|
||||
# wait for nginx container
|
||||
until (echo > /dev/tcp/"${NGINX_HOST_IP}"/443) &> /dev/null; do
|
||||
echo "waiting for nginx to start..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# start mesh
|
||||
node node_modules/meshcentral
|
||||
|
||||
@@ -5,7 +5,7 @@ ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
|
||||
RUN apk add --no-cache inotify-tools supervisor bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY docker/containers/tactical-nats/entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -11,6 +11,9 @@ done
|
||||
mkdir -p /var/log/supervisor
|
||||
mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# wait for config changes
|
||||
|
||||
|
||||
supervisor_config="$(cat << EOF
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
@@ -18,13 +21,13 @@ nodaemon=true
|
||||
files = /etc/supervisor/conf.d/*.conf
|
||||
|
||||
[program:nats-server]
|
||||
command=nats-server -DVV --config "${TACTICAL_DIR}/api/nats-rmm.conf"
|
||||
command=nats-server --config ${TACTICAL_DIR}/api/nats-rmm.conf
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:config-watcher]
|
||||
command="inotifywait -m -e close_write ${TACTICAL_DIR}/api/nats-rmm.conf"; | while read events; do "nats-server --signal reload"; done;
|
||||
command=/bin/bash -c "inotifywait -mq -e modify "${TACTICAL_DIR}/api/nats-rmm.conf" | while read event; do nats-server --signal reload; done;"
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
@@ -4,7 +4,7 @@ ENV TACTICAL_DIR /opt/tactical
|
||||
|
||||
RUN apk add --no-cache openssl bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/
|
||||
RUN chmod +x /docker-entrypoint.d/entrypoint.sh
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
ENV SALT_USER saltapi
|
||||
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \
|
||||
wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \
|
||||
|
||||
@@ -4,10 +4,10 @@ set -e
|
||||
|
||||
: "${SALT_USER:='saltapi'}"
|
||||
|
||||
# wait for salt password to be generated by tactical-init
|
||||
until [ -f "${TACTICAL_DIR}/tmp/salt_pass" ]; do
|
||||
echo "waiting for salt password to be generated..."
|
||||
sleep 10
|
||||
sleep 15
|
||||
until [ -f "${TACTICAL_READY_FILE}" ]; do
|
||||
echo "waiting for init container to finish install or update..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
|
||||
@@ -15,7 +15,10 @@ SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
|
||||
echo "${SALT_USER}:${SALT_PASS}" | chpasswd
|
||||
|
||||
cherrypy_config="$(cat << EOF
|
||||
module_dirs: ['/opt/tactical/_modules']
|
||||
file_roots:
|
||||
base:
|
||||
- /srv/salt
|
||||
- ${TACTICAL_DIR}
|
||||
timeout: 20
|
||||
gather_job_timeout: 25
|
||||
max_event_size: 30485760
|
||||
@@ -42,11 +45,15 @@ nodaemon=true
|
||||
files = /etc/supervisor/conf.d/*.conf
|
||||
|
||||
[program:salt-master]
|
||||
command=/bin/bash -c "salt-master -l debug"
|
||||
command=/bin/bash -c "salt-master -l info"
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:salt-api]
|
||||
command=/bin/bash -c "salt-api -l debug"
|
||||
command=/bin/bash -c "salt-api -l info"
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
EOF
|
||||
)"
|
||||
|
||||
@@ -69,6 +69,9 @@ DEBUG = False
|
||||
|
||||
DOCKER_BUILD = True
|
||||
|
||||
CERT_FILE = '/opt/tactical/certs/fullchain.pem'
|
||||
KEY_FILE = '/opt/tactical/certs/privkey.pem'
|
||||
|
||||
SCRIPTS_DIR = '/opt/tactical/scripts'
|
||||
|
||||
ALLOWED_HOSTS = ['${API_HOST}']
|
||||
@@ -177,4 +180,4 @@ fi
|
||||
if [ "$1" = 'tactical-celerywinupdate' ]; then
|
||||
check_tactical_ready
|
||||
celery -A tacticalrmm worker -Q wupdate
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -51,6 +51,7 @@ services:
|
||||
POSTGRES_PASS: ${POSTGRES_PASS}
|
||||
APP_HOST: ${APP_HOST}
|
||||
API_HOST: ${API_HOST}
|
||||
MESH_USER: ${MESH_USER}
|
||||
MESH_HOST: ${MESH_HOST}
|
||||
TRMM_USER: ${TRMM_USER}
|
||||
TRMM_PASS: ${TRMM_PASS}
|
||||
@@ -100,8 +101,10 @@ services:
|
||||
MONGODB_USER: ${MONGODB_USER}
|
||||
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
- mesh-db
|
||||
proxy:
|
||||
aliases:
|
||||
- ${MESH_HOST}
|
||||
mesh-db:
|
||||
volumes:
|
||||
- tactical_data:/opt/tactical
|
||||
- mesh_data:/home/node/app/meshcentral-data
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -5421,9 +5421,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"internal-ip": {
|
||||
|
||||
85
install.sh
85
install.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="23"
|
||||
SCRIPT_VERSION="27"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -205,12 +205,47 @@ sudo apt install -y mongodb-org
|
||||
sudo systemctl enable mongod
|
||||
sudo systemctl restart mongod
|
||||
|
||||
|
||||
|
||||
print_green 'Installing python, redis and git'
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git
|
||||
|
||||
print_green 'Installing postgresql'
|
||||
|
||||
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-13
|
||||
|
||||
print_green 'Creating database for the rmm'
|
||||
|
||||
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
|
||||
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
|
||||
|
||||
sudo mkdir /rmm
|
||||
sudo chown ${USER}:${USER} /rmm
|
||||
sudo mkdir -p /var/log/celery
|
||||
sudo chown ${USER}:${USER} /var/log/celery
|
||||
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
|
||||
cd /rmm
|
||||
git config user.email "admin@example.com"
|
||||
git config user.name "Bob"
|
||||
git checkout master
|
||||
|
||||
print_green 'Installing MeshCentral'
|
||||
|
||||
MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
|
||||
|
||||
sudo mkdir -p /meshcentral/meshcentral-data
|
||||
sudo chown ${USER}:${USER} -R /meshcentral
|
||||
cd /meshcentral
|
||||
npm install meshcentral@0.6.84
|
||||
npm install meshcentral@${MESH_VER}
|
||||
sudo chown ${USER}:${USER} -R /meshcentral
|
||||
|
||||
meshcfg="$(cat << EOF
|
||||
@@ -253,37 +288,6 @@ EOF
|
||||
)"
|
||||
echo "${meshcfg}" > /meshcentral/meshcentral-data/config.json
|
||||
|
||||
print_green 'Installing python, redis and git'
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git
|
||||
|
||||
print_green 'Installing postgresql'
|
||||
|
||||
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-13
|
||||
|
||||
print_green 'Creating database for the rmm'
|
||||
|
||||
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
|
||||
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
|
||||
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
|
||||
|
||||
sudo mkdir /rmm
|
||||
sudo chown ${USER}:${USER} /rmm
|
||||
sudo mkdir -p /var/log/celery
|
||||
sudo chown ${USER}:${USER} /var/log/celery
|
||||
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
|
||||
cd /rmm
|
||||
git config user.email "admin@example.com"
|
||||
git config user.name "Bob"
|
||||
git checkout master
|
||||
|
||||
localvars="$(cat << EOF
|
||||
SECRET_KEY = "${DJANGO_SEKRET}"
|
||||
|
||||
@@ -348,7 +352,7 @@ python3 -m venv env
|
||||
source /rmm/api/env/bin/activate
|
||||
cd /rmm/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
|
||||
pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1
|
||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
|
||||
python manage.py migrate
|
||||
python manage.py collectstatic --no-input
|
||||
@@ -504,14 +508,7 @@ nginxmesh="$(cat << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${meshdomain};
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:800;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-Host \$host:\$server_port;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -530,6 +527,7 @@ server {
|
||||
proxy_pass http://127.0.0.1:4430/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Host \$host:\$server_port;
|
||||
@@ -557,7 +555,6 @@ sleep 30
|
||||
|
||||
saltvars="$(cat << EOF
|
||||
timeout: 20
|
||||
worker_threads: 15
|
||||
gather_job_timeout: 25
|
||||
max_event_size: 30485760
|
||||
external_auth:
|
||||
@@ -572,8 +569,6 @@ rest_cherrypy:
|
||||
port: 8123
|
||||
disable_ssl: True
|
||||
max_request_body_size: 30485760
|
||||
thread_pool: 300
|
||||
socket_queue_size: 100
|
||||
|
||||
EOF
|
||||
)"
|
||||
|
||||
62
restore.sh
62
restore.sh
@@ -7,7 +7,7 @@ pgpw="hunter2"
|
||||
|
||||
#####################################################
|
||||
|
||||
SCRIPT_VERSION="6"
|
||||
SCRIPT_VERSION="10"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -93,13 +93,25 @@ sudo apt update
|
||||
sudo apt install -y curl wget
|
||||
sudo mkdir -p /usr/local/rmmgo
|
||||
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
|
||||
wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P ${go_tmp}
|
||||
wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P ${go_tmp}
|
||||
|
||||
tar -xzf ${go_tmp}/go1.15.linux-amd64.tar.gz -C ${go_tmp}
|
||||
tar -xzf ${go_tmp}/go1.15.5.linux-amd64.tar.gz -C ${go_tmp}
|
||||
|
||||
sudo mv ${go_tmp}/go /usr/local/rmmgo/
|
||||
rm -rf ${go_tmp}
|
||||
|
||||
print_green 'Downloading NATS'
|
||||
|
||||
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
|
||||
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
|
||||
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
|
||||
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/nats-server
|
||||
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
|
||||
rm -rf ${nats_tmp}
|
||||
|
||||
print_green 'Installing NodeJS'
|
||||
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
@@ -133,15 +145,13 @@ print_green 'Restoring certbot'
|
||||
sudo apt install -y software-properties-common
|
||||
sudo apt install -y certbot openssl
|
||||
|
||||
if [ -f "${tmp_dir}/certs/certs.tar.gz" ]; then
|
||||
sudo mkdir /certs
|
||||
sudo tar -xzf $tmp_dir/certs/certs.tar.gz -C /certs
|
||||
else
|
||||
sudo rm -rf /etc/letsencrypt
|
||||
sudo mkdir /etc/letsencrypt
|
||||
sudo tar -xzf $tmp_dir/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt
|
||||
fi
|
||||
print_green 'Restoring certs'
|
||||
|
||||
sudo rm -rf /etc/letsencrypt
|
||||
sudo mkdir /etc/letsencrypt
|
||||
sudo tar -xzf $tmp_dir/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt
|
||||
sudo chown ${USER}:${USER} -R /etc/letsencrypt
|
||||
sudo chmod 775 -R /etc/letsencrypt
|
||||
|
||||
print_green 'Restoring celery configs'
|
||||
|
||||
@@ -169,7 +179,7 @@ print_green 'Installing postgresql'
|
||||
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-12
|
||||
sudo apt install -y postgresql-13
|
||||
sleep 2
|
||||
|
||||
print_green 'Restoring the database'
|
||||
@@ -197,15 +207,6 @@ sudo systemctl restart mongod
|
||||
sleep 5
|
||||
mongorestore --gzip $tmp_dir/meshcentral/mongo
|
||||
|
||||
print_green 'Restoring MeshCentral'
|
||||
|
||||
sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C /
|
||||
sudo chown ${USER}:${USER} -R /meshcentral
|
||||
cd /meshcentral
|
||||
npm install meshcentral@0.6.62
|
||||
|
||||
|
||||
print_green 'Restoring the backend'
|
||||
|
||||
sudo mkdir /rmm
|
||||
sudo chown ${USER}:${USER} /rmm
|
||||
@@ -213,8 +214,21 @@ sudo mkdir -p /var/log/celery
|
||||
sudo chown ${USER}:${USER} /var/log/celery
|
||||
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
|
||||
cd /rmm
|
||||
git config user.email "admin@example.com"
|
||||
git config user.name "Bob"
|
||||
git checkout master
|
||||
|
||||
print_green 'Restoring MeshCentral'
|
||||
|
||||
MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
|
||||
sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C /
|
||||
sudo chown ${USER}:${USER} -R /meshcentral
|
||||
cd /meshcentral
|
||||
npm install meshcentral@${MESH_VER}
|
||||
|
||||
|
||||
print_green 'Restoring the backend'
|
||||
|
||||
cp $tmp_dir/rmm/local_settings.py /rmm/api/tacticalrmm/tacticalrmm/
|
||||
cp $tmp_dir/rmm/env /rmm/web/.env
|
||||
cp $tmp_dir/rmm/app.ini /rmm/api/tacticalrmm/
|
||||
@@ -232,11 +246,15 @@ python3 -m venv env
|
||||
source /rmm/api/env/bin/activate
|
||||
cd /rmm/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
|
||||
pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1
|
||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
|
||||
python manage.py collectstatic --no-input
|
||||
python manage.py reload_nats
|
||||
deactivate
|
||||
|
||||
sudo systemctl enable nats.service
|
||||
sudo systemctl start nats.service
|
||||
|
||||
|
||||
print_green 'Installing Salt Master'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="98"
|
||||
SCRIPT_VERSION="100"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh'
|
||||
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -177,6 +177,11 @@ sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/
|
||||
sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo
|
||||
sudo chmod +x /usr/local/bin/goversioninfo
|
||||
|
||||
printf >&2 "${GREEN}Running postgres vacuum${NC}\n"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage"
|
||||
|
||||
if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then
|
||||
rm -rf /rmm/api/env
|
||||
cd /rmm/api
|
||||
@@ -184,7 +189,7 @@ if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then
|
||||
source /rmm/api/env/bin/activate
|
||||
cd /rmm/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
|
||||
pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
else
|
||||
source /rmm/api/env/bin/activate
|
||||
|
||||
1093
web/package-lock.json
generated
1093
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,20 +9,22 @@
|
||||
"test:unit": "quasar test --unit jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.9.10",
|
||||
"@quasar/extras": "^1.9.11",
|
||||
"axios": "^0.21.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"quasar": "^1.14.5"
|
||||
"quasar": "^1.14.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app": "^2.1.8",
|
||||
"@quasar/app": "^2.1.11",
|
||||
"@quasar/cli": "^1.1.2",
|
||||
"@quasar/quasar-app-extension-testing": "^1.0.0",
|
||||
"@quasar/quasar-app-extension-testing": "^1.0.3",
|
||||
"@quasar/quasar-app-extension-testing-unit-jest": "^1.0.1",
|
||||
"core-js": "^3.6.5",
|
||||
"core-js": "^3.8.1",
|
||||
"flush-promises": "^1.0.2",
|
||||
"fs-extra": "^9.0.1"
|
||||
"fs-extra": "^9.0.1",
|
||||
"prismjs": "^1.22.0",
|
||||
"vue-prism-editor": "^1.2.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 4 Chrome versions",
|
||||
|
||||
@@ -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" />
|
||||
@@ -123,6 +122,30 @@
|
||||
<q-item-section>Run Script</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-ripple @click="getFavoriteScripts">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="star" />
|
||||
</q-item-section>
|
||||
<q-item-section>Run Favorited Script</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="keyboard_arrow_right" />
|
||||
</q-item-section>
|
||||
<q-menu auto-close anchor="top end" self="top start">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="script in favoriteScripts"
|
||||
:key="script.value"
|
||||
dense
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="runFavScript(script.value, props.row.id)"
|
||||
>
|
||||
{{ script.label }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click.stop.prevent="remoteBG(props.row.id)">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-cogs" />
|
||||
@@ -274,6 +297,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 +340,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 -->
|
||||
@@ -375,6 +410,7 @@ export default {
|
||||
policyAddPk: null,
|
||||
showPendingActions: false,
|
||||
pendingActionAgentPk: null,
|
||||
favoriteScripts: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -384,6 +420,7 @@ export default {
|
||||
let availability = null;
|
||||
let checks = false;
|
||||
let patches = false;
|
||||
let actions = false;
|
||||
let reboot = false;
|
||||
let search = "";
|
||||
|
||||
@@ -394,6 +431,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 +444,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;
|
||||
@@ -437,6 +476,32 @@ export default {
|
||||
this.showEditAgentModal = true;
|
||||
}, 500);
|
||||
},
|
||||
runFavScript(scriptpk, agentpk) {
|
||||
const data = {
|
||||
pk: agentpk,
|
||||
timeout: 900,
|
||||
scriptPK: scriptpk,
|
||||
output: "forget",
|
||||
args: [],
|
||||
};
|
||||
this.$axios
|
||||
.post("/agents/runscript/", data)
|
||||
.then(r => this.notifySuccess(r.data))
|
||||
.catch(e => this.notifyError(e.response.data));
|
||||
},
|
||||
getFavoriteScripts() {
|
||||
this.favoriteScripts = [];
|
||||
this.$axios.get("/scripts/scripts/").then(r => {
|
||||
if (r.data.filter(k => k.favorite === true).length === 0) {
|
||||
this.notifyWarning("You don't have any scripts favorited!");
|
||||
return;
|
||||
}
|
||||
this.favoriteScripts = r.data
|
||||
.filter(k => k.favorite === true)
|
||||
.map(script => ({ label: script.name, value: script.id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
});
|
||||
},
|
||||
runPatchStatusScan(pk, hostname) {
|
||||
axios.get(`/winupdate/${pk}/runupdatescan/`).then(r => {
|
||||
this.notifySuccess(`Scan will be run shortly on ${hostname}`);
|
||||
@@ -548,13 +613,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 +667,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 -->
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<q-menu auto-close>
|
||||
<q-list dense style="min-width: 100px">
|
||||
<!-- script manager -->
|
||||
<q-item clickable v-close-popup @click="showScriptManager">
|
||||
<q-item clickable v-close-popup @click="showScriptManager = true">
|
||||
<q-item-section>Script Manager</q-item-section>
|
||||
</q-item>
|
||||
<!-- automation manager -->
|
||||
@@ -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,12 +178,15 @@
|
||||
<!-- 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 -->
|
||||
<ScriptManager />
|
||||
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog v-model="showScriptManager">
|
||||
<ScriptManager @close="showScriptManager = false" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- Automation Manager -->
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog v-model="showAutomationManager">
|
||||
@@ -196,16 +203,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 +234,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 +253,12 @@ export default {
|
||||
AuditManager,
|
||||
BulkAction,
|
||||
Deployment,
|
||||
ServerMaintenance,
|
||||
},
|
||||
props: ["clients"],
|
||||
data() {
|
||||
return {
|
||||
showServerMaintenance: false,
|
||||
showClientFormModal: false,
|
||||
showSiteFormModal: false,
|
||||
clientOp: null,
|
||||
@@ -262,6 +274,7 @@ export default {
|
||||
bulkMode: null,
|
||||
showDeployment: false,
|
||||
showDebugLog: false,
|
||||
showScriptManager: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -287,9 +300,6 @@ export default {
|
||||
this.bulkMode = null;
|
||||
this.showBulkAction = false;
|
||||
},
|
||||
showScriptManager() {
|
||||
this.$store.commit("TOGGLE_SCRIPT_MANAGER", true);
|
||||
},
|
||||
edited() {
|
||||
this.$emit("edited");
|
||||
},
|
||||
|
||||
@@ -1,150 +1,251 @@
|
||||
<template>
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog :value="toggleScriptManager" @hide="hideScriptManager" @show="getScripts">
|
||||
<q-card style="min-width: 70vw">
|
||||
<q-bar>
|
||||
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm row">
|
||||
<div style="width: 60vw; max-width: 90vw">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm row">
|
||||
<q-btn-dropdown icon="add" label="New" no-caps dense flat>
|
||||
<q-list dense>
|
||||
<q-item clickable v-close-popup @click="newScript">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="add" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>New Script</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showScriptUploadModal = true">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="cloud_upload" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Upload Script</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-btn
|
||||
label="Edit"
|
||||
:disable="!isRowSelected || isBuiltInScript(selectedScript.id)"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="editScript(selectedScript)"
|
||||
/>
|
||||
<q-btn
|
||||
label="Delete"
|
||||
:disable="!isRowSelected || isBuiltInScript(selectedScript.id)"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteScript(selectedScript.id)"
|
||||
/>
|
||||
<q-btn
|
||||
label="View Code"
|
||||
:disable="!isRowSelected"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="remove_red_eye"
|
||||
@click="viewCode(selectedScript)"
|
||||
/>
|
||||
<q-btn
|
||||
label="Download Script"
|
||||
:disable="!isRowSelected"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="cloud_download"
|
||||
@click="downloadScript(selectedScript)"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
style="min-height: 30vw; max-height: 30vw"
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="settings-tbl-sticky scroll"
|
||||
:data="visibleScripts"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
:pagination.sync="pagination"
|
||||
:filter="search"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
flat
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template v-slot:header-cell-favorite="props">
|
||||
<q-th :props="props" auto-width>
|
||||
<q-icon name="star" color="yellow-8" size="sm" />
|
||||
</q-th>
|
||||
</template>
|
||||
<template v-slot:top>
|
||||
<q-btn
|
||||
label="New"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="
|
||||
showScript('add');
|
||||
clearRow();
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
label="Edit"
|
||||
:disable="scriptpk === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showScript('edit')"
|
||||
/>
|
||||
<q-btn
|
||||
label="Delete"
|
||||
:disable="scriptpk === null || isBuiltInScript(scriptpk)"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteScript"
|
||||
/>
|
||||
<q-btn
|
||||
label="View Code"
|
||||
:disable="scriptpk === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="remove_red_eye"
|
||||
@click="viewCode"
|
||||
/>
|
||||
<q-btn
|
||||
label="Download Script"
|
||||
:disable="scriptpk === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="cloud_download"
|
||||
@click="downloadScript"
|
||||
class="q-ml-sm"
|
||||
:label="showCommunityScripts ? 'Hide Community Scripts' : 'Show Community Scripts'"
|
||||
:icon="showCommunityScripts ? 'visibility_off' : 'visibility'"
|
||||
@click="setShowCommunityScripts(!showCommunityScripts)"
|
||||
/>
|
||||
<q-space />
|
||||
<q-toggle :value="showBuiltIn" label="Show Community Scripts" @input="showBuiltIn = !showBuiltIn" />
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="settings-tbl-sticky"
|
||||
:data="visibleScripts"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
:pagination.sync="pagination"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
flat
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr
|
||||
:class="rowSelectedClass(props.row.id)"
|
||||
@click="
|
||||
scriptpk = props.row.id;
|
||||
filename = props.row.filename;
|
||||
code = props.row.code;
|
||||
"
|
||||
>
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td>
|
||||
{{ truncateText(props.row.description) }}
|
||||
<q-tooltip v-if="props.row.description.length >= 60" content-style="font-size: 12px">{{
|
||||
props.row.description
|
||||
}}</q-tooltip>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.filename }}</q-td>
|
||||
<q-td>{{ props.row.shell }}</q-td>
|
||||
<q-td v-show="props.row.script_type === 'userdefined'">User Defined</q-td>
|
||||
<q-td v-show="props.row.script_type === 'builtin'">Community Uploaded</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<q-card-section></q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section></q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showScriptModal">
|
||||
<ScriptModal :mode="mode" :scriptpk="scriptpk" @close="showScriptModal = false" @uploaded="getScripts" />
|
||||
<q-input
|
||||
v-model="search"
|
||||
style="width: 300px"
|
||||
label="Search"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
class="q-pr-md q-pb-xs"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" color="primary" />
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr
|
||||
:class="`${rowSelectedClass(props.row.id)} cursor-pointer`"
|
||||
@click="selectedScript = props.row"
|
||||
@contextmenu="selectedScript = props.row"
|
||||
>
|
||||
<!-- Context Menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editScript(props.row)"
|
||||
id="context-edit"
|
||||
v-if="props.row.script_type !== 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteScript(props.row.id)"
|
||||
id="context-delete"
|
||||
v-if="props.row.script_type !== 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
<q-item-section>Delete</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="favoriteScript(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="star" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ favoriteText(props.row.favorite) }}</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item clickable v-close-popup @click="viewCode(props.row)" id="context-view">
|
||||
<q-item-section side>
|
||||
<q-icon name="remove_red_eye" />
|
||||
</q-item-section>
|
||||
<q-item-section>View Code</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="downloadScript(props.row)" id="context-download">
|
||||
<q-item-section side>
|
||||
<q-icon name="cloud_download" />
|
||||
</q-item-section>
|
||||
<q-item-section>Download Script</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<q-td>
|
||||
<q-icon v-if="props.row.favorite" color="yellow-8" name="star" size="sm" />
|
||||
</q-td>
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td>{{ props.row.category }}</q-td>
|
||||
<q-td>{{ props.row.shell }}</q-td>
|
||||
<q-td>
|
||||
{{ truncateText(props.row.description) }}
|
||||
<q-tooltip v-if="props.row.description.length >= 60" content-style="font-size: 12px">{{
|
||||
props.row.description
|
||||
}}</q-tooltip>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<q-separator />
|
||||
<q-card-section></q-card-section>
|
||||
</q-card>
|
||||
<q-dialog v-model="showScriptUploadModal">
|
||||
<ScriptUploadModal
|
||||
:script="selectedScript"
|
||||
:categories="categories"
|
||||
@close="showScriptUploadModal = false"
|
||||
@added="getScripts"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapState } from "vuex";
|
||||
import ScriptModal from "@/components/modals/scripts/ScriptModal";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import ScriptUploadModal from "@/components/modals/scripts/ScriptUploadModal";
|
||||
import ScriptFormModal from "@/components/modals/scripts/ScriptFormModal";
|
||||
|
||||
export default {
|
||||
name: "ScriptManager",
|
||||
components: { ScriptModal },
|
||||
components: { ScriptUploadModal },
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
mode: "add",
|
||||
scriptpk: null,
|
||||
showScriptModal: false,
|
||||
filename: null,
|
||||
code: null,
|
||||
showBuiltIn: true,
|
||||
scripts: [],
|
||||
selectedScript: {},
|
||||
showScriptUploadModal: false,
|
||||
search: "",
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "script_type",
|
||||
sortBy: "favorite",
|
||||
descending: true,
|
||||
},
|
||||
columns: [
|
||||
{ name: "id", label: "ID", field: "id" },
|
||||
{
|
||||
name: "favorite",
|
||||
label: "",
|
||||
field: "favorite",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
@@ -153,16 +254,9 @@ export default {
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
align: "left",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
label: "File",
|
||||
field: "filename",
|
||||
name: "category",
|
||||
label: "Category",
|
||||
field: "category",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
@@ -174,37 +268,57 @@ export default {
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "script_type",
|
||||
label: "Type",
|
||||
field: "script_type",
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
sortable: false,
|
||||
},
|
||||
],
|
||||
visibleColumns: ["name", "desc", "file", "shell", "script_type"],
|
||||
visibleColumns: ["favorite", "name", "category", "desc", "shell"],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getScripts() {
|
||||
this.clearRow();
|
||||
this.$store.dispatch("getScripts");
|
||||
},
|
||||
hideScriptManager() {
|
||||
this.$store.commit("TOGGLE_SCRIPT_MANAGER", false);
|
||||
},
|
||||
clearRow() {
|
||||
this.scriptpk = null;
|
||||
this.filename = null;
|
||||
},
|
||||
viewCode() {
|
||||
this.$q.dialog({
|
||||
title: this.filename,
|
||||
message: `<pre>${this.code}</pre>`,
|
||||
html: true,
|
||||
style: "width: 70vw; max-width: 80vw;",
|
||||
this.$axios.get("/scripts/scripts/").then(r => {
|
||||
this.scripts = r.data;
|
||||
});
|
||||
},
|
||||
deleteScript() {
|
||||
setShowCommunityScripts(show) {
|
||||
this.$store.dispatch("setShowCommunityScripts", show);
|
||||
},
|
||||
clearRow() {
|
||||
this.selectedScript = {};
|
||||
},
|
||||
viewCode(script) {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: ScriptFormModal,
|
||||
parent: this,
|
||||
script: script,
|
||||
readonly: true,
|
||||
})
|
||||
.onDismiss(() => {
|
||||
this.getScripts();
|
||||
});
|
||||
},
|
||||
favoriteScript(script) {
|
||||
this.$q.loading.show();
|
||||
const notifyText = !script.favorite ? "Script was favorited!" : "Script was removed as a favorite!";
|
||||
this.$axios
|
||||
.put(`/scripts/${script.id}/script/`, { favorite: !script.favorite })
|
||||
.then(() => {
|
||||
this.getScripts();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(notifyText);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError("Something went wrong");
|
||||
});
|
||||
},
|
||||
deleteScript(scriptpk) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Delete script?",
|
||||
@@ -212,8 +326,8 @@ export default {
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
axios
|
||||
.delete(`/scripts/${this.scriptpk}/script/`)
|
||||
this.$axios
|
||||
.delete(`/scripts/${scriptpk}/script/`)
|
||||
.then(r => {
|
||||
this.getScripts();
|
||||
this.notifySuccess(r.data);
|
||||
@@ -221,29 +335,18 @@ export default {
|
||||
.catch(() => this.notifySuccess("Something went wrong"));
|
||||
});
|
||||
},
|
||||
downloadScript() {
|
||||
axios
|
||||
.get(`/scripts/${this.scriptpk}/download/`, { responseType: "blob" })
|
||||
downloadScript(script) {
|
||||
this.$axios
|
||||
.get(`/scripts/${script.id}/download/`)
|
||||
.then(({ data }) => {
|
||||
const blob = new Blob([data], { type: "text/plain" });
|
||||
const blob = new Blob([data.code], { type: "text/plain;charset=utf-8" });
|
||||
let link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = this.filename;
|
||||
link.download = data.filename;
|
||||
link.click();
|
||||
})
|
||||
.catch(() => this.notifyError("Something went wrong"));
|
||||
},
|
||||
showScript(mode) {
|
||||
switch (mode) {
|
||||
case "add":
|
||||
this.mode = "add";
|
||||
break;
|
||||
case "edit":
|
||||
this.mode = "edit";
|
||||
break;
|
||||
}
|
||||
this.showScriptModal = true;
|
||||
},
|
||||
truncateText(txt) {
|
||||
return txt.length >= 60 ? txt.substring(0, 60) + "..." : txt;
|
||||
},
|
||||
@@ -255,16 +358,55 @@ export default {
|
||||
}
|
||||
},
|
||||
rowSelectedClass(id) {
|
||||
if (this.scriptpk === id) return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
if (this.selectedScript.id === id) return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
},
|
||||
favoriteText(isFavorite) {
|
||||
return isFavorite ? "Remove as Favorite" : "Add as Favorite";
|
||||
},
|
||||
newScript() {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: ScriptFormModal,
|
||||
parent: this,
|
||||
categories: this.categories,
|
||||
readonly: false,
|
||||
})
|
||||
.onDismiss(() => {
|
||||
this.getScripts();
|
||||
});
|
||||
},
|
||||
editScript(script) {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: ScriptFormModal,
|
||||
parent: this,
|
||||
script: script,
|
||||
categories: this.categories,
|
||||
readonly: false,
|
||||
})
|
||||
.onDismiss(() => {
|
||||
this.getScripts();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
toggleScriptManager: state => state.toggleScriptManager,
|
||||
scripts: state => state.scripts,
|
||||
}),
|
||||
...mapState(["showCommunityScripts"]),
|
||||
visibleScripts() {
|
||||
return this.showBuiltIn ? this.scripts : this.scripts.filter(i => i.script_type !== "builtin");
|
||||
return this.showCommunityScripts ? this.scripts : this.scripts.filter(i => i.script_type !== "builtin");
|
||||
},
|
||||
categories() {
|
||||
let list = [];
|
||||
this.scripts.forEach(script => {
|
||||
if (!!script.category && !list.includes(script.category)) {
|
||||
if (script.category !== "Community") {
|
||||
list.push(script.category);
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
},
|
||||
isRowSelected() {
|
||||
return this.selectedScript.id !== null && this.selectedScript.id !== undefined;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
hide-bottom
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn dense flat push @click="refreshServices" icon="refresh" />
|
||||
<q-btn dense flat push @click="getServices" icon="refresh" />
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
@@ -242,7 +242,7 @@ export default {
|
||||
.then(r => {
|
||||
this.serviceDetailVisible = false;
|
||||
this.serviceDetailsModal = false;
|
||||
this.refreshServices();
|
||||
this.getServices();
|
||||
this.notifySuccess(`Service ${name} was edited!`);
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -303,7 +303,7 @@ export default {
|
||||
this.$axios
|
||||
.post("/services/serviceaction/", data)
|
||||
.then(r => {
|
||||
this.refreshServices();
|
||||
this.getServices();
|
||||
this.serviceDetailsModal = false;
|
||||
this.notifySuccess(`Service ${fullname} was ${status}!`);
|
||||
})
|
||||
@@ -313,19 +313,9 @@ export default {
|
||||
});
|
||||
},
|
||||
getServices() {
|
||||
this.$q.loading.show({ message: "Loading services..." });
|
||||
this.$axios
|
||||
.get(`/services/${this.pk}/services/`)
|
||||
.then(r => {
|
||||
this.servicesData = [r.data][0].services;
|
||||
})
|
||||
.catch(e => {
|
||||
this.notifyError(e.response.data);
|
||||
});
|
||||
},
|
||||
refreshServices() {
|
||||
this.$q.loading.show({ message: "Reloading services..." });
|
||||
this.$axios
|
||||
.get(`/services/${this.pk}/refreshedservices/`)
|
||||
.then(r => {
|
||||
this.servicesData = [r.data][0].services;
|
||||
this.$q.loading.hide();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div v-if="!this.selectedAgentPk">No agent selected</div>
|
||||
<div v-else-if="!Array.isArray(software) || !software.length">No software</div>
|
||||
<div v-else>
|
||||
<div class="row q-pt-xs items-start">
|
||||
<q-btn
|
||||
@@ -31,7 +30,7 @@
|
||||
:pagination.sync="pagination"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
row-key="name"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div v-if="Object.keys(summary).length === 0">No agent selected</div>
|
||||
<div v-else>
|
||||
<q-btn class="q-mr-sm" dense flat push icon="refresh" @click="refreshSummary" />
|
||||
<span>
|
||||
<b>{{ summary.hostname }}</b>
|
||||
<span v-if="summary.maintenance_mode"> • <q-badge color="warning"> Maintenance Mode </q-badge> </span>
|
||||
@@ -87,8 +88,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "SummaryTab",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
@@ -96,8 +101,22 @@ export default {
|
||||
awaitingSync(total, passing, failing) {
|
||||
return total !== 0 && passing === 0 && failing === 0 ? true : false;
|
||||
},
|
||||
refreshSummary() {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.get(`/agents/${this.selectedAgentPk}/wmi/`)
|
||||
.then(r => {
|
||||
this.$store.dispatch("loadSummary", this.selectedAgentPk);
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError(e.response.data);
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk"]),
|
||||
summary() {
|
||||
return this.$store.state.agentSummary;
|
||||
},
|
||||
|
||||
@@ -26,23 +26,25 @@
|
||||
>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>--log DEBUG</code>
|
||||
<code>-log debug</code>
|
||||
</q-badge>
|
||||
<span>To enable verbose output during the install</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>--local-salt "C:\\<some folder or path>\\salt-minion-setup.exe"</code>
|
||||
<code>-silent</code>
|
||||
</q-badge>
|
||||
<span>
|
||||
To skip downloading the salt-minion during the install. Download it
|
||||
<a v-if="info.arch === '64'" :href="info.data.salt64">here</a>
|
||||
<a v-else :href="info.data.salt32">here</a>
|
||||
</span>
|
||||
<span>Do not popup any message boxes during install</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>--local-mesh "C:\\<some folder or path>\\meshagent.exe"</code>
|
||||
<code>-nosalt</code>
|
||||
</q-badge>
|
||||
<span> Do not install salt during agent install. </span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>-local-mesh "C:\\<some folder or path>\\meshagent.exe"</code>
|
||||
</q-badge>
|
||||
<span>
|
||||
To skip downloading the Mesh Agent during the install. Download it
|
||||
@@ -53,16 +55,10 @@
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>--cert "C:\\<some folder or path>\\ca.pem"</code>
|
||||
<code>-cert "C:\\<some folder or path>\\ca.pem"</code>
|
||||
</q-badge>
|
||||
<span> To use a domain CA </span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>--timeout NUMBER_IN_SECONDS</code>
|
||||
</q-badge>
|
||||
<span> To increase the default timeout of 900 seconds for the installer. Use on slow computers.</span>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
<br />
|
||||
<p class="text-italic">Note: the auth token above will be valid for {{ info.expires }} hours.</p>
|
||||
|
||||
@@ -27,10 +27,13 @@
|
||||
<p>Fix issues with the Tactical Checkrunner windows service which handles running all checks.</p>
|
||||
</q-card-section>
|
||||
<q-card-section v-show="mode === 'salt'">
|
||||
<p>Fix issues with the salt-minion which handles windows updates, chocolatey and scheduled tasks.</p>
|
||||
<p>Fix issues with the salt-minion which handles windows updates and chocolatey.</p>
|
||||
</q-card-section>
|
||||
<q-card-section v-show="mode === 'rpc'">
|
||||
<p>Fix issues with the Tactical RPC service which handles most of the agent's realtime functions.</p>
|
||||
<p>
|
||||
Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled
|
||||
tasks.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section v-show="mode === 'command'">
|
||||
<p>Run a shell command on the agent.</p>
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
<br />
|
||||
</q-card-section>
|
||||
<q-form @submit.prevent="send">
|
||||
<q-card-section>
|
||||
<div class="q-pa-none">
|
||||
<p>Agent Type</p>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio dense v-model="monType" val="all" label="All" />
|
||||
<q-radio dense v-model="monType" val="servers" label="Servers" />
|
||||
<q-radio dense v-model="monType" val="workstations" label="Workstations" />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-pa-none">
|
||||
<p>Choose Target</p>
|
||||
@@ -154,8 +164,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "BulkAction",
|
||||
@@ -166,7 +176,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
target: "client",
|
||||
monType: "all",
|
||||
selected_mode: null,
|
||||
scriptOptions: [],
|
||||
scriptPK: null,
|
||||
timeout: 900,
|
||||
client: null,
|
||||
@@ -182,20 +194,30 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["scripts"]),
|
||||
...mapState(["showCommunityScripts"]),
|
||||
sites() {
|
||||
return !!this.client ? this.formatSiteOptions(this.client.sites) : [];
|
||||
},
|
||||
scriptOptions() {
|
||||
const ret = this.scripts.map(script => ({ label: script.name, value: script.id }));
|
||||
return ret.sort((a, b) => a.label.localeCompare(b.label));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getScripts() {
|
||||
let scripts;
|
||||
this.$axios.get("/scripts/scripts/").then(r => {
|
||||
if (this.showCommunityScripts) {
|
||||
scripts = r.data;
|
||||
} else {
|
||||
scripts = r.data.filter(i => i.script_type !== "builtin");
|
||||
}
|
||||
this.scriptOptions = scripts
|
||||
.map(script => ({ label: script.name, value: script.id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
});
|
||||
},
|
||||
send() {
|
||||
this.$q.loading.show();
|
||||
const data = {
|
||||
mode: this.selected_mode,
|
||||
monType: this.monType,
|
||||
target: this.target,
|
||||
site: this.site.value,
|
||||
client: this.client.value,
|
||||
@@ -253,6 +275,7 @@ export default {
|
||||
this.setTitles();
|
||||
this.getClients();
|
||||
this.getAgents();
|
||||
this.getScripts();
|
||||
|
||||
this.selected_mode = this.mode;
|
||||
},
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user