Compare commits

..

51 Commits

Author SHA1 Message Date
wh1te909
4aec4257da Release 0.1.4 2020-11-11 05:05:37 +00:00
wh1te909
d654f856d1 version 0.1.4 2020-11-11 05:05:10 +00:00
wh1te909
8d3b0a2069 optimize query 2020-11-11 02:47:09 +00:00
wh1te909
54a96f35e8 fix slow query 2020-11-10 20:48:51 +00:00
wh1te909
2dc56d72f6 show agent info in take control title #163 2020-11-10 10:11:17 +00:00
wh1te909
4b6ddb535a fix filter 2020-11-10 09:51:37 +00:00
wh1te909
697e2250d4 add back sort 2020-11-10 09:50:16 +00:00
wh1te909
6a75035b04 fix last run column 2020-11-10 08:06:40 +00:00
wh1te909
46b166bc41 fix resetpatchpolicy 2020-11-10 08:05:38 +00:00
wh1te909
6bbc0987ad show correct timezone for checks/tasks last run column #166 2020-11-10 03:12:58 +00:00
wh1te909
8c480b43e2 allow sorting of checks status and more #169 2020-11-10 02:38:35 +00:00
Tragic Bronson
079f6731dd Merge pull request #173 from sadnub/develop
more agent table filter options
2020-11-09 16:03:00 -08:00
sadnub
f99d5754cd add buttons to filter popup to apply and clear filter 2020-11-09 16:54:22 -05:00
sadnub
bf8c41e362 bump app ver 2020-11-09 12:21:30 -05:00
sadnub
7f7bc06eb4 add advanced filter functions to agent table 2020-11-09 12:20:42 -05:00
wh1te909
b507e59359 fix bulk actions 2020-11-09 11:19:30 +00:00
wh1te909
72078ac6bf fix log modal 2020-11-09 10:29:06 +00:00
wh1te909
0db9e082e2 fix audit manager 2020-11-09 10:21:55 +00:00
wh1te909
0c44394a76 fix edit agent 2020-11-09 10:10:40 +00:00
wh1te909
e20aa0cf04 fix deployments 2020-11-09 06:34:11 +00:00
wh1te909
fa30a50a91 fix pending actions 2020-11-09 06:06:15 +00:00
wh1te909
f6629ff12c update reqs 2020-11-08 22:51:50 +00:00
wh1te909
4128e4db73 client tests 2020-11-08 11:13:15 +00:00
wh1te909
34cac5685f client/site modal fixes 2020-11-08 10:11:45 +00:00
Tragic Bronson
4c9b91d536 Merge pull request #171 from sadnub/rework-agent
Rework agent
2020-11-07 14:07:52 -08:00
sadnub
95b95a8998 fix relations view 2020-11-06 17:40:47 -05:00
wh1te909
617738bb28 Release 0.1.3 2020-11-06 21:06:04 +00:00
wh1te909
f6ac15d790 fix auto update for older agents 2020-11-06 21:04:44 +00:00
sadnub
79e1324ead small optimization 2020-11-06 16:03:29 -05:00
sadnub
4ef9f010f0 fix client tree to pull the correct agents in table 2020-11-06 15:55:51 -05:00
sadnub
e6e8865708 fix audit manager 2020-11-06 15:15:18 -05:00
sadnub
33cd8f9b0d fix a few property name issues 2020-11-06 15:06:20 -05:00
sadnub
a7138e019c bump app version 2020-11-06 13:58:42 -05:00
sadnub
049b72bd50 fix install agent modal 2020-11-06 13:53:39 -05:00
sadnub
f3f1987515 fix pending actions 2020-11-06 13:53:39 -05:00
sadnub
a9395d89cd fix bulk actions modal 2020-11-06 13:53:39 -05:00
sadnub
bc2fcee8ba finish fixing tests 2020-11-06 13:53:39 -05:00
sadnub
242ff2ceca fix agent tests 2020-11-06 13:53:39 -05:00
sadnub
70790ac762 fix client and automation tests 2020-11-06 13:52:55 -05:00
sadnub
0f98869b61 Rework clients app and rename client and site property to name 2020-11-06 13:51:18 -05:00
sadnub
9ddc02140f fix up automation app 2020-11-06 13:51:18 -05:00
sadnub
ee631b3d20 add reverse migration function 2020-11-06 13:51:18 -05:00
sadnub
32f56e60d8 most rework finished 2020-11-06 13:51:18 -05:00
sadnub
6102b51d9e create migrations to link to correct site 2020-11-06 13:51:18 -05:00
sadnub
2baee27859 agent rework start 2020-11-06 13:51:18 -05:00
Tragic Bronson
144a3dedbb Merge pull request #165 from sadnub/develop
fix strange test issue
2020-11-02 09:30:57 -08:00
sadnub
f90d966f1a fix strange test issue 2020-11-02 10:18:07 -05:00
wh1te909
b188e2ea97 more agent tasks tests 2020-11-02 10:20:22 +00:00
wh1te909
b63b2002a9 Release 0.1.2 2020-11-02 06:54:28 +00:00
wh1te909
059edc36e4 version 0.1.2 2020-11-02 06:53:21 +00:00
wh1te909
902034ecf0 fix agent update for 32bit agents 2020-11-02 06:46:35 +00:00
73 changed files with 2277 additions and 2330 deletions

1
.gitignore vendored
View File

