Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aec4257da | ||
|
|
d654f856d1 | ||
|
|
8d3b0a2069 | ||
|
|
54a96f35e8 | ||
|
|
2dc56d72f6 | ||
|
|
4b6ddb535a | ||
|
|
697e2250d4 | ||
|
|
6a75035b04 | ||
|
|
46b166bc41 | ||
|
|
6bbc0987ad | ||
|
|
8c480b43e2 | ||
|
|
079f6731dd | ||
|
|
f99d5754cd | ||
|
|
bf8c41e362 | ||
|
|
7f7bc06eb4 | ||
|
|
b507e59359 | ||
|
|
72078ac6bf | ||
|
|
0db9e082e2 | ||
|
|
0c44394a76 | ||
|
|
e20aa0cf04 | ||
|
|
fa30a50a91 | ||
|
|
f6629ff12c | ||
|
|
4128e4db73 | ||
|
|
34cac5685f | ||
|
|
4c9b91d536 | ||
|
|
95b95a8998 | ||
|
|
617738bb28 | ||
|
|
f6ac15d790 | ||
|
|
79e1324ead | ||
|
|
4ef9f010f0 | ||
|
|
e6e8865708 | ||
|
|
33cd8f9b0d | ||
|
|
a7138e019c | ||
|
|
049b72bd50 | ||
|
|
f3f1987515 | ||
|
|
a9395d89cd | ||
|
|
bc2fcee8ba | ||
|
|
242ff2ceca | ||
|
|
70790ac762 | ||
|
|
0f98869b61 | ||
|
|
9ddc02140f | ||
|
|
ee631b3d20 | ||
|
|
32f56e60d8 | ||
|
|
6102b51d9e | ||
|
|
2baee27859 | ||
|
|
144a3dedbb | ||
|
|
f90d966f1a | ||
|
|
b188e2ea97 | ||
|
|
b63b2002a9 | ||
|
|
059edc36e4 | ||
|
|
902034ecf0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
20
api/tacticalrmm/agents/migrations/0021_agent_site_link.py
Normal file
20
api/tacticalrmm/agents/migrations/0021_agent_site_link.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
21
api/tacticalrmm/agents/migrations/0023_auto_20201101_2312.py
Normal file
21
api/tacticalrmm/agents/migrations/0023_auto_20201101_2312.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/agents/migrations/0024_auto_20201101_2319.py
Normal file
18
api/tacticalrmm/agents/migrations/0024_auto_20201101_2319.py
Normal 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',
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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),
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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/"
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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',)},
|
||||
),
|
||||
]
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"]);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
189
web/src/components/modals/clients/ClientsForm.vue
Normal file
189
web/src/components/modals/clients/ClientsForm.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
197
web/src/components/modals/clients/SitesForm.vue
Normal file
197
web/src/components/modals/clients/SitesForm.vue
Normal 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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user