Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eb91c08aa | ||
|
|
ded5437522 | ||
|
|
9348657951 | ||
|
|
bca85933f7 | ||
|
|
4b84062d62 | ||
|
|
d6d0f8fa17 | ||
|
|
dd72c875d3 | ||
|
|
1a1df50300 | ||
|
|
53cbb527b4 | ||
|
|
8b87b2717e | ||
|
|
1007d6dac7 | ||
|
|
6799fac120 | ||
|
|
558e6288ca | ||
|
|
d9cb73291b | ||
|
|
d0f7be3ac3 | ||
|
|
331e16d3ca | ||
|
|
0db246c311 | ||
|
|
94dc62ff58 | ||
|
|
e68ecf6844 | ||
|
|
5167b0a8c6 | ||
|
|
77e3d3786d | ||
|
|
708d4d39bc | ||
|
|
2a8cda2a1e | ||
|
|
8d783840ad | ||
|
|
abe39d5790 | ||
|
|
d7868e9e5a | ||
|
|
7b84e36e15 | ||
|
|
6cab6d69d8 | ||
|
|
87846d7aef | ||
|
|
2557769c6a | ||
|
|
48375f3878 | ||
|
|
176c85d8c1 | ||
|
|
17cad71ede | ||
|
|
e8bf9d4e6f | ||
|
|
7bdd2038ef | ||
|
|
e9f6e7943a | ||
|
|
e74ba387ab | ||
|
|
27c79e5b99 | ||
|
|
8170d5ea73 | ||
|
|
196f73705d | ||
|
|
ad0bbf5248 | ||
|
|
4cae9cd90d | ||
|
|
be7bc55a76 | ||
|
|
684b545e8f | ||
|
|
7835cc3b10 | ||
|
|
f8706b51e8 | ||
|
|
d97f8fd5da | ||
|
|
f8fa87441e | ||
|
|
d42537814a | ||
|
|
792421b0e2 | ||
|
|
72d55a010b | ||
|
|
880d8258ce | ||
|
|
b79bf82efb | ||
|
|
b3118b6253 | ||
|
|
ba172e2e25 | ||
|
|
892d53abeb | ||
|
|
5cbaa1ce98 | ||
|
|
7b35d9ad2e | ||
|
|
8462de7911 | ||
|
|
8721f44298 | ||
|
|
c7a2d69afa | ||
|
|
0453d81e7a |
@@ -8,7 +8,7 @@
|
||||
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
|
||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
||||
|
||||
# [LIVE DEMO](https://rmm.xlawgaming.com/)
|
||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
|
||||
|
||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-09 02:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0012_user_agents_per_page'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='client_tree_sort',
|
||||
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -15,6 +15,11 @@ AGENT_TBL_TAB_CHOICES = [
|
||||
("mixed", "Mixed"),
|
||||
]
|
||||
|
||||
CLIENT_TREE_SORT_CHOICES = [
|
||||
("alphafail", "Move failing clients to the top"),
|
||||
("alpha", "Sort alphabetically"),
|
||||
]
|
||||
|
||||
|
||||
class User(AbstractUser, BaseAuditModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
@@ -27,7 +32,10 @@ class User(AbstractUser, BaseAuditModel):
|
||||
default_agent_tbl_tab = models.CharField(
|
||||
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||
)
|
||||
agents_per_page = models.PositiveIntegerField(default=50)
|
||||
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
|
||||
client_tree_sort = models.CharField(
|
||||
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
|
||||
)
|
||||
|
||||
agent = models.OneToOneField(
|
||||
"agents.Agent",
|
||||
|
||||
@@ -4,6 +4,18 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserUISerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"dark_mode",
|
||||
"show_community_scripts",
|
||||
"agent_dblclick_action",
|
||||
"default_agent_tbl_tab",
|
||||
"client_tree_sort",
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
@@ -271,19 +271,13 @@ class TestUserAction(TacticalTestCase):
|
||||
|
||||
def test_user_ui(self):
|
||||
url = "/accounts/users/ui/"
|
||||
data = {"dark_mode": False}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data = {"show_community_scripts": True}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data = {
|
||||
"userui": True,
|
||||
"dark_mode": True,
|
||||
"show_community_scripts": True,
|
||||
"agent_dblclick_action": "editagent",
|
||||
"default_agent_tbl_tab": "mixed",
|
||||
"agents_per_page": 1000,
|
||||
"client_tree_sort": "alpha",
|
||||
}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -14,7 +14,15 @@ from logs.models import AuditLog
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import User
|
||||
from .serializers import TOTPSetupSerializer, UserSerializer
|
||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
|
||||
|
||||
|
||||
def _is_root_user(request, user) -> bool:
|
||||
return (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
)
|
||||
|
||||
|
||||
class CheckCreds(KnoxLoginView):
|
||||
@@ -105,11 +113,7 @@ 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
|
||||
):
|
||||
if _is_root_user(request, user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
||||
@@ -120,11 +124,7 @@ class GetUpdateDeleteUser(APIView):
|
||||
|
||||
def delete(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
|
||||
):
|
||||
if _is_root_user(request, user):
|
||||
return notify_error("The root user cannot be deleted from the UI")
|
||||
|
||||
user.delete()
|
||||
@@ -137,11 +137,7 @@ 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
|
||||
):
|
||||
if _is_root_user(request, user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.set_password(request.data["password"])
|
||||
@@ -152,11 +148,7 @@ 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
|
||||
):
|
||||
if _is_root_user(request, user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.totp_key = ""
|
||||
@@ -184,23 +176,9 @@ class TOTPSetup(APIView):
|
||||
|
||||
class UserUI(APIView):
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
|
||||
if "dark_mode" in request.data.keys():
|
||||
user.dark_mode = request.data["dark_mode"]
|
||||
user.save(update_fields=["dark_mode"])
|
||||
|
||||
if "show_community_scripts" in request.data.keys():
|
||||
user.show_community_scripts = request.data["show_community_scripts"]
|
||||
user.save(update_fields=["show_community_scripts"])
|
||||
|
||||
if "userui" in request.data.keys():
|
||||
user.agent_dblclick_action = request.data["agent_dblclick_action"]
|
||||
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
|
||||
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
|
||||
|
||||
if "agents_per_page" in request.data.keys():
|
||||
user.agents_per_page = request.data["agents_per_page"]
|
||||
user.save(update_fields=["agents_per_page"])
|
||||
|
||||
serializer = UserUISerializer(
|
||||
instance=request.user, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-04 03:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0006_auto_20210217_1736'),
|
||||
('agents', '0030_agent_offline_time'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='alert_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
|
||||
),
|
||||
]
|
||||
@@ -20,7 +20,6 @@ from nats.aio.client import Client as NATS
|
||||
from nats.aio.errors import ErrTimeout
|
||||
from packaging import version as pyver
|
||||
|
||||
from alerts.models import AlertTemplate
|
||||
from core.models import TZ_CHOICES, CoreSettings
|
||||
from logs.models import BaseAuditModel
|
||||
|
||||
@@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
|
||||
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
||||
)
|
||||
maintenance_mode = models.BooleanField(default=False)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="agents",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
"clients.Site",
|
||||
related_name="agents",
|
||||
@@ -85,7 +91,7 @@ class Agent(BaseAuditModel):
|
||||
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
# check if new agent has been create
|
||||
# check if new agent has been created
|
||||
# or check if policy have changed on agent
|
||||
# or if site has changed on agent and if so generate-policies
|
||||
if (
|
||||
@@ -104,14 +110,6 @@ class Agent(BaseAuditModel):
|
||||
def client(self):
|
||||
return self.site.client
|
||||
|
||||
@property
|
||||
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
|
||||
@@ -271,6 +269,20 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
return ["unknown disk"]
|
||||
|
||||
def check_run_interval(self) -> int:
|
||||
interval = self.check_interval
|
||||
# determine if any agent checks have a custom interval and set the lowest interval
|
||||
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
|
||||
if check.run_interval and check.run_interval < interval:
|
||||
|
||||
# don't allow check runs less than 15s
|
||||
if check.run_interval < 15:
|
||||
interval = 15
|
||||
else:
|
||||
interval = check.run_interval
|
||||
|
||||
return interval
|
||||
|
||||
def run_script(
|
||||
self,
|
||||
scriptpk: int,
|
||||
@@ -461,9 +473,9 @@ class Agent(BaseAuditModel):
|
||||
)
|
||||
)
|
||||
|
||||
# returns alert template assigned in the following order: policy, site, client, global
|
||||
# will return None if nothing is found
|
||||
def get_alert_template(self) -> Union[AlertTemplate, None]:
|
||||
# sets alert template assigned in the following order: policy, site, client, global
|
||||
# sets None if nothing is found
|
||||
def set_alert_template(self):
|
||||
|
||||
site = self.site
|
||||
client = self.client
|
||||
@@ -563,9 +575,16 @@ class Agent(BaseAuditModel):
|
||||
continue
|
||||
|
||||
else:
|
||||
# save alert_template to agent cache field
|
||||
self.alert_template = template
|
||||
self.save()
|
||||
|
||||
return template
|
||||
|
||||
# no alert templates found or agent has been excluded
|
||||
self.alert_template = None
|
||||
self.save()
|
||||
|
||||
return None
|
||||
|
||||
def generate_checks_from_policies(self):
|
||||
@@ -703,11 +722,11 @@ class Agent(BaseAuditModel):
|
||||
# for clearing duplicate pending actions on agent
|
||||
def remove_matching_pending_task_actions(self, task_id):
|
||||
# remove any other pending actions on agent with same task_id
|
||||
for action in self.pendingactions.exclude(status="completed"): # type: ignore
|
||||
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
|
||||
if action.details["task_id"] == task_id:
|
||||
action.delete()
|
||||
|
||||
def should_create_alert(self, alert_template):
|
||||
def should_create_alert(self, alert_template=None):
|
||||
return (
|
||||
self.overdue_dashboard_alert
|
||||
or self.overdue_email_alert
|
||||
@@ -726,7 +745,6 @@ class Agent(BaseAuditModel):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_mail(
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||
(
|
||||
@@ -735,14 +753,13 @@ class Agent(BaseAuditModel):
|
||||
f"agent {self.hostname} "
|
||||
"within the expected time."
|
||||
),
|
||||
alert_template=alert_template,
|
||||
alert_template=self.alert_template,
|
||||
)
|
||||
|
||||
def send_recovery_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_mail(
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||
(
|
||||
@@ -751,27 +768,25 @@ class Agent(BaseAuditModel):
|
||||
f"agent {self.hostname} "
|
||||
"after an interruption in data transmission."
|
||||
),
|
||||
alert_template=alert_template,
|
||||
alert_template=self.alert_template,
|
||||
)
|
||||
|
||||
def send_outage_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.get_alert_template()
|
||||
CORE = CoreSettings.objects.first()
|
||||
CORE.send_sms(
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||
alert_template=alert_template,
|
||||
alert_template=self.alert_template,
|
||||
)
|
||||
|
||||
def send_recovery_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_sms(
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||
alert_template=alert_template,
|
||||
alert_template=self.alert_template,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -57,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
alert_template = obj.get_alert_template()
|
||||
|
||||
if not alert_template:
|
||||
if not obj.alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.agent_always_email,
|
||||
"always_text": alert_template.agent_always_text,
|
||||
"always_alert": alert_template.agent_always_alert,
|
||||
"name": obj.alert_template.name,
|
||||
"always_email": obj.alert_template.agent_always_email,
|
||||
"always_text": obj.alert_template.agent_always_text,
|
||||
"always_alert": obj.alert_template.agent_always_alert,
|
||||
}
|
||||
|
||||
def get_pending_actions(self, obj):
|
||||
|
||||
@@ -200,20 +200,6 @@ def agent_outages_task() -> None:
|
||||
Alert.handle_alert_failure(agent)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_agent_recovery_task(pk: int) -> None:
|
||||
sleep(10)
|
||||
from agents.models import RecoveryAction
|
||||
|
||||
action = RecoveryAction.objects.get(pk=pk)
|
||||
if action.mode == "command":
|
||||
data = {"func": "recoverycmd", "recoverycommand": action.command}
|
||||
else:
|
||||
data = {"func": "recover", "payload": {"mode": action.mode}}
|
||||
|
||||
asyncio.run(action.agent.nats_cmd(data, wait=False))
|
||||
|
||||
|
||||
@app.task
|
||||
def run_script_email_results_task(
|
||||
agentpk: int,
|
||||
|
||||
@@ -198,11 +198,6 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_get_processes(self, mock_ret):
|
||||
agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
|
||||
url_old = f"/agents/{agent_old.pk}/getprocs/"
|
||||
r = self.client.get(url_old)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
|
||||
url = f"/agents/{agent.pk}/getprocs/"
|
||||
|
||||
@@ -340,6 +335,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "schedreboot",
|
||||
"deleteafter": True,
|
||||
"trigger": "once",
|
||||
"name": r.data["task_name"], # type: ignore
|
||||
"year": 2025,
|
||||
@@ -421,42 +417,69 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_recover(self):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_recover(self, nats_cmd):
|
||||
from agents.models import RecoveryAction
|
||||
|
||||
self.agent.version = "0.11.1"
|
||||
self.agent.save(update_fields=["version"])
|
||||
RecoveryAction.objects.all().delete()
|
||||
url = "/agents/recover/"
|
||||
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
|
||||
# test mesh realtime
|
||||
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
|
||||
nats_cmd.return_value = "ok"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||
nats_cmd.assert_called_with(
|
||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
|
||||
)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
data["mode"] = "mesh"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertIn("pending", r.json())
|
||||
|
||||
RecoveryAction.objects.all().delete()
|
||||
data["mode"] = "command"
|
||||
data["cmd"] = "ipconfig /flushdns"
|
||||
# test mesh with agent rpc not working
|
||||
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
|
||||
nats_cmd.return_value = "timeout"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
RecoveryAction.objects.all().delete()
|
||||
data["cmd"] = None
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
self.assertEqual(RecoveryAction.objects.count(), 1)
|
||||
mesh_recovery = RecoveryAction.objects.first()
|
||||
self.assertEqual(mesh_recovery.mode, "mesh")
|
||||
nats_cmd.reset_mock()
|
||||
RecoveryAction.objects.all().delete()
|
||||
|
||||
self.agent.version = "0.9.4"
|
||||
self.agent.save(update_fields=["version"])
|
||||
data["mode"] = "mesh"
|
||||
# test tacagent realtime
|
||||
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
|
||||
nats_cmd.return_value = "ok"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||
nats_cmd.assert_called_with(
|
||||
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
|
||||
)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test tacagent with rpc not working
|
||||
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
|
||||
nats_cmd.return_value = "timeout"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertIn("0.9.5", r.json())
|
||||
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
# test shell cmd without command
|
||||
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||
|
||||
# test shell cmd
|
||||
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(RecoveryAction.objects.count(), 1)
|
||||
cmd_recovery = RecoveryAction.objects.first()
|
||||
self.assertEqual(cmd_recovery.mode, "command")
|
||||
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
|
||||
|
||||
def test_agents_agent_detail(self):
|
||||
url = f"/agents/{self.agent.pk}/agentdetail/"
|
||||
|
||||
@@ -69,10 +69,9 @@ def update_agents(request):
|
||||
def ping(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
status = "offline"
|
||||
if agent.has_nats:
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
|
||||
return Response({"name": agent.hostname, "status": status})
|
||||
|
||||
@@ -80,8 +79,7 @@ def ping(request, pk):
|
||||
@api_view(["DELETE"])
|
||||
def uninstall(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
if agent.has_nats:
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||
|
||||
name = agent.hostname
|
||||
agent.delete()
|
||||
@@ -147,9 +145,6 @@ def agent_detail(request, pk):
|
||||
@api_view()
|
||||
def get_processes(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if pyver.parse(agent.version) < pyver.parse("1.2.0"):
|
||||
return notify_error("Requires agent version 1.2.0 or greater")
|
||||
|
||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||
if r == "timeout":
|
||||
return notify_error("Unable to contact the agent")
|
||||
@@ -159,9 +154,6 @@ def get_processes(request, pk):
|
||||
@api_view()
|
||||
def kill_proc(request, pk, pid):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
|
||||
r = asyncio.run(
|
||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||
)
|
||||
@@ -177,8 +169,6 @@ def kill_proc(request, pk, pid):
|
||||
@api_view()
|
||||
def get_event_log(request, pk, logtype, days):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
data = {
|
||||
"func": "eventlog",
|
||||
@@ -198,8 +188,6 @@ def get_event_log(request, pk, logtype, days):
|
||||
@api_view(["POST"])
|
||||
def send_raw_cmd(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")
|
||||
timeout = int(request.data["timeout"])
|
||||
data = {
|
||||
"func": "rawcmd",
|
||||
@@ -228,26 +216,28 @@ class AgentsTableList(APIView):
|
||||
def patch(self, request):
|
||||
if "sitePK" in request.data.keys():
|
||||
queryset = (
|
||||
Agent.objects.select_related("site")
|
||||
Agent.objects.select_related("site", "policy", "alert_template")
|
||||
.prefetch_related("agentchecks")
|
||||
.filter(site_id=request.data["sitePK"])
|
||||
)
|
||||
elif "clientPK" in request.data.keys():
|
||||
queryset = (
|
||||
Agent.objects.select_related("site")
|
||||
Agent.objects.select_related("site", "policy", "alert_template")
|
||||
.prefetch_related("agentchecks")
|
||||
.filter(site__client_id=request.data["clientPK"])
|
||||
)
|
||||
else:
|
||||
queryset = Agent.objects.select_related("site").prefetch_related(
|
||||
"agentchecks"
|
||||
)
|
||||
queryset = Agent.objects.select_related(
|
||||
"site", "policy", "alert_template"
|
||||
).prefetch_related("agentchecks")
|
||||
|
||||
queryset = queryset.only(
|
||||
"pk",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"site",
|
||||
"policy",
|
||||
"alert_template",
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"needs_reboot",
|
||||
@@ -294,9 +284,6 @@ 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")
|
||||
|
||||
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
@@ -306,8 +293,6 @@ class Reboot(APIView):
|
||||
# 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")
|
||||
|
||||
try:
|
||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
||||
@@ -322,6 +307,7 @@ class Reboot(APIView):
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "schedreboot",
|
||||
"deleteafter": True,
|
||||
"trigger": "once",
|
||||
"name": task_name,
|
||||
"year": int(dt.datetime.strftime(obj, "%Y")),
|
||||
@@ -332,9 +318,6 @@ class Reboot(APIView):
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -487,20 +470,12 @@ def recover(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
mode = request.data["mode"]
|
||||
|
||||
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
|
||||
return notify_error("Only available in agent version greater than 0.9.5")
|
||||
|
||||
if not agent.has_nats:
|
||||
if mode == "tacagent" or mode == "rpc":
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
|
||||
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
|
||||
if agent.has_nats:
|
||||
if mode == "tacagent" or mode == "mesh":
|
||||
data = {"func": "recover", "payload": {"mode": mode}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
||||
if r == "ok":
|
||||
return Response("Successfully completed recovery")
|
||||
# attempt a realtime recovery, otherwise fall back to old recovery method
|
||||
if mode == "tacagent" or mode == "mesh":
|
||||
data = {"func": "recover", "payload": {"mode": mode}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
||||
if r == "ok":
|
||||
return Response("Successfully completed recovery")
|
||||
|
||||
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
|
||||
return notify_error(
|
||||
@@ -567,9 +542,6 @@ def run_script(request):
|
||||
@api_view()
|
||||
def recover_mesh(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")
|
||||
|
||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
||||
if r != "ok":
|
||||
@@ -749,9 +721,6 @@ def agent_maintenance(request):
|
||||
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")
|
||||
|
||||
@@ -158,7 +158,7 @@ class Alert(models.Model):
|
||||
email_alert = instance.overdue_email_alert
|
||||
text_alert = instance.overdue_text_alert
|
||||
dashboard_alert = instance.overdue_dashboard_alert
|
||||
alert_template = instance.get_alert_template()
|
||||
alert_template = instance.alert_template
|
||||
maintenance_mode = instance.maintenance_mode
|
||||
alert_severity = "error"
|
||||
agent = instance
|
||||
@@ -194,7 +194,7 @@ class Alert(models.Model):
|
||||
email_alert = instance.email_alert
|
||||
text_alert = instance.text_alert
|
||||
dashboard_alert = instance.dashboard_alert
|
||||
alert_template = instance.agent.get_alert_template()
|
||||
alert_template = instance.agent.alert_template
|
||||
maintenance_mode = instance.agent.maintenance_mode
|
||||
alert_severity = instance.alert_severity
|
||||
agent = instance.agent
|
||||
@@ -227,7 +227,7 @@ class Alert(models.Model):
|
||||
email_alert = instance.email_alert
|
||||
text_alert = instance.text_alert
|
||||
dashboard_alert = instance.dashboard_alert
|
||||
alert_template = instance.agent.get_alert_template()
|
||||
alert_template = instance.agent.alert_template
|
||||
maintenance_mode = instance.agent.maintenance_mode
|
||||
alert_severity = instance.alert_severity
|
||||
agent = instance.agent
|
||||
@@ -336,7 +336,7 @@ class Alert(models.Model):
|
||||
resolved_email_task = agent_recovery_email_task
|
||||
resolved_text_task = agent_recovery_sms_task
|
||||
|
||||
alert_template = instance.get_alert_template()
|
||||
alert_template = instance.alert_template
|
||||
alert = cls.objects.get(agent=instance, resolved=False)
|
||||
maintenance_mode = instance.maintenance_mode
|
||||
agent = instance
|
||||
@@ -354,7 +354,7 @@ class Alert(models.Model):
|
||||
resolved_email_task = handle_resolved_check_email_alert_task
|
||||
resolved_text_task = handle_resolved_check_sms_alert_task
|
||||
|
||||
alert_template = instance.agent.get_alert_template()
|
||||
alert_template = instance.agent.alert_template
|
||||
alert = cls.objects.get(assigned_check=instance, resolved=False)
|
||||
maintenance_mode = instance.agent.maintenance_mode
|
||||
agent = instance.agent
|
||||
@@ -372,7 +372,7 @@ class Alert(models.Model):
|
||||
resolved_email_task = handle_resolved_task_email_alert
|
||||
resolved_text_task = handle_resolved_task_sms_alert
|
||||
|
||||
alert_template = instance.agent.get_alert_template()
|
||||
alert_template = instance.agent.alert_template
|
||||
alert = cls.objects.get(assigned_task=instance, resolved=False)
|
||||
maintenance_mode = instance.agent.maintenance_mode
|
||||
agent = instance.agent
|
||||
|
||||
@@ -12,3 +12,13 @@ def unsnooze_alerts() -> str:
|
||||
)
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def cache_agents_alert_template():
|
||||
from agents.models import Agent
|
||||
|
||||
for agent in Agent.objects.only("pk"):
|
||||
agent.set_alert_template()
|
||||
|
||||
return "ok"
|
||||
|
||||
@@ -5,6 +5,8 @@ from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from autotasks.models import AutomatedTask
|
||||
from core.models import CoreSettings
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
@@ -395,8 +397,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
alert_templates = baker.make("alerts.AlertTemplate", _quantity=6)
|
||||
|
||||
# should be None
|
||||
self.assertFalse(workstation.get_alert_template())
|
||||
self.assertFalse(server.get_alert_template())
|
||||
self.assertFalse(workstation.set_alert_template())
|
||||
self.assertFalse(server.set_alert_template())
|
||||
|
||||
# assign first Alert Template as to a policy and apply it as default
|
||||
policy.alert_template = alert_templates[0] # type: ignore
|
||||
@@ -405,15 +407,15 @@ class TestAlertTasks(TacticalTestCase):
|
||||
core.server_policy = policy
|
||||
core.save()
|
||||
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
|
||||
# assign second Alert Template to as default alert template
|
||||
core.alert_template = alert_templates[1] # type: ignore
|
||||
core.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
|
||||
# assign third Alert Template to client
|
||||
workstation.client.alert_template = alert_templates[2] # type: ignore
|
||||
@@ -421,8 +423,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.client.save()
|
||||
server.client.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
|
||||
# apply policy to client and should override
|
||||
workstation.client.workstation_policy = policy
|
||||
@@ -430,8 +432,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.client.save()
|
||||
server.client.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
|
||||
# assign fouth Alert Template to site
|
||||
workstation.site.alert_template = alert_templates[3] # type: ignore
|
||||
@@ -439,8 +441,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.site.save()
|
||||
server.site.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
|
||||
# apply policy to site
|
||||
workstation.site.workstation_policy = policy
|
||||
@@ -448,8 +450,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.site.save()
|
||||
server.site.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
|
||||
# apply policy to agents
|
||||
workstation.policy = policy
|
||||
@@ -457,35 +459,35 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.save()
|
||||
server.save()
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
|
||||
|
||||
# test disabling alert template
|
||||
alert_templates[0].is_active = False # type: ignore
|
||||
alert_templates[0].save() # type: ignore
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
|
||||
# test policy exclusions
|
||||
alert_templates[3].excluded_agents.set([workstation.pk]) # type: ignore
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
|
||||
# test workstation exclusions
|
||||
alert_templates[2].exclude_workstations = True # type: ignore
|
||||
alert_templates[2].save() # type: ignore
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
|
||||
|
||||
# test server exclusions
|
||||
alert_templates[3].exclude_servers = True # type: ignore
|
||||
alert_templates[3].save() # type: ignore
|
||||
|
||||
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore
|
||||
|
||||
@patch("agents.tasks.sleep")
|
||||
@patch("core.models.CoreSettings.send_mail")
|
||||
@@ -504,6 +506,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email,
|
||||
sleep,
|
||||
):
|
||||
from agents.models import Agent
|
||||
from agents.tasks import (
|
||||
agent_outage_email_task,
|
||||
agent_outage_sms_task,
|
||||
@@ -564,6 +567,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent_email_alert = baker.make_recipe(
|
||||
"agents.overdue_agent", overdue_email_alert=True
|
||||
)
|
||||
|
||||
cache_agents_alert_template()
|
||||
agent_outages_task()
|
||||
|
||||
# should have created 6 alerts
|
||||
@@ -663,6 +668,9 @@ class TestAlertTasks(TacticalTestCase):
|
||||
alert_template_always_text.agent_text_on_resolved = True # type: ignore
|
||||
alert_template_always_text.save() # type: ignore
|
||||
|
||||
agent_template_text = Agent.objects.get(pk=agent_template_text.pk)
|
||||
agent_template_email = Agent.objects.get(pk=agent_template_email.pk)
|
||||
|
||||
# have the two agents checkin
|
||||
url = "/api/v3/checkin/"
|
||||
|
||||
@@ -719,7 +727,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email,
|
||||
sleep,
|
||||
):
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from checks.models import Check
|
||||
from checks.tasks import (
|
||||
handle_check_email_alert_task,
|
||||
handle_check_sms_alert_task,
|
||||
@@ -800,6 +809,14 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"checks.script_check", agent=agent_no_settings
|
||||
)
|
||||
|
||||
# update alert template and pull new checks from DB
|
||||
cache_agents_alert_template()
|
||||
check_template_email = Check.objects.get(pk=check_template_email.pk)
|
||||
check_template_dashboard_text = Check.objects.get(
|
||||
pk=check_template_dashboard_text.pk
|
||||
)
|
||||
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
|
||||
|
||||
# test agent with check that has alert settings
|
||||
check_agent.alert_severity = "warning"
|
||||
check_agent.status = "failing"
|
||||
@@ -905,11 +922,11 @@ class TestAlertTasks(TacticalTestCase):
|
||||
Alert.objects.filter(assigned_check=check_template_email).count(), 1
|
||||
)
|
||||
|
||||
alert_template_email.check_periodic_alert_days = 1
|
||||
alert_template_email.save()
|
||||
alert_template_email.check_periodic_alert_days = 1 # type: ignore
|
||||
alert_template_email.save() # type: ignore
|
||||
|
||||
alert_template_dashboard_text.check_periodic_alert_days = 1
|
||||
alert_template_dashboard_text.save()
|
||||
alert_template_dashboard_text.check_periodic_alert_days = 1 # type: ignore
|
||||
alert_template_dashboard_text.save() # type: ignore
|
||||
|
||||
# set last email time for alert in the past
|
||||
alert_email = Alert.objects.get(assigned_check=check_template_email)
|
||||
@@ -921,6 +938,13 @@ class TestAlertTasks(TacticalTestCase):
|
||||
alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20)
|
||||
alert_sms.save()
|
||||
|
||||
# refresh checks to get alert template changes
|
||||
check_template_email = Check.objects.get(pk=check_template_email.pk)
|
||||
check_template_dashboard_text = Check.objects.get(
|
||||
pk=check_template_dashboard_text.pk
|
||||
)
|
||||
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
|
||||
|
||||
Alert.handle_alert_failure(check_template_email)
|
||||
Alert.handle_alert_failure(check_template_dashboard_text)
|
||||
|
||||
@@ -939,11 +963,18 @@ class TestAlertTasks(TacticalTestCase):
|
||||
resolved_email.assert_not_called()
|
||||
|
||||
# test resolved notifications
|
||||
alert_template_email.check_email_on_resolved = True
|
||||
alert_template_email.save()
|
||||
alert_template_email.check_email_on_resolved = True # type: ignore
|
||||
alert_template_email.save() # type: ignore
|
||||
|
||||
alert_template_dashboard_text.check_text_on_resolved = True
|
||||
alert_template_dashboard_text.save()
|
||||
alert_template_dashboard_text.check_text_on_resolved = True # type: ignore
|
||||
alert_template_dashboard_text.save() # type: ignore
|
||||
|
||||
# refresh checks to get alert template changes
|
||||
check_template_email = Check.objects.get(pk=check_template_email.pk)
|
||||
check_template_dashboard_text = Check.objects.get(
|
||||
pk=check_template_dashboard_text.pk
|
||||
)
|
||||
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
|
||||
|
||||
Alert.handle_alert_resolve(check_template_email)
|
||||
|
||||
@@ -980,7 +1011,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email,
|
||||
sleep,
|
||||
):
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.tasks import (
|
||||
handle_resolved_task_email_alert,
|
||||
handle_resolved_task_sms_alert,
|
||||
@@ -1052,8 +1084,14 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning"
|
||||
)
|
||||
|
||||
# update alert template and pull new checks from DB
|
||||
cache_agents_alert_template()
|
||||
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
|
||||
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
|
||||
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
|
||||
|
||||
# test agent with task that has alert settings
|
||||
Alert.handle_alert_failure(task_agent)
|
||||
Alert.handle_alert_failure(task_agent) # type: ignore
|
||||
|
||||
# alert should have been created and sms, email notifications sent
|
||||
self.assertTrue(Alert.objects.filter(assigned_task=task_agent).exists())
|
||||
@@ -1081,7 +1119,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_sms.reset_mock()
|
||||
|
||||
# test task with an agent that has an email always alert template
|
||||
Alert.handle_alert_failure(task_template_email)
|
||||
Alert.handle_alert_failure(task_template_email) # type: ignore
|
||||
|
||||
self.assertTrue(Alert.objects.filter(assigned_task=task_template_email))
|
||||
alertpk = Alert.objects.get(assigned_task=task_template_email).pk
|
||||
@@ -1099,7 +1137,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email.reset_mock()
|
||||
|
||||
# test task with an agent that has an email always alert template
|
||||
Alert.handle_alert_failure(task_template_dashboard_text)
|
||||
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
|
||||
|
||||
self.assertTrue(
|
||||
Alert.objects.filter(assigned_task=task_template_dashboard_text).exists()
|
||||
@@ -1110,11 +1148,11 @@ class TestAlertTasks(TacticalTestCase):
|
||||
outage_sms.assert_not_called
|
||||
|
||||
# update task alert seveity to error
|
||||
task_template_dashboard_text.alert_severity = "error"
|
||||
task_template_dashboard_text.save()
|
||||
task_template_dashboard_text.alert_severity = "error" # type: ignore
|
||||
task_template_dashboard_text.save() # type: ignore
|
||||
|
||||
# now should trigger alert
|
||||
Alert.handle_alert_failure(task_template_dashboard_text)
|
||||
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
|
||||
outage_sms.assert_called_with(pk=alertpk, alert_interval=0)
|
||||
outage_sms.reset_mock()
|
||||
|
||||
@@ -1130,21 +1168,21 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_sms.reset_mock()
|
||||
|
||||
# test task with an agent that has a blank alert template
|
||||
Alert.handle_alert_failure(task_template_blank)
|
||||
Alert.handle_alert_failure(task_template_blank) # type: ignore
|
||||
|
||||
self.assertFalse(
|
||||
Alert.objects.filter(assigned_task=task_template_blank).exists()
|
||||
)
|
||||
|
||||
# test task that has no template and no settings
|
||||
Alert.handle_alert_failure(task_no_settings)
|
||||
Alert.handle_alert_failure(task_no_settings) # type: ignore
|
||||
|
||||
self.assertFalse(Alert.objects.filter(assigned_task=task_no_settings).exists())
|
||||
|
||||
# test periodic notifications
|
||||
|
||||
# make sure a failing task won't trigger another notification and only create a single alert
|
||||
Alert.handle_alert_failure(task_template_email)
|
||||
Alert.handle_alert_failure(task_template_email) # type: ignore
|
||||
send_email.assert_not_called()
|
||||
send_sms.assert_not_called()
|
||||
|
||||
@@ -1152,11 +1190,11 @@ class TestAlertTasks(TacticalTestCase):
|
||||
Alert.objects.filter(assigned_task=task_template_email).count(), 1
|
||||
)
|
||||
|
||||
alert_template_email.task_periodic_alert_days = 1
|
||||
alert_template_email.save()
|
||||
alert_template_email.task_periodic_alert_days = 1 # type: ignore
|
||||
alert_template_email.save() # type: ignore
|
||||
|
||||
alert_template_dashboard_text.task_periodic_alert_days = 1
|
||||
alert_template_dashboard_text.save()
|
||||
alert_template_dashboard_text.task_periodic_alert_days = 1 # type: ignore
|
||||
alert_template_dashboard_text.save() # type: ignore
|
||||
|
||||
# set last email time for alert in the past
|
||||
alert_email = Alert.objects.get(assigned_task=task_template_email)
|
||||
@@ -1168,8 +1206,13 @@ class TestAlertTasks(TacticalTestCase):
|
||||
alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20)
|
||||
alert_sms.save()
|
||||
|
||||
Alert.handle_alert_failure(task_template_email)
|
||||
Alert.handle_alert_failure(task_template_dashboard_text)
|
||||
# refresh automated tasks to get new alert templates
|
||||
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
|
||||
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
|
||||
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
|
||||
|
||||
Alert.handle_alert_failure(task_template_email) # type: ignore
|
||||
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
|
||||
|
||||
outage_email.assert_called_with(pk=alert_email.pk, alert_interval=1)
|
||||
outage_sms.assert_called_with(pk=alert_sms.pk, alert_interval=1)
|
||||
@@ -1177,7 +1220,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
outage_sms.reset_mock()
|
||||
|
||||
# test resolving alerts
|
||||
Alert.handle_alert_resolve(task_agent)
|
||||
Alert.handle_alert_resolve(task_agent) # type: ignore
|
||||
|
||||
self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved)
|
||||
self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved_on)
|
||||
@@ -1186,19 +1229,24 @@ class TestAlertTasks(TacticalTestCase):
|
||||
resolved_email.assert_not_called()
|
||||
|
||||
# test resolved notifications
|
||||
alert_template_email.task_email_on_resolved = True
|
||||
alert_template_email.save()
|
||||
alert_template_email.task_email_on_resolved = True # type: ignore
|
||||
alert_template_email.save() # type: ignore
|
||||
|
||||
alert_template_dashboard_text.task_text_on_resolved = True
|
||||
alert_template_dashboard_text.save()
|
||||
alert_template_dashboard_text.task_text_on_resolved = True # type: ignore
|
||||
alert_template_dashboard_text.save() # type: ignore
|
||||
|
||||
Alert.handle_alert_resolve(task_template_email)
|
||||
# refresh automated tasks to get new alert templates
|
||||
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
|
||||
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
|
||||
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
|
||||
|
||||
Alert.handle_alert_resolve(task_template_email) # type: ignore
|
||||
|
||||
resolved_email.assert_called_with(pk=alert_email.pk)
|
||||
resolved_sms.assert_not_called()
|
||||
resolved_email.reset_mock()
|
||||
|
||||
Alert.handle_alert_resolve(task_template_dashboard_text)
|
||||
Alert.handle_alert_resolve(task_template_dashboard_text) # type: ignore
|
||||
|
||||
resolved_sms.assert_called_with(pk=alert_sms.pk)
|
||||
resolved_email.assert_not_called()
|
||||
@@ -1276,6 +1324,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent.client.alert_template = alert_template
|
||||
agent.client.save()
|
||||
|
||||
agent.set_alert_template()
|
||||
|
||||
agent_outages_task()
|
||||
|
||||
# this is what data should be
|
||||
|
||||
@@ -14,6 +14,7 @@ from .serializers import (
|
||||
AlertTemplateRelationSerializer,
|
||||
AlertTemplateSerializer,
|
||||
)
|
||||
from .tasks import cache_agents_alert_template
|
||||
|
||||
|
||||
class GetAddAlerts(APIView):
|
||||
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# cache alert_template value on agents
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# cache alert_template value on agents
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(AlertTemplate, pk=pk).delete()
|
||||
|
||||
# cache alert_template value on agents
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
import os
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
@@ -18,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
|
||||
def test_get_checks(self):
|
||||
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
|
||||
|
||||
# add a check
|
||||
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
|
||||
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
|
||||
|
||||
# override check run interval
|
||||
check2 = baker.make_recipe(
|
||||
"checks.ping_check", agent=self.agent, run_interval=20
|
||||
)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
|
||||
|
||||
# Set last_run on both checks and should return an empty list
|
||||
check1.last_run = djangotime.now()
|
||||
check1.save()
|
||||
check2.last_run = djangotime.now()
|
||||
check2.save()
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||
self.assertFalse(r.data["checks"]) # type: ignore
|
||||
|
||||
# set last_run greater than interval
|
||||
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||
check1.save()
|
||||
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||
check2.save()
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
|
||||
|
||||
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
||||
r = self.client.get(url)
|
||||
@@ -54,6 +90,45 @@ class TestAPIv3(TacticalTestCase):
|
||||
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
|
||||
)
|
||||
|
||||
# add check to agent with check interval set
|
||||
check = baker.make_recipe(
|
||||
"checks.ping_check", agent=self.agent, run_interval=30
|
||||
)
|
||||
|
||||
r = self.client.get(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(
|
||||
r.json(),
|
||||
{"agent": self.agent.pk, "check_interval": 30},
|
||||
)
|
||||
|
||||
# minimum check run interval is 15 seconds
|
||||
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
|
||||
|
||||
r = self.client.get(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(
|
||||
r.json(),
|
||||
{"agent": self.agent.pk, "check_interval": 15},
|
||||
)
|
||||
|
||||
def test_run_checks(self):
|
||||
# force run all checks regardless of interval
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
baker.make_recipe("checks.ping_check", agent=agent)
|
||||
baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
baker.make_recipe("checks.cpuload_check", agent=agent)
|
||||
baker.make_recipe("checks.memory_check", agent=agent)
|
||||
baker.make_recipe("checks.eventlog_check", agent=agent)
|
||||
for _ in range(10):
|
||||
baker.make_recipe("checks.script_check", agent=agent)
|
||||
|
||||
url = f"/api/v3/{agent.agent_id}/runchecks/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.json()["agent"], agent.pk)
|
||||
self.assertIsInstance(r.json()["check_interval"], int)
|
||||
self.assertEqual(len(r.json()["checks"]), 15)
|
||||
|
||||
def test_checkin_patch(self):
|
||||
from logs.models import PendingAction
|
||||
|
||||
@@ -89,3 +164,42 @@ class TestAPIv3(TacticalTestCase):
|
||||
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||
self.assertEqual(action.status, "completed")
|
||||
action.delete()
|
||||
|
||||
@patch("apiv3.views.reload_nats")
|
||||
def test_agent_recovery(self, reload_nats):
|
||||
reload_nats.return_value = "ok"
|
||||
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
url = f"/api/v3/{agent.agent_id}/recovery/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
|
||||
reload_nats.assert_not_called()
|
||||
|
||||
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
|
||||
reload_nats.assert_not_called()
|
||||
|
||||
baker.make(
|
||||
"agents.RecoveryAction",
|
||||
agent=agent,
|
||||
mode="command",
|
||||
command="shutdown /r /t 5 /f",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(
|
||||
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
|
||||
)
|
||||
reload_nats.assert_not_called()
|
||||
|
||||
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
|
||||
reload_nats.assert_called_once()
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
|
||||
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
|
||||
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
|
||||
path("meshexe/", views.MeshExe.as_view()),
|
||||
@@ -18,4 +19,5 @@ urlpatterns = [
|
||||
path("winupdates/", views.WinUpdates.as_view()),
|
||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
|
||||
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
|
||||
]
|
||||
|
||||
@@ -65,13 +65,6 @@ class CheckIn(APIView):
|
||||
if Alert.objects.filter(agent=agent, resolved=False).exists():
|
||||
Alert.handle_alert_resolve(agent)
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
|
||||
if recovery is not None:
|
||||
recovery.last_run = djangotime.now()
|
||||
recovery.save(update_fields=["last_run"])
|
||||
handle_agent_recovery_task.delay(pk=recovery.pk) # type: ignore
|
||||
return Response("ok")
|
||||
|
||||
# get any pending actions
|
||||
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
|
||||
agent.handle_pending_actions()
|
||||
@@ -267,14 +260,13 @@ class SupersededWinUpdate(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class CheckRunner(APIView):
|
||||
class RunChecks(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
|
||||
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_interval,
|
||||
@@ -282,6 +274,42 @@ class CheckRunner(APIView):
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class CheckRunner(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
|
||||
|
||||
run_list = [
|
||||
check
|
||||
for check in checks
|
||||
# always run if check hasn't run yet
|
||||
if not check.last_run
|
||||
# if a check interval is set, see if the correct amount of seconds have passed
|
||||
or (
|
||||
check.run_interval
|
||||
and (
|
||||
check.last_run
|
||||
< djangotime.now()
|
||||
- djangotime.timedelta(seconds=check.run_interval)
|
||||
)
|
||||
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||
)
|
||||
or (
|
||||
check.last_run
|
||||
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
|
||||
)
|
||||
]
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_run_interval(),
|
||||
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request):
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
check.last_run = djangotime.now()
|
||||
@@ -297,7 +325,10 @@ class CheckRunnerInterval(APIView):
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
|
||||
|
||||
return Response(
|
||||
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
|
||||
)
|
||||
|
||||
|
||||
class TaskRunner(APIView):
|
||||
@@ -513,3 +544,27 @@ class ChocoResult(APIView):
|
||||
action.status = "completed"
|
||||
action.save(update_fields=["details", "status"])
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class AgentRecovery(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
|
||||
ret = {"mode": "pass", "shellcmd": ""}
|
||||
if recovery is None:
|
||||
return Response(ret)
|
||||
|
||||
recovery.last_run = djangotime.now()
|
||||
recovery.save(update_fields=["last_run"])
|
||||
|
||||
ret["mode"] = recovery.mode
|
||||
|
||||
if recovery.mode == "command":
|
||||
ret["shellcmd"] = recovery.command
|
||||
elif recovery.mode == "rpc":
|
||||
reload_nats()
|
||||
|
||||
return Response(ret)
|
||||
|
||||
@@ -28,6 +28,7 @@ class Policy(BaseAuditModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_from_policies_task
|
||||
|
||||
# get old policy if exists
|
||||
@@ -42,6 +43,9 @@ class Policy(BaseAuditModel):
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if old_policy.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
|
||||
error_threshold=check.error_threshold,
|
||||
alert_severity=check.alert_severity,
|
||||
name=check.name,
|
||||
run_interval=check.run_interval,
|
||||
disk=check.disk,
|
||||
fails_b4_alert=check.fails_b4_alert,
|
||||
ip=check.ip,
|
||||
@@ -98,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
|
||||
event_message=check.event_message,
|
||||
fail_when=check.fail_when,
|
||||
search_last_days=check.search_last_days,
|
||||
number_of_events_b4_alert=check.number_of_events_b4_alert,
|
||||
email_alert=check.email_alert,
|
||||
text_alert=check.text_alert,
|
||||
dashboard_alert=check.dashboard_alert,
|
||||
|
||||
@@ -426,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||
def test_sync_policy(self, generate_checks):
|
||||
url = "/automation/sync/"
|
||||
|
||||
# test invalid data
|
||||
data = {"invalid": 7}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
data = {"policy": policy.pk} # type: ignore
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
|
||||
class TestPolicyTasks(TacticalTestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
||||
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
|
||||
path("policies/overview/", views.OverviewPolicy.as_view()),
|
||||
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
|
||||
path("sync/", views.PolicySync.as_view()),
|
||||
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
|
||||
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
|
||||
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
|
||||
|
||||
@@ -8,6 +8,7 @@ from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from tacticalrmm.utils import notify_error
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
@@ -72,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class PolicySync(APIView):
|
||||
def post(self, request):
|
||||
if "policy" in request.data.keys():
|
||||
from automation.tasks import generate_agent_checks_from_policies_task
|
||||
|
||||
generate_agent_checks_from_policies_task.delay(
|
||||
request.data["policy"], create_tasks=True
|
||||
)
|
||||
return Response("ok")
|
||||
|
||||
else:
|
||||
return notify_error("The request was invalid")
|
||||
|
||||
|
||||
class PolicyAutoTask(APIView):
|
||||
|
||||
# tasks associated with policy
|
||||
|
||||
@@ -223,7 +223,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
create_win_task_schedule.delay(task.pk)
|
||||
|
||||
def should_create_alert(self, alert_template):
|
||||
def should_create_alert(self, alert_template=None):
|
||||
return (
|
||||
self.dashboard_alert
|
||||
or self.email_alert
|
||||
@@ -242,7 +242,6 @@ class AutomatedTask(BaseAuditModel):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
@@ -254,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_mail(subject, body, alert_template)
|
||||
CORE.send_mail(subject, body, self.agent.alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
@@ -273,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_resolved_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = (
|
||||
@@ -287,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_resolved_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.agent.get_alert_template()
|
||||
CORE = CoreSettings.objects.first()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||
|
||||
@@ -18,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
def get_alert_template(self, obj):
|
||||
|
||||
if obj.agent:
|
||||
alert_template = obj.agent.get_alert_template()
|
||||
alert_template = obj.agent.alert_template
|
||||
else:
|
||||
alert_template = None
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
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}}
|
||||
@@ -52,15 +51,6 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test old agent version
|
||||
data = {
|
||||
"autotask": {"script": script.id},
|
||||
"agent": old_agent.id,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# test add task to agent
|
||||
data = {
|
||||
"autotask": {
|
||||
@@ -203,13 +193,6 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
old_agent = baker.make_recipe("agents.agent", version="1.0.2")
|
||||
task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
|
||||
url = f"/tasks/runwintask/{task2.id}/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
nats_cmd.assert_not_called()
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,6 @@ 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}
|
||||
|
||||
check = None
|
||||
@@ -128,8 +125,5 @@ class AutoTask(APIView):
|
||||
@api_view()
|
||||
def run_task(request, pk):
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
if not task.agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
|
||||
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
||||
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-06 02:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0021_auto_20210212_1429'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='number_of_events_b4_alert',
|
||||
field=models.PositiveIntegerField(blank=True, default=1, null=True),
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/checks/migrations/0023_check_run_interval.py
Normal file
18
api/tacticalrmm/checks/migrations/0023_check_run_interval.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-06 02:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0022_check_number_of_events_b4_alert'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='run_interval',
|
||||
field=models.PositiveIntegerField(blank=True, default=0),
|
||||
),
|
||||
]
|
||||
@@ -93,6 +93,7 @@ class Check(BaseAuditModel):
|
||||
fail_count = models.PositiveIntegerField(default=0)
|
||||
outage_history = models.JSONField(null=True, blank=True) # store
|
||||
extra_details = models.JSONField(null=True, blank=True)
|
||||
run_interval = models.PositiveIntegerField(blank=True, default=0)
|
||||
# check specific fields
|
||||
|
||||
# for eventlog, script, ip, and service alert severity
|
||||
@@ -181,6 +182,9 @@ class Check(BaseAuditModel):
|
||||
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
|
||||
)
|
||||
search_last_days = models.PositiveIntegerField(null=True, blank=True)
|
||||
number_of_events_b4_alert = models.PositiveIntegerField(
|
||||
null=True, blank=True, default=1
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.agent:
|
||||
@@ -259,7 +263,7 @@ class Check(BaseAuditModel):
|
||||
"modified_time",
|
||||
]
|
||||
|
||||
def should_create_alert(self, alert_template):
|
||||
def should_create_alert(self, alert_template=None):
|
||||
|
||||
return (
|
||||
self.dashboard_alert
|
||||
@@ -488,13 +492,13 @@ class Check(BaseAuditModel):
|
||||
log.append(i)
|
||||
|
||||
if self.fail_when == "contains":
|
||||
if log:
|
||||
if log and len(log) >= self.number_of_events_b4_alert:
|
||||
self.status = "failing"
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
elif self.fail_when == "not_contains":
|
||||
if log:
|
||||
if log and len(log) >= self.number_of_events_b4_alert:
|
||||
self.status = "passing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
@@ -563,6 +567,7 @@ class Check(BaseAuditModel):
|
||||
text_alert=self.text_alert,
|
||||
fails_b4_alert=self.fails_b4_alert,
|
||||
extra_details=self.extra_details,
|
||||
run_interval=self.run_interval,
|
||||
error_threshold=self.error_threshold,
|
||||
warning_threshold=self.warning_threshold,
|
||||
disk=self.disk,
|
||||
@@ -586,6 +591,7 @@ class Check(BaseAuditModel):
|
||||
event_message=self.event_message,
|
||||
fail_when=self.fail_when,
|
||||
search_last_days=self.search_last_days,
|
||||
number_of_events_b4_alert=self.number_of_events_b4_alert,
|
||||
)
|
||||
|
||||
def is_duplicate(self, check):
|
||||
@@ -613,11 +619,10 @@ class Check(BaseAuditModel):
|
||||
def send_email(self):
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
body: str = ""
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
@@ -695,12 +700,11 @@ class Check(BaseAuditModel):
|
||||
except:
|
||||
continue
|
||||
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
body: str = ""
|
||||
|
||||
if self.agent:
|
||||
@@ -744,21 +748,21 @@ class Check(BaseAuditModel):
|
||||
elif self.check_type == "eventlog":
|
||||
body = subject
|
||||
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_resolved_email(self):
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = f"{self} is now back to normal"
|
||||
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_resolved_sms(self):
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
CORE.send_sms(subject, alert_template=alert_template)
|
||||
CORE.send_sms(subject, alert_template=self.agent.alert_template)
|
||||
|
||||
|
||||
class CheckHistory(models.Model):
|
||||
|
||||
@@ -25,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
if obj.agent:
|
||||
alert_template = obj.agent.get_alert_template()
|
||||
alert_template = obj.agent.alert_template
|
||||
else:
|
||||
alert_template = None
|
||||
|
||||
|
||||
@@ -310,14 +310,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_checks(self, nats_cmd):
|
||||
agent = baker.make_recipe("agents.agent", version="1.4.1")
|
||||
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
|
||||
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
|
||||
|
||||
url = f"/checks/runchecks/{agent_old.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
|
||||
|
||||
url = f"/checks/runchecks/{agent_b4_141.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
@@ -1003,6 +997,12 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"source": "source",
|
||||
"message": "a test message",
|
||||
},
|
||||
{
|
||||
"eventType": "error",
|
||||
"eventID": 123,
|
||||
"source": "source",
|
||||
"message": "a test message",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1107,3 +1107,61 @@ class TestCheckTasks(TacticalTestCase):
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
|
||||
# test multiple events found and contains
|
||||
# this should pass since only two events are found
|
||||
eventlog.number_of_events_b4_alert = 3
|
||||
eventlog.event_id_is_wildcard = False
|
||||
eventlog.event_source = None
|
||||
eventlog.event_message = None
|
||||
eventlog.event_id = 123
|
||||
eventlog.event_type = "error"
|
||||
eventlog.fail_when = "contains"
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
|
||||
# this should pass since there are two events returned
|
||||
eventlog.number_of_events_b4_alert = 2
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
|
||||
# test not contains
|
||||
# this should fail since only two events are found
|
||||
eventlog.number_of_events_b4_alert = 3
|
||||
eventlog.event_id_is_wildcard = False
|
||||
eventlog.event_source = None
|
||||
eventlog.event_message = None
|
||||
eventlog.event_id = 123
|
||||
eventlog.event_type = "error"
|
||||
eventlog.fail_when = "not_contains"
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
|
||||
# this should pass since there are two events returned
|
||||
eventlog.number_of_events_b4_alert = 2
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
|
||||
@@ -161,8 +161,6 @@ class CheckHistory(APIView):
|
||||
@api_view()
|
||||
def run_checks(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")
|
||||
|
||||
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
|
||||
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
|
||||
|
||||
@@ -32,6 +32,7 @@ class Client(BaseAuditModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
|
||||
# get old client if exists
|
||||
@@ -54,6 +55,9 @@ class Client(BaseAuditModel):
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if old_client and old_client.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
@@ -127,6 +131,7 @@ class Site(BaseAuditModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
|
||||
# get old client if exists
|
||||
@@ -149,6 +154,9 @@ class Site(BaseAuditModel):
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if old_site and old_site.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ class CoreSettings(BaseAuditModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
|
||||
if not self.pk and CoreSettings.objects.exists():
|
||||
@@ -105,6 +106,9 @@ class CoreSettings(BaseAuditModel):
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
|
||||
if old_settings and old_settings.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
def __str__(self):
|
||||
return "Global Site Settings"
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ def dashboard_info(request):
|
||||
"show_community_scripts": request.user.show_community_scripts,
|
||||
"dbl_click_action": request.user.agent_dblclick_action,
|
||||
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
|
||||
"agents_per_page": request.user.agents_per_page,
|
||||
"client_tree_sort": request.user.client_tree_sort,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -250,8 +250,10 @@ class PendingAction(models.Model):
|
||||
if self.action_type == "schedreboot":
|
||||
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
|
||||
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
||||
elif self.action_type == "taskaction" or self.action_type == "agentupdate":
|
||||
elif self.action_type == "taskaction":
|
||||
return "Next agent check-in"
|
||||
elif self.action_type == "agentupdate":
|
||||
return "Next update cycle"
|
||||
elif self.action_type == "chocoinstall":
|
||||
return "ASAP"
|
||||
|
||||
|
||||
@@ -218,8 +218,8 @@ class TestAuditViews(TacticalTestCase):
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
|
||||
self.assertEqual(r.data["completed_count"], 26) # type: ignore
|
||||
self.assertEqual(r.data["total"], 26) # type: ignore
|
||||
self.assertEqual(r.data["completed_count"], 12) # type: ignore
|
||||
self.assertEqual(r.data["total"], 12) # type: ignore
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
|
||||
@@ -113,16 +113,24 @@ class PendingActions(APIView):
|
||||
actions = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"], status=status_filter
|
||||
)
|
||||
total = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"]
|
||||
).count()
|
||||
completed = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"], status="completed"
|
||||
).count()
|
||||
|
||||
else:
|
||||
actions = PendingAction.objects.filter(status=status_filter).select_related(
|
||||
"agent"
|
||||
)
|
||||
total = PendingAction.objects.count()
|
||||
completed = PendingAction.objects.filter(status="completed").count()
|
||||
|
||||
ret = {
|
||||
"actions": PendingActionSerializer(actions, many=True).data,
|
||||
"completed_count": PendingAction.objects.filter(status="completed").count(),
|
||||
"total": PendingAction.objects.count(),
|
||||
"completed_count": completed,
|
||||
"total": total,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ future==0.18.2
|
||||
kombu==5.0.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.2
|
||||
packaging==20.8
|
||||
packaging==20.9
|
||||
psycopg2-binary==2.8.6
|
||||
pycparser==2.20
|
||||
pycryptodome==3.10.1
|
||||
@@ -28,10 +28,10 @@ redis==3.5.3
|
||||
requests==2.25.1
|
||||
six==1.15.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.52.0
|
||||
twilio==6.53.0
|
||||
urllib3==1.26.3
|
||||
uWSGI==2.0.19.1
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
websockets==8.1
|
||||
zipp==3.4.0
|
||||
zipp==3.4.1
|
||||
|
||||
@@ -208,5 +208,33 @@
|
||||
"name": "Verify Antivirus Status",
|
||||
"description": "Verify and display status for all installed Antiviruses",
|
||||
"shell": "powershell"
|
||||
},
|
||||
{
|
||||
"filename": "CreateAllUserLogonScript.ps1",
|
||||
"submittedBy": "https://github.com/nr-plaxon",
|
||||
"name": "Create User Logon Script",
|
||||
"description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
|
||||
"shell": "powershell"
|
||||
},
|
||||
{
|
||||
"filename": "Chocolatey_Update_Installed.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Chocolatey Update Installed Apps",
|
||||
"description": "Update all apps that were installed using Chocolatey.",
|
||||
"shell": "cmd"
|
||||
},
|
||||
{
|
||||
"filename": "AD_Check_And_Enable_AD_Recycle_Bin.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "AD - Check and Enable AD Recycle Bin",
|
||||
"description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
|
||||
"shell": "powershell"
|
||||
},
|
||||
{
|
||||
"filename": "Check_Events_for_Bluescreens.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Event Viewer - Check for Bluescreens",
|
||||
"description": "This will check for Bluescreen events on your system",
|
||||
"shell": "powershell"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,8 +7,6 @@ from tacticalrmm.celery import app
|
||||
|
||||
@app.task
|
||||
def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
|
||||
agents = Agent.objects.filter(pk__in=agentpks)
|
||||
agents_nats = [agent for agent in agents if agent.has_nats]
|
||||
nats_data = {
|
||||
"func": "rawcmd",
|
||||
"timeout": timeout,
|
||||
@@ -17,15 +15,13 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
|
||||
"shell": shell,
|
||||
},
|
||||
}
|
||||
for agent in agents_nats:
|
||||
for agent in Agent.objects.filter(pk__in=agentpks):
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
agents = Agent.objects.filter(pk__in=agentpks)
|
||||
agents_nats = [agent for agent in agents if agent.has_nats]
|
||||
nats_data = {
|
||||
"func": "runscript",
|
||||
"timeout": timeout,
|
||||
@@ -35,5 +31,5 @@ def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
|
||||
"shell": script.shell,
|
||||
},
|
||||
}
|
||||
for agent in agents_nats:
|
||||
for agent in Agent.objects.filter(pk__in=agentpks):
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
|
||||
@@ -18,8 +18,6 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
@api_view()
|
||||
def get_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")
|
||||
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
|
||||
|
||||
if r == "timeout":
|
||||
@@ -38,8 +36,6 @@ def default_services(request):
|
||||
@api_view(["POST"])
|
||||
def service_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")
|
||||
action = request.data["sv_action"]
|
||||
data = {
|
||||
"func": "winsvcaction",
|
||||
@@ -80,8 +76,6 @@ def service_action(request):
|
||||
@api_view()
|
||||
def service_detail(request, pk, svcname):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
data = {"func": "winsvcdetail", "payload": {"name": svcname}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
||||
if r == "timeout":
|
||||
@@ -93,8 +87,6 @@ def service_detail(request, pk, svcname):
|
||||
@api_view(["POST"])
|
||||
def edit_service(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")
|
||||
data = {
|
||||
"func": "editwinsvc",
|
||||
"payload": {
|
||||
|
||||
@@ -63,8 +63,6 @@ def get_installed(request, pk):
|
||||
@api_view()
|
||||
def refresh_installed(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")
|
||||
|
||||
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
|
||||
@@ -15,20 +15,20 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.4.21"
|
||||
TRMM_VERSION = "0.4.26"
|
||||
|
||||
# bump this version everytime vue code is changed
|
||||
# to alert user they need to manually refresh their browser
|
||||
APP_VER = "0.0.119"
|
||||
APP_VER = "0.0.122"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.4.8"
|
||||
LATEST_AGENT_VER = "1.4.12"
|
||||
|
||||
MESH_VER = "0.7.79"
|
||||
MESH_VER = "0.7.88"
|
||||
|
||||
# for the update script, bump when need to recreate venv or npm install
|
||||
PIP_VER = "10"
|
||||
NPM_VER = "9"
|
||||
PIP_VER = "11"
|
||||
NPM_VER = "10"
|
||||
|
||||
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"
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM node:12-alpine AS builder
|
||||
WORKDIR /home/node/app
|
||||
|
||||
COPY ./web/package.json .
|
||||
RUN npm install -g npm
|
||||
RUN npm install
|
||||
|
||||
COPY ./web .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nats:2.1-alpine
|
||||
FROM nats:2.2-alpine
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django, Vue and Golang.
|
||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in Golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
||||
|
||||
## [LIVE DEMO](https://rmm.xlawgaming.com/)
|
||||
## [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||
|
||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ ufw allow proto tcp from any to any port 4222
|
||||
```
|
||||
|
||||
!!!info
|
||||
SSH is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
|
||||
SSH (port 22 tcp) is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
|
||||
Allow ssh from everywhere (__not__ recommended)
|
||||
```bash
|
||||
ufw allow ssh
|
||||
|
||||
@@ -20,7 +20,7 @@ SSH into your server as the linux user you created during install.<br/><br/>
|
||||
__Never__ run any update scripts or commands as the `root` user.<br/>This will mess up permissions and break your installation.<br/><br/>
|
||||
Download the update script and run it:<br/>
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
|
||||
wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
|
||||
chmod +x update.sh
|
||||
./update.sh
|
||||
```
|
||||
|
||||
21
install.sh
21
install.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="41"
|
||||
SCRIPT_VERSION="43"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
|
||||
|
||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
|
||||
@@ -185,9 +185,9 @@ print_green 'Installing golang'
|
||||
|
||||
sudo mkdir -p /usr/local/rmmgo
|
||||
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
|
||||
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
|
||||
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
|
||||
|
||||
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
|
||||
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
|
||||
|
||||
sudo mv ${go_tmp}/go /usr/local/rmmgo/
|
||||
rm -rf ${go_tmp}
|
||||
@@ -195,11 +195,11 @@ 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}
|
||||
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
|
||||
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
|
||||
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
|
||||
sudo mv ${nats_tmp}/nats-server-v2.2.0-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}
|
||||
@@ -216,6 +216,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt update
|
||||
sudo apt install -y gcc g++ make
|
||||
sudo apt install -y nodejs
|
||||
sudo npm install -g npm
|
||||
|
||||
print_green 'Installing MongoDB'
|
||||
|
||||
@@ -251,6 +252,10 @@ echo "$postgresql_repo" | sudo tee /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
|
||||
sleep 2
|
||||
sudo systemctl enable postgresql
|
||||
sudo systemctl restart postgresql
|
||||
sleep 5
|
||||
|
||||
print_green 'Creating database for the rmm'
|
||||
|
||||
@@ -366,7 +371,7 @@ MESH_USERNAME = "${meshusername}"
|
||||
MESH_SITE = "https://${meshdomain}"
|
||||
REDIS_HOST = "localhost"
|
||||
KEEP_SALT = False
|
||||
ADMIN_ENABLED = False
|
||||
ADMIN_ENABLED = True
|
||||
EOF
|
||||
)"
|
||||
echo "${localvars}" > /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
|
||||
@@ -811,6 +816,8 @@ python manage.py reload_nats
|
||||
deactivate
|
||||
sudo systemctl start nats.service
|
||||
|
||||
## disable django admin
|
||||
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
|
||||
|
||||
print_green 'Restarting services'
|
||||
for i in rmm.service celery.service celerybeat.service natsapi.service
|
||||
|
||||
18
restore.sh
18
restore.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="20"
|
||||
SCRIPT_VERSION="21"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
|
||||
|
||||
sudo apt update
|
||||
@@ -108,9 +108,9 @@ print_green 'Installing golang'
|
||||
sudo apt update
|
||||
sudo mkdir -p /usr/local/rmmgo
|
||||
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
|
||||
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
|
||||
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
|
||||
|
||||
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
|
||||
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
|
||||
|
||||
sudo mv ${go_tmp}/go /usr/local/rmmgo/
|
||||
rm -rf ${go_tmp}
|
||||
@@ -118,11 +118,11 @@ 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}
|
||||
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
|
||||
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
|
||||
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
|
||||
sudo mv ${nats_tmp}/nats-server-v2.2.0-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}
|
||||
@@ -133,6 +133,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt update
|
||||
sudo apt install -y gcc g++ make
|
||||
sudo apt install -y nodejs
|
||||
sudo npm install -g npm
|
||||
|
||||
print_green 'Restoring Nginx'
|
||||
|
||||
@@ -205,9 +206,8 @@ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-13
|
||||
sleep 2
|
||||
|
||||
|
||||
|
||||
sudo systemctl enable postgresql
|
||||
sudo systemctl restart postgresql
|
||||
|
||||
print_green 'Restoring MongoDB'
|
||||
|
||||
|
||||
17
scripts/AD_Check_And_Enable_AD_Recycle_Bin.ps1
Normal file
17
scripts/AD_Check_And_Enable_AD_Recycle_Bin.ps1
Normal file
@@ -0,0 +1,17 @@
|
||||
#Please only run on a domain controller
|
||||
#This script will first check if there are any AD Recycle Bin scopes set up - if there are no scopes it is assumed recycle bin feature is not enabled for the domain
|
||||
#The script then pulls the domain that the machine running the script is on - queries the domain for the Infrastructure Master and then will attempt to enable the feature
|
||||
|
||||
$adRecycleBinScope = Get-ADOptionalFeature -Identity 'Recycle Bin Feature' | Select -ExpandProperty EnabledScopes
|
||||
$ADDomain = Get-ADDomain | Select -ExpandProperty Forest
|
||||
$ADInfraMaster = Get-ADDomain | Select-Object InfrastructureMaster
|
||||
|
||||
if ($adRecycleBinScope -eq $null){
|
||||
Write-Host "Recycle Bin Disabled"
|
||||
Write-Host "Attempting to enable AD Recycle Bin"
|
||||
Enable-ADOptionalFeature -Identity 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target $ADDomain -Server $ADInfraMaster.InfrastructureMaster -Confirm:$false
|
||||
Write-Host "AD Recycle Bin enabled for domain $($ADDomain)"
|
||||
}
|
||||
else{
|
||||
Write-Host "Recycle Bin already Enabled For: $($ADDomain)`n Scope: $($adRecycleBinScope)"
|
||||
}
|
||||
20
scripts/Check_Events_for_Bluescreens.ps1
Normal file
20
scripts/Check_Events_for_Bluescreens.ps1
Normal file
@@ -0,0 +1,20 @@
|
||||
# This will check for Bluescreen events on your system
|
||||
|
||||
$ErrorActionPreference= 'silentlycontinue'
|
||||
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
|
||||
|
||||
if (Get-WinEvent -FilterHashtable @{LogName='application';ID='1001';ProviderName='Windows Error Reporting';Level=4;Data='BlueScreen';StartTime=$TimeSpan})
|
||||
|
||||
{
|
||||
Write-Output "There has been bluescreen events detected on your system"
|
||||
Get-WinEvent -FilterHashtable @{LogName='application';ID='1001';ProviderName='Windows Error Reporting';Level=4;Data='BlueScreen';StartTime=$TimeSpan}
|
||||
exit 1
|
||||
}
|
||||
|
||||
{
|
||||
else
|
||||
Write-Output "No bluescreen events detected"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Exit $LASTEXITCODE
|
||||
1
scripts/Chocolatey_Update_Installed.bat
Normal file
1
scripts/Chocolatey_Update_Installed.bat
Normal file
@@ -0,0 +1 @@
|
||||
cup -y all
|
||||
33
scripts/CreateAllUserLogonScript.ps1
Normal file
33
scripts/CreateAllUserLogonScript.ps1
Normal file
@@ -0,0 +1,33 @@
|
||||
<#
|
||||
Creates a powershell script that runs at logon of any user on the machine in the security context of the user.
|
||||
Useful to set HKCU registry items
|
||||
Log is written to C:\Users\Public\UserLogonLog.txt
|
||||
#>
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "$ENV:WINDIR\TRMM"
|
||||
$logonfile = "$ENV:WINDIR\TRMM\logonscript.ps1"
|
||||
$logfile = "C:\Users\Public\UserLogonLog.txt"
|
||||
|
||||
# === LogonScript ===
|
||||
$logonscript=@'
|
||||
Start-Transcript -Path $logfile
|
||||
|
||||
# Example: Disable Automatically Hide Scrollbars
|
||||
# $registryPath = "HKCU:\Control Panel\Accessibility"
|
||||
# $Name = "DynamicScrollbars"
|
||||
# $value = "0"
|
||||
# New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
|
||||
|
||||
Stop-Transcript
|
||||
'@
|
||||
|
||||
$logonscript | Out-File $logonfile
|
||||
|
||||
# === Create a link in all users startup folder ===
|
||||
|
||||
$Shell = New-Object -ComObject ("WScript.Shell")
|
||||
$ShortCut = $Shell.CreateShortcut($env:PROGRAMDATA + "\Microsoft\Windows\Start Menu\Programs\StartUp\UserLogon.lnk")
|
||||
$ShortCut.TargetPath="%systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$ShortCut.Arguments="-executionpolicy bypass -WindowStyle Hidden -file $logonfile"
|
||||
$ShortCut.WorkingDirectory = "$ENV:WINDIR\TRMM";
|
||||
$ShortCut.Save()
|
||||
@@ -1,5 +1,5 @@
|
||||
#Windows Defender Exclusions for Tactical
|
||||
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
|
||||
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
|
||||
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
|
||||
Add-MpPreference -ExclusionPath 'C:\salt\*'
|
||||
Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*"
|
||||
Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*"
|
||||
Add-MpPreference -ExclusionPath "C:\Windows\Temp\trmm\*"
|
||||
Add-MpPreference -ExclusionPath "C:\Windows\Temp\winagent-v*.exe"
|
||||
|
||||
21
update.sh
21
update.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="113"
|
||||
SCRIPT_VERSION="114"
|
||||
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'
|
||||
@@ -172,6 +172,10 @@ printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
|
||||
sudo systemctl stop ${i}
|
||||
done
|
||||
|
||||
printf >&2 "${GREEN}Restarting postgresql database${NC}\n"
|
||||
sudo systemctl restart postgresql
|
||||
sleep 5
|
||||
|
||||
rm -f /rmm/api/tacticalrmm/app.ini
|
||||
|
||||
numprocs=$(nproc)
|
||||
@@ -242,6 +246,21 @@ if ! [[ $HAS_PY39 ]]; then
|
||||
sudo rm -rf Python-3.9.2 Python-3.9.2.tgz
|
||||
fi
|
||||
|
||||
HAS_NATS220=$(/usr/local/bin/nats-server -version | grep v2.2.0)
|
||||
if ! [[ $HAS_NATS220 ]]; then
|
||||
printf >&2 "${GREEN}Updating nats to v2.2.0${NC}\n"
|
||||
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
|
||||
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
|
||||
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
|
||||
sudo rm -f /usr/local/bin/nats-server
|
||||
sudo mv ${nats_tmp}/nats-server-v2.2.0-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}
|
||||
fi
|
||||
|
||||
sudo npm install -g npm
|
||||
|
||||
cd /rmm
|
||||
git config user.email "admin@example.com"
|
||||
git config user.name "Bob"
|
||||
|
||||
30773
web/package-lock.json
generated
30773
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,25 +10,25 @@
|
||||
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.9.17",
|
||||
"axios": "^0.21.1",
|
||||
"@quasar/extras": "^1.9.19",
|
||||
"apexcharts": "^3.23.1",
|
||||
"axios": "^0.21.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"quasar": "^1.15.4",
|
||||
"quasar": "^1.15.5",
|
||||
"vue-apexcharts": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app": "^2.1.15",
|
||||
"@quasar/app": "^2.2.1",
|
||||
"@quasar/cli": "^1.1.3",
|
||||
"@quasar/quasar-app-extension-testing": "^1.0.3",
|
||||
"@quasar/quasar-app-extension-testing-e2e-cypress": "^3.0.1",
|
||||
"core-js": "^3.8.1",
|
||||
"eslint-plugin-cypress": "^2.11.1",
|
||||
"flush-promises": "^1.0.2",
|
||||
"fs-extra": "^9.0.1",
|
||||
"prismjs": "^1.22.0",
|
||||
"vue-prism-editor": "^1.2.2",
|
||||
"eslint-plugin-cypress": "^2.11.1"
|
||||
"vue-prism-editor": "^1.2.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 4 Chrome versions",
|
||||
|
||||
@@ -11,30 +11,6 @@
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn ref="new" label="New" dense flat push unelevated no-caps icon="add" @click="showAddUserModal" />
|
||||
<q-btn
|
||||
ref="edit"
|
||||
label="Edit"
|
||||
:disable="selected.length === 0"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showEditUserModal(selected[0])"
|
||||
/>
|
||||
<q-btn
|
||||
ref="delete"
|
||||
label="Delete"
|
||||
:disable="selected.length === 0 || selected[0].username === logged_in_user"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteUser(selected[0])"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
@@ -97,7 +73,7 @@
|
||||
v-close-popup
|
||||
@click="deleteUser(props.row)"
|
||||
id="context-delete"
|
||||
v-if="props.row.username !== logged_in_user"
|
||||
:disable="props.row.username === logged_in_user"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
@@ -226,7 +202,7 @@ export default {
|
||||
.onOk(() => {
|
||||
this.$store
|
||||
.dispatch("admin/deleteUser", data.id)
|
||||
.then(response => {
|
||||
.then(() => {
|
||||
this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`));
|
||||
})
|
||||
.catch(e => {
|
||||
|
||||
@@ -12,30 +12,6 @@
|
||||
<div class="q-pa-sm" style="min-height: 65vh; max-height: 65vh">
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn ref="new" label="New" dense flat push unelevated no-caps icon="add" @click="showAddTemplateModal" />
|
||||
<q-btn
|
||||
ref="edit"
|
||||
label="Edit"
|
||||
:disable="!selectedTemplate"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showEditTemplateModal(selectedTemplate)"
|
||||
/>
|
||||
<q-btn
|
||||
ref="delete"
|
||||
label="Delete"
|
||||
:disable="!selectedTemplate"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteTemplate(selectedTemplate)"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
|
||||
@@ -30,28 +30,6 @@
|
||||
</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"
|
||||
@@ -148,7 +126,7 @@
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editScript(props.node)"
|
||||
v-if="props.node.script_type !== 'builtin'"
|
||||
:disable="props.node.script_type === 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
@@ -160,7 +138,7 @@
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteScript(props.node.id)"
|
||||
v-if="props.node.script_type !== 'builtin'"
|
||||
:disable="props.node.script_type === 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
@@ -241,7 +219,7 @@
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editScript(props.row)"
|
||||
v-if="props.row.script_type !== 'builtin'"
|
||||
:disable="props.row.script_type === 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
@@ -253,7 +231,7 @@
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteScript(props.row.id)"
|
||||
v-if="props.row.script_type !== 'builtin'"
|
||||
:disable="props.row.script_type === 'builtin'"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
field: "install_date",
|
||||
sortable: false,
|
||||
format: (val, row) => {
|
||||
return val === "01/01/1" ? "" : val;
|
||||
return val === "01/01/1" || val === "01-1-01" ? "" : val;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<q-tooltip>Missing</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.severity }}</q-td>
|
||||
<q-td>{{ formatSeverity(props.row.severity) }}</q-td>
|
||||
<q-td>{{ formatMessage(props.row.title) }}</q-td>
|
||||
<q-td
|
||||
@click.native="
|
||||
@@ -155,6 +155,9 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatSeverity(severity) {
|
||||
return !severity ? "Other" : severity;
|
||||
},
|
||||
editPolicy(pk, policy) {
|
||||
const data = { pk: pk, policy: policy };
|
||||
axios.patch(`/winupdate/editpolicy/`, data).then(r => {
|
||||
|
||||
@@ -12,28 +12,6 @@
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddPolicyForm" />
|
||||
<q-btn
|
||||
label="Edit"
|
||||
:disable="!selectedPolicy"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showEditPolicyForm(selectedPolicy)"
|
||||
/>
|
||||
<q-btn
|
||||
label="Delete"
|
||||
:disable="!selectedPolicy"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deletePolicy(selectedPolicy)"
|
||||
/>
|
||||
<q-btn
|
||||
label="Policy Overview"
|
||||
dense
|
||||
@@ -127,6 +105,13 @@
|
||||
<q-item-section>Policy Exclusions</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="syncPolicies(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="sync" />
|
||||
</q-item-section>
|
||||
<q-item-section>Sync Policies</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="showPatchPolicyForm(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="system_update" />
|
||||
@@ -450,6 +435,24 @@ export default {
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
syncPolicies(policy) {
|
||||
this.$q.loading.show();
|
||||
|
||||
const data = {
|
||||
policy: policy.id,
|
||||
};
|
||||
|
||||
this.$axios
|
||||
.post(`/automation/sync/`, data)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess("Sync request sent successfully. The task will be run on all affected agents");
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError("An Error occured while sending policy sync request");
|
||||
});
|
||||
},
|
||||
toggleCheckbox(policy, type) {
|
||||
this.$q.loading.show();
|
||||
let text = "";
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
class="col-2"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 60 || 'Minimum is 60 seconds',
|
||||
val => val <= 3600 || 'Maximum is 3600 seconds',
|
||||
val => val >= 15 || 'Minimum is 15 seconds',
|
||||
val => val <= 86400 || 'Maximum is 86400 seconds',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="cpuloadcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -63,6 +73,7 @@ export default {
|
||||
check_type: "cpuload",
|
||||
warning_threshold: 70,
|
||||
error_threshold: 90,
|
||||
run_interval: 0,
|
||||
fails_b4_alert: 1,
|
||||
},
|
||||
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
|
||||
@@ -45,6 +45,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="diskcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -75,6 +85,7 @@ export default {
|
||||
warning_threshold: 25,
|
||||
error_threshold: 10,
|
||||
fails_b4_alert: 1,
|
||||
run_interval: 0,
|
||||
},
|
||||
diskOptions: [],
|
||||
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
|
||||
@@ -95,6 +95,15 @@
|
||||
label="Alert Severity"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
label="Number of events found before alert"
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="eventlogcheck.number_of_events_b4_alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
outlined
|
||||
@@ -105,6 +114,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
type="number"
|
||||
v-model.number="eventlogcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -137,8 +156,10 @@ export default {
|
||||
fail_when: "contains",
|
||||
search_last_days: 1,
|
||||
fails_b4_alert: 1,
|
||||
number_of_events_b4_alert: 1,
|
||||
event_id_is_wildcard: false,
|
||||
alert_severity: "warning",
|
||||
run_interval: 0,
|
||||
},
|
||||
eventMessage: false,
|
||||
eventSource: false,
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
type="number"
|
||||
v-model.number="memcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -64,6 +74,7 @@ export default {
|
||||
warning_threshold: 70,
|
||||
error_threshold: 85,
|
||||
fails_b4_alert: 1,
|
||||
run_interval: 0,
|
||||
},
|
||||
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
};
|
||||
|
||||
@@ -38,6 +38,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
type="number"
|
||||
v-model.number="pingcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -67,6 +77,7 @@ export default {
|
||||
ip: null,
|
||||
alert_severity: "warning",
|
||||
fails_b4_alert: 1,
|
||||
run_interval: 0,
|
||||
},
|
||||
severityOptions: [
|
||||
{ label: "Informational", value: "info" },
|
||||
|
||||
@@ -90,6 +90,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
type="number"
|
||||
v-model.number="scriptcheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -120,6 +130,7 @@ export default {
|
||||
fails_b4_alert: 1,
|
||||
info_return_codes: [],
|
||||
warning_return_codes: [],
|
||||
run_interval: 0,
|
||||
},
|
||||
scriptOptions: [],
|
||||
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
|
||||
@@ -112,6 +112,16 @@
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="winsvccheck.run_interval"
|
||||
label="Check run interval (s)"
|
||||
hint="Setting this will override the check run interval on the agent"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
|
||||
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
|
||||
@@ -146,6 +156,7 @@ export default {
|
||||
restart_if_stopped: false,
|
||||
fails_b4_alert: 1,
|
||||
alert_severity: "warning",
|
||||
run_interval: 0,
|
||||
},
|
||||
severityOptions: [
|
||||
{ label: "Informational", value: "info" },
|
||||
|
||||
@@ -46,6 +46,20 @@
|
||||
class="col-4"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Client Sort:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-select
|
||||
map-options
|
||||
emit-value
|
||||
outlined
|
||||
dense
|
||||
options-dense
|
||||
v-model="clientTreeSort"
|
||||
:options="clientTreeSortOptions"
|
||||
class="col-8"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
@@ -68,8 +82,19 @@ export default {
|
||||
return {
|
||||
agentDblClickAction: "",
|
||||
defaultAgentTblTab: "",
|
||||
clientTreeSort: "",
|
||||
tab: "ui",
|
||||
splitterModel: 20,
|
||||
clientTreeSortOptions: [
|
||||
{
|
||||
label: "Sort alphabetically, moving failing clients to the top",
|
||||
value: "alphafail",
|
||||
},
|
||||
{
|
||||
label: "Sort alphabetically only",
|
||||
value: "alpha",
|
||||
},
|
||||
],
|
||||
agentDblClickOptions: [
|
||||
{
|
||||
label: "Edit Agent",
|
||||
@@ -105,17 +130,19 @@ export default {
|
||||
this.$axios.get("/core/dashinfo/").then(r => {
|
||||
this.agentDblClickAction = r.data.dbl_click_action;
|
||||
this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
|
||||
this.clientTreeSort = r.data.client_tree_sort;
|
||||
});
|
||||
},
|
||||
editUserPrefs() {
|
||||
const data = {
|
||||
userui: true,
|
||||
agent_dblclick_action: this.agentDblClickAction,
|
||||
default_agent_tbl_tab: this.defaultAgentTblTab,
|
||||
client_tree_sort: this.clientTreeSort,
|
||||
};
|
||||
this.$axios.patch("/accounts/users/ui/", data).then(r => {
|
||||
this.notifySuccess("Preferences were saved!");
|
||||
this.$emit("edited");
|
||||
this.$store.dispatch("loadTree");
|
||||
this.$emit("close");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -254,7 +254,7 @@ export default {
|
||||
}
|
||||
|
||||
.prism-editor__container {
|
||||
height: 60000em;
|
||||
height: 30000em;
|
||||
}
|
||||
|
||||
/* optional class for removing the outline */
|
||||
@@ -264,7 +264,7 @@ export default {
|
||||
|
||||
.prism-editor__textarea,
|
||||
.prism-editor__container {
|
||||
width: 10000em !important;
|
||||
width: 1000em !important;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function () {
|
||||
showCommunityScripts: false,
|
||||
agentDblClickAction: "",
|
||||
defaultAgentTblTab: "server",
|
||||
clientTreeSort: "alphafail",
|
||||
},
|
||||
getters: {
|
||||
loggedIn(state) {
|
||||
@@ -139,6 +140,9 @@ export default function () {
|
||||
},
|
||||
SET_DEFAULT_AGENT_TBL_TAB(state, tab) {
|
||||
state.defaultAgentTblTab = tab
|
||||
},
|
||||
SET_CLIENT_TREE_SORT(state, val) {
|
||||
state.clientTreeSort = val
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -215,7 +219,7 @@ export default function () {
|
||||
loadSites(context) {
|
||||
return axios.get("/clients/sites/");
|
||||
},
|
||||
loadTree({ commit }) {
|
||||
loadTree({ commit, state }) {
|
||||
axios.get("/clients/tree/").then(r => {
|
||||
|
||||
if (r.data.length === 0) {
|
||||
@@ -263,9 +267,15 @@ export default function () {
|
||||
output.push(clientNode);
|
||||
}
|
||||
|
||||
// move failing clients to the top
|
||||
const sortedByFailing = output.sort(a => a.color === "negative" ? -1 : 1)
|
||||
commit("loadTree", sortedByFailing);
|
||||
|
||||
if (state.clientTreeSort === "alphafail") {
|
||||
// move failing clients to the top
|
||||
const sortedByFailing = output.sort(a => a.color === "negative" ? -1 : 1);
|
||||
commit("loadTree", sortedByFailing);
|
||||
} else {
|
||||
commit("loadTree", output);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
checkVer(context) {
|
||||
|
||||
@@ -504,7 +504,7 @@ export default {
|
||||
name: "agentstatus",
|
||||
field: "status",
|
||||
align: "left",
|
||||
sortable: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "last_seen",
|
||||
@@ -712,7 +712,10 @@ export default {
|
||||
},
|
||||
getDashInfo(edited = true) {
|
||||
this.$store.dispatch("getDashInfo").then(r => {
|
||||
if (edited) this.$store.commit("SET_DEFAULT_AGENT_TBL_TAB", r.data.default_agent_tbl_tab);
|
||||
if (edited) {
|
||||
this.$store.commit("SET_DEFAULT_AGENT_TBL_TAB", r.data.default_agent_tbl_tab);
|
||||
this.$store.commit("SET_CLIENT_TREE_SORT", r.data.client_tree_sort);
|
||||
}
|
||||
this.darkMode = r.data.dark_mode;
|
||||
this.$q.dark.set(this.darkMode);
|
||||
this.currentTRMMVersion = r.data.trmm_version;
|
||||
|
||||
Reference in New Issue
Block a user