@@ -34,6 +34,7 @@ app.ini
create_services.py
gen_random.py
sync_salt_modules.py
change_times.py
rmm-*.exe
rmm-*.ps1
api/tacticalrmm/accounts/management/commands/*.json

View File

@@ -1,14 +1,34 @@
from .models import Agent
import random
import string
import os
import json
from model_bakery.recipe import Recipe, seq
from itertools import cycle
from django.utils import timezone as djangotime
from django.conf import settings
from .models import Agent
def generate_agent_id(hostname):
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
return f"{rand}-{hostname}"
def get_wmi_data():
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
) as f:
return json.load(f)
agent = Recipe(
Agent,
client="Default",
site="Default",
hostname=seq("TestHostname"),
hostname="DESKTOP-TEST123",
monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
)
server_agent = agent.extend(
@@ -49,3 +69,5 @@ agent_with_services = agent.extend(
},
],
)
agent_with_wmi = agent.extend(wmi=get_wmi_data())

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.2 on 2020-11-01 22:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('clients', '0006_deployment'),
('agents', '0020_auto_20201025_2129'),
]
operations = [
migrations.AddField(
model_name='agent',
name='site_link',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='clients.site'),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.2 on 2020-11-01 22:54
from django.db import migrations
def link_sites_to_agents(apps, schema_editor):
Agent = apps.get_model("agents", "Agent")
Site = apps.get_model("clients", "Site")
for agent in Agent.objects.all():
site = Site.objects.get(client__client=agent.client, site=agent.site)
agent.site_link = site
agent.save()
def reverse(apps, schema_editor):
Agent = apps.get_model("agents", "Agent")
for agent in Agent.objects.all():
agent.site = agent.site_link.site
agent.client = agent.site_link.client.client
agent.save()
class Migration(migrations.Migration):
dependencies = [
("agents", "0021_agent_site_link"),
]
operations = [
migrations.RunPython(link_sites_to_agents, reverse),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.2 on 2020-11-01 23:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0022_update_site_primary_key'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='client',
),
migrations.RemoveField(
model_name='agent',
name='site',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.2 on 2020-11-01 23:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0023_auto_20201101_2312'),
]
operations = [
migrations.RenameField(
model_name='agent',
old_name='site_link',
new_name='site',
),
]

View File

@@ -44,9 +44,7 @@ class Agent(BaseAuditModel):
boot_time = models.FloatField(null=True, blank=True)
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
client = models.CharField(max_length=200)
antivirus = models.CharField(default="n/a", max_length=255) # deprecated
site = models.CharField(max_length=150)
monitoring_type = models.CharField(max_length=30)
description = models.CharField(null=True, blank=True, max_length=255)
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
@@ -62,6 +60,13 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
site = models.ForeignKey(
"clients.Site",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
policy = models.ForeignKey(
"automation.Policy",
related_name="agents",
@@ -73,6 +78,10 @@ class Agent(BaseAuditModel):
def __str__(self):
return self.hostname
@property
def client(self):
return self.site.client
@property
def timezone(self):
# return the default timezone unless the timezone is explicity set per agent
@@ -86,27 +95,35 @@ class Agent(BaseAuditModel):
@property
def arch(self):
if self.operating_system is not None:
if "64bit" in self.operating_system:
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
return "64"
elif "32bit" in self.operating_system:
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
return "32"
return "64"
return None
@property
def winagent_dl(self):
return settings.DL_64 if self.arch == "64" else settings.DL_32
if self.arch == "64":
return settings.DL_64
elif self.arch == "32":
return settings.DL_32
return None
@property
def winsalt_dl(self):
return settings.SALT_64 if self.arch == "64" else settings.SALT_32
if self.arch == "64":
return settings.SALT_64
elif self.arch == "32":
return settings.SALT_32
return None
@property
def win_inno_exe(self):
return (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
if self.arch == "64"
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
)
if self.arch == "64":
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
elif self.arch == "32":
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
return None
@property
def status(self):
@@ -273,11 +290,9 @@ class Agent(BaseAuditModel):
# returns agent policy merged with a client or site specific policy
def get_patch_policy(self):
from clients.models import Client, Site
# check if site has a patch policy and if so use it
client = Client.objects.get(client=self.client)
site = Site.objects.get(client=client, site=self.site)
site = self.site
core_settings = CoreSettings.objects.first()
patch_policy = None
agent_policy = self.winupdatepolicy.get()
@@ -659,10 +674,10 @@ class AgentOutage(models.Model):
CORE = CoreSettings.objects.first()
CORE.send_mail(
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data overdue",
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue",
(
f"Data has not been received from client {self.agent.client}, "
f"site {self.agent.site}, "
f"Data has not been received from client {self.agent.client.name}, "
f"site {self.agent.site.name}, "
f"agent {self.agent.hostname} "
"within the expected time."
),
@@ -673,10 +688,10 @@ class AgentOutage(models.Model):
CORE = CoreSettings.objects.first()
CORE.send_mail(
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data received",
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received",
(
f"Data has been received from client {self.agent.client}, "
f"site {self.agent.site}, "
f"Data has been received from client {self.agent.client.name}, "
f"site {self.agent.site.name}, "
f"agent {self.agent.hostname} "
"after an interruption in data transmission."
),
@@ -687,7 +702,7 @@ class AgentOutage(models.Model):
CORE = CoreSettings.objects.first()
CORE.send_sms(
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data overdue"
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue"
)
def send_recovery_sms(self):
@@ -695,7 +710,7 @@ class AgentOutage(models.Model):
CORE = CoreSettings.objects.first()
CORE.send_sms(
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data received"
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received"
)
def __str__(self):

View File

@@ -1,10 +1,12 @@
import pytz
from rest_framework import serializers
from rest_framework.fields import ReadOnlyField
from .models import Agent, Note
from winupdate.serializers import WinUpdatePolicySerializer
from clients.serializers import ClientSerializer
class AgentSerializer(serializers.ModelSerializer):
@@ -19,6 +21,8 @@ class AgentSerializer(serializers.ModelSerializer):
checks = serializers.ReadOnlyField()
timezone = serializers.ReadOnlyField()
all_timezones = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name")
def get_all_timezones(self, obj):
return pytz.all_timezones
@@ -35,6 +39,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
status = serializers.ReadOnlyField()
checks = serializers.ReadOnlyField()
last_seen = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name")
def get_last_seen(self, obj):
if obj.time_zone is not None:
@@ -50,8 +56,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
"id",
"hostname",
"agent_id",
"client",
"site",
"site_name",
"client_name",
"monitoring_type",
"description",
"needs_reboot",
@@ -66,11 +72,13 @@ class AgentTableSerializer(serializers.ModelSerializer):
"last_logged_in_user",
"maintenance_mode",
]
depth = 2
class AgentEditSerializer(serializers.ModelSerializer):
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
all_timezones = serializers.SerializerMethodField()
client = ClientSerializer(read_only=True)
def get_all_timezones(self, obj):
return pytz.all_timezones
@@ -107,6 +115,9 @@ class WinAgentSerializer(serializers.ModelSerializer):
class AgentHostnameSerializer(serializers.ModelSerializer):
client = serializers.ReadOnlyField(source="client.name")
site = serializers.ReadOnlyField(source="site.name")
class Meta:
model = Agent
fields = (

View File

@@ -30,6 +30,14 @@ def send_agent_update_task(pks, version):
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
@@ -42,6 +50,9 @@ def send_agent_update_task(pks, version):
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -49,6 +60,7 @@ def send_agent_update_task(pks, version):
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)
@@ -56,6 +68,7 @@ def send_agent_update_task(pks, version):
def auto_self_agent_update_task():
core = CoreSettings.objects.first()
if not core.agent_auto_update:
logger.info("Agent auto update is disabled. Skipping.")
return
q = Agent.objects.only("pk", "version")
@@ -64,12 +77,21 @@ def auto_self_agent_update_task():
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
logger.info(f"Updating {len(agents)}")
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
@@ -82,6 +104,9 @@ def auto_self_agent_update_task():
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -89,6 +114,7 @@ def auto_self_agent_update_task():
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)

View File

@@ -6,19 +6,42 @@ from model_bakery import baker
from itertools import cycle
from django.conf import settings
from django.utils import timezone as djangotime
from rest_framework.authtoken.models import Token
from tacticalrmm.test import BaseTestCase, TacticalTestCase
from accounts.models import User
from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .tasks import (
auto_self_agent_update_task,
update_salt_minion_task,
get_wmi_detail_task,
sync_salt_modules_task,
batch_sync_modules_task,
batch_sysinfo_task,
OLD_64_PY_AGENT,
OLD_32_PY_AGENT,
)
from winupdate.models import WinUpdatePolicy
class TestAgentViews(BaseTestCase):
class TestAgentViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
client = baker.make("clients.Client", name="Google")
site = baker.make("clients.Site", client=client, name="LA Office")
self.agent = baker.make_recipe("agents.online_agent", site=site)
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
def test_get_patch_policy(self):
# make sure get_patch_policy doesn't error out when agent has policy with
# an empty patch policy
self.agent.policy = self.policy
policy = baker.make("automation.Policy")
self.agent.policy = policy
self.agent.save(update_fields=["policy"])
_ = self.agent.get_patch_policy()
@@ -29,8 +52,8 @@ class TestAgentViews(BaseTestCase):
self.agent.policy = None
self.agent.save(update_fields=["policy"])
self.coresettings.server_policy = self.policy
self.coresettings.workstation_policy = self.policy
self.coresettings.server_policy = policy
self.coresettings.workstation_policy = policy
self.coresettings.save(update_fields=["server_policy", "workstation_policy"])
_ = self.agent.get_patch_policy()
@@ -102,10 +125,16 @@ class TestAgentViews(BaseTestCase):
@patch("agents.tasks.uninstall_agent_task.delay")
def test_uninstall_catch_no_user(self, mock_task):
# setup data
agent_user = User.objects.create_user(
username=self.agent.agent_id, password=User.objects.make_random_password(60)
)
agent_token = Token.objects.create(user=agent_user)
url = "/agents/uninstall/"
data = {"pk": self.agent.pk}
self.agent_user.delete()
agent_user.delete()
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 200)
@@ -277,9 +306,10 @@ class TestAgentViews(BaseTestCase):
def test_install_agent(self, mock_subprocess, mock_file_exists):
url = f"/agents/installagent/"
site = baker.make("clients.Site")
data = {
"client": "Google",
"site": "LA Office",
"client": site.client.id,
"site": site.id,
"arch": "64",
"expires": 23,
"installMethod": "exe",
@@ -381,12 +411,14 @@ class TestAgentViews(BaseTestCase):
self.check_not_authenticated("get", url)
def test_edit_agent(self):
# setup data
site = baker.make("clients.Site", name="Ny Office")
url = "/agents/editagent/"
edit = {
"id": self.agent.pk,
"client": "Facebook",
"site": "NY Office",
"site": site.id,
"monitoring_type": "workstation",
"description": "asjdk234andasd",
"overdue_time": 300,
@@ -416,7 +448,7 @@ class TestAgentViews(BaseTestCase):
agent = Agent.objects.get(pk=self.agent.pk)
data = AgentSerializer(agent).data
self.assertEqual(data["site"], "NY Office")
self.assertEqual(data["site"], site.id)
policy = WinUpdatePolicy.objects.get(agent=self.agent)
data = WinUpdatePolicySerializer(policy).data
@@ -440,6 +472,8 @@ class TestAgentViews(BaseTestCase):
self.assertIn("mstsc.html?login=", r.data["webrdp"])
self.assertEqual(self.agent.hostname, r.data["hostname"])
self.assertEqual(self.agent.client.name, r.data["client"])
self.assertEqual(self.agent.site.name, r.data["site"])
self.assertEqual(r.status_code, 200)
@@ -450,28 +484,28 @@ class TestAgentViews(BaseTestCase):
self.check_not_authenticated("get", url)
def test_by_client(self):
url = "/agents/byclient/Google/"
url = f"/agents/byclient/{self.agent.client.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/byclient/Majh3 Akj34 ad/"
url = f"/agents/byclient/500/"
r = self.client.get(url)
self.assertFalse(r.data) # returns empty list
self.check_not_authenticated("get", url)
def test_by_site(self):
url = f"/agents/bysite/Google/Main Office/"
url = f"/agents/bysite/{self.agent.site.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/bysite/Google/Ajdaksd Office/"
url = f"/agents/bysite/500/"
r = self.client.get(url)
self.assertFalse(r.data)
self.assertEqual(r.data, [])
self.check_not_authenticated("get", url)
@@ -574,7 +608,7 @@ class TestAgentViews(BaseTestCase):
payload = {
"mode": "command",
"target": "client",
"client": "Google",
"client": self.agent.client.id,
"site": None,
"agentPKs": [
self.agent.pk,
@@ -590,8 +624,8 @@ class TestAgentViews(BaseTestCase):
payload = {
"mode": "command",
"target": "client",
"client": "Google",
"site": "Main Office",
"client": self.agent.client.id,
"site": self.agent.site.id,
"agentPKs": [
self.agent.pk,
],
@@ -603,25 +637,9 @@ class TestAgentViews(BaseTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
payload = {
"mode": "command",
"target": "site",
"client": "A ASJDHkjASHDASD",
"site": "asdasdasdasda",
"agentPKs": [
self.agent.pk,
],
"cmd": "gpupdate /force",
"timeout": 300,
"shell": "cmd",
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 404)
mock_ret.return_value = "timeout"
payload["client"] = "Google"
payload["site"] = "Main Office"
payload["client"] = self.agent.client.id
payload["site"] = self.agent.site.id
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
@@ -642,7 +660,7 @@ class TestAgentViews(BaseTestCase):
payload = {
"mode": "install",
"target": "client",
"client": "Google",
"client": self.agent.client.id,
"site": None,
"agentPKs": [
self.agent.pk,
@@ -746,13 +764,13 @@ class TestAgentViewsNew(TacticalTestCase):
def test_agent_maintenance_mode(self):
url = "/agents/maintenance/"
# create data
client = baker.make("clients.Client", client="Default")
site = baker.make("clients.Site", client=client, site="Site")
agent = baker.make_recipe("agents.agent", client=client.client, site=site.site)
# setup data
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site)
# Test client toggle maintenance mode
data = {"type": "Client", "id": client.id, "action": True}
data = {"type": "Client", "id": site.client.id, "action": True}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
@@ -779,3 +797,224 @@ class TestAgentViewsNew(TacticalTestCase):
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)
class TestAgentTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.salt_api_async", return_value=None)
def test_get_wmi_detail_task(self, salt_api_async):
self.agent = baker.make_recipe("agents.agent")
ret = get_wmi_detail_task.s(self.agent.pk).apply()
salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info")
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_salt_modules_task(self, salt_api_cmd):
self.agent = baker.make_recipe("agents.agent")
salt_api_cmd.return_value = {"return": [{f"{self.agent.salt_id}": []}]}
ret = sync_salt_modules_task.s(self.agent.pk).apply()
salt_api_cmd.assert_called_with(timeout=35, func="saltutil.sync_modules")
self.assertEqual(
ret.result, f"Successfully synced salt modules on {self.agent.hostname}"
)
self.assertEqual(ret.status, "SUCCESS")
salt_api_cmd.return_value = "timeout"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
salt_api_cmd.return_value = "error"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async):
# chunks of 50, 60 online should run only 2 times
baker.make_recipe(
"agents.online_agent", last_seen=djangotime.now(), _quantity=60
)
baker.make_recipe(
"agents.overdue_agent",
last_seen=djangotime.now() - djangotime.timedelta(minutes=9),
_quantity=115,
)
ret = batch_sync_modules_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 2)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async):
# chunks of 30, 70 online should run only 3 times
self.online = baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=70
)
self.overdue = baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=115
)
ret = batch_sysinfo_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 3)
self.assertEqual(ret.status, "SUCCESS")
salt_batch_async.reset_mock()
[i.delete() for i in self.online]
[i.delete() for i in self.overdue]
# test old agents, should not run
self.online_old = baker.make_recipe(
"agents.online_agent", version="0.10.2", _quantity=70
)
self.overdue_old = baker.make_recipe(
"agents.overdue_agent", version="0.10.2", _quantity=115
)
ret = batch_sysinfo_task.s().apply()
salt_batch_async.assert_not_called()
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_update_salt_minion_task(self, mock_sleep, salt_api_async):
# test agents that need salt update
self.agents = baker.make_recipe(
"agents.agent",
version=settings.LATEST_AGENT_VER,
salt_ver="1.0.3",
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(salt_api_async.call_count, 53)
self.assertEqual(ret.status, "SUCCESS")
[i.delete() for i in self.agents]
salt_api_async.reset_mock()
# test agents that need salt update but agent version too low
self.agents = baker.make_recipe(
"agents.agent",
version="0.10.2",
salt_ver="1.0.3",
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(ret.status, "SUCCESS")
salt_api_async.assert_not_called()
[i.delete() for i in self.agents]
salt_api_async.reset_mock()
# test agents already on latest salt ver
self.agents = baker.make_recipe(
"agents.agent",
version=settings.LATEST_AGENT_VER,
salt_ver=settings.LATEST_SALT_VER,
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(ret.status, "SUCCESS")
salt_api_async.assert_not_called()
@patch("agents.models.Agent.salt_api_async")
@patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
# test 64bit golang agent
self.agent64 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
"url": settings.DL_64,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64.delete()
salt_api_async.reset_mock()
# test 32bit golang agent
self.agent32 = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="1.0.0",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
"url": settings.DL_32,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent32.delete()
salt_api_async.reset_mock()
# test agent that has a null os field
self.agentNone = baker.make_recipe(
"agents.agent",
operating_system=None,
version="1.0.0",
)
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
self.agentNone.delete()
salt_api_async.reset_mock()
# test auto update disabled in global settings
self.agent64 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0",
)
self.coresettings.agent_auto_update = False
self.coresettings.save(update_fields=["agent_auto_update"])
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
# reset core settings
self.agent64.delete()
salt_api_async.reset_mock()
self.coresettings.agent_auto_update = True
self.coresettings.save(update_fields=["agent_auto_update"])
# test 64bit python agent
self.agent64py = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2.exe",
"url": OLD_64_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64py.delete()
salt_api_async.reset_mock()
# test 32bit python agent
self.agent32py = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2-x86.exe",
"url": OLD_32_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS")

View File

@@ -5,8 +5,8 @@ urlpatterns = [
path("listagents/", views.AgentsTableList.as_view()),
path("listagentsnodetail/", views.list_agents_no_detail),
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
path("byclient/<client>/", views.by_client),
path("bysite/<client>/<site>/", views.by_site),
path("byclient/<int:clientpk>/", views.by_client),
path("bysite/<int:sitepk>/", views.by_site),
path("overdueaction/", views.overdue_action),
path("sendrawcmd/", views.send_raw_cmd),
path("<pk>/agentdetail/", views.agent_detail),

View File

@@ -103,7 +103,7 @@ def edit_agent(request):
a_serializer.is_valid(raise_exception=True)
a_serializer.save()
policy = WinUpdatePolicy.objects.get(agent=agent)
policy = agent.winupdatepolicy.get()
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
@@ -145,6 +145,8 @@ def meshcentral(request, pk):
"file": file,
"webrdp": webrdp,
"status": agent.status,
"client": agent.client.name,
"site": agent.site.name,
}
return Response(ret)
@@ -249,24 +251,27 @@ def send_raw_cmd(request):
class AgentsTableList(generics.ListAPIView):
queryset = Agent.objects.prefetch_related("agentchecks").only(
"pk",
"hostname",
"agent_id",
"client",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
queryset = (
Agent.objects.select_related("site")
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
)
)
serializer_class = AgentTableSerializer
@@ -281,7 +286,7 @@ class AgentsTableList(generics.ListAPIView):
@api_view()
def list_agents_no_detail(request):
agents = Agent.objects.all()
agents = Agent.objects.select_related("site").only("pk", "hostname", "site")
return Response(AgentHostnameSerializer(agents, many=True).data)
@@ -292,15 +297,15 @@ def agent_edit_details(request, pk):
@api_view()
def by_client(request, client):
def by_client(request, clientpk):
agents = (
Agent.objects.filter(client=client)
Agent.objects.select_related("site")
.filter(site__client_id=clientpk)
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"client",
"site",
"monitoring_type",
"description",
@@ -321,15 +326,15 @@ def by_client(request, client):
@api_view()
def by_site(request, client, site):
def by_site(request, sitepk):
agents = (
Agent.objects.filter(client=client, site=site)
Agent.objects.filter(site_id=sitepk)
.select_related("site")
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"client",
"site",
"monitoring_type",
"description",
@@ -398,8 +403,8 @@ def reboot_later(request):
def install_agent(request):
from knox.models import AuthToken
client = get_object_or_404(Client, client=request.data["client"])
site = get_object_or_404(Site, client=client, site=request.data["site"])
client_id = request.data["client"]
site_id = request.data["site"]
version = settings.LATEST_AGENT_VER
arch = request.data["arch"]
@@ -454,8 +459,8 @@ def install_agent(request):
"build",
f"-ldflags=\"-X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client.pk}'",
f"-X 'main.Site={site.pk}'",
f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'",
f"-X 'main.Atype={atype}'",
f"-X 'main.Rdp={rdp}'",
f"-X 'main.Ping={ping}'",
@@ -563,9 +568,9 @@ def install_agent(request):
"--api",
request.data["api"],
"--client-id",
client.pk,
client_id,
"--site-id",
site.pk,
site_id,
"--agent-type",
request.data["agenttype"],
"--auth",
@@ -597,8 +602,8 @@ def install_agent(request):
replace_dict = {
"innosetupchange": inno,
"clientchange": str(client.pk),
"sitechange": str(site.pk),
"clientchange": str(client_id),
"sitechange": str(site_id),
"apichange": request.data["api"],
"atypechange": request.data["agenttype"],
"powerchange": str(request.data["power"]),
@@ -807,14 +812,9 @@ def bulk(request):
return notify_error("Must select at least 1 agent")
if request.data["target"] == "client":
client = get_object_or_404(Client, client=request.data["client"])
agents = Agent.objects.filter(client=client.client)
agents = Agent.objects.filter(site__client_id=request.data["client"])
elif request.data["target"] == "site":
client = get_object_or_404(Client, client=request.data["client"])
site = (
Site.objects.filter(client=client).filter(site=request.data["site"]).get()
)
agents = Agent.objects.filter(client=client.client).filter(site=site.site)
agents = Agent.objects.filter(site_id=request.data["site"])
elif request.data["target"] == "agents":
agents = Agent.objects.filter(pk__in=request.data["agentPKs"])
elif request.data["target"] == "all":
@@ -904,14 +904,12 @@ def agent_counts(request):
@api_view(["POST"])
def agent_maintenance(request):
if request.data["type"] == "Client":
client = Client.objects.get(pk=request.data["id"])
Agent.objects.filter(client=client.client).update(
Agent.objects.filter(site__client_id=request.data["id"]).update(
maintenance_mode=request.data["action"]
)
elif request.data["type"] == "Site":
site = Site.objects.get(pk=request.data["id"])
Agent.objects.filter(client=site.client.client, site=site.site).update(
Agent.objects.filter(site_id=request.data["id"]).update(
maintenance_mode=request.data["action"]
)

View File

@@ -1,17 +1,20 @@
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
class TestAPIv2(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
self.agent_setup()
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_modules(self, mock_ret):
# setup data
agent = baker.make_recipe("agents.agent")
url = "/api/v2/saltminion/"
payload = {"agent_id": self.agent.agent_id}
payload = {"agent_id": agent.agent_id}
mock_ret.return_value = "error"
r = self.client.patch(url, payload, format="json")

View File

@@ -2,11 +2,18 @@ import os
import json
from django.conf import settings
from tacticalrmm.test import BaseTestCase
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
class TestAPIv3(BaseTestCase):
class TestAPIv3(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
self.agent = baker.make_recipe("agents.agent")
def test_get_checks(self):
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"

View File

@@ -16,9 +16,7 @@ from rest_framework.authtoken.models import Token
from agents.models import Agent
from checks.models import Check
from autotasks.models import AutomatedTask
from winupdate.models import WinUpdate
from accounts.models import User
from clients.models import Client, Site
from winupdate.models import WinUpdatePolicy
from checks.serializers import CheckRunnerGetSerializerV3
from agents.serializers import WinAgentSerializer
@@ -419,14 +417,10 @@ class NewAgent(APIView):
"Agent already exists. Remove old agent first if trying to re-install"
)
client = get_object_or_404(Client, pk=int(request.data["client"]))
site = get_object_or_404(Site, pk=int(request.data["site"]))
agent = Agent(
agent_id=request.data["agent_id"],
hostname=request.data["hostname"],
client=client.client,
site=site.site,
site_id=int(request.data["site"]),
monitoring_type=request.data["monitoring_type"],
description=request.data["description"],
mesh_node_id=request.data["mesh_node_id"],

View File

@@ -1,6 +1,5 @@
from django.contrib import admin
from .models import Policy, PolicyExclusions
from .models import Policy
admin.site.register(Policy)
admin.site.register(PolicyExclusions)

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.2 on 2020-11-02 19:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('automation', '0005_auto_20200922_1344'),
]
operations = [
migrations.DeleteModel(
name='PolicyExclusions',
),
]

View File

@@ -32,16 +32,15 @@ class Policy(BaseAuditModel):
filtered_agents_pks = Policy.objects.none()
for site in explicit_sites:
if site.client not in explicit_clients:
filtered_agents_pks |= Agent.objects.filter(
client=site.client.client,
site=site.site,
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= Agent.objects.filter(
site__in=[
site for site in explicit_sites if site.client not in explicit_clients
],
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= Agent.objects.filter(
client__in=[client.client for client in explicit_clients],
site__client__in=[client for client in explicit_clients],
monitoring_type=mon_type,
).values_list("pk", flat=True)
@@ -68,8 +67,8 @@ class Policy(BaseAuditModel):
]
# Get policies applied to agent and agent site and client
client = Client.objects.get(client=agent.client)
site = Site.objects.filter(client=client).get(site=agent.site)
client = agent.client
site = agent.site
default_policy = None
client_policy = None
@@ -121,8 +120,8 @@ class Policy(BaseAuditModel):
]
# Get policies applied to agent and agent site and client
client = Client.objects.get(client=agent.client)
site = Site.objects.filter(client=client).get(site=agent.site)
client = agent.client
site = agent.site
default_policy = None
client_policy = None
@@ -300,11 +299,3 @@ class Policy(BaseAuditModel):
if tasks:
for task in tasks:
task.create_policy_task(agent)
class PolicyExclusions(models.Model):
policy = models.ForeignKey(
Policy, related_name="exclusions", on_delete=models.CASCADE
)
agents = models.ManyToManyField(Agent, related_name="policy_exclusions")
sites = models.ManyToManyField(Site, related_name="policy_exclusions")

View File

@@ -5,6 +5,9 @@ from rest_framework.serializers import (
ReadOnlyField,
)
from clients.serializers import ClientSerializer, SiteSerializer
from agents.serializers import AgentHostnameSerializer
from .models import Policy
from agents.models import Agent
from autotasks.models import AutomatedTask
@@ -21,11 +24,11 @@ class PolicySerializer(ModelSerializer):
class PolicyTableSerializer(ModelSerializer):
server_clients = StringRelatedField(many=True, read_only=True)
server_sites = StringRelatedField(many=True, read_only=True)
workstation_clients = StringRelatedField(many=True, read_only=True)
workstation_sites = StringRelatedField(many=True, read_only=True)
agents = StringRelatedField(many=True, read_only=True)
server_clients = ClientSerializer(many=True, read_only=True)
server_sites = SiteSerializer(many=True, read_only=True)
workstation_clients = ClientSerializer(many=True, read_only=True)
workstation_sites = SiteSerializer(many=True, read_only=True)
agents = AgentHostnameSerializer(many=True, read_only=True)
default_server_policy = ReadOnlyField(source="is_default_server_policy")
default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
agents_count = SerializerMethodField(read_only=True)
@@ -43,7 +46,7 @@ class PolicyTableSerializer(ModelSerializer):
class PolicyOverviewSerializer(ModelSerializer):
class Meta:
model = Client
fields = ("pk", "client", "sites", "workstation_policy", "server_policy")
fields = ("pk", "name", "sites", "workstation_policy", "server_policy")
depth = 2

View File

@@ -71,8 +71,8 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks and checks
policy = baker.make("automation.Policy")
checks = self.create_checks(policy=policy)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
self.create_checks(policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
# test copy tasks and checks to another policy
data = {
@@ -152,7 +152,7 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks
policy = baker.make("automation.Policy")
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/"
resp = self.client.get(url, format="json")
@@ -202,6 +202,8 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
def test_policy_overview(self):
from clients.models import Client
url = "/automation/policies/overview/"
policies = baker.make(
@@ -213,7 +215,7 @@ class TestPolicyViews(TacticalTestCase):
workstation_policy=cycle(policies),
_quantity=5,
)
sites = baker.make(
baker.make(
"clients.Site",
client=cycle(clients),
server_policy=cycle(policies),
@@ -221,8 +223,9 @@ class TestPolicyViews(TacticalTestCase):
_quantity=4,
)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=3)
baker.make("clients.Site", client=cycle(clients), _quantity=3)
resp = self.client.get(url, format="json")
clients = Client.objects.all()
serializer = PolicyOverviewSerializer(clients, many=True)
self.assertEqual(resp.status_code, 200)
@@ -256,31 +259,31 @@ class TestPolicyViews(TacticalTestCase):
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client", client="Test Client")
site = baker.make("clients.Site", client=client, site="Test Site")
agent = baker.make_recipe("agents.agent", client=client.client, site=site.site)
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
agent = baker.make_recipe("agents.agent", site=site)
# test add client to policy data
client_server_payload = {
"type": "client",
"pk": client.pk,
"pk": agent.client.pk,
"server_policy": policy.pk,
}
client_workstation_payload = {
"type": "client",
"pk": client.pk,
"pk": agent.client.pk,
"workstation_policy": policy.pk,
}
# test add site to policy data
site_server_payload = {
"type": "site",
"pk": site.pk,
"pk": agent.site.pk,
"server_policy": policy.pk,
}
site_workstation_payload = {
"type": "site",
"pk": site.pk,
"pk": agent.site.pk,
"workstation_policy": policy.pk,
}
@@ -293,7 +296,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -306,7 +309,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -319,7 +322,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -332,7 +335,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -391,7 +394,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -404,7 +407,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -417,7 +420,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -430,7 +433,7 @@ class TestPolicyViews(TacticalTestCase):
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -471,14 +474,14 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_relation_by_type(self):
def test_get_relation_by_type(self):
url = f"/automation/related/"
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client", client="Test Client")
site = baker.make("clients.Site", client=client, site="Test Site")
agent = baker.make_recipe("agents.agent", client=client.client, site=site.site)
client = baker.make("clients.Client", workstation_policy=policy)
site = baker.make("clients.Site", server_policy=policy)
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
client_payload = {"type": "client", "pk": client.pk}
@@ -621,43 +624,38 @@ class TestPolicyViews(TacticalTestCase):
"reprocess_failed_inherit": True,
}
# create agents in sites
clients = baker.make("clients.Client", client=seq("Client"), _quantity=3)
sites = baker.make(
"clients.Site", client=cycle(clients), site=seq("Site"), _quantity=6
)
clients = baker.make("clients.Client", _quantity=6)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
agents = baker.make_recipe(
"agents.agent",
client=cycle([x.client for x in clients]),
site=cycle([x.site for x in sites]),
site=cycle(sites),
_quantity=6,
)
# create patch policies
patch_policies = baker.make_recipe(
baker.make_recipe(
"winupdate.winupdate_approve", agent=cycle(agents), _quantity=6
)
# test reset agents in site
data = {"client": clients[0].client, "site": "Site0"}
data = {"site": sites[0].id}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(client=clients[0].client, site="Site0")
agents = Agent.objects.filter(site=sites[0])
for agent in agents:
for k, v in inherit_fields.items():
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
# test reset agents in client
data = {"client": clients[1].client}
data = {"client": clients[1].id}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(client=clients[1].client)
agents = Agent.objects.filter(site__client=clients[1])
for agent in agents:
for k, v in inherit_fields.items():
@@ -703,40 +701,24 @@ class TestPolicyTasks(TacticalTestCase):
def test_policy_related(self):
# Get Site and Client from an agent in list
clients = baker.make("clients.Client", client=seq("Client"), _quantity=5)
sites = baker.make(
"clients.Site", client=cycle(clients), site=seq("Site"), _quantity=25
)
clients = baker.make("clients.Client", _quantity=5)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
server_agents = baker.make_recipe(
"agents.server_agent",
client=cycle([x.client for x in clients]),
site=seq("Site"),
site=cycle(sites),
_quantity=25,
)
workstation_agents = baker.make_recipe(
"agents.workstation_agent",
client=cycle([x.client for x in clients]),
site=seq("Site"),
site=cycle(sites),
_quantity=25,
)
server_client = clients[3]
server_site = server_client.sites.all()[3]
workstation_client = clients[1]
workstation_site = server_client.sites.all()[2]
server_agent = baker.make_recipe(
"agents.server_agent", client=server_client.client, site=server_site.site
)
workstation_agent = baker.make_recipe(
"agents.workstation_agent",
client=workstation_client.client,
site=workstation_site.site,
)
policy = baker.make("automation.Policy", active=True)
# Add Client to Policy
policy.server_clients.add(server_client)
policy.workstation_clients.add(workstation_client)
policy.server_clients.add(server_agents[13].client)
policy.workstation_clients.add(workstation_agents[15].client)
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json"
@@ -747,19 +729,19 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_clients"]), 1)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 12)
self.assertEquals(len(resp.data["agents"]), 10)
# Add Site to Policy and the agents and sites length shouldn't change
policy.server_sites.add(server_site)
policy.workstation_sites.add(workstation_site)
policy.server_sites.add(server_agents[13].site)
policy.workstation_sites.add(workstation_agents[15].site)
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 12)
self.assertEquals(len(resp.data["agents"]), 10)
# Add Agent to Policy and the agents length shouldn't change
policy.agents.add(server_agent)
policy.agents.add(workstation_agent)
self.assertEquals(len(resp.data["agents"]), 12)
policy.agents.add(server_agents[13])
policy.agents.add(workstation_agents[15])
self.assertEquals(len(resp.data["agents"]), 10)
def test_generating_agent_policy_checks(self):
from .tasks import generate_agent_checks_from_policies_task
@@ -767,9 +749,8 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
checks = self.create_checks(policy=policy)
client = baker.make("clients.Client", client="Default")
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe("agents.agent", policy=policy)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id, clear=True)
@@ -815,9 +796,8 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True, enforced=True)
script = baker.make_recipe("scripts.script")
self.create_checks(policy=policy, script=script)
client = baker.make("clients.Client", client="Default")
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe("agents.agent", policy=policy)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
self.create_checks(agent=agent, script=script)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
@@ -839,25 +819,18 @@ class TestPolicyTasks(TacticalTestCase):
self.create_checks(policy=policy)
clients = baker.make(
"clients.Client",
client=seq("Default"),
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
baker.make(
"clients.Site", client=cycle(clients), site=seq("Default"), _quantity=4
)
server_agent = baker.make_recipe(
"agents.server_agent", client="Default1", site="Default1"
)
workstation_agent = baker.make_recipe(
"agents.workstation_agent", client="Default1", site="Default3"
)
agent1 = baker.make_recipe("agents.agent", client="Default2", site="Default2")
agent2 = baker.make_recipe("agents.agent", client="Default2", site="Default4")
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.server_agent", site=sites[1])
agent2 = baker.make_recipe("agents.workstation_agent", site=sites[3])
generate_agent_checks_by_location_task(
{"client": "Default1", "site": "Default1"},
{"site_id": sites[0].id},
"server",
clear=True,
create_tasks=True,
@@ -871,7 +844,10 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
generate_agent_checks_by_location_task(
{"client": "Default1"}, "workstation", clear=True, create_tasks=True
{"site__client_id": clients[0].id},
"workstation",
clear=True,
create_tasks=True,
)
# workstation_agent should now have policy checks and the other agents should not
self.assertEqual(
@@ -888,18 +864,12 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
clients = baker.make("clients.Client", client=seq("Default"), _quantity=2)
baker.make(
"clients.Site", client=cycle(clients), site=seq("Default"), _quantity=4
site = baker.make("clients.Site")
server_agents = baker.make_recipe("agents.server_agent", site=site, _quantity=3)
workstation_agents = baker.make_recipe(
"agents.workstation_agent", site=site, _quantity=4
)
server_agent = baker.make_recipe(
"agents.server_agent", client="Default1", site="Default1"
)
workstation_agent = baker.make_recipe(
"agents.workstation_agent", client="Default1", site="Default3"
)
agent1 = baker.make_recipe("agents.agent", client="Default2", site="Default2")
agent2 = baker.make_recipe("agents.agent", client="Default2", site="Default4")
core = CoreSettings.objects.first()
core.server_policy = policy
core.workstation_policy = policy
@@ -908,22 +878,20 @@ class TestPolicyTasks(TacticalTestCase):
generate_all_agent_checks_task("server", clear=True, create_tasks=True)
# all servers should have 7 checks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent2.id).agentchecks.count(), 0)
for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
generate_all_agent_checks_task("workstation", clear=True, create_tasks=True)
# all agents should have 7 checks now
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent2.id).agentchecks.count(), 7)
for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
def test_delete_policy_check(self):
from .tasks import delete_policy_check_task
@@ -931,11 +899,8 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
client = baker.make("clients.Client", client="Default", server_policy=policy)
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe(
"agents.server_agent", client="Default", site="Default"
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_checks_from_policies()
# make sure agent has 7 checks
@@ -960,11 +925,7 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
client = baker.make("clients.Client", client="Default", server_policy=policy)
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe(
"agents.server_agent", client="Default", site="Default"
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
agent.generate_checks_from_policies()
# make sure agent has 7 checks
@@ -997,11 +958,8 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
client = baker.make("clients.Client", client="Default")
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe(
"agents.server_agent", client="Default", site="Default", policy=policy
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
generate_agent_tasks_from_policies_task(policy.id, clear=True)
@@ -1027,33 +985,26 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
clients = baker.make(
"clients.Client",
client=seq("Default"),
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
baker.make(
"clients.Site", client=cycle(clients), site=seq("Default"), _quantity=4
)
server_agent = baker.make_recipe(
"agents.server_agent", client="Default1", site="Default1"
)
workstation_agent = baker.make_recipe(
"agents.workstation_agent", client="Default1", site="Default3"
)
agent1 = baker.make_recipe("agents.agent", client="Default2", site="Default2")
agent2 = baker.make_recipe("agents.agent", client="Default2", site="Default4")
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.agent", site=sites[1])
agent2 = baker.make_recipe("agents.agent", site=sites[3])
generate_agent_tasks_by_location_task(
{"client": "Default1", "site": "Default1"}, "server", clear=True
{"site_id": sites[0].id}, "server", clear=True
)
# all servers in Default1 and site Default1 should have 3 tasks
# all servers in site1 and site2 should have 3 tasks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 0
)
@@ -1062,7 +1013,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
generate_agent_tasks_by_location_task(
{"client": "Default1"}, "workstation", clear=True
{"site__client_id": clients[0].id}, "workstation", clear=True
)
# all workstations in Default1 should have 3 tasks
@@ -1079,11 +1030,8 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
client = baker.make("clients.Client", client="Default", server_policy=policy)
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe(
"agents.server_agent", client="Default", site="Default"
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
delete_policy_autotask_task(tasks[0].id)
@@ -1103,7 +1051,7 @@ class TestPolicyTasks(TacticalTestCase):
for task in tasks:
run_win_task.assert_any_call(task.id)
def test_updated_policy_tasks(self):
def test_update_policy_tasks(self):
from .tasks import update_policy_task_fields_task
from autotasks.models import AutomatedTask
@@ -1112,11 +1060,8 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make(
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
)
client = baker.make("clients.Client", client="Default", server_policy=policy)
baker.make("clients.Site", client=client, site="Default")
agent = baker.make_recipe(
"agents.server_agent", client="Default", site="Default"
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
tasks[0].enabled = False

View File

@@ -1,4 +1,3 @@
from django.db import DataError
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
@@ -12,7 +11,7 @@ from checks.models import Check
from autotasks.models import AutomatedTask
from winupdate.models import WinUpdatePolicy
from clients.serializers import ClientSerializer, TreeSerializer
from clients.serializers import ClientSerializer, SiteSerializer
from agents.serializers import AgentHostnameSerializer
from winupdate.serializers import WinUpdatePolicySerializer
@@ -33,7 +32,6 @@ from .tasks import (
generate_agent_checks_from_policies_task,
generate_agent_checks_by_location_task,
generate_agent_tasks_from_policies_task,
generate_agent_tasks_by_location_task,
run_win_policy_autotask_task,
)
@@ -172,7 +170,7 @@ class GetRelated(APIView):
if site not in policy.server_sites.all():
filtered_server_sites.append(site)
response["server_sites"] = TreeSerializer(
response["server_sites"] = SiteSerializer(
filtered_server_sites + list(policy.server_sites.all()), many=True
).data
@@ -181,7 +179,7 @@ class GetRelated(APIView):
if site not in policy.workstation_sites.all():
filtered_workstation_sites.append(site)
response["workstation_sites"] = TreeSerializer(
response["workstation_sites"] = SiteSerializer(
filtered_workstation_sites + list(policy.workstation_sites.all()), many=True
).data
@@ -218,7 +216,7 @@ class GetRelated(APIView):
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -236,7 +234,7 @@ class GetRelated(APIView):
site.workstation_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -258,7 +256,7 @@ class GetRelated(APIView):
client.server_policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -276,7 +274,7 @@ class GetRelated(APIView):
site.server_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -296,7 +294,7 @@ class GetRelated(APIView):
client.workstation_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -311,7 +309,7 @@ class GetRelated(APIView):
site.workstation_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
@@ -329,7 +327,7 @@ class GetRelated(APIView):
client.server_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.client},
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
@@ -343,7 +341,7 @@ class GetRelated(APIView):
site.server_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.site},
location={"site_id": site.pk},
mon_type="server",
clear=True,
create_tasks=True,
@@ -423,12 +421,10 @@ class UpdatePatchPolicy(APIView):
def patch(self, request):
agents = None
if "client" in request.data and "site" in request.data:
agents = Agent.objects.filter(
client=request.data["client"], site=request.data["site"]
)
elif "client" in request.data:
agents = Agent.objects.filter(client=request.data["client"])
if "client" in request.data:
agents = Agent.objects.filter(site__client_id=request.data["client"])
elif "site" in request.data:
agents = Agent.objects.filter(site_id=request.data["site"])
else:
agents = Agent.objects.all()

View File

@@ -1,3 +1,4 @@
import pytz
import random
import string
import datetime as dt
@@ -122,6 +123,15 @@ class AutomatedTask(BaseAuditModel):
days = ",".join(ret)
return f"{days} at {run_time_nice}"
@property
def last_run_as_timezone(self):
if self.last_run is not None and self.agent is not None:
return self.last_run.astimezone(
pytz.timezone(self.agent.timezone)
).strftime("%b-%d-%Y - %H:%M")
return self.last_run
@staticmethod
def generate_task_name():
chars = string.ascii_letters
@@ -137,7 +147,7 @@ class AutomatedTask(BaseAuditModel):
def create_policy_task(self, agent=None, policy=None):
from .tasks import create_win_task_schedule
# exit is neither are set or if both are set
# exit if neither are set or if both are set
if not agent and not policy or agent and policy:
return

View File

@@ -1,3 +1,4 @@
import pytz
from rest_framework import serializers
from .models import AutomatedTask
@@ -12,6 +13,7 @@ class TaskSerializer(serializers.ModelSerializer):
assigned_check = CheckSerializer(read_only=True)
schedule = serializers.ReadOnlyField()
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
class Meta:
model = AutomatedTask

View File

@@ -1,3 +1,4 @@
import pytz
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
@@ -9,6 +10,7 @@ from agents.models import Agent
from checks.models import Check
from scripts.models import Script
from core.models import CoreSettings
from .serializers import TaskSerializer, AutoTaskSerializer
@@ -68,8 +70,12 @@ class AddAutoTask(APIView):
class AutoTask(APIView):
def get(self, request, pk):
agent = Agent.objects.only("pk").get(pk=pk)
return Response(AutoTaskSerializer(agent).data)
agent = get_object_or_404(Agent, pk=pk)
ctx = {
"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone),
"agent_tz": agent.time_zone,
}
return Response(AutoTaskSerializer(agent, context=ctx).data)
def patch(self, request, pk):
from automation.tasks import update_policy_task_fields_task

View File

@@ -2,6 +2,7 @@ import base64
import string
import os
import json
import pytz
import zlib
from statistics import mean
@@ -177,6 +178,15 @@ class Check(BaseAuditModel):
if self.check_type == "cpuload" or self.check_type == "memory":
return ", ".join(str(f"{x}%") for x in self.history[-6:])
@property
def last_run_as_timezone(self):
if self.last_run is not None and self.agent is not None:
return self.last_run.astimezone(
pytz.timezone(self.agent.timezone)
).strftime("%b-%d-%Y - %H:%M")
return self.last_run
@property
def non_editable_fields(self):
return [
@@ -199,6 +209,10 @@ class Check(BaseAuditModel):
"parent_check",
"managed_by_policy",
"overriden_by_policy",
"created_by",
"created_time",
"modified_by",
"modified_time",
]
def handle_checkv2(self, data):
@@ -518,7 +532,7 @@ class Check(BaseAuditModel):
CORE = CoreSettings.objects.first()
if self.agent:
subject = f"{self.agent.client}, {self.agent.site}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
else:
subject = f"{self} Failed"
@@ -594,7 +608,7 @@ class Check(BaseAuditModel):
CORE = CoreSettings.objects.first()
if self.agent:
subject = f"{self.agent.client}, {self.agent.site}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
else:
subject = f"{self} Failed"

View File

@@ -18,6 +18,7 @@ class CheckSerializer(serializers.ModelSerializer):
readable_desc = serializers.ReadOnlyField()
script = ScriptSerializer(read_only=True)
assigned_task = serializers.SerializerMethodField()
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
history_info = serializers.ReadOnlyField()
## Change to return only array of tasks after 9/25/2020
@@ -47,12 +48,11 @@ class CheckSerializer(serializers.ModelSerializer):
.filter(check_type="diskspace")
.exclude(managed_by_policy=True)
)
if checks:
for check in checks:
if val["disk"] in check.disk:
raise serializers.ValidationError(
f"A disk check for Drive {val['disk']} already exists!"
)
for check in checks:
if val["disk"] in check.disk:
raise serializers.ValidationError(
f"A disk check for Drive {val['disk']} already exists!"
)
# ping checks
if check_type == "ping":

View File

@@ -1,26 +1,38 @@
from tacticalrmm.test import BaseTestCase
from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer
from model_bakery import baker
from itertools import cycle
class TestCheckViews(TacticalTestCase):
def setUp(self):
self.authenticate()
class TestCheckViews(BaseTestCase):
def test_get_disk_check(self):
url = f"/checks/{self.agentDiskCheck.pk}/check/"
# setup data
disk_check = baker.make_recipe("checks.diskspace_check")
url = f"/checks/{disk_check.pk}/check/"
resp = self.client.get(url, format="json")
serializer = CheckSerializer(self.agentDiskCheck)
serializer = CheckSerializer(disk_check)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("post", url)
def test_add_disk_check(self):
# setup data
agent = baker.make_recipe("agents.agent")
url = "/checks/checks/"
valid_payload = {
"pk": self.agent.pk,
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "D:",
"disk": "C:",
"threshold": 55,
"fails_b4_alert": 3,
},
@@ -31,7 +43,7 @@ class TestCheckViews(BaseTestCase):
# this should fail because we already have a check for drive C: in setup
invalid_payload = {
"pk": self.agent.pk,
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
@@ -44,23 +56,30 @@ class TestCheckViews(BaseTestCase):
self.assertEqual(resp.status_code, 400)
def test_get_policy_disk_check(self):
url = f"/checks/{self.policyDiskCheck.pk}/check/"
# setup data
policy = baker.make("automation.Policy")
disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
url = f"/checks/{disk_check.pk}/check/"
resp = self.client.get(url, format="json")
serializer = CheckSerializer(self.policyDiskCheck)
serializer = CheckSerializer(disk_check)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("post", url)
def test_add_policy_disk_check(self):
# setup data
policy = baker.make("automation.Policy")
url = "/checks/checks/"
valid_payload = {
"policy": self.policy.pk,
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"disk": "D:",
"disk": "M:",
"threshold": 86,
"fails_b4_alert": 2,
},
@@ -71,7 +90,7 @@ class TestCheckViews(BaseTestCase):
# this should fail because we already have a check for drive M: in setup
invalid_payload = {
"policy": self.policy.pk,
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"disk": "M:",
@@ -90,8 +109,14 @@ class TestCheckViews(BaseTestCase):
self.assertEqual(26, len(r.data))
def test_edit_check_alert(self):
url_a = f"/checks/{self.agentDiskCheck.pk}/check/"
url_p = f"/checks/{self.policyDiskCheck.pk}/check/"
# setup data
policy = baker.make("automation.Policy")
agent = baker.make_recipe("agents.agent")
policy_disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
agent_disk_check = baker.make_recipe("checks.diskspace_check", agent=agent)
url_a = f"/checks/{agent_disk_check.pk}/check/"
url_p = f"/checks/{policy_disk_check.pk}/check/"
valid_payload = {"email_alert": False, "check_alert": True}
invalid_payload = {"email_alert": False}

View File

@@ -2,7 +2,7 @@ from django.urls import path
from . import views
urlpatterns = [
path("checks/", views.GetAddCheck.as_view()),
path("checks/", views.AddCheck.as_view()),
path("<int:pk>/check/", views.GetUpdateDeleteCheck.as_view()),
path("<pk>/loadchecks/", views.load_checks),
path("getalldisks/", views.get_disks_for_policies),

View File

@@ -22,11 +22,7 @@ from automation.tasks import (
)
class GetAddCheck(APIView):
def get(self, request):
checks = Check.objects.all()
return Response(CheckSerializer(checks, many=True).data)
class AddCheck(APIView):
def post(self, request):
policy = None
agent = None

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2020-11-02 19:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('clients', '0006_deployment'),
]
operations = [
migrations.RenameField(
model_name='client',
old_name='client',
new_name='name',
),
migrations.RenameField(
model_name='site',
old_name='site',
new_name='name',
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.2 on 2020-11-03 14:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('clients', '0007_auto_20201102_1920'),
]
operations = [
migrations.AlterModelOptions(
name='client',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='site',
options={'ordering': ('name',)},
),
]

View File

@@ -7,7 +7,7 @@ from logs.models import BaseAuditModel
class Client(BaseAuditModel):
client = models.CharField(max_length=255, unique=True)
name = models.CharField(max_length=255, unique=True)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_clients",
@@ -24,13 +24,16 @@ class Client(BaseAuditModel):
on_delete=models.SET_NULL,
)
class Meta:
ordering = ("name",)
def __str__(self):
return self.client
return self.name
@property
def has_maintenanace_mode_agents(self):
return (
Agent.objects.filter(client=self.client, maintenance_mode=True).count() > 0
Agent.objects.filter(site__client=self, maintenance_mode=True).count() > 0
)
@property
@@ -44,7 +47,7 @@ class Client(BaseAuditModel):
"last_seen",
"overdue_time",
)
.filter(client=self.client)
.filter(site__client=self)
.prefetch_related("agentchecks")
)
for agent in agents:
@@ -67,7 +70,7 @@ class Client(BaseAuditModel):
class Site(BaseAuditModel):
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
site = models.CharField(max_length=255)
name = models.CharField(max_length=255)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_sites",
@@ -84,17 +87,15 @@ class Site(BaseAuditModel):
on_delete=models.SET_NULL,
)
class Meta:
ordering = ("name",)
def __str__(self):
return self.site
return self.name
@property
def has_maintenanace_mode_agents(self):
return (
Agent.objects.filter(
client=self.client.client, site=self.site, maintenance_mode=True
).count()
> 0
)
return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
@property
def has_failing_checks(self):
@@ -107,7 +108,7 @@ class Site(BaseAuditModel):
"last_seen",
"overdue_time",
)
.filter(client=self.client.client, site=self.site)
.filter(site=self)
.prefetch_related("agentchecks")
)
for agent in agents:
@@ -128,13 +129,6 @@ class Site(BaseAuditModel):
return SiteSerializer(site).data
def validate_name(name):
if "|" in name:
return False
else:
return True
MON_TYPE_CHOICES = [
("server", "Server"),
("workstation", "Workstation"),

View File

@@ -3,19 +3,25 @@ from .models import Client, Site, Deployment
class SiteSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.name")
class Meta:
model = Site
fields = "__all__"
def validate(self, val):
if "|" in val["site"]:
if "|" in val["name"]:
raise ValidationError("Site name cannot contain the | character")
if self.context:
client = Client.objects.get(pk=self.context["clientpk"])
if Site.objects.filter(client=client, name=val["name"]).exists():
raise ValidationError(f"Site {val['name']} already exists")
return val
class ClientSerializer(ModelSerializer):
sites = SiteSerializer(many=True, read_only=True)
class Meta:
@@ -30,29 +36,38 @@ class ClientSerializer(ModelSerializer):
if len(self.context["site"]) > 255:
raise ValidationError("Site name too long")
if "|" in val["client"]:
if "|" in val["name"]:
raise ValidationError("Client name cannot contain the | character")
return val
class TreeSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.client")
class SiteTreeSerializer(ModelSerializer):
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
class Meta:
model = Site
fields = (
"id",
"site",
"client_name",
)
fields = "__all__"
ordering = ("failing_checks",)
class ClientTreeSerializer(ModelSerializer):
sites = SiteTreeSerializer(many=True, read_only=True)
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
class Meta:
model = Client
fields = "__all__"
ordering = ("failing_checks",)
class DeploymentSerializer(ModelSerializer):
client_id = ReadOnlyField(source="client.id")
site_id = ReadOnlyField(source="site.id")
client_name = ReadOnlyField(source="client.client")
site_name = ReadOnlyField(source="site.site")
client_name = ReadOnlyField(source="client.name")
site_name = ReadOnlyField(source="site.name")
class Meta:
model = Deployment

View File

@@ -1,16 +1,32 @@
import uuid
from unittest import mock
from tacticalrmm.test import BaseTestCase
from tacticalrmm.test import TacticalTestCase
from model_bakery import baker
from .models import Client, Site, Deployment
from rest_framework.serializers import ValidationError
from .serializers import (
ClientSerializer,
SiteSerializer,
ClientTreeSerializer,
DeploymentSerializer,
)
class TestClientViews(BaseTestCase):
class TestClientViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_clients(self):
# setup data
baker.make("clients.Client", _quantity=5)
clients = Client.objects.all()
url = "/clients/clients/"
r = self.client.get(url)
r = self.client.get(url, format="json")
serializer = ClientSerializer(clients, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.check_not_authenticated("get", url)
@@ -21,15 +37,42 @@ class TestClientViews(BaseTestCase):
self.assertEqual(r.status_code, 200)
payload["client"] = "Company1|askd"
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(
ValidationError, "Client name cannot contain the | character"
):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload = {"client": "Company 1", "site": "Site2|a34"}
payload = {"client": "Company 156", "site": "Site2|a34"}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(
ValidationError, "Site name cannot contain the | character"
):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test unique
payload = {"client": "Company 1", "site": "Site 1"}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(
ValidationError, "client with this name already exists."
):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test long site name
payload = {"client": "Company 2394", "site": "Site123" * 100}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(ValidationError, "Site name too long"):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
@@ -41,88 +84,177 @@ class TestClientViews(BaseTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
def test_get_sites(self):
url = "/clients/sites/"
r = self.client.get(url)
self.check_not_authenticated("post", url)
def test_edit_client(self):
# setup data
client = baker.make("clients.Client")
# test invalid id
r = self.client.put("/clients/500/client/", format="json")
self.assertEqual(r.status_code, 404)
data = {"id": client.id, "name": "New Name"}
url = f"/clients/{client.id}/client/"
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Client.objects.filter(name="New Name").exists())
self.check_not_authenticated("put", url)
def test_delete_client(self):
# setup data
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
agent = baker.make_recipe("agents.agent", site=site)
# test invalid id
r = self.client.delete("/clients/500/client/", format="json")
self.assertEqual(r.status_code, 404)
url = f"/clients/{client.id}/client/"
# test deleting with agents under client
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 400)
# test successful deletion
agent.delete()
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertFalse(Client.objects.filter(pk=client.id).exists())
self.assertFalse(Site.objects.filter(pk=site.id).exists())
self.check_not_authenticated("put", url)
def test_get_sites(self):
# setup data
baker.make("clients.Site", _quantity=5)
sites = Site.objects.all()
url = "/clients/sites/"
r = self.client.get(url, format="json")
serializer = SiteSerializer(sites, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.check_not_authenticated("get", url)
def test_add_site(self):
url = "/clients/addsite/"
# setup data
site = baker.make("clients.Site")
payload = {"client": "Google", "site": "LA Office"}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
url = "/clients/sites/"
payload = {"client": "Google", "site": "LA Off|ice |*&@#$"}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload = {"client": "Google", "site": "KN Office"}
# test success add
payload = {"client": site.client.id, "name": "LA Office"}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(
Site.objects.filter(
name="LA Office", client__name=site.client.name
).exists()
)
# test with | symbol
payload = {"client": site.client.id, "name": "LA Off|ice |*&@#$"}
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
with self.assertRaisesMessage(
ValidationError, "Site name cannot contain the | character"
):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test site already exists
payload = {"client": site.client.id, "name": "LA Office"}
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
self.assertFalse(serializer.is_valid(raise_exception=True))
self.check_not_authenticated("post", url)
def test_list_clients(self):
url = "/clients/listclients/"
def test_edit_site(self):
# setup data
site = baker.make("clients.Site")
r = self.client.get(url)
# test invalid id
r = self.client.put("/clients/500/site/", format="json")
self.assertEqual(r.status_code, 404)
data = {"id": site.id, "name": "New Name", "client": site.client.id}
url = f"/clients/{site.id}/site/"
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Site.objects.filter(name="New Name").exists())
self.check_not_authenticated("get", url)
self.check_not_authenticated("put", url)
def test_load_tree(self):
def test_delete_site(self):
# setup data
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site)
with mock.patch(
"clients.models.Client.has_failing_checks",
new_callable=mock.PropertyMock,
return_value=True,
):
# test invalid id
r = self.client.delete("/clients/500/site/", format="json")
self.assertEqual(r.status_code, 404)
url = "/clients/loadtree/"
url = f"/clients/{site.id}/site/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# test deleting with last site under client
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 400)
client = Client.objects.get(client="Facebook")
self.assertTrue(f"Facebook|{client.pk}|negative" in r.data.keys())
# test deletion when agents exist under site
baker.make("clients.Site", client=site.client)
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 400)
with mock.patch(
"clients.models.Site.has_failing_checks",
new_callable=mock.PropertyMock,
return_value=False,
):
client = Client.objects.get(client="Google")
site = Site.objects.get(client=client, site="LA Office")
self.assertTrue(
f"LA Office|{site.pk}|black" in [i for i in r.data.values()][0]
)
self.check_not_authenticated("get", url)
def test_load_clients(self):
url = "/clients/loadclients/"
r = self.client.get(url)
# test successful deletion
agent.delete()
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertFalse(Site.objects.filter(pk=site.id).exists())
self.check_not_authenticated("delete", url)
def test_get_tree(self):
# setup data
baker.make("clients.Site", _quantity=10)
clients = Client.objects.all()
url = "/clients/tree/"
r = self.client.get(url, format="json")
serializer = ClientTreeSerializer(clients, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.check_not_authenticated("get", url)
def test_get_deployments(self):
# setup data
deployments = baker.make("clients.Deployment", _quantity=5)
url = "/clients/deployments/"
r = self.client.get(url)
serializer = DeploymentSerializer(deployments, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.check_not_authenticated("get", url)
def test_add_deployment(self):
# setup data
site = baker.make("clients.Site")
url = "/clients/deployments/"
payload = {
"client": "Google",
"site": "Main Office",
"client": site.client.id,
"site": site.id,
"expires": "2037-11-23 18:53",
"power": 1,
"ping": 0,
@@ -134,36 +266,26 @@ class TestClientViews(BaseTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
payload["site"] = "ASDkjh23k4jh"
payload["site"] = "500"
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 404)
payload["client"] = "324234ASDqwe"
payload["client"] = "500"
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 404)
self.check_not_authenticated("post", url)
def test_delete_deployment(self):
# setup data
deployment = baker.make("clients.Deployment")
url = "/clients/deployments/"
payload = {
"client": "Google",
"site": "Main Office",
"expires": "2037-11-23 18:53",
"power": 1,
"ping": 0,
"rdp": 1,
"agenttype": "server",
"arch": "64",
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
dep = Deployment.objects.last()
url = f"/clients/{dep.pk}/deployment/"
url = f"/clients/{deployment.id}/deployment/"
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
url = "/clients/32348/deployment/"
r = self.client.delete(url)

View File

@@ -4,14 +4,9 @@ from . import views
urlpatterns = [
path("clients/", views.GetAddClients.as_view()),
path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
path("tree/", views.GetClientTree.as_view()),
path("sites/", views.GetAddSites.as_view()),
path("listclients/", views.list_clients),
path("listsites/", views.list_sites),
path("addsite/", views.add_site),
path("editsite/", views.edit_site),
path("deletesite/", views.delete_site),
path("loadtree/", views.load_tree),
path("loadclients/", views.load_clients),
path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
path("deployments/", views.AgentDeployment.as_view()),
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),

View File

@@ -22,10 +22,10 @@ from rest_framework.decorators import api_view
from .serializers import (
ClientSerializer,
SiteSerializer,
TreeSerializer,
ClientTreeSerializer,
DeploymentSerializer,
)
from .models import Client, Site, Deployment, validate_name
from .models import Client, Site, Deployment
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.utils import notify_error
@@ -39,51 +39,50 @@ class GetAddClients(APIView):
def post(self, request):
if "initialsetup" in request.data:
client = {"client": request.data["client"]["client"].strip()}
site = {"site": request.data["client"]["site"].strip()}
client = {"name": request.data["client"]["client"].strip()}
site = {"name": request.data["client"]["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data["client"])
serializer.is_valid(raise_exception=True)
core = CoreSettings.objects.first()
core.default_time_zone = request.data["timezone"]
core.save(update_fields=["default_time_zone"])
else:
client = {"client": request.data["client"].strip()}
site = {"site": request.data["site"].strip()}
client = {"name": request.data["client"].strip()}
site = {"name": request.data["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Site(client=obj, site=site["site"]).save()
Site(client=obj, name=site["name"]).save()
return Response(f"{obj} was added!")
class GetUpdateDeleteClient(APIView):
def patch(self, request, pk):
def put(self, request, pk):
client = get_object_or_404(Client, pk=pk)
orig = client.client
serializer = ClientSerializer(data=request.data, instance=client)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
serializer.save()
agents = Agent.objects.filter(client=orig)
for agent in agents:
agent.client = obj.client
agent.save(update_fields=["client"])
return Response(f"{orig} renamed to {obj}")
return Response("The Client was renamed")
def delete(self, request, pk):
client = get_object_or_404(Client, pk=pk)
agents = Agent.objects.filter(client=client.client)
if agents.exists():
agent_count = Agent.objects.filter(site__client=client).count()
if agent_count > 0:
return notify_error(
f"Cannot delete {client} while {agents.count()} agents exist in it. Move the agents to another client first."
f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
)
client.delete()
return Response(f"{client.client} was deleted!")
return Response(f"{client.name} was deleted!")
class GetClientTree(APIView):
def get(self, request):
clients = Client.objects.all()
return Response(ClientTreeSerializer(clients, many=True).data)
class GetAddSites(APIView):
@@ -91,126 +90,42 @@ class GetAddSites(APIView):
sites = Site.objects.all()
return Response(SiteSerializer(sites, many=True).data)
def post(self, request):
name = request.data["name"].strip()
serializer = SiteSerializer(
data={"name": name, "client": request.data["client"]},
context={"clientpk": request.data["client"]},
)
serializer.is_valid(raise_exception=True)
serializer.save()
@api_view(["POST"])
def add_site(request):
client = Client.objects.get(client=request.data["client"].strip())
site = request.data["site"].strip()
if not validate_name(site):
content = {"error": "Site name cannot contain the | character"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
if Site.objects.filter(client=client).filter(site=site):
content = {"error": f"Site {site} already exists"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
try:
Site(client=client, site=site).save()
except DataError:
content = {"error": "Site name too long (max 255 chars)"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
else:
return Response("ok")
@api_view(["PATCH"])
def edit_site(request):
new_name = request.data["name"].strip()
class GetUpdateDeleteSite(APIView):
def put(self, request, pk):
if not validate_name(new_name):
err = "Site name cannot contain the | character"
return Response(err, status=status.HTTP_400_BAD_REQUEST)
site = get_object_or_404(Site, pk=pk)
serializer = SiteSerializer(instance=site, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
client = get_object_or_404(Client, client=request.data["client"])
site = Site.objects.filter(client=client).filter(site=request.data["site"]).get()
return Response("ok")
agents = Agent.objects.filter(client=client.client).filter(site=site.site)
def delete(self, request, pk):
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
return notify_error(f"A client must have at least 1 site.")
site.site = new_name
site.save(update_fields=["site"])
agent_count = Agent.objects.filter(site=site).count()
for agent in agents:
agent.site = new_name
agent.save(update_fields=["site"])
if agent_count > 0:
return notify_error(
f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
)
return Response("ok")
@api_view(["DELETE"])
def delete_site(request):
client = get_object_or_404(Client, client=request.data["client"])
if client.sites.count() == 1:
return notify_error(f"A client must have at least 1 site.")
site = Site.objects.filter(client=client).filter(site=request.data["site"]).get()
agents = Agent.objects.filter(client=client.client).filter(site=site.site)
if agents.exists():
return notify_error(
f"Cannot delete {site} while {agents.count()} agents exist in it. Move the agents to another site first."
)
site.delete()
return Response(f"{site} was deleted!")
@api_view()
# for vue
def list_clients(request):
clients = Client.objects.all()
return Response(ClientSerializer(clients, many=True).data)
@api_view()
# for vue
def list_sites(request):
sites = Site.objects.all()
return Response(TreeSerializer(sites, many=True).data)
@api_view()
def load_tree(request):
clients = Client.objects.all()
new = {}
for x in clients:
b = []
sites = Site.objects.filter(client=x)
for i in sites:
if i.has_maintenanace_mode_agents:
b.append(f"{i.site}|{i.pk}|warning")
elif i.has_failing_checks:
b.append(f"{i.site}|{i.pk}|negative")
else:
b.append(f"{i.site}|{i.pk}|black")
if x.has_maintenanace_mode_agents:
new[f"{x.client}|{x.pk}|warning"] = b
elif x.has_failing_checks:
new[f"{x.client}|{x.pk}|negative"] = b
else:
new[f"{x.client}|{x.pk}|black"] = b
return Response(new)
@api_view()
def load_clients(request):
clients = Client.objects.all()
new = {}
for x in clients:
b = []
sites = Site.objects.filter(client=x)
for i in sites:
b.append(i.site)
new[x.client] = b
return Response(new)
site.delete()
return Response(f"{site.name} was deleted!")
class AgentDeployment(APIView):
@@ -221,8 +136,8 @@ class AgentDeployment(APIView):
def post(self, request):
from knox.models import AuthToken
client = get_object_or_404(Client, client=request.data["client"])
site = get_object_or_404(Site, client=client, site=request.data["site"])
client = get_object_or_404(Client, pk=request.data["client"])
site = get_object_or_404(Site, pk=request.data["site"])
expires = dt.datetime.strptime(
request.data["expires"], "%Y-%m-%d %H:%M"
@@ -285,8 +200,8 @@ class GenerateAgent(APIView):
)
download_url = settings.DL_64 if d.arch == "64" else settings.DL_32
client = d.client.client.replace(" ", "").lower()
site = d.site.site.replace(" ", "").lower()
client = d.client.name.replace(" ", "").lower()
site = d.site.name.replace(" ", "").lower()
client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
site = re.sub(r"([^a-zA-Z0-9]+)", "", site)

View File

@@ -1,8 +1,12 @@
from tacticalrmm.test import BaseTestCase
from tacticalrmm.test import TacticalTestCase
from core.tasks import core_maintenance_tasks
class TestCoreTasks(BaseTestCase):
class TestCoreTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.authenticate()
def test_core_maintenance_tasks(self):
task = core_maintenance_tasks.s().apply()
self.assertEqual(task.state, "SUCCESS")

View File

@@ -22,8 +22,8 @@ class PendingActionSerializer(serializers.ModelSerializer):
hostname = serializers.ReadOnlyField(source="agent.hostname")
salt_id = serializers.ReadOnlyField(source="agent.salt_id")
client = serializers.ReadOnlyField(source="agent.client")
site = serializers.ReadOnlyField(source="agent.site")
client = serializers.ReadOnlyField(source="agent.client.name")
site = serializers.ReadOnlyField(source="agent.site.name")
due = serializers.ReadOnlyField()
description = serializers.ReadOnlyField()

View File

@@ -1,16 +1,16 @@
amqp==2.6.1
asgiref==3.2.10
asgiref==3.3.0
billiard==3.6.3.0
celery==4.4.6
certifi==2020.6.20
certifi==2020.11.8
cffi==1.14.3
chardet==3.0.4
cryptography==3.2
cryptography==3.2.1
decorator==4.4.2
Django==3.1.2
Django==3.1.3
django-cors-headers==3.5.0
django-rest-knox==4.1.0
djangorestframework==3.12.1
djangorestframework==3.12.2
future==0.18.2
idna==2.10
kombu==4.6.11
@@ -19,19 +19,19 @@ msgpack==1.0.0
packaging==20.4
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.9.8
pycryptodome==3.9.9
pyotp==2.4.1
pyparsing==2.4.7
pytz==2020.1
pytz==2020.4
qrcode==6.1
redis==3.5.3
requests==2.24.0
six==1.15.0
sqlparse==0.4.1
twilio==6.46.0
urllib3==1.25.10
twilio==6.47.0
urllib3==1.25.11
uWSGI==2.0.19.1
validators==0.18.1
vine==1.3.0
websockets==8.1
zipp==3.3.1
zipp==3.4.0

View File

@@ -10,11 +10,11 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.1.1"
TRMM_VERSION = "0.1.4"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.84"
APP_VER = "0.0.86"
# https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.1.0"
@@ -25,7 +25,7 @@ LATEST_AGENT_VER = "1.0.1"
MESH_VER = "0.6.62"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "1"
PIP_VER = "2"
NPM_VER = "1"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"

View File

@@ -1,24 +1,11 @@
import json
import os
import random
import string
from django.test import TestCase, override_settings
from django.utils import timezone as djangotime
from django.conf import settings
from model_bakery import baker
from rest_framework.test import APIClient
from rest_framework.authtoken.models import Token
from accounts.models import User
from agents.models import Agent
from winupdate.models import WinUpdatePolicy
from clients.models import Client, Site
from automation.models import Policy
from core.models import CoreSettings
from checks.models import Check
from autotasks.models import AutomatedTask
from rest_framework.authtoken.models import Token
class TacticalTestCase(TestCase):
@@ -29,6 +16,12 @@ class TacticalTestCase(TestCase):
self.client_setup()
self.client.force_authenticate(user=self.john)
def setup_agent_auth(self, agent):
agent_user = User.objects.create_user(
username=agent.agent_id, password=User.objects.make_random_password(60)
)
Token.objects.create(user=agent_user)
def client_setup(self):
self.client = APIClient()
@@ -51,62 +44,6 @@ class TacticalTestCase(TestCase):
r = switch.get(method)
self.assertEqual(r.status_code, 401)
def agent_setup(self):
self.agent = Agent.objects.create(
operating_system="Windows 10",
plat="windows",
plat_release="windows-Server2019",
hostname="DESKTOP-TEST123",
salt_id="aksdjaskdjs",
local_ip="10.0.25.188",
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
services=[
{
"pid": 880,
"name": "AeLookupSvc",
"status": "stopped",
"binpath": "C:\\Windows\\system32\\svchost.exe -k netsvcs",
"username": "localSystem",
"start_type": "manual",
"description": "Processes application compatibility cache requests for applications as they are launched",
"display_name": "Application Experience",
},
{
"pid": 812,
"name": "ALG",
"status": "stopped",
"binpath": "C:\\Windows\\System32\\alg.exe",
"username": "NT AUTHORITY\\LocalService",
"start_type": "manual",
"description": "Provides support for 3rd party protocol plug-ins for Internet Connection Sharing",
"display_name": "Application Layer Gateway Service",
},
],
public_ip="74.13.24.14",
total_ram=16,
used_ram=33,
disks={
"C:": {
"free": "42.3G",
"used": "17.1G",
"total": "59.5G",
"device": "C:",
"fstype": "NTFS",
"percent": 28,
}
},
boot_time=8173231.4,
logged_in_username="John",
client="Google",
site="Main Office",
monitoring_type="server",
description="Test PC",
mesh_node_id="abcdefghijklmnopAABBCCDD77443355##!!AI%@#$%#*",
last_seen=djangotime.now(),
)
self.update_policy = WinUpdatePolicy.objects.create(agent=self.agent)
def create_checks(self, policy=None, agent=None, script=None):
if not policy and not agent:
@@ -132,136 +69,3 @@ class TacticalTestCase(TestCase):
baker.make_recipe(recipe, policy=policy, agent=agent, script=script)
)
return checks
class BaseTestCase(TestCase):
def setUp(self):
self.john = User(username="john")
self.john.set_password("password")
self.john.save()
self.client = APIClient()
self.client.force_authenticate(user=self.john)
self.coresettings = CoreSettings.objects.create()
self.agent = self.create_agent("DESKTOP-TEST123", "Google", "Main Office")
self.agent_user = User.objects.create_user(
username=self.agent.agent_id, password=User.objects.make_random_password(60)
)
self.agent_token = Token.objects.create(user=self.agent_user)
self.update_policy = WinUpdatePolicy.objects.create(agent=self.agent)
Client.objects.create(client="Google")
Client.objects.create(client="Facebook")
google = Client.objects.get(client="Google")
facebook = Client.objects.get(client="Facebook")
Site.objects.create(client=google, site="Main Office")
Site.objects.create(client=google, site="LA Office")
Site.objects.create(client=google, site="MO Office")
Site.objects.create(client=facebook, site="Main Office")
Site.objects.create(client=facebook, site="NY Office")
self.policy = Policy.objects.create(
name="testpolicy",
desc="my awesome policy",
active=True,
)
self.policy.server_clients.add(google)
self.policy.workstation_clients.add(facebook)
self.agentDiskCheck = Check.objects.create(
agent=self.agent,
check_type="diskspace",
disk="C:",
threshold=41,
fails_b4_alert=4,
)
self.policyDiskCheck = Check.objects.create(
policy=self.policy,
check_type="diskspace",
disk="M:",
threshold=87,
fails_b4_alert=1,
)
self.policyTask = AutomatedTask.objects.create(
policy=self.policy, name="Test Task"
)
def check_not_authenticated(self, method, url):
self.client.logout()
switch = {
"get": self.client.get(url),
"post": self.client.post(url),
"put": self.client.put(url),
"patch": self.client.patch(url),
"delete": self.client.delete(url),
}
r = switch.get(method)
self.assertEqual(r.status_code, 401)
def create_agent(self, hostname, client, site, monitoring_type="server"):
with open(
os.path.join(
settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
)
) as f:
wmi_py = json.load(f)
return Agent.objects.create(
operating_system="Windows 10",
plat="windows",
plat_release="windows-Server2019",
hostname=f"{hostname}",
salt_id=self.generate_agent_id(hostname),
local_ip="10.0.25.188",
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
services=[
{
"pid": 880,
"name": "AeLookupSvc",
"status": "stopped",
"binpath": "C:\\Windows\\system32\\svchost.exe -k netsvcs",
"username": "localSystem",
"start_type": "manual",
"description": "Processes application compatibility cache requests for applications as they are launched",
"display_name": "Application Experience",
},
{
"pid": 812,
"name": "ALG",
"status": "stopped",
"binpath": "C:\\Windows\\System32\\alg.exe",
"username": "NT AUTHORITY\\LocalService",
"start_type": "manual",
"description": "Provides support for 3rd party protocol plug-ins for Internet Connection Sharing",
"display_name": "Application Layer Gateway Service",
},
],
public_ip="74.13.24.14",
total_ram=16,
used_ram=33,
disks={
"C:": {
"free": "42.3G",
"used": "17.1G",
"total": "59.5G",
"device": "C:",
"fstype": "NTFS",
"percent": 28,
}
},
boot_time=8173231.4,
logged_in_username="John",
client=f"{client}",
site=f"{site}",
monitoring_type=monitoring_type,
description="Test PC",
mesh_node_id="abcdefghijklmnopAABBCCDD77443355##!!AI%@#$%#*",
last_seen=djangotime.now(),
wmi_detail=wmi_py,
)
def generate_agent_id(self, hostname):
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
return f"{rand}-{hostname}"

View File

@@ -14,7 +14,7 @@ class TestWinUpdateViews(TacticalTestCase):
def test_get_winupdates(self):
agent = baker.make_recipe("agents.agent")
winupdates = baker.make("winupdate.WinUpdate", agent=agent, _quantity=4)
baker.make("winupdate.WinUpdate", agent=agent, _quantity=4)
# test a call where agent doesn't exist
resp = self.client.get("/winupdate/500/getwinupdates/", format="json")
@@ -107,9 +107,11 @@ class WinupdateTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
baker.make("clients.Site", site="Default", client__client="Default")
self.online_agents = baker.make_recipe("agents.online_agent", _quantity=2)
self.offline_agent = baker.make_recipe("agents.agent")
site = baker.make("clients.Site")
self.online_agents = baker.make_recipe(
"agents.online_agent", site=site, _quantity=2
)
self.offline_agent = baker.make_recipe("agents.agent", site=site)
@patch("winupdate.tasks.check_for_updates_task.apply_async")
def test_auto_approve_task(self, check_updates_task):

View File

@@ -6,6 +6,7 @@
:style="{ 'max-height': agentTableHeight }"
:data="filter"
:filter="search"
:filter-method="filterTable"
:columns="columns"
:visible-columns="visibleColumns"
row-key="id"
@@ -84,7 +85,7 @@
<q-item-section>Edit {{ props.row.hostname }}</q-item-section>
</q-item>
<!-- agent pending actions -->
<q-item clickable v-close-popup @click="showPendingActions(props.row.id, props.row.hostname)">
<q-item clickable v-close-popup @click="showPendingActionsModal(props.row.id)">
<q-item-section side>
<q-icon size="xs" name="far fa-clock" />
</q-item-section>
@@ -255,8 +256,8 @@
<q-tooltip>Checks passing</q-tooltip>
</q-icon>
</q-td>
<q-td key="client" :props="props">{{ props.row.client }}</q-td>
<q-td key="site" :props="props">{{ props.row.site }}</q-td>
<q-td key="client_name" :props="props">{{ props.row.client_name }}</q-td>
<q-td key="site_name" :props="props">{{ props.row.site_name }}</q-td>
<q-td key="hostname" :props="props">{{ props.row.hostname }}</q-td>
<q-td key="description" :props="props">{{ props.row.description }}</q-td>
@@ -306,7 +307,11 @@
<RebootLater @close="showRebootLaterModal = false" />
</q-dialog>
<!-- pending actions modal -->
<PendingActions />
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showPendingActions" @hide="closePendingActionsModal">
<PendingActions :agentpk="pendingActionAgentPk" @close="closePendingActionsModal" />
</q-dialog>
</div>
<!-- add policy modal -->
<q-dialog v-model="showPolicyAddModal">
<PolicyAdd @close="showPolicyAddModal = false" type="agent" :pk="policyAddPk" />
@@ -367,9 +372,61 @@ export default {
showAgentRecovery: false,
showRunScript: false,
policyAddPk: null,
showPendingActions: false,
pendingActionAgentPk: null,
};
},
methods: {
filterTable(rows, terms, cols, cellValue) {
const lowerTerms = terms ? terms.toLowerCase() : "";
let advancedFilter = false;
let availability = null;
let checks = false;
let patches = false;
let reboot = false;
let search = "";
const params = lowerTerms.trim().split(" ");
// parse search text and set variables
params.forEach(param => {
if (param.includes("is:")) {
advancedFilter = true;
let filter = param.split(":")[1];
if (filter === "patchespending") patches = true;
else if (filter === "checksfailing") checks = true;
else if (filter === "rebootneeded") reboot = true;
else if (filter === "online" || filter === "offline" || filter === "expired") availability = filter;
} else {
search = param + "";
}
});
return rows.filter(row => {
if (advancedFilter) {
if (checks && !row.checks.has_failing_checks) return false;
if (patches && !row.patches_pending) return false;
if (reboot && !row.needs_reboot) return false;
if (availability === "online" && row.status !== "online") return false;
else if (availability === "offline" && row.status !== "overdue") return false;
else if (availability === "expired") {
const nowPlus30Days = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const unixtime = Date.parse(row.last_seen);
if (unixtime > nowPlus30Days) return false;
}
}
// fix for last_logged_in_user not filtering
if (row.logged_in_username === "None" && row.status === "online" && !!row.last_logged_in_user)
return row.last_logged_in_user.toLowerCase().indexOf(search) !== -1;
// Normal text filter
return cols.some(col => {
const val = cellValue(col, row) + "";
const haystack = val === "undefined" || val === "null" ? "" : val.toLowerCase();
return haystack.indexOf(search) !== -1;
});
});
},
rowDoubleClicked(pk) {
this.$store.commit("setActiveRow", pk);
this.$q.loading.show();
@@ -400,9 +457,13 @@ export default {
agentEdited() {
this.$emit("refreshEdit");
},
showPendingActions(pk, hostname) {
const data = { action: true, agentpk: pk, hostname: hostname };
this.$store.commit("logs/TOGGLE_PENDING_ACTIONS", data);
showPendingActionsModal(pk) {
this.showPendingActions = true;
this.pendingActionAgentPk = pk;
},
closePendingActionsModal() {
this.showPendingActions = false;
this.pendingActionAgentPk = null;
},
takeControl(pk) {
const url = this.$router.resolve(`/takecontrol/${pk}`).href;

View File

@@ -182,8 +182,8 @@ export default {
pattern: needle,
};
this.$store
.dispatch("logs/optionsFilter", data)
this.$axios
.post(`logs/auditlogs/optionsfilter/`, data)
.then(r => {
this.userOptions = r.data.map(user => user.username);
this.$q.loading.hide();
@@ -209,8 +209,8 @@ export default {
pattern: needle,
};
this.$store
.dispatch("logs/optionsFilter", data)
this.$axios
.post(`logs/auditlogs/optionsfilter/`, data)
.then(r => {
this.agentOptions = r.data.map(agent => agent.hostname);
this.$q.loading.hide();
@@ -264,8 +264,8 @@ export default {
data["timeFilter"] = this.timeFilter;
}
this.$store
.dispatch("logs/loadAuditLogs", data)
this.$axios
.patch("/logs/auditlogs/", data)
.then(r => {
this.$q.loading.hide();
this.auditLogs = r.data;

View File

@@ -12,10 +12,10 @@
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showAddClientModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'add')">
<q-item-section>Add Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showAddSiteModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'add')">
<q-item-section>Add Site</q-item-section>
</q-item>
</q-list>
@@ -29,10 +29,10 @@
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showDeleteClientModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'delete')">
<q-item-section>Delete Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showDeleteSiteModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'delete')">
<q-item-section>Delete Site</q-item-section>
</q-item>
</q-list>
@@ -45,7 +45,7 @@
<q-item clickable v-close-popup @click="showAuditManager = true">
<q-item-section>Audit Log</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="getLog">
<q-item clickable v-close-popup @click="showDebugLog = true">
<q-item-section>Debug Log</q-item-section>
</q-item>
</q-list>
@@ -55,10 +55,10 @@
<q-btn size="md" dense no-caps flat label="Edit">
<q-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showEditClientsModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'edit')">
<q-item-section>Edit Clients</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showEditSitesModal = true">
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'edit')">
<q-item-section>Edit Sites</q-item-section>
</q-item>
</q-list>
@@ -68,7 +68,7 @@
<q-btn size="md" dense no-caps flat label="View">
<q-menu auto-close>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showPendingActions">
<q-item clickable v-close-popup @click="showPendingActions = true">
<q-item-section>Pending Actions</q-item-section>
</q-item>
</q-list>
@@ -119,15 +119,15 @@
<q-menu auto-close>
<q-list dense style="min-width: 100px">
<!-- bulk command -->
<q-item clickable v-close-popup @click="showBulkCommand = true">
<q-item clickable v-close-popup @click="showBulkActionModal('command')">
<q-item-section>Bulk Command</q-item-section>
</q-item>
<!-- bulk script -->
<q-item clickable v-close-popup @click="showBulkScript = true">
<q-item clickable v-close-popup @click="showBulkActionModal('script')">
<q-item-section>Bulk Script</q-item-section>
</q-item>
<!-- bulk patch management -->
<q-item clickable v-close-popup @click="showBulkPatchManagement = true">
<q-item clickable v-close-popup @click="showBulkActionModal('scan')">
<q-item-section>Bulk Patch Management</q-item-section>
</q-item>
</q-list>
@@ -135,34 +135,31 @@
</q-btn>
</q-btn-group>
<q-space />
<!-- add client modal -->
<q-dialog v-model="showAddClientModal">
<AddClient @close="showAddClientModal = false" />
<!-- client form modal -->
<q-dialog v-model="showClientFormModal" @hide="closeClientsFormModal">
<ClientsForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<q-dialog v-model="showEditClientsModal">
<EditClients @close="showEditClientsModal = false" @edited="edited" />
</q-dialog>
<!-- add site modal -->
<q-dialog v-model="showAddSiteModal">
<AddSite @close="showAddSiteModal = false" :clients="clients" />
</q-dialog>
<q-dialog v-model="showEditSitesModal">
<EditSites @close="showEditSitesModal = false" @edited="edited" />
</q-dialog>
<!-- delete -->
<q-dialog v-model="showDeleteClientModal">
<DeleteClient @close="showDeleteClientModal = false" @edited="edited" />
</q-dialog>
<q-dialog v-model="showDeleteSiteModal">
<DeleteSite @close="showDeleteSiteModal = false" @edited="edited" />
<!-- site form modal -->
<q-dialog v-model="showSiteFormModal" @hide="closeClientsFormModal">
<SitesForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<!-- edit core settings modal -->
<q-dialog v-model="showEditCoreSettingsModal">
<EditCoreSettings @close="showEditCoreSettingsModal = false" />
</q-dialog>
<!-- debug log modal -->
<LogModal />
<!-- audit log modal -->
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showDebugLog" maximized transition-show="slide-up" transition-hide="slide-down">
<LogModal @close="showDebugLog = false" />
</q-dialog>
</div>
<!-- pending actions modal -->
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showPendingActions">
<PendingActions @close="showPendingActions = false" />
</q-dialog>
</div>
<!-- audit manager -->
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showAuditManager" maximized transition-show="slide-up" transition-hide="slide-down">
<AuditManager @close="showAuditManager = false" />
@@ -200,19 +197,9 @@
<UploadMesh @close="showUploadMesh = false" />
</q-dialog>
<!-- Bulk command modal -->
<q-dialog v-model="showBulkCommand" position="top">
<BulkCommand @close="showBulkCommand = false" />
</q-dialog>
<!-- Bulk script modal -->
<q-dialog v-model="showBulkScript" position="top">
<BulkScript @close="showBulkScript = false" />
</q-dialog>
<!-- Bulk patch management -->
<q-dialog v-model="showBulkPatchManagement" position="top">
<BulkPatchManagement @close="showBulkPatchManagement = false" />
<!-- Bulk action modal -->
<q-dialog v-model="showBulkAction" @hide="closeBulkActionModal" position="top">
<BulkAction :mode="bulkMode" @close="closeBulkActionModal" />
</q-dialog>
<!-- Agent Deployment -->
@@ -225,12 +212,9 @@
<script>
import LogModal from "@/components/modals/logs/LogModal";
import AddClient from "@/components/modals/clients/AddClient";
import EditClients from "@/components/modals/clients/EditClients";
import AddSite from "@/components/modals/clients/AddSite";
import EditSites from "@/components/modals/clients/EditSites";
import DeleteClient from "@/components/modals/clients/DeleteClient";
import DeleteSite from "@/components/modals/clients/DeleteSite";
import PendingActions from "@/components/modals/logs/PendingActions";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import UpdateAgents from "@/components/modals/agents/UpdateAgents";
import ScriptManager from "@/components/ScriptManager";
import EditCoreSettings from "@/components/modals/coresettings/EditCoreSettings";
@@ -239,21 +223,16 @@ import AdminManager from "@/components/AdminManager";
import InstallAgent from "@/components/modals/agents/InstallAgent";
import UploadMesh from "@/components/modals/core/UploadMesh";
import AuditManager from "@/components/AuditManager";
import BulkCommand from "@/components/modals/agents/BulkCommand";
import BulkScript from "@/components/modals/agents/BulkScript";
import BulkPatchManagement from "@/components/modals/agents/BulkPatchManagement";
import BulkAction from "@/components/modals/agents/BulkAction";
import Deployment from "@/components/Deployment";
export default {
name: "FileBar",
components: {
LogModal,
AddClient,
EditClients,
AddSite,
EditSites,
DeleteClient,
DeleteSite,
PendingActions,
ClientsForm,
SitesForm,
UpdateAgents,
ScriptManager,
EditCoreSettings,
@@ -262,20 +241,15 @@ export default {
UploadMesh,
AdminManager,
AuditManager,
BulkCommand,
BulkScript,
BulkPatchManagement,
BulkAction,
Deployment,
},
props: ["clients"],
data() {
return {
showAddClientModal: false,
showEditClientsModal: false,
showAddSiteModal: false,
showEditSitesModal: false,
showDeleteClientModal: false,
showDeleteSiteModal: false,
showClientFormModal: false,
showSiteFormModal: false,
clientOp: null,
showUpdateAgentsModal: false,
showEditCoreSettingsModal: false,
showAutomationManager: false,
@@ -283,19 +257,35 @@ export default {
showInstallAgent: false,
showUploadMesh: false,
showAuditManager: false,
showBulkCommand: false,
showBulkScript: false,
showBulkPatchManagement: false,
showBulkAction: false,
showPendingActions: false,
bulkMode: null,
showDeployment: false,
showDebugLog: false,
};
},
methods: {
getLog() {
this.$store.commit("logs/TOGGLE_LOG_MODAL", true);
showClientsFormModal(type, op) {
this.clientOp = op;
if (type === "client") {
this.showClientFormModal = true;
} else if (type === "site") {
this.showSiteFormModal = true;
}
},
showPendingActions() {
const data = { action: true, agentpk: null, hostname: null };
this.$store.commit("logs/TOGGLE_PENDING_ACTIONS", data);
closeClientsFormModal() {
this.clientOp = null;
this.showClientFormModal = null;
this.showSiteFormModal = null;
},
showBulkActionModal(mode) {
this.bulkMode = mode;
this.showBulkAction = true;
},
closeBulkActionModal() {
this.bulkMode = null;
this.showBulkAction = false;
},
showScriptManager() {
this.$store.commit("TOGGLE_SCRIPT_MANAGER", true);

View File

@@ -1,5 +1,5 @@
<template>
<div style="width: 900px; max-width: 90vw">
<div style="width: 60vw; max-width: 90vw">
<q-card>
<q-bar>
<q-btn ref="refresh" @click="refresh" class="q-mr-sm" dense flat push icon="refresh" />Automation Manager
@@ -208,9 +208,9 @@
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-scroll-area :thumb-style="thumbStyle" style="height: 70vh">
<div class="scroll" style="height: 70vh">
<PatchPolicyForm :policy="policy" @close="closePatchPolicyModal" />
</q-scroll-area>
</div>
</q-card>
</q-dialog>
</div>
@@ -277,13 +277,6 @@ export default {
pagination: {
rowsPerPage: 9999,
},
thumbStyle: {
right: "2px",
borderRadius: "5px",
backgroundColor: "#027be3",
width: "5px",
opacity: 0.75,
},
};
},
methods: {

View File

@@ -37,12 +37,7 @@
<q-tab name="checks" icon="fas fa-check-double" label="Checks" />
<q-tab name="tasks" icon="fas fa-tasks" label="Tasks" />
</q-tabs>
<q-tab-panels
v-model="selectedTab"
animated
transition-prev="jump-up"
transition-next="jump-up"
>
<q-tab-panels v-model="selectedTab" animated transition-prev="jump-up" transition-next="jump-up">
<q-tab-panel name="checks">
<PolicyChecksTab />
</q-tab-panel>
@@ -127,7 +122,7 @@ export default {
for (let client in data) {
var client_temp = {};
client_temp["label"] = data[client].client;
client_temp["label"] = data[client].name;
client_temp["id"] = unique_id;
client_temp["icon"] = "business";
client_temp["selectable"] = false;
@@ -170,7 +165,7 @@ export default {
// Iterate through Sites
for (let site in data[client].sites) {
var site_temp = {};
site_temp["label"] = data[client].sites[site].site;
site_temp["label"] = data[client].sites[site].name;
site_temp["id"] = unique_id;
site_temp["icon"] = "apartment";
site_temp["selectable"] = false;

View File

@@ -37,7 +37,7 @@
<q-list separator padding>
<q-item :key="item.id + 'servers'" v-for="item in related.server_clients">
<q-item-section>
<q-item-label>{{ item.client }}</q-item-label>
<q-item-label>{{ item.name }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>
@@ -47,7 +47,7 @@
</q-item>
<q-item :key="item.id + 'workstations'" v-for="item in related.workstation_clients">
<q-item-section>
<q-item-label>{{ item.client }}</q-item-label>
<q-item-label>{{ item.name }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>
@@ -62,7 +62,7 @@
<q-list separator padding>
<q-item :key="item.id + 'servers'" v-for="item in related.server_sites">
<q-item-section>
<q-item-label>{{ item.site }}</q-item-label>
<q-item-label>{{ item.name }}</q-item-label>
<q-item-label caption>{{ item.client_name }}</q-item-label>
</q-item-section>
<q-item-section side>
@@ -73,7 +73,7 @@
</q-item>
<q-item :key="item.id + 'workstations'" v-for="item in related.workstation_sites">
<q-item-section>
<q-item-label>{{ item.site }}</q-item-label>
<q-item-label>{{ item.name }}</q-item-label>
<q-item-label caption>{{ item.client_name }}</q-item-label>
</q-item-section>
<q-item-section side>

View File

@@ -2,8 +2,8 @@
<q-card style="min-width: 50vw">
<q-card-section class="row items-center">
<div class="text-h6">
Run Bulk Script
<div class="text-caption">Run a script on multiple agents in parallel</div>
{{ modalTitle }}
<div v-if="modalCaption !== null" class="text-caption">{{ modalCaption }}</div>
</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
@@ -15,14 +15,25 @@
<p>Choose Target</p>
<div class="q-gutter-sm">
<q-radio dense v-model="target" val="client" label="Client" @input="agentMultiple = []" />
<q-radio dense v-model="target" val="site" label="Site" @input="agentMultiple = []" />
<q-radio
dense
v-model="target"
val="site"
label="Site"
@input="
() => {
agentMultiple = [];
site = sites[0];
}
"
/>
<q-radio dense v-model="target" val="agents" label="Selected Agents" />
<q-radio dense v-model="target" val="all" label="All Agents" @input="agentMultiple = []" />
</div>
</div>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'client'">
<q-card-section v-if="target === 'client' || target === 'site'" class="q-pb-none">
<q-select
dense
:rules="[val => !!val || '*Required']"
@@ -30,22 +41,11 @@
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
/>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'site'">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="site = sites[0]"
:options="client_options"
@input="target === 'site' ? (site = sites[0]) : () => {}"
/>
<q-select
v-if="target === 'site'"
dense
:rules="[val => !!val || '*Required']"
outlined
@@ -55,8 +55,7 @@
:options="sites"
/>
</q-card-section>
<q-card-section v-if="agents.length !== 0 && target === 'agents'">
<q-card-section v-if="target === 'agents'">
<q-select
dense
options-dense
@@ -72,7 +71,7 @@
/>
</q-card-section>
<q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<q-select
:rules="[val => !!val || '*Required']"
dense
@@ -85,7 +84,7 @@
options-dense
/>
</q-card-section>
<q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<q-select
label="Script Arguments (press Enter after typing each argument)"
filled
@@ -99,7 +98,28 @@
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-card-section v-if="mode === 'command'">
<p>Shell</p>
<div class="q-gutter-sm">
<q-radio dense v-model="shell" val="cmd" label="CMD" />
<q-radio dense v-model="shell" val="powershell" label="Powershell" />
</div>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<q-input
v-model="cmd"
outlined
label="Command"
stack-label
:placeholder="
shell === 'cmd' ? 'rmdir /S /Q C:\\Windows\\System32' : 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input
v-model.number="timeout"
dense
@@ -115,6 +135,17 @@
]"
/>
</q-card-section>
<q-card-section v-if="mode === 'scan'">
<div class="q-pa-none">
<p>Action</p>
<div class="q-gutter-sm">
<q-radio dense v-model="selected_mode" val="scan" label="Run Patch Status Scan" />
<q-radio dense v-model="selected_mode" val="install" label="Install Pending Patches Now" />
</div>
</div>
</q-card-section>
<q-card-actions align="center">
<q-btn label="Run" color="primary" class="full-width" type="submit" />
</q-card-actions>
@@ -127,34 +158,36 @@ import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
export default {
name: "BulkScript",
name: "BulkAction",
mixins: [mixins],
props: {
mode: !String,
},
data() {
return {
target: "client",
selected_mode: null,
scriptPK: null,
timeout: 900,
tree: null,
client: null,
client_options: [],
site: null,
agents: [],
agentMultiple: [],
args: [],
cmd: "",
shell: "cmd",
modalTitle: null,
modalCaption: null,
};
},
computed: {
...mapGetters(["scripts"]),
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
return !!this.client ? this.formatSiteOptions(this.client.sites) : [];
},
scriptOptions() {
const ret = [];
this.scripts.forEach(i => {
ret.push({ label: i.name, value: i.id });
});
const ret = this.scripts.map(script => ({ label: script.name, value: script.id }));
return ret.sort((a, b) => a.label.localeCompare(b.label));
},
},
@@ -162,14 +195,17 @@ export default {
send() {
this.$q.loading.show();
const data = {
mode: "script",
mode: this.selected_mode,
target: this.target,
client: this.client,
site: this.site,
site: this.site.value,
client: this.client.value,
agentPKs: this.agentMultiple,
scriptPK: this.scriptPK,
timeout: this.timeout,
args: this.args,
shell: this.shell,
timeout: this.timeout,
cmd: this.cmd,
};
this.$axios
.post("/agents/bulk/", data)
@@ -183,25 +219,42 @@ export default {
this.notifyError(e.response.data);
});
},
getTree() {
this.$axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.client_options = this.formatClientOptions(r.data);
this.client = this.client_options[0];
this.site = this.sites[0];
});
},
getAgents() {
this.$axios.get("/agents/listagentsnodetail/").then(r => {
const ret = [];
r.data.forEach(i => {
ret.push({ label: i.hostname, value: i.pk });
});
const ret = r.data.map(agent => ({ label: agent.hostname, value: agent.pk }));
this.agents = Object.freeze(ret.sort((a, b) => a.label.localeCompare(b.label)));
});
},
setTitles() {
switch (this.mode) {
case "command":
this.modalTitle = "Run Bulk Command";
this.modalCaption = "Run a shell command on multiple agents in parallel";
break;
case "script":
this.modalTitle = "Run Bulk Script";
this.modalCaption = "Run a script on multiple agents in parallel";
break;
case "scan":
this.modalTitle = "Bulk Patch Management";
break;
}
},
},
created() {
this.getTree();
this.setTitles();
this.getClients();
this.getAgents();
this.selected_mode = this.mode;
},
};
</script>

View File

@@ -1,189 +0,0 @@
<template>
<q-card style="min-width: 50vw">
<q-card-section class="row items-center">
<div class="text-h6">
Send Bulk Command
<div class="text-caption">Run a shell command on multiple agents in parallel</div>
</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-form @submit.prevent="send">
<q-card-section>
<div class="q-pa-none">
<p>Choose Target</p>
<div class="q-gutter-sm">
<q-radio dense v-model="target" val="client" label="Client" @input="agentMultiple = []" />
<q-radio dense v-model="target" val="site" label="Site" @input="agentMultiple = []" />
<q-radio dense v-model="target" val="agents" label="Selected Agents" />
<q-radio dense v-model="target" val="all" label="All Agents" @input="agentMultiple = []" />
</div>
</div>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'client'">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
/>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'site'">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="site = sites[0]"
/>
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="site"
:options="sites"
/>
</q-card-section>
<q-card-section v-if="agents.length !== 0 && target === 'agents'">
<q-select
dense
options-dense
filled
v-model="agentMultiple"
multiple
:options="agents"
use-chips
stack-label
map-options
emit-value
label="Select Agents"
/>
</q-card-section>
<q-card-section>
<p>Shell</p>
<div class="q-gutter-sm">
<q-radio dense v-model="shell" val="cmd" label="CMD" />
<q-radio dense v-model="shell" val="powershell" label="Powershell" />
</div>
</q-card-section>
<q-card-section>
<q-input
v-model="cmd"
outlined
label="Command"
stack-label
:placeholder="
shell === 'cmd' ? 'rmdir /S /Q C:\\Windows\\System32' : 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section>
<q-input
v-model.number="timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[
val => !!val || '*Required',
val => val >= 10 || 'Minimum is 10 seconds',
val => val <= 3600 || 'Maximum is 3600 seconds',
]"
/>
</q-card-section>
<q-card-actions align="center">
<q-btn label="Send" color="primary" class="full-width" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "BulkCommand",
mixins: [mixins],
data() {
return {
target: "client",
shell: "cmd",
timeout: 300,
cmd: null,
tree: null,
client: null,
site: null,
agents: [],
agentMultiple: [],
};
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
},
},
methods: {
send() {
this.$q.loading.show();
const data = {
mode: "command",
target: this.target,
client: this.client,
site: this.site,
agentPKs: this.agentMultiple,
cmd: this.cmd,
timeout: this.timeout,
shell: this.shell,
};
this.$axios
.post("/agents/bulk/", data)
.then(r => {
this.$q.loading.hide();
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
});
},
getTree() {
this.$axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
});
},
getAgents() {
this.$axios.get("/agents/listagentsnodetail/").then(r => {
const ret = [];
r.data.forEach(i => {
ret.push({ label: i.hostname, value: i.pk });
});
this.agents = Object.freeze(ret.sort((a, b) => a.label.localeCompare(b.label)));
});
},
},
created() {
this.getTree();
this.getAgents();
},
};
</script>

View File

@@ -1,156 +0,0 @@
<template>
<q-card style="min-width: 50vw">
<q-card-section class="row items-center">
<div class="text-h6">Bulk Patch Management</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-form @submit.prevent="send">
<q-card-section>
<div class="q-pa-none">
<p>Choose Target</p>
<div class="q-gutter-sm">
<q-radio dense v-model="target" val="client" label="Client" @input="agentMultiple = []" />
<q-radio dense v-model="target" val="site" label="Site" @input="agentMultiple = []" />
<q-radio dense v-model="target" val="agents" label="Selected Agents" />
<q-radio dense v-model="target" val="all" label="All Agents" @input="agentMultiple = []" />
</div>
</div>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'client'">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
/>
</q-card-section>
<q-card-section v-if="tree !== null && client !== null && target === 'site'">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="site = sites[0]"
/>
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="site"
:options="sites"
/>
</q-card-section>
<q-card-section v-if="agents.length !== 0 && target === 'agents'">
<q-select
dense
options-dense
filled
v-model="agentMultiple"
multiple
:options="agents"
use-chips
stack-label
map-options
emit-value
label="Select Agents"
/>
</q-card-section>
<q-card-section>
<div class="q-pa-none">
<p>Action</p>
<div class="q-gutter-sm">
<q-radio dense v-model="mode" val="scan" label="Run Patch Status Scan" />
<q-radio dense v-model="mode" val="install" label="Install Pending Patches Now" />
</div>
</div>
</q-card-section>
<q-card-actions align="center">
<q-btn label="Send" color="primary" class="full-width" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "BulkPatchManagement",
mixins: [mixins],
data() {
return {
target: "client",
tree: null,
client: null,
site: null,
agents: [],
agentMultiple: [],
mode: "scan",
};
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
},
},
methods: {
send() {
this.$q.loading.show();
const data = {
mode: this.mode,
target: this.target,
client: this.client,
site: this.site,
agentPKs: this.agentMultiple,
};
this.$axios
.post("/agents/bulk/", data)
.then(r => {
this.$q.loading.hide();
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
});
},
getTree() {
this.$axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
});
},
getAgents() {
this.$axios.get("/agents/listagentsnodetail/").then(r => {
const ret = [];
r.data.forEach(i => {
ret.push({ label: i.hostname, value: i.pk });
});
this.agents = Object.freeze(ret.sort((a, b) => a.label.localeCompare(b.label)));
});
},
},
created() {
this.getTree();
this.getAgents();
},
};
</script>

View File

@@ -14,7 +14,7 @@
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-scroll-area :thumb-style="thumbStyle" style="height: 500px">
<div class="scroll" style="max-height: 65vh">
<q-tab-panels v-model="tab" animated transition-prev="jump-up" transition-next="jump-up">
<!-- general -->
<q-tab-panel name="general">
@@ -22,19 +22,28 @@
<div class="col-2">Client:</div>
<div class="col-2"></div>
<q-select
@input="agent.site = sites[0]"
@input="agent.site = site_options[0]"
dense
options-dense
outlined
v-model="agent.client"
:options="Object.keys(tree).sort()"
:options="client_options"
class="col-8"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Site:</div>
<div class="col-2"></div>
<q-select class="col-8" dense options-dense outlined v-model="agent.site" :options="sites" />
<q-select
class="col-8"
dense
options-dense
emit-value
map-options
outlined
v-model="agent.site"
:options="site_options"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Type:</div>
@@ -106,10 +115,9 @@
<PatchPolicyForm :agent="agent" />
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
</div>
<q-card-section class="row items-center">
<q-btn label="Save" color="primary" type="submit" />
<q-btn label="Cancel" v-close-popup />
</q-card-section>
</q-form>
</template>
@@ -118,7 +126,6 @@
</template>
<script>
import axios from "axios";
import { mapGetters } from "vuex";
import mixins from "@/mixins/mixins";
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm";
@@ -133,25 +140,18 @@ export default {
clientsLoaded: false,
agent: {},
monTypes: ["server", "workstation"],
tree: {},
client_options: [],
splitterModel: 15,
tab: "general",
timezone: null,
tz_inherited: true,
original_tz: null,
allTimezones: [],
thumbStyle: {
right: "2px",
borderRadius: "5px",
backgroundColor: "#027be3",
width: "5px",
opacity: 0.75,
},
};
},
methods: {
getAgentInfo() {
axios.get(`/agents/${this.selectedAgentPk}/agenteditdetails/`).then(r => {
this.$axios.get(`/agents/${this.selectedAgentPk}/agenteditdetails/`).then(r => {
this.agent = r.data;
this.allTimezones = Object.freeze(r.data.all_timezones);
@@ -168,29 +168,38 @@ export default {
this.original_tz = r.data.time_zone;
}
this.agent.client = { label: r.data.client.name, id: r.data.client.id, sites: r.data.client.sites };
this.agentLoaded = true;
});
},
getClientsSites() {
axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.$axios.get("/clients/clients/").then(r => {
this.client_options = this.formatClientOptions(r.data);
this.clientsLoaded = true;
});
},
editAgent() {
let data = this.agent;
delete this.agent.all_timezones;
delete this.agent.client;
delete this.agent.sites;
delete this.agent.timezone;
delete this.agent.winupdatepolicy[0].created_by;
delete this.agent.winupdatepolicy[0].created_time;
delete this.agent.winupdatepolicy[0].modified_by;
delete this.agent.winupdatepolicy[0].modified_time;
delete this.agent.winupdatepolicy[0].policy;
// only send the timezone data if it has changed
// this way django will keep the db column as null and inherit from the global setting
// until we explicity change the agent's timezone
if (this.timezone !== this.original_tz) {
data.time_zone = this.timezone;
this.agent.time_zone = this.timezone;
}
delete data.all_timezones;
this.agent.site = this.agent.site.value;
axios
.patch("/agents/editagent/", data)
this.$axios
.patch("/agents/editagent/", this.agent)
.then(r => {
this.$emit("close");
this.$emit("edited");
@@ -201,9 +210,9 @@ export default {
},
computed: {
...mapGetters(["selectedAgentPk"]),
sites() {
site_options() {
if (this.agentLoaded && this.clientsLoaded) {
return this.tree[this.agent.client].sort();
return this.formatSiteOptions(this.agent.client["sites"]);
}
},
},

View File

@@ -1,5 +1,5 @@
<template>
<q-card style="min-width: 35vw" v-if="loaded">
<q-card style="min-width: 35vw">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Add an agent</div>
@@ -11,14 +11,14 @@
</q-card-section>
<q-card-section>
<q-form @submit.prevent="addAgent">
<q-card-section v-if="tree !== null" class="q-gutter-sm">
<q-card-section class="q-gutter-sm">
<q-select
outlined
dense
options-dense
label="Client"
v-model="client"
:options="Object.keys(tree).sort()"
:options="client_options"
@input="site = sites[0]"
/>
</q-card-section>
@@ -88,8 +88,7 @@ export default {
components: { AgentDownload },
data() {
return {
loaded: false,
tree: {},
client_options: [],
client: null,
site: null,
agenttype: "server",
@@ -104,14 +103,14 @@ export default {
};
},
methods: {
getClientsSites() {
getClients() {
this.$q.loading.show();
axios
.get("/clients/loadclients/")
.get("/clients/clients/")
.then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
this.loaded = true;
this.client_options = this.formatClientOptions(r.data);
this.client = this.client_options[0];
this.site = this.sites[0];
this.$q.loading.hide();
})
.catch(() => {
@@ -121,19 +120,19 @@ export default {
},
addAgent() {
const api = axios.defaults.baseURL;
const clientStripped = this.client
const clientStripped = this.client.label
.replace(/\s/g, "")
.toLowerCase()
.replace(/([^a-zA-Z0-9]+)/g, "");
const siteStripped = this.site
const siteStripped = this.site.label
.replace(/\s/g, "")
.toLowerCase()
.replace(/([^a-zA-Z0-9]+)/g, "");
const data = {
installMethod: this.installMethod,
client: this.client,
site: this.site,
client: this.client.value,
site: this.site.value,
expires: this.expires,
agenttype: this.agenttype,
power: this.power ? 1 : 0,
@@ -240,17 +239,14 @@ export default {
},
showDLMessage() {
this.$q.dialog({
message: `Installer for ${this.client}, ${this.site} (${this.agenttype}) will now be downloaded.
message: `Installer for ${this.client.label}, ${this.site.label} (${this.agenttype}) will now be downloaded.
You may reuse this installer for ${this.expires} hours before it expires. No command line arguments are needed.`,
});
},
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
return !!this.client ? this.formatSiteOptions(this.client.sites) : [];
},
installButtonText() {
let text;
@@ -270,7 +266,7 @@ export default {
},
},
created() {
this.getClientsSites();
this.getClients();
},
};
</script>

View File

@@ -1,73 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Add Client</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="addClient">
<q-card-section>
<q-input
outlined
v-model="client.client"
label="Client:"
:rules="[ val => val && val.length > 0 || '*Required']"
/>
</q-card-section>
<q-card-section>
<q-input
outlined
v-model="client.site"
label="Default first site:"
:rules="[ val => val && val.length > 0 || '*Required']"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn label="Add Client" color="primary" type="submit" />
<q-btn label="Cancel" v-close-popup />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
name: "AddClient",
mixins: [mixins],
data() {
return {
client: {
client: null,
site: null,
},
};
},
methods: {
addClient() {
axios
.post("/clients/clients/", this.client)
.then(r => {
this.$emit("close");
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
this.notifySuccess(r.data);
})
.catch(e => {
if (e.response.data.client) {
this.notifyError(e.response.data.client);
} else {
this.notifyError(e.response.data.non_field_errors);
}
});
},
},
};
</script>

View File

@@ -1,71 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Add Site</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="addSite">
<q-card-section>
<q-select options-dense outlined v-model="clientName" :options="Object.keys(clients).sort()" />
</q-card-section>
<q-card-section>
<q-input
outlined
v-model="siteName"
label="Site Name:"
:rules="[val => (val && val.length > 0) || 'This field is required']"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn label="Add Site" color="primary" type="submit" />
<q-btn label="Cancel" v-close-popup />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
name: "AddSite",
props: ["clients"],
mixins: [mixins],
data() {
return {
clientName: "",
siteName: "",
};
},
methods: {
loadFirstClient() {
axios.get("/clients/listclients/").then(resp => {
this.clientName = resp.data.map(k => k.client).sort()[0];
});
},
addSite() {
axios
.post("/clients/addsite/", {
client: this.clientName,
site: this.siteName,
})
.then(() => {
this.$emit("close");
this.$store.dispatch("loadTree");
this.notifySuccess(`Site ${this.siteName} was added!`);
})
.catch(err => this.notifyError(err.response.data.error));
},
},
created() {
this.loadFirstClient();
},
};
</script>

View File

@@ -0,0 +1,189 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">{{ modalTitle }}</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="submit">
<q-card-section v-if="op === 'edit' || op === 'delete'">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="selected_client"
:options="client_options"
/>
</q-card-section>
<q-card-section v-if="op === 'add'">
<q-input
outlined
v-model="client.name"
label="Client"
:rules="[val => (val && val.length > 0) || '*Required']"
/>
</q-card-section>
<q-card-section v-if="op === 'add' || op === 'edit'">
<q-input
v-if="op === 'add'"
:rules="[val => !!val || '*Required']"
outlined
v-model="client.site"
label="Default first site"
/>
<q-input
v-else-if="op === 'edit'"
:rules="[val => !!val || '*Required']"
outlined
v-model="client.name"
label="Rename client"
/>
</q-card-section>
<q-card-actions align="left">
<q-btn
:label="capitalize(op)"
:color="op === 'delete' ? 'negative' : 'primary'"
type="submit"
class="full-width"
/>
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "ClientsForm",
mixins: [mixins],
props: {
op: !String,
clientpk: Number,
},
data() {
return {
client_options: [],
selected_client: {},
client: {
id: null,
name: "",
site: "",
},
};
},
watch: {
selected_client(newClient, oldClient) {
this.client.id = newClient.value;
this.client.name = newClient.label;
},
},
computed: {
modalTitle() {
if (this.op === "add") return "Add Client";
if (this.op === "edit") return "Edit Client";
if (this.op === "delete") return "Delete Client";
},
},
methods: {
submit() {
if (this.op === "add") this.addClient();
if (this.op === "edit") this.editClient();
if (this.op === "delete") this.deleteClient();
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.client_options = r.data.map(client => ({ label: client.name, value: client.id }));
if (this.clientpk !== undefined && this.clientpk !== null) {
let client = this.client_options.find(client => client.value === this.clientpk);
this.selected_client = client;
} else {
this.selected_client = this.client_options[0];
}
});
},
addClient() {
this.$q.loading.show();
const data = {
client: this.client.name,
site: this.client.site,
};
this.$axios
.post("/clients/clients/", data)
.then(r => {
this.$emit("close");
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
if (e.response.data.client) {
this.notifyError(e.response.data.client);
} else {
this.notifyError(e.response.data.non_field_errors);
}
});
},
editClient() {
this.$q.loading.show();
const data = {
id: this.client.id,
name: this.client.name,
};
this.$axios
.put(`/clients/${this.client.id}/client/`, this.client)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
if (e.response.data.client) {
this.notifyError(e.response.data.client);
} else {
this.notifyError(e.response.data.non_field_errors);
}
});
},
deleteClient() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete client ${this.client.name}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/${this.client.id}/client/`)
.then(r => {
this.$q.loading.hide();
this.$emit("edited");
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 6000);
});
});
},
},
created() {
if (this.op !== "add") this.getClients();
},
};
</script>

View File

@@ -1,97 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Delete Client</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="deleteClient">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client.id"
:options="clients"
@input="onChange"
emit-value
map-options
/>
</q-card-section>
<q-card-section></q-card-section>
<q-card-actions align="left">
<q-btn
:disable="client.client === null"
:label="deleteLabel"
class="full-width"
color="negative"
type="submit"
/>
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "DeleteClient",
mixins: [mixins],
data() {
return {
clients: [],
client: {
client: null,
id: null,
},
};
},
computed: {
deleteLabel() {
return this.client.client !== null ? `Delete ${this.client.client}` : "Delete";
},
},
methods: {
getClients() {
this.$axios.get("/clients/clients/").then(r => {
r.data.forEach(client => {
this.clients.push({ label: client.client, value: client.id });
});
this.clients.sort((a, b) => a.label.localeCompare(b.label));
});
},
onChange() {
this.client.client = this.clients.find(i => i.value === this.client.id).label;
},
deleteClient() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete client ${this.client.client}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$axios
.delete(`/clients/${this.client.id}/client/`)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => this.notifyError(e.response.data, 6000));
});
},
},
created() {
this.getClients();
},
};
</script>

View File

@@ -1,98 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Delete Site</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="deleteSite">
<q-card-section v-if="tree !== null">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="site = sites[0]"
/>
</q-card-section>
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="site"
:options="sites"
/>
</q-card-section>
<q-card-actions align="left">
<q-btn :disable="site === null" :label="`Delete ${site}`" class="full-width" color="negative" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "DeleteSite",
mixins: [mixins],
data() {
return {
tree: null,
client: null,
site: null,
};
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
},
},
methods: {
getTree() {
this.$axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
});
},
deleteSite() {
const data = {
client: this.client,
site: this.site,
};
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site ${this.site}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$axios
.delete("/clients/deletesite/", { data })
.then(r => {
this.$emit("edited");
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => this.notifyError(e.response.data, 6000));
});
},
},
created() {
this.getTree();
},
};
</script>

View File

@@ -1,94 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Edit Clients</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="editClient">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client.id"
:options="clients"
@input="onChange"
emit-value
map-options
/>
</q-card-section>
<q-card-section>
<q-input :rules="[val => !!val || '*Required']" outlined v-model="client.client" label="Rename client" />
</q-card-section>
<q-card-actions align="left">
<q-btn :disable="!nameChanged" label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
name: "EditClients",
mixins: [mixins],
data() {
return {
clients: [],
client: {
client: null,
id: null,
},
};
},
computed: {
nameChanged() {
if (this.clients.length !== 0 && this.client.client !== null) {
const origName = this.clients.find(i => i.value === this.client.id).label;
return this.client.client === origName ? false : true;
}
},
},
methods: {
getClients() {
axios.get("/clients/clients/").then(r => {
r.data.forEach(client => {
this.clients.push({ label: client.client, value: client.id });
});
this.clients.sort((a, b) => a.label.localeCompare(b.label));
});
},
onChange() {
this.client.client = this.clients.find(i => i.value === this.client.id).label;
},
editClient() {
axios
.patch(`/clients/${this.client.id}/client/`, this.client)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
if (e.response.data.client) {
this.notifyError(e.response.data.client);
} else {
this.notifyError(e.response.data.non_field_errors);
}
});
},
},
created() {
this.getClients();
},
};
</script>

View File

@@ -1,105 +0,0 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Edit Sites</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="editSite">
<q-card-section v-if="tree !== null">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="
site = sites[0];
newName = sites[0];
"
/>
</q-card-section>
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="site"
:options="sites"
@input="newName = site"
/>
</q-card-section>
<q-card-section>
<q-input :rules="[val => !!val || '*Required']" outlined v-model="newName" label="Rename site" />
</q-card-section>
<q-card-actions align="left">
<q-btn :disable="!nameChanged" label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
name: "EditSites",
mixins: [mixins],
data() {
return {
tree: null,
client: null,
site: null,
newName: null,
};
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
this.newName = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
},
nameChanged() {
if (this.site !== null) {
return this.newName === this.site ? false : true;
}
},
},
methods: {
getTree() {
axios.get("/clients/loadclients/").then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
});
},
editSite() {
const data = {
client: this.client,
site: this.site,
name: this.newName,
};
axios
.patch("/clients/editsite/", data)
.then(() => {
this.$emit("edited");
this.$emit("close");
this.notifySuccess("Site was edited");
})
.catch(e => this.notifyError(e.response.data));
},
},
created() {
this.getTree();
},
};
</script>

View File

@@ -1,5 +1,5 @@
<template>
<q-card style="min-width: 25vw" v-if="loaded">
<q-card style="min-width: 25vw">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Create a Deployment</div>
@@ -11,19 +11,19 @@
</q-card-section>
<q-card-section>
<q-form @submit.prevent="create">
<q-card-section v-if="tree !== null" class="q-gutter-sm">
<q-card-section class="q-gutter-sm">
<q-select
outlined
dense
options-dense
label="Client"
v-model="client"
:options="Object.keys(tree).sort()"
@input="site = sites[0]"
:options="client_options"
@input="site = sites[0].value"
/>
</q-card-section>
<q-card-section class="q-gutter-sm">
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" />
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" map-options emit-value />
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
@@ -84,9 +84,8 @@ export default {
mixins: [mixins],
data() {
return {
client_options: [],
datetime: null,
loaded: false,
tree: {},
client: null,
site: null,
agenttype: "server",
@@ -99,7 +98,7 @@ export default {
methods: {
create() {
const data = {
client: this.client,
client: this.client.value,
site: this.site,
expires: this.datetime,
agenttype: this.agenttype,
@@ -122,14 +121,14 @@ export default {
d.setDate(d.getDate() + 30);
this.datetime = date.formatDate(d, "YYYY-MM-DD HH:mm");
},
getClientsSites() {
getClients() {
this.$q.loading.show();
this.$axios
.get("/clients/loadclients/")
.get("/clients/clients/")
.then(r => {
this.tree = r.data;
this.client = Object.keys(r.data).sort()[0];
this.loaded = true;
this.client_options = this.formatClientOptions(r.data);
this.client = this.client_options[0];
this.site = this.formatSiteOptions(this.client.sites)[0].value;
this.$q.loading.hide();
})
.catch(() => {
@@ -140,15 +139,12 @@ export default {
},
computed: {
sites() {
if (this.tree !== null && this.client !== null) {
this.site = this.tree[this.client].sort()[0];
return this.tree[this.client].sort();
}
return this.client !== null ? this.formatSiteOptions(this.client.sites) : [];
},
},
created() {
this.getCurrentDate();
this.getClientsSites();
this.getClients();
},
};
</script>

View File

@@ -0,0 +1,197 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">{{ modalTitle }}</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="submit">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="selected_client"
:options="client_options"
@input="op === 'edit' || op === 'delete' ? (selected_site = sites[0]) : () => {}"
/>
</q-card-section>
<q-card-section v-if="op === 'edit' || op === 'delete'">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="selected_site"
:options="sites"
/>
</q-card-section>
<q-card-section v-if="op === 'add' || op === 'edit'">
<q-input
v-if="op === 'add'"
outlined
v-model="site.name"
label="Site"
:rules="[val => !!val || '*Required']"
/>
<q-input
v-else-if="op === 'edit'"
:rules="[val => !!val || '*Required']"
outlined
v-model="site.name"
label="Rename site"
/>
</q-card-section>
<q-card-actions align="left">
<q-btn
:label="capitalize(op)"
:color="op === 'delete' ? 'negative' : 'primary'"
type="submit"
class="full-width"
/>
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "SitesForm",
mixins: [mixins],
props: {
op: !String,
sitepk: Number,
},
data() {
return {
client_options: [],
selected_client: null,
selected_site: null,
site: {
id: null,
name: "",
},
};
},
watch: {
selected_site(newSite, oldSite) {
this.site.id = newSite.value;
this.site.name = newSite.label;
},
},
computed: {
sites() {
return !!this.selected_client ? this.formatSiteOptions(this.selected_client.sites) : [];
},
modalTitle() {
if (this.op === "add") return "Add Site";
if (this.op === "edit") return "Edit Site";
if (this.op === "delete") return "Delete Site";
},
},
methods: {
submit() {
if (this.op === "add") this.addSite();
if (this.op === "edit") this.editSite();
if (this.op === "delete") this.deleteSite();
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.client_options = this.formatClientOptions(r.data);
if (this.sitepk !== undefined && this.sitepk !== null) {
this.client_options.forEach(client => {
let site = client.sites.find(site => site.id === this.sitepk);
if (site !== undefined) {
this.selected_client = client;
this.selected_site = { value: site.id, label: site.name };
}
});
} else {
this.selected_client = this.client_options[0];
if (this.op !== "add") this.selected_site = this.sites[0];
}
});
},
addSite() {
this.$q.loading.show();
const data = {
client: this.selected_client.value,
name: this.site.name,
};
this.$axios
.post("/clients/sites/", data)
.then(() => {
this.$emit("close");
this.$store.dispatch("loadTree");
this.$q.loading.hide();
this.notifySuccess(`Site ${this.site.name} was added!`);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data.non_field_errors);
});
},
editSite() {
this.$q.loading.show();
const data = {
id: this.site.id,
name: this.site.name,
client: this.selected_client.value,
};
this.$axios
.put(`/clients/${this.site.id}/site/`, data)
.then(() => {
this.$emit("edited");
this.$emit("close");
this.$q.loading.hide();
this.notifySuccess("Site was edited");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data.non_field_errors);
});
},
deleteSite() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site ${this.site.name}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/${this.site.id}/site/`)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 6000);
});
});
},
},
created() {
this.getClients();
},
};
</script>

View File

@@ -24,6 +24,7 @@
outlined
v-model="client"
:options="client_options"
@input="client !== null ? (site = site_options[0]) : () => {}"
/>
</q-card-section>
<q-card-section>
@@ -48,10 +49,12 @@
</template>
<script>
import mixins from "@/mixins/mixins";
import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
export default {
name: "ResetPatchPolicy",
mixins: [mixins],
data() {
return {
client: null,
@@ -66,11 +69,11 @@ export default {
let data = {};
if (this.client !== null) {
data.client = this.client.label;
data.client = this.client.value;
}
if (this.site !== null) {
data.site = this.site.label;
data.site = this.site.value;
}
this.$store
@@ -89,7 +92,7 @@ export default {
this.$store
.dispatch("loadClients")
.then(r => {
this.client_options = r.data.map(client => ({ label: client.client, value: client.id, sites: client.sites }));
this.client_options = this.formatClientOptions(r.data);
})
.catch(e => {
this.$q.notify(notifyErrorConfig("There was an error loading the clients!"));
@@ -105,7 +108,7 @@ export default {
},
computed: {
site_options() {
return !!this.client ? this.client.sites.map(site => ({ label: site.site, value: site.id })) : [];
return !!this.client ? this.formatSiteOptions(this.client.sites) : [];
},
buttonText() {
return !this.client ? "Clear Policies for ALL Agents" : "Clear Policies";

View File

@@ -1,67 +1,42 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-dialog
:value="toggleLogModal"
@hide="hideLogModal"
@show="getLog"
maximized
transition-show="slide-up"
transition-hide="slide-down"
>
<q-card class="bg-grey-10 text-white">
<q-bar>
<q-btn @click="getLog" class="q-mr-sm" dense flat push icon="refresh" label="Refresh" />Debug Log
<q-space />
<q-btn color="primary" text-color="white" label="Download log" @click="downloadLog" />
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<div class="q-pa-md row">
<div class="col-2">
<q-select
dark
dense
options-dense
outlined
v-model="agent"
:options="agents"
label="Filter Agent"
@input="getLog"
/>
</div>
<div class="col-1">
<q-select
dark
dense
options-dense
outlined
v-model="order"
:options="orders"
label="Order"
@input="getLog"
/>
</div>
</div>
<q-card-section>
<q-radio dark v-model="loglevel" color="cyan" val="info" label="Info" @input="getLog" />
<q-radio dark v-model="loglevel" color="red" val="critical" label="Critical" @input="getLog" />
<q-radio dark v-model="loglevel" color="red" val="error" label="Error" @input="getLog" />
<q-radio dark v-model="loglevel" color="yellow" val="warning" label="Warning" @input="getLog" />
</q-card-section>
<q-separator />
<q-card-section>
<q-scroll-area
:thumb-style="{ right: '4px', borderRadius: '5px', background: 'red', width: '10px', opacity: 1 }"
style="height: 60vh"
>
<pre>{{ logContent }}</pre>
</q-scroll-area>
</q-card-section>
</q-card>
</q-dialog>
</div>
<q-card class="bg-grey-10 text-white">
<q-bar>
<q-btn @click="getLog" class="q-mr-sm" dense flat push icon="refresh" label="Refresh" />Debug Log
<q-space />
<q-btn color="primary" text-color="white" label="Download log" @click="downloadLog" />
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<div class="q-pa-md row">
<div class="col-2">
<q-select
dark
dense
options-dense
outlined
v-model="agent"
:options="agents"
label="Filter Agent"
@input="getLog"
/>
</div>
<div class="col-1">
<q-select dark dense options-dense outlined v-model="order" :options="orders" label="Order" @input="getLog" />
</div>
</div>
<q-card-section>
<q-radio dark v-model="loglevel" color="cyan" val="info" label="Info" @input="getLog" />
<q-radio dark v-model="loglevel" color="red" val="critical" label="Critical" @input="getLog" />
<q-radio dark v-model="loglevel" color="red" val="error" label="Error" @input="getLog" />
<q-radio dark v-model="loglevel" color="yellow" val="warning" label="Warning" @input="getLog" />
</q-card-section>
<q-separator />
<q-card-section class="scroll" style="max-height: 80vh">
<pre>{{ logContent }}</pre>
</q-card-section>
</q-card>
</template>
<script>
@@ -99,14 +74,9 @@ export default {
this.agents.unshift("all");
});
},
hideLogModal() {
this.$store.commit("logs/TOGGLE_LOG_MODAL", false);
},
},
computed: {
...mapState({
toggleLogModal: state => state.logs.toggleLogModal,
}),
created() {
this.getLog();
},
};
</script>

View File

@@ -1,101 +1,97 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-dialog :value="togglePendingActions" @hide="hidePendingActions" @show="getPendingActions">
<q-card style="width: 900px; max-width: 90vw;">
<q-inner-loading :showing="actionsLoading">
<q-spinner size="40px" color="primary" />
</q-inner-loading>
<q-bar>
<q-btn @click="getPendingActions" class="q-mr-sm" dense flat push icon="refresh" />
{{ title }}
<q-space />
<q-btn dense flat icon="close" v-close-popup />
</q-bar>
<div v-if="actions.length !== 0" class="q-pa-md">
<div class="row">
<div class="col">
<q-btn
label="Cancel Action"
:disable="selectedRow === null || selectedStatus === 'completed' || actionType === 'taskaction'"
color="red"
icon="cancel"
dense
unelevated
no-caps
size="md"
@click="cancelPendingAction"
/>
</div>
<div class="col-7"></div>
<div class="col">
<q-btn
:label="showCompleted ? `Hide ${completedCount} Completed` : `Show ${completedCount} Completed`"
:icon="showCompleted ? 'visibility_off' : 'visibility'"
@click="showCompleted = !showCompleted"
dense
unelevated
no-caps
size="md"
/>
</div>
</div>
<q-table
<q-card style="width: 900px; max-width: 90vw">
<q-bar>
<q-btn @click="getPendingActions" class="q-mr-sm" dense flat push icon="refresh" />
{{ title }}
<q-space />
<q-btn dense flat icon="close" v-close-popup />
</q-bar>
<div v-if="actions.length !== 0" class="q-pa-md">
<div class="row">
<div class="col">
<q-btn
label="Cancel Action"
:disable="selectedRow === null || selectedStatus === 'completed' || actionType === 'taskaction'"
color="red"
icon="cancel"
dense
class="remote-bg-tbl-sticky"
:data="filter"
:columns="columns"
:visible-columns="visibleColumns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-bottom
virtual-scroll
flat
:rows-per-page-options="[0]"
>
<template slot="body" slot-scope="props" :props="props">
<q-tr
:class="rowClass(props.row.id, props.row.status)"
@click="rowSelected(props.row.id, props.row.status, props.row.action_type)"
>
<q-td v-if="props.row.action_type === 'schedreboot'">
<q-icon name="power_settings_new" size="sm" />
</q-td>
<q-td v-else-if="props.row.action_type === 'taskaction'">
<q-icon name="fas fa-tasks" size="sm" />
</q-td>
<q-td>{{ props.row.due }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td v-show="agentpk === null">{{ props.row.hostname }}</q-td>
<q-td v-show="agentpk === null">{{ props.row.client }}</q-td>
<q-td v-show="agentpk === null">{{ props.row.site }}</q-td>
</q-tr>
</template>
</q-table>
unelevated
no-caps
size="md"
@click="cancelPendingAction"
/>
</div>
<div v-else class="q-pa-md">No pending actions</div>
<q-card-section></q-card-section>
<q-separator />
<q-card-section></q-card-section>
</q-card>
</q-dialog>
</div>
<div class="col-7"></div>
<div class="col">
<q-btn
:label="showCompleted ? `Hide ${completedCount} Completed` : `Show ${completedCount} Completed`"
:icon="showCompleted ? 'visibility_off' : 'visibility'"
@click="showCompleted = !showCompleted"
dense
unelevated
no-caps
size="md"
/>
</div>
</div>
<q-table
dense
class="remote-bg-tbl-sticky"
:data="filter"
:columns="columns"
:visible-columns="visibleColumns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-bottom
virtual-scroll
flat
:rows-per-page-options="[0]"
>
<template slot="body" slot-scope="props" :props="props">
<q-tr
:class="rowClass(props.row.id, props.row.status)"
@click="rowSelected(props.row.id, props.row.status, props.row.action_type)"
>
<q-td v-if="props.row.action_type === 'schedreboot'">
<q-icon name="power_settings_new" size="sm" />
</q-td>
<q-td v-else-if="props.row.action_type === 'taskaction'">
<q-icon name="fas fa-tasks" size="sm" />
</q-td>
<q-td>{{ props.row.due }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td v-show="!!!agentpk">{{ props.row.hostname }}</q-td>
<q-td v-show="!!!agentpk">{{ props.row.client }}</q-td>
<q-td v-show="!!!agentpk">{{ props.row.site }}</q-td>
</q-tr>
</template>
</q-table>
</div>
<div v-else class="q-pa-md">No pending actions</div>
<q-card-section></q-card-section>
<q-separator />
<q-card-section></q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
export default {
name: "PendingActions",
mixins: [mixins],
props: {
agentpk: Number,
},
data() {
return {
actions: [],
selectedRow: null,
showCompleted: false,
selectedStatus: null,
actionType: null,
hostname: "",
pagination: {
rowsPerPage: 0,
sortBy: "due",
@@ -124,8 +120,18 @@ export default {
},
methods: {
getPendingActions() {
this.$q.loading.show();
this.clearRow();
this.$store.dispatch("logs/getPendingActions");
this.$axios
.get(this.url)
.then(r => {
this.actions = Object.freeze(r.data);
if (!!this.agentpk) this.hostname = r.data[0].hostname;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
});
},
cancelPendingAction() {
this.$q
@@ -137,7 +143,7 @@ export default {
.onOk(() => {
this.$q.loading.show();
const data = { pk: this.selectedRow };
axios
this.$axios
.delete("/logs/cancelpendingaction/", { data: data })
.then(r => {
this.$q.loading.hide();
@@ -150,11 +156,6 @@ export default {
});
});
},
hidePendingActions() {
this.showCompleted = false;
this.selectedStatus = null;
this.$store.commit("logs/CLEAR_PENDING_ACTIONS");
},
rowSelected(pk, status, actiontype) {
this.selectedRow = pk;
this.selectedStatus = status;
@@ -162,6 +163,8 @@ export default {
},
clearRow() {
this.selectedRow = null;
this.selectedStatus = null;
this.actionType = null;
},
rowClass(id, status) {
if (this.selectedRow === id && status !== "completed") {
@@ -172,28 +175,27 @@ export default {
},
},
computed: {
...mapGetters({
hostname: "logs/actionsHostname",
togglePendingActions: "logs/togglePendingActions",
actions: "logs/allPendingActions",
agentpk: "logs/actionsAgentPk",
actionsLoading: "logs/pendingActionsLoading",
}),
url() {
return !!this.agentpk ? `/logs/${this.agentpk}/pendingactions/` : "/logs/allpendingactions/";
},
filter() {
return this.showCompleted ? this.actions : this.actions.filter(k => k.status === "pending");
},
columns() {
return this.agentpk === null ? this.all_columns : this.agent_columns;
return !!this.agentpk ? this.agent_columns : this.all_columns;
},
visibleColumns() {
return this.agentpk === null ? this.all_visibleColumns : this.agent_visibleColumns;
return !!this.agentpk ? this.agent_visibleColumns : this.all_visibleColumns;
},
title() {
return this.agentpk === null ? "All Pending Actions" : `Pending Actions for ${this.hostname}`;
return !!this.agentpk ? `Pending Actions for ${this.hostname}` : "All Pending Actions";
},
completedCount() {
return this.actions.filter(k => k.status === "completed").length;
},
},
created() {
this.getPendingActions();
},
};
</script>

View File

@@ -1,19 +0,0 @@
export default {
methods: {
formatClients(clients) {
return clients.map(client => ({
label: client.client,
value: client.id
})
);
},
formatSites(sites) {
return sites.map(site => ({
label: site.site,
value: site.id,
client: site.client_name
})
);
}
}
};

View File

@@ -94,6 +94,15 @@ export default {
let formatted = months[dt.getMonth()] + "-" + appendLeadingZeroes(dt.getDate()) + "-" + appendLeadingZeroes(dt.getFullYear()) + " - " + appendLeadingZeroes(dt.getHours()) + ":" + appendLeadingZeroes(dt.getMinutes())
return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted
},
formatClientOptions(clients) {
return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites }))
},
formatSiteOptions(sites) {
return sites.map(site => ({ label: site.name, value: site.id }))
},
capitalize(string) {
return string[0].toUpperCase() + string.substring(1)
}
}
};

View File

@@ -2,7 +2,6 @@ import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import { Notify } from "quasar";
import logModule from "./logs";
import alertsModule from "./alerts";
import automationModule from "./automation";
import adminModule from "./admin.js"
@@ -12,7 +11,6 @@ Vue.use(Vuex);
export default function () {
const Store = new Vuex.Store({
modules: {
logs: logModule,
automation: automationModule,
alerts: alertsModule,
admin: adminModule
@@ -210,7 +208,7 @@ export default function () {
return axios.delete(`/tasks/${pk}/automatedtasks/`);
},
getUpdatedSites(context) {
axios.get("/clients/loadclients/").then(r => {
axios.get("/clients/clients/").then(r => {
context.commit("getUpdatedSites", r.data);
});
},
@@ -218,54 +216,56 @@ export default function () {
return axios.get("/clients/clients/");
},
loadSites(context) {
return axios.get("/clients/listsites/");
return axios.get("/clients/sites/");
},
loadAgents(context) {
return axios.get("/agents/listagents/");
},
loadTree({ commit }) {
axios.get("/clients/loadtree/").then(r => {
const input = r.data;
if (
Object.entries(input).length === 0 &&
input.constructor === Object
) {
axios.get("/clients/tree/").then(r => {
if (r.data.length === 0) {
this.$router.push({ name: "InitialSetup" });
}
const output = [];
for (let prop in input) {
let sites_arr = input[prop];
let child_single = [];
for (let i = 0; i < sites_arr.length; i++) {
child_single.push({
label: sites_arr[i].split("|")[0],
id: sites_arr[i].split("|")[1],
raw: `Site|${sites_arr[i]}`,
let output = [];
for (let client of r.data) {
let childSites = [];
for (let site of client.sites) {
let site_color = "black"
if (site.maintenance_mode) { site_color = "warning" }
else if (site.failing_checks) { site_color = "negative" }
childSites.push({
label: site.name,
id: site.id,
raw: `Site|${site.id}`,
header: "generic",
icon: "apartment",
color: sites_arr[i].split("|")[2]
color: site_color
});
}
// sort alphabetically by site name
let alphaSort = child_single.sort((a, b) => a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1);
let client_color = "black"
if (client.maintenance_mode) { client_color = "warning" }
else if (client.failing_checks) { client_color = "negative" }
output.push({
label: prop.split("|")[0],
id: prop.split("|")[1],
raw: `Client|${prop}`,
label: client.name,
id: client.id,
raw: `Client|${client.id}`,
header: "root",
icon: "business",
color: prop.split("|")[2],
children: alphaSort
color: client_color,
children: childSites
});
}
// first sort alphabetically, then move failing clients to the top
const sortedAlpha = output.sort((a, b) => (a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1));
const sortedByFailing = sortedAlpha.sort(a =>
a.color === "negative" ? -1 : 1
);
// move failing clients to the top
const sortedByFailing = output.sort(a => a.color === "negative" ? -1 : 1)
commit("loadTree", sortedByFailing);
//commit("destroySubTable");
});
},
checkVer(context) {

View File

@@ -1,71 +0,0 @@
import axios from "axios";
export default {
namespaced: true,
state: {
toggleLogModal: false,
togglePendingActions: false,
pendingActionsLoading: false,
actionsAgentPk: null,
actionsHostname: null,
allPendingActions: []
},
getters: {
actionsHostname(state) {
return state.actionsHostname;
},
togglePendingActions(state) {
return state.togglePendingActions;
},
allPendingActions(state) {
return state.allPendingActions;
},
actionsAgentPk(state) {
return state.actionsAgentPk;
},
pendingActionsLoading(state) {
return state.pendingActionsLoading;
}
},
mutations: {
PENDING_ACTIONS_LOADING(state, visible) {
state.pendingActionsLoading = visible;
},
TOGGLE_LOG_MODAL(state, action) {
state.toggleLogModal = action;
},
TOGGLE_PENDING_ACTIONS(state, { action, agentpk, hostname }) {
state.actionsAgentPk = agentpk;
state.actionsHostname = hostname;
state.togglePendingActions = action;
},
SET_PENDING_ACTIONS(state, actions) {
state.allPendingActions = actions;
},
CLEAR_PENDING_ACTIONS(state) {
state.togglePendingActions = false;
state.allPendingActions = [];
state.actionsAgentPk = null;
state.actionsHostname = null;
},
},
actions: {
getPendingActions({ commit, state }) {
commit("PENDING_ACTIONS_LOADING", true);
const url = state.actionsAgentPk === null
? "/logs/allpendingactions/"
: `/logs/${state.actionsAgentPk}/pendingactions/`;
axios.get(url).then(r => {
commit("SET_PENDING_ACTIONS", r.data);
commit("PENDING_ACTIONS_LOADING", false);
})
},
loadAuditLogs(context, data) {
return axios.patch("/logs/auditlogs/", data)
},
optionsFilter(context, data) {
return axios.post(`logs/auditlogs/optionsfilter/`, data)
}
}
}

View File

@@ -60,7 +60,7 @@
</q-menu>
</q-chip>
<AlertsIcon />
<!--<AlertsIcon />-->
<q-btn-dropdown flat no-caps stretch :label="user">
<q-list>
@@ -105,13 +105,13 @@
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditModal(props.node)">
<q-item clickable v-close-popup @click="showEditModal(props.node, 'edit')">
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showDeleteModal(props.node)">
<q-item clickable v-close-popup @click="showDeleteModal(props.node, 'delete')">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
@@ -168,10 +168,111 @@
<q-tab name="mixed" label="Mixed" />
</q-tabs>
<q-space />
<q-input v-model="search" label="Search" dense outlined clearable class="q-pr-md q-pb-xs">
<q-input
autogrow
v-model="search"
style="width: 450px"
label="Search"
dense
outlined
clearable
@clear="clearFilter"
class="q-pr-md q-pb-xs"
>
<template v-slot:prepend>
<q-icon name="search" color="primary" />
</template>
<template v-slot:after>
<q-btn round dense flat icon="filter_alt" :color="isFilteringTable ? 'green' : 'black'">
<q-menu>
<q-list dense>
<q-item-label header>Filter Agent Table</q-item-label>
<q-item>
<q-item-section side>
<q-checkbox v-model="filterChecksFailing" />
</q-item-section>
<q-item-section>
<q-item-label>Checks Failing</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section side>
<q-checkbox v-model="filterPatchesPending" />
</q-item-section>
<q-item-section>
<q-item-label>Patches Pending</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section side>
<q-checkbox v-model="filterRebootNeeded" />
</q-item-section>
<q-item-section>
<q-item-label>Reboot Needed</q-item-label>
</q-item-section>
</q-item>
<q-item-label header>Availability</q-item-label>
<q-item>
<q-item-section side>
<q-radio val="all" v-model="filterAvailability" />
</q-item-section>
<q-item-section>
<q-item-label>Show All Agents</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section side>
<q-radio val="online" v-model="filterAvailability" />
</q-item-section>
<q-item-section>
<q-item-label>Show Online Only</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section side>
<q-radio val="offline" v-model="filterAvailability" />
</q-item-section>
<q-item-section>
<q-item-label>Show Offline Only</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section side>
<q-radio val="offline_30days" v-model="filterAvailability" />
</q-item-section>
<q-item-section>
<q-item-label>Show Offline for over 30 days</q-item-label>
</q-item-section>
</q-item>
</q-list>
<div class="row no-wrap q-pa-md">
<div class="column">
<q-btn v-close-popup label="Apply" color="primary" @click="applyFilter" />
</div>
<q-space />
<div class="column">
<q-btn label="Clear" @click="clearFilter" />
</div>
</div>
</q-menu>
</q-btn>
</template>
</q-input>
</div>
<AgentTable
@@ -196,21 +297,23 @@
</q-splitter>
</q-page-container>
<!-- edit client modal -->
<q-dialog v-model="showEditClientModal">
<EditClients @close="showEditClientModal = false" @edited="refreshEntireSite" />
<!-- client form modal -->
<q-dialog v-model="showClientsFormModal" @hide="closeClientsFormModal">
<ClientsForm
@close="closeClientsFormModal"
:op="clientOp"
:clientpk="deleteEditModalPk"
@edited="refreshEntireSite"
/>
</q-dialog>
<!-- edit site modal -->
<q-dialog v-model="showEditSiteModal">
<EditSites @close="showEditSiteModal = false" @edited="refreshEntireSite" />
</q-dialog>
<!-- delete client modal -->
<q-dialog v-model="showDeleteClientModal">
<DeleteClient @close="showDeleteClientModal = false" @edited="refreshEntireSite" />
</q-dialog>
<!-- delete site modal -->
<q-dialog v-model="showDeleteSiteModal">
<DeleteSite @close="showDeleteSiteModal = false" @edited="refreshEntireSite" />
<q-dialog v-model="showSitesFormModal" @hide="closeClientsFormModal">
<SitesForm
@close="closeClientsFormModal"
:op="clientOp"
:sitepk="deleteEditModalPk"
@edited="refreshEntireSite"
/>
</q-dialog>
<!-- add policy modal -->
<q-dialog v-model="showPolicyAddModal">
@@ -228,10 +331,8 @@ import AgentTable from "@/components/AgentTable";
import SubTableTabs from "@/components/SubTableTabs";
import AlertsIcon from "@/components/AlertsIcon";
import PolicyAdd from "@/components/automation/modals/PolicyAdd";
import EditSites from "@/components/modals/clients/EditSites";
import EditClients from "@/components/modals/clients/EditClients";
import DeleteClient from "@/components/modals/clients/DeleteClient";
import DeleteSite from "@/components/modals/clients/DeleteSite";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
export default {
components: {
@@ -240,18 +341,16 @@ export default {
SubTableTabs,
AlertsIcon,
PolicyAdd,
EditSites,
EditClients,
DeleteClient,
DeleteSite,
ClientsForm,
SitesForm,
},
data() {
return {
showEditClientModal: false,
showEditSiteModal: false,
showDeleteClientModal: false,
showDeleteSiteModal: false,
showClientsFormModal: false,
showSitesFormModal: false,
showPolicyAddModal: false,
deleteEditModalPk: null,
clientOp: null,
policyAddType: null,
policyAddPk: null,
serverCount: 0,
@@ -266,7 +365,12 @@ export default {
siteActive: "",
frame: [],
poll: null,
search: null,
search: "",
filterTextLength: 0,
filterAvailability: "all",
filterPatchesPending: false,
filterChecksFailing: false,
filterRebootNeeded: false,
currentTRMMVersion: null,
columns: [
{
@@ -280,18 +384,21 @@ export default {
{
name: "checks-status",
align: "left",
field: "checks",
sortable: true,
sort: (a, b, rowA, rowB) => parseInt(b.failing) - a.failing,
},
{
name: "client",
name: "client_name",
label: "Client",
field: "client",
field: "client_name",
sortable: true,
align: "left",
},
{
name: "site",
name: "site_name",
label: "Site",
field: "site",
field: "site_name",
sortable: true,
align: "left",
},
@@ -325,17 +432,21 @@ export default {
},
{
name: "patchespending",
field: "patches_pending",
align: "left",
sortable: true,
},
{
name: "agentstatus",
field: "status",
align: "left",
sortable: true,
},
{
name: "needsreboot",
field: "needs_reboot",
align: "left",
sortable: true,
},
{
name: "lastseen",
@@ -356,8 +467,8 @@ export default {
"smsalert",
"emailalert",
"checks-status",
"client",
"site",
"client_name",
"site_name",
"hostname",
"description",
"user",
@@ -369,6 +480,12 @@ export default {
],
};
},
watch: {
search(newVal, oldVal) {
if (newVal === "") this.clearFilter();
else if (newVal.length < this.filterTextLength) this.clearFilter();
},
},
methods: {
refreshEntireSite() {
this.$store.dispatch("loadTree");
@@ -394,30 +511,25 @@ export default {
loadFrame(activenode, destroySub = true) {
if (destroySub) this.$store.commit("destroySubTable");
let client, site, url;
try {
client = this.$refs.tree.meta[activenode].parent.key.split("|")[1];
site = activenode.split("|")[1];
url = `/agents/bysite/${client}/${site}/`;
} catch (e) {
try {
client = activenode.split("|")[1];
} catch (e) {
return false;
let url, urlType, id;
if (typeof activenode === "string") {
urlType = activenode.split("|")[0];
id = activenode.split("|")[1];
if (urlType === "Client") {
url = `/agents/byclient/${id}/`;
} else if (urlType === "Site") {
url = `/agents/bysite/${id}/`;
}
if (client === null || client === undefined) {
url = null;
} else {
url = `/agents/byclient/${client}/`;
if (url) {
this.$store.commit("AGENT_TABLE_LOADING", true);
axios.get(url).then(r => {
this.frame = r.data;
this.$store.commit("AGENT_TABLE_LOADING", false);
});
}
}
if (url) {
this.$store.commit("AGENT_TABLE_LOADING", true);
axios.get(url).then(r => {
this.frame = r.data;
this.$store.commit("AGENT_TABLE_LOADING", false);
});
}
},
getTree() {
this.loadAllClients();
@@ -451,20 +563,30 @@ export default {
this.showPolicyAddModal = true;
}
},
showEditModal(node) {
showEditModal(node, op) {
this.deleteEditModalPk = node.id;
this.clientOp = op;
if (node.children) {
this.showEditClientModal = true;
this.showClientsFormModal = true;
} else {
this.showEditSiteModal = true;
this.showSitesFormModal = true;
}
},
showDeleteModal(node) {
showDeleteModal(node, op) {
this.deleteEditModalPk = node.id;
this.clientOp = op;
if (node.children) {
this.showDeleteClientModal = true;
this.showClientsFormModal = true;
} else {
this.showDeleteSiteModal = true;
this.showSitesFormModal = true;
}
},
closeClientsFormModal() {
this.showClientsFormModal = false;
this.showSitesFormModal = false;
this.deleteEditModalPk = null;
this.clientOp = null;
},
reload() {
this.$store.dispatch("reload");
},
@@ -510,6 +632,51 @@ export default {
menuMaintenanceText(node) {
return node.color === "warning" ? "Disable Maintenance Mode" : "Enable Maintenance Mode";
},
clearFilter() {
this.filterPatchesPending = false;
this.filterRebootNeeded = false;
this.filterChecksFailing = false;
this.filterAvailability = "all";
this.search = "";
},
applyFilter() {
// clear search if availability changes to all
if (
this.filterAvailability === "all" &&
(this.search.includes("is:online") || this.search.includes("is:offline") || this.search.includes("is:expired"))
)
this.clearFilter();
// don't apply filter if nothing is being filtered
if (!this.isFilteringTable) return;
let filterText = "";
if (this.filterPatchesPending) {
filterText += "is:patchespending ";
}
if (this.filterChecksFailing) {
filterText += "is:checksfailing ";
}
if (this.filterRebootNeeded) {
filterText += "is:rebootneeded ";
}
if (this.filterAvailability !== "all") {
if (this.filterAvailability === "online") {
filterText += "is:online ";
} else if (this.filterAvailability === "offline") {
filterText += "is:offline ";
} else if (this.filterAvailability === "offline_30days") {
filterText += "is:expired ";
}
}
this.search = filterText;
this.filterTextLength = filterText.length - 1;
},
},
computed: {
...mapState({
@@ -534,6 +701,14 @@ export default {
site: this.siteActive,
};
},
isFilteringTable() {
return (
this.filterPatchesPending ||
this.filterChecksFailing ||
this.filterRebootNeeded ||
this.filterAvailability !== "all"
);
},
totalAgents() {
return this.serverCount + this.workstationCount;
},

View File

@@ -10,12 +10,7 @@
<q-form @submit.prevent="finish">
<q-card-section>
<div>Add Client:</div>
<q-input
dense
outlined
v-model="client.client"
:rules="[ val => !!val || '*Required' ]"
>
<q-input dense outlined v-model="client.client" :rules="[val => !!val || '*Required']">
<template v-slot:prepend>
<q-icon name="business" />
</template>
@@ -23,12 +18,7 @@
</q-card-section>
<q-card-section>
<div>Add Site:</div>
<q-input
dense
outlined
v-model="client.site"
:rules="[ val => !!val || '*Required' ]"
>
<q-input dense outlined v-model="client.site" :rules="[val => !!val || '*Required']">
<template v-slot:prepend>
<q-icon name="apartment" />
</template>
@@ -42,7 +32,7 @@
<div class="row">
<q-file
v-model="meshagent"
:rules="[ val => !!val || '*Required' ]"
:rules="[val => !!val || '*Required']"
label="Upload MeshAgent"
stack-label
filled
@@ -112,8 +102,8 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
if (e.response.data.client) {
this.notifyError(e.response.data.client);
if (e.response.data.name) {
this.notifyError(e.response.data.name);
} else {
this.notifyError(e.response.data.non_field_errors);
}

View File

@@ -20,7 +20,7 @@
<q-tab-panels v-model="tab">
<q-tab-panel name="terminal">
<iframe
style="overflow:hidden;height:715px;"
style="overflow: hidden; height: 715px"
:src="terminal"
width="100%"
height="100%"
@@ -37,13 +37,7 @@
<EventLog :pk="pk" />
</q-tab-panel>
<q-tab-panel name="filebrowser">
<iframe
style="overflow:hidden;height:715px;"
:src="file"
width="100%"
height="100%"
scrolling="no"
></iframe>
<iframe style="overflow: hidden; height: 715px" :src="file" width="100%" height="100%" scrolling="no"></iframe>
</q-tab-panel>
</q-tab-panels>
</div>
@@ -75,7 +69,7 @@ export default {
axios.get(`/agents/${this.pk}/meshcentral/`).then(r => {
this.terminal = r.data.terminal;
this.file = r.data.file;
this.title = `${r.data.hostname} | Remote Background`;
this.title = `${r.data.hostname} - ${r.data.client} - ${r.data.site} | Remote Background`;
});
},
},

View File

@@ -6,25 +6,12 @@
<q-badge :color="statusColor" :label="status" />
</span>
<q-space />
<q-btn
class="q-mr-md"
color="primary"
size="sm"
label="Restart Connection"
icon="refresh"
@click="restart"
/>
<q-btn
color="negative"
size="sm"
label="Recover Connection"
icon="fas fa-first-aid"
@click="repair"
/>
<q-btn class="q-mr-md" color="primary" size="sm" label="Restart Connection" icon="refresh" @click="restart" />
<q-btn color="negative" size="sm" label="Recover Connection" icon="fas fa-first-aid" @click="repair" />
<q-space />
</div>
<q-video v-show="visible" :ratio="16/9" :src="control"></q-video>
<q-video v-show="visible" :ratio="16 / 9" :src="control"></q-video>
</div>
</template>
@@ -39,6 +26,7 @@ export default {
control: "",
visible: true,
status: null,
title: "",
};
},
computed: {
@@ -60,6 +48,11 @@ export default {
}
},
},
meta() {
return {
title: this.title,
};
},
methods: {
genURL() {
this.$q.loading.show();
@@ -67,6 +60,7 @@ export default {
this.$axios
.get(`/agents/${this.$route.params.pk}/meshcentral/`)
.then(r => {
this.title = `${r.data.hostname} - ${r.data.client} - ${r.data.site} | Take Control`;
this.control = r.data.control;
this.status = r.data.status;
this.$q.loading.hide();