Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159ecd3e4f | ||
|
|
717803c665 | ||
|
|
0d40589e8a | ||
|
|
8c5544bfad | ||
|
|
0c9be9f84f | ||
|
|
497729ecd6 | ||
|
|
21a8efa3b8 | ||
|
|
c2f942a51e | ||
|
|
63b4b95240 | ||
|
|
955f37e005 | ||
|
|
cd2ae89b0e | ||
|
|
0b013fa438 | ||
|
|
478b657354 | ||
|
|
65b6aabe69 | ||
|
|
3fabae5b5f | ||
|
|
96c46a9e12 | ||
|
|
381b93e8eb | ||
|
|
f51e5b6fbf | ||
|
|
20befd1ca2 | ||
|
|
ac6c6130f8 | ||
|
|
d776a2325c | ||
|
|
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 |
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
|
||||
|
||||
26
api/tacticalrmm/accounts/migrations/0006_user_agent.py
Normal file
26
api/tacticalrmm/accounts/migrations/0006_user_agent.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-10 20:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("agents", "0024_auto_20201101_2319"),
|
||||
("accounts", "0005_auto_20201002_1303"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="agent",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="user",
|
||||
to="agents.agent",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-01 22:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def link_agents_to_users(apps, schema_editor):
|
||||
Agent = apps.get_model("agents", "Agent")
|
||||
User = apps.get_model("accounts", "User")
|
||||
for agent in Agent.objects.all():
|
||||
user = User.objects.filter(username=agent.agent_id).first()
|
||||
|
||||
if user:
|
||||
user.agent = agent
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0006_user_agent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(link_agents_to_users, migrations.RunPython.noop),
|
||||
]
|
||||
18
api/tacticalrmm/accounts/migrations/0008_user_dark_mode.py
Normal file
18
api/tacticalrmm/accounts/migrations/0008_user_dark_mode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-12 00:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0007_update_agent_primary_key'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='dark_mode',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,15 @@ from logs.models import BaseAuditModel
|
||||
class User(AbstractUser, BaseAuditModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
totp_key = models.CharField(max_length=50, null=True, blank=True)
|
||||
dark_mode = models.BooleanField(default=True)
|
||||
|
||||
agent = models.OneToOneField(
|
||||
"agents.Agent",
|
||||
related_name="user",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def serialize(user):
|
||||
|
||||
@@ -195,6 +195,14 @@ class TestUserAction(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_darkmode(self):
|
||||
url = "/accounts/users/ui/"
|
||||
data = {"dark_mode": False}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
|
||||
class TestTOTPSetup(TacticalTestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -7,4 +7,5 @@ urlpatterns = [
|
||||
path("users/reset/", views.UserActions.as_view()),
|
||||
path("users/reset_totp/", views.UserActions.as_view()),
|
||||
path("users/setup_totp/", views.TOTPSetup.as_view()),
|
||||
path("users/ui/", views.UserUI.as_view()),
|
||||
]
|
||||
|
||||
@@ -74,8 +74,7 @@ class LoginView(KnoxLoginView):
|
||||
|
||||
class GetAddUsers(APIView):
|
||||
def get(self, request):
|
||||
agents = Agent.objects.values_list("agent_id", flat=True)
|
||||
users = User.objects.exclude(username__in=agents)
|
||||
users = User.objects.filter(agent=None)
|
||||
|
||||
return Response(UserSerializer(users, many=True).data)
|
||||
|
||||
@@ -157,3 +156,11 @@ class TOTPSetup(APIView):
|
||||
return Response(TOTPSetupSerializer(user).data)
|
||||
|
||||
return Response("totp token already set")
|
||||
|
||||
|
||||
class UserUI(APIView):
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
user.dark_mode = request.data["dark_mode"]
|
||||
user.save(update_fields=["dark_mode"])
|
||||
return Response("ok")
|
||||
@@ -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,9 +95,9 @@ class Agent(BaseAuditModel):
|
||||
@property
|
||||
def arch(self):
|
||||
if self.operating_system is not None:
|
||||
if "64 bit" in self.operating_system:
|
||||
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
|
||||
return "64"
|
||||
elif "32 bit" in self.operating_system:
|
||||
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
|
||||
return "32"
|
||||
return None
|
||||
|
||||
@@ -281,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()
|
||||
@@ -667,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."
|
||||
),
|
||||
@@ -681,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."
|
||||
),
|
||||
@@ -695,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):
|
||||
@@ -703,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 = (
|
||||
|
||||
@@ -33,6 +33,9 @@ def send_agent_update_task(pks, version):
|
||||
|
||||
# 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
|
||||
@@ -47,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={
|
||||
@@ -54,13 +60,15 @@ def send_agent_update_task(pks, version):
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
logger.info(f"{agent.salt_id}: {r}")
|
||||
sleep(10)
|
||||
|
||||
|
||||
@app.task
|
||||
def auto_self_agent_update_task(test=False):
|
||||
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")
|
||||
@@ -69,6 +77,7 @@ def auto_self_agent_update_task(test=False):
|
||||
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))
|
||||
|
||||
@@ -78,6 +87,9 @@ def auto_self_agent_update_task(test=False):
|
||||
|
||||
# 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
|
||||
@@ -92,6 +104,9 @@ def auto_self_agent_update_task(test=False):
|
||||
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={
|
||||
@@ -99,8 +114,8 @@ def auto_self_agent_update_task(test=False):
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
if not test:
|
||||
sleep(10)
|
||||
logger.info(f"{agent.salt_id}: {r}")
|
||||
sleep(10)
|
||||
|
||||
|
||||
@app.task
|
||||
|
||||
@@ -6,20 +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, OLD_64_PY_AGENT, OLD_32_PY_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()
|
||||
|
||||
@@ -30,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()
|
||||
|
||||
@@ -103,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)
|
||||
@@ -278,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",
|
||||
@@ -382,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,
|
||||
@@ -417,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
|
||||
@@ -441,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)
|
||||
|
||||
@@ -451,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)
|
||||
|
||||
@@ -575,7 +608,7 @@ class TestAgentViews(BaseTestCase):
|
||||
payload = {
|
||||
"mode": "command",
|
||||
"target": "client",
|
||||
"client": "Google",
|
||||
"client": self.agent.client.id,
|
||||
"site": None,
|
||||
"agentPKs": [
|
||||
self.agent.pk,
|
||||
@@ -591,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,
|
||||
],
|
||||
@@ -604,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)
|
||||
|
||||
@@ -643,7 +660,7 @@ class TestAgentViews(BaseTestCase):
|
||||
payload = {
|
||||
"mode": "install",
|
||||
"target": "client",
|
||||
"client": "Google",
|
||||
"client": self.agent.client.id,
|
||||
"site": None,
|
||||
"agentPKs": [
|
||||
self.agent.pk,
|
||||
@@ -712,7 +729,6 @@ class TestAgentViews(BaseTestCase):
|
||||
class TestAgentViewsNew(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_agent_counts(self):
|
||||
url = "/agents/agent_counts/"
|
||||
@@ -748,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)
|
||||
@@ -782,8 +798,125 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
|
||||
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")
|
||||
def test_auto_self_agent_update_task(self, 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",
|
||||
@@ -791,7 +924,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
version="1.0.0",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
@@ -810,7 +943,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
version="1.0.0",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
@@ -828,7 +961,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
operating_system=None,
|
||||
version="1.0.0",
|
||||
)
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_not_called()
|
||||
self.agentNone.delete()
|
||||
salt_api_async.reset_mock()
|
||||
@@ -841,7 +974,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
)
|
||||
self.coresettings.agent_auto_update = False
|
||||
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_not_called()
|
||||
|
||||
# reset core settings
|
||||
@@ -857,7 +990,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
version="0.11.1",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
@@ -876,7 +1009,7 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
version="0.11.1",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s(test=True).apply()
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
@@ -884,4 +1017,4 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
"url": OLD_32_PY_AGENT,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
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),
|
||||
|
||||
@@ -80,13 +80,6 @@ def ping(request, pk):
|
||||
@api_view(["DELETE"])
|
||||
def uninstall(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
# just in case agent-user gets deleted accidentaly from django-admin
|
||||
# we can still remove the agent
|
||||
try:
|
||||
user = User.objects.get(username=agent.agent_id)
|
||||
user.delete()
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
salt_id = agent.salt_id
|
||||
name = agent.hostname
|
||||
@@ -103,7 +96,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 +138,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 +244,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 +279,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 +290,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 +319,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 +396,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 +452,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 +561,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 +595,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 +805,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":
|
||||
@@ -824,6 +817,8 @@ def bulk(request):
|
||||
|
||||
minions = [agent.salt_id for agent in agents]
|
||||
|
||||
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
|
||||
|
||||
if request.data["mode"] == "command":
|
||||
r = Agent.salt_batch_async(
|
||||
minions=minions,
|
||||
@@ -904,14 +899,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/"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -16,9 +17,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
|
||||
@@ -146,10 +145,23 @@ class CheckRunner(APIView):
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request):
|
||||
from logs.models import AuditLog
|
||||
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
check.last_run = djangotime.now()
|
||||
check.save(update_fields=["last_run"])
|
||||
status = check.handle_checkv2(request.data)
|
||||
|
||||
# create audit entry
|
||||
AuditLog.objects.create(
|
||||
username=check.agent.hostname,
|
||||
agent=check.agent.hostname,
|
||||
object_type="agent",
|
||||
action="check_run",
|
||||
message=f"{check.readable_desc} was run on {check.agent.hostname}. Status: {status}",
|
||||
after_value=Check.serialize(check),
|
||||
)
|
||||
|
||||
return Response(status)
|
||||
|
||||
|
||||
@@ -167,6 +179,8 @@ class TaskRunner(APIView):
|
||||
return Response(TaskGOGetSerializer(task).data)
|
||||
|
||||
def patch(self, request, pk, agentid):
|
||||
from logs.models import AuditLog
|
||||
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
@@ -175,6 +189,17 @@ class TaskRunner(APIView):
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_run=djangotime.now())
|
||||
|
||||
new_task = AutomatedTask.objects.get(pk=task.pk)
|
||||
AuditLog.objects.create(
|
||||
username=agent.hostname,
|
||||
agent=agent.hostname,
|
||||
object_type="agent",
|
||||
action="task_run",
|
||||
message=f"Scheduled Task {task.name} was run on {agent.hostname}",
|
||||
after_value=AutomatedTask.serialize(new_task),
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@@ -387,31 +412,9 @@ class MeshExe(APIView):
|
||||
|
||||
|
||||
class NewAgent(APIView):
|
||||
""" For the installer """
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Creates and returns the agents auth token
|
||||
which is stored in the agent's local db
|
||||
and used to authenticate every agent request
|
||||
"""
|
||||
from logs.models import AuditLog
|
||||
|
||||
if "agent_id" not in request.data:
|
||||
return notify_error("Invalid payload")
|
||||
|
||||
agentid = request.data["agent_id"]
|
||||
if Agent.objects.filter(agent_id=agentid).exists():
|
||||
return notify_error(
|
||||
"Agent already exists. Remove old agent first if trying to re-install"
|
||||
)
|
||||
|
||||
user = User.objects.create_user(
|
||||
username=agentid, password=User.objects.make_random_password(60)
|
||||
)
|
||||
token = Token.objects.create(user=user)
|
||||
return Response({"token": token.key})
|
||||
|
||||
def patch(self, request):
|
||||
""" Creates the agent """
|
||||
|
||||
if Agent.objects.filter(agent_id=request.data["agent_id"]).exists():
|
||||
@@ -419,14 +422,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"],
|
||||
@@ -436,6 +435,14 @@ class NewAgent(APIView):
|
||||
agent.salt_id = f"{agent.hostname}-{agent.pk}"
|
||||
agent.save(update_fields=["salt_id"])
|
||||
|
||||
user = User.objects.create_user(
|
||||
username=request.data["agent_id"],
|
||||
agent=agent,
|
||||
password=User.objects.make_random_password(60),
|
||||
)
|
||||
|
||||
token = Token.objects.create(user=user)
|
||||
|
||||
if agent.monitoring_type == "workstation":
|
||||
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
|
||||
else:
|
||||
@@ -445,4 +452,20 @@ class NewAgent(APIView):
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
return Response({"pk": agent.pk, "saltid": f"{agent.hostname}-{agent.pk}"})
|
||||
# create agent install audit record
|
||||
AuditLog.objects.create(
|
||||
username=request.user,
|
||||
agent=agent.hostname,
|
||||
object_type="agent",
|
||||
action="agent_install",
|
||||
message=f"{request.user} installed new agent {agent.hostname}",
|
||||
after_value=Agent.serialize(agent),
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"pk": agent.pk,
|
||||
"saltid": f"{agent.hostname}-{agent.pk}",
|
||||
"token": token.key,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -68,7 +68,9 @@ def version(request):
|
||||
|
||||
@api_view()
|
||||
def dashboard_info(request):
|
||||
return Response({"trmm_version": settings.TRMM_VERSION})
|
||||
return Response(
|
||||
{"trmm_version": settings.TRMM_VERSION, "dark_mode": request.user.dark_mode}
|
||||
)
|
||||
|
||||
|
||||
@api_view()
|
||||
|
||||
18
api/tacticalrmm/logs/migrations/0008_auto_20201110_1431.py
Normal file
18
api/tacticalrmm/logs/migrations/0008_auto_20201110_1431.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-10 14:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0007_auditlog_debug_info'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('login', 'User Login'), ('failed_login', 'Failed User Login'), ('delete', 'Delete Object'), ('modify', 'Modify Object'), ('add', 'Add Object'), ('view', 'View Object'), ('check_run', 'Check Run'), ('task_run', 'Task Run'), ('remote_session', 'Remote Session'), ('execute_script', 'Execute Script'), ('execute_command', 'Execute Command')], max_length=100),
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/logs/migrations/0009_auto_20201110_1431.py
Normal file
18
api/tacticalrmm/logs/migrations/0009_auto_20201110_1431.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-10 14:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0008_auto_20201110_1431'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('login', 'User Login'), ('failed_login', 'Failed User Login'), ('delete', 'Delete Object'), ('modify', 'Modify Object'), ('add', 'Add Object'), ('view', 'View Object'), ('check_run', 'Check Run'), ('task_run', 'Task Run'), ('agent_install', 'Agent Install'), ('remote_session', 'Remote Session'), ('execute_script', 'Execute Script'), ('execute_command', 'Execute Command')], max_length=100),
|
||||
),
|
||||
]
|
||||
23
api/tacticalrmm/logs/migrations/0010_auto_20201110_2238.py
Normal file
23
api/tacticalrmm/logs/migrations/0010_auto_20201110_2238.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-10 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0009_auto_20201110_1431'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('login', 'User Login'), ('failed_login', 'Failed User Login'), ('delete', 'Delete Object'), ('modify', 'Modify Object'), ('add', 'Add Object'), ('view', 'View Object'), ('check_run', 'Check Run'), ('task_run', 'Task Run'), ('agent_install', 'Agent Install'), ('remote_session', 'Remote Session'), ('execute_script', 'Execute Script'), ('execute_command', 'Execute Command'), ('bulk_action', 'Bulk Action')], max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='object_type',
|
||||
field=models.CharField(choices=[('user', 'User'), ('script', 'Script'), ('agent', 'Agent'), ('policy', 'Policy'), ('winupdatepolicy', 'Patch Policy'), ('client', 'Client'), ('site', 'Site'), ('check', 'Check'), ('automatedtask', 'Automated Task'), ('coresettings', 'Core Settings'), ('bulk', 'Bulk')], max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime as dt
|
||||
import json
|
||||
from abc import abstractmethod
|
||||
from django.db import models
|
||||
from tacticalrmm.middleware import get_username, get_debug_info
|
||||
@@ -15,9 +16,13 @@ AUDIT_ACTION_TYPE_CHOICES = [
|
||||
("modify", "Modify Object"),
|
||||
("add", "Add Object"),
|
||||
("view", "View Object"),
|
||||
("check_run", "Check Run"),
|
||||
("task_run", "Task Run"),
|
||||
("agent_install", "Agent Install"),
|
||||
("remote_session", "Remote Session"),
|
||||
("execute_script", "Execute Script"),
|
||||
("execute_command", "Execute Command"),
|
||||
("bulk_action", "Bulk Action"),
|
||||
]
|
||||
|
||||
AUDIT_OBJECT_TYPE_CHOICES = [
|
||||
@@ -31,6 +36,7 @@ AUDIT_OBJECT_TYPE_CHOICES = [
|
||||
("check", "Check"),
|
||||
("automatedtask", "Automated Task"),
|
||||
("coresettings", "Core Settings"),
|
||||
("bulk", "Bulk"),
|
||||
]
|
||||
|
||||
# taskaction details format
|
||||
@@ -170,6 +176,45 @@ class AuditLog(models.Model):
|
||||
debug_info=debug_info,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def audit_bulk_action(username, action, affected, debug_info={}):
|
||||
from clients.models import Client, Site
|
||||
from agents.models import Agent
|
||||
from scripts.models import Script
|
||||
|
||||
target = ""
|
||||
agents = None
|
||||
|
||||
if affected["target"] == "all":
|
||||
target = "on all agents"
|
||||
elif affected["target"] == "client":
|
||||
client = Client.objects.get(pk=affected["client"])
|
||||
target = f"on all agents within client: {client.name}"
|
||||
elif affected["target"] == "site":
|
||||
site = Site.objects.get(pk=affected["site"])
|
||||
target = f"on all agents within site: {site.client.name}\\{site.name}"
|
||||
elif affected["target"] == "agents":
|
||||
agents = Agent.objects.filter(pk__in=affected["agentPKs"]).values_list(
|
||||
"hostname", flat=True
|
||||
)
|
||||
target = "on multiple agents"
|
||||
|
||||
if action == "script":
|
||||
script = Script.objects.get(pk=affected["scriptPK"])
|
||||
action = f"script: {script.name}"
|
||||
|
||||
if agents:
|
||||
affected["agent_hostnames"] = list(agents)
|
||||
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
object_type="bulk",
|
||||
action="bulk_action",
|
||||
message=f"{username} executed bulk {action} {target}",
|
||||
debug_info=debug_info,
|
||||
after_value=affected,
|
||||
)
|
||||
|
||||
|
||||
class DebugLog(models.Model):
|
||||
pass
|
||||
@@ -246,28 +291,31 @@ class BaseAuditModel(models.Model):
|
||||
|
||||
before_value = {}
|
||||
object_class = type(self)
|
||||
object_name = object_class.__name__.lower()
|
||||
username = get_username()
|
||||
|
||||
# populate created_by and modified_by fields on instance
|
||||
if not getattr(self, "created_by", None):
|
||||
self.created_by = get_username()
|
||||
self.created_by = username
|
||||
if hasattr(self, "modified_by"):
|
||||
self.modified_by = get_username()
|
||||
self.modified_by = username
|
||||
|
||||
# capture object properties before edit
|
||||
if self.pk:
|
||||
before_value = object_class.objects.get(pk=self.id)
|
||||
|
||||
# dont create entry for agent add since that is done in view
|
||||
if not self.pk:
|
||||
AuditLog.audit_object_add(
|
||||
get_username(),
|
||||
object_class.__name__.lower(),
|
||||
username,
|
||||
object_name,
|
||||
object_class.serialize(self),
|
||||
self.__str__(),
|
||||
debug_info=get_debug_info(),
|
||||
)
|
||||
else:
|
||||
AuditLog.audit_object_changed(
|
||||
get_username(),
|
||||
username,
|
||||
object_class.__name__.lower(),
|
||||
object_class.serialize(before_value),
|
||||
object_class.serialize(self),
|
||||
@@ -280,6 +328,7 @@ class BaseAuditModel(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
if get_username():
|
||||
|
||||
object_class = type(self)
|
||||
AuditLog.audit_object_delete(
|
||||
get_username(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class TestAuditViews(TacticalTestCase):
|
||||
self.setup_coresettings()
|
||||
|
||||
def create_audit_records(self):
|
||||
|
||||
# create clients for client filter
|
||||
site = baker.make("clients.Site")
|
||||
baker.make_recipe("agents.agent", site=site, hostname="AgentHostname1")
|
||||
# user jim agent logs
|
||||
baker.make_recipe(
|
||||
"logs.agent_logs",
|
||||
@@ -75,11 +79,13 @@ class TestAuditViews(TacticalTestCase):
|
||||
_quantity=13,
|
||||
)
|
||||
|
||||
return site
|
||||
|
||||
def test_get_audit_logs(self):
|
||||
url = "/logs/auditlogs/"
|
||||
|
||||
# create data
|
||||
self.create_audit_records()
|
||||
site = self.create_audit_records()
|
||||
|
||||
# test data and result counts
|
||||
data = [
|
||||
@@ -111,6 +117,9 @@ class TestAuditViews(TacticalTestCase):
|
||||
"count": 40,
|
||||
},
|
||||
{"filter": {"timeFilter": 35, "userFilter": ["james", "jim"]}, "count": 81},
|
||||
{"filter": {"objectFilter": ["user"]}, "count": 26},
|
||||
{"filter": {"actionFilter": ["login"]}, "count": 12},
|
||||
{"filter": {"clientFilter": [site.client.id]}, "count": 23},
|
||||
]
|
||||
|
||||
for req in data:
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone as djangotime
|
||||
from django.db.models import Q
|
||||
from datetime import datetime as dt
|
||||
|
||||
from rest_framework.response import Response
|
||||
@@ -22,32 +23,52 @@ from .tasks import cancel_pending_action_task
|
||||
|
||||
class GetAuditLogs(APIView):
|
||||
def patch(self, request):
|
||||
from clients.models import Client
|
||||
from agents.models import Agent
|
||||
|
||||
auditLogs = None
|
||||
if "agentFilter" in request.data and "userFilter" in request.data:
|
||||
audit_logs = AuditLog.objects.filter(
|
||||
agent__in=request.data["agentFilter"],
|
||||
username__in=request.data["userFilter"],
|
||||
agentFilter = Q()
|
||||
clientFilter = Q()
|
||||
actionFilter = Q()
|
||||
objectFilter = Q()
|
||||
userFilter = Q()
|
||||
timeFilter = Q()
|
||||
|
||||
if "agentFilter" in request.data:
|
||||
agentFilter = Q(agent__in=request.data["agentFilter"])
|
||||
|
||||
elif "clientFilter" in request.data:
|
||||
clients = Client.objects.filter(
|
||||
pk__in=request.data["clientFilter"]
|
||||
).values_list("id")
|
||||
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
|
||||
"hostname"
|
||||
)
|
||||
clientFilter = Q(agent__in=agents)
|
||||
|
||||
elif "userFilter" in request.data:
|
||||
audit_logs = AuditLog.objects.filter(
|
||||
username__in=request.data["userFilter"]
|
||||
)
|
||||
if "userFilter" in request.data:
|
||||
userFilter = Q(username__in=request.data["userFilter"])
|
||||
|
||||
elif "agentFilter" in request.data:
|
||||
audit_logs = AuditLog.objects.filter(agent__in=request.data["agentFilter"])
|
||||
if "actionFilter" in request.data:
|
||||
actionFilter = Q(action__in=request.data["actionFilter"])
|
||||
|
||||
else:
|
||||
audit_logs = AuditLog.objects.all()
|
||||
if "objectFilter" in request.data:
|
||||
objectFilter = Q(object_type__in=request.data["objectFilter"])
|
||||
|
||||
if audit_logs and "timeFilter" in request.data:
|
||||
audit_logs = audit_logs.filter(
|
||||
if "timeFilter" in request.data:
|
||||
timeFilter = Q(
|
||||
entry_time__lte=djangotime.make_aware(dt.today()),
|
||||
entry_time__gt=djangotime.make_aware(dt.today())
|
||||
- djangotime.timedelta(days=request.data["timeFilter"]),
|
||||
)
|
||||
|
||||
audit_logs = (
|
||||
AuditLog.objects.filter(agentFilter | clientFilter)
|
||||
.filter(userFilter)
|
||||
.filter(actionFilter)
|
||||
.filter(objectFilter)
|
||||
.filter(timeFilter)
|
||||
)
|
||||
|
||||
return Response(AuditLogSerializer(audit_logs, many=True).data)
|
||||
|
||||
|
||||
@@ -58,9 +79,8 @@ class FilterOptionsAuditLog(APIView):
|
||||
return Response(AgentHostnameSerializer(agents, many=True).data)
|
||||
|
||||
if request.data["type"] == "user":
|
||||
agents = Agent.objects.values_list("agent_id", flat=True)
|
||||
users = User.objects.exclude(username__in=agents).filter(
|
||||
username__icontains=request.data["pattern"]
|
||||
users = User.objects.filter(
|
||||
username__icontains=request.data["pattern"], agent=None
|
||||
)
|
||||
return Response(UserSerializer(users, many=True).data)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -62,7 +62,13 @@ class Script(BaseAuditModel):
|
||||
# load community uploaded scripts into the database
|
||||
# skip ones that already exist, only updating name / desc in case it changes
|
||||
# files will be copied by the update script or in docker to /srv/salt/scripts
|
||||
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
|
||||
|
||||
# for install script
|
||||
try:
|
||||
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
|
||||
# for docker
|
||||
except:
|
||||
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[0], "scripts")
|
||||
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
|
||||
|
||||
@@ -10,22 +10,22 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.1.2"
|
||||
TRMM_VERSION = "0.1.6"
|
||||
|
||||
# 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.89"
|
||||
|
||||
# https://github.com/wh1te909/salt
|
||||
LATEST_SALT_VER = "1.1.0"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.0.1"
|
||||
LATEST_AGENT_VER = "1.0.2"
|
||||
|
||||
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):
|
||||
|
||||
@@ -20,16 +20,18 @@ ARG ADMIN_URL
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext-base wget
|
||||
RUN apt-get update && apt-get install -y gettext-base wget ca-certificates
|
||||
COPY ./api/tacticalrmm/requirements.txt .
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN wget --no-check-certificate https://golang.org/dl/go1.15.linux-amd64.tar.gz -P /tmp
|
||||
RUN wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P /tmp
|
||||
COPY ./api/tacticalrmm/ .
|
||||
COPY ./scripts/ /scripts
|
||||
COPY ./docker/api/prestart.sh .
|
||||
COPY ./docker/api/uwsgi.ini .
|
||||
COPY ./docker/api/api.conf /app/api.conf.tmp
|
||||
COPY ./api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
|
||||
RUN envsubst '\$APP_HOST, \$API_HOST' < /app/api.conf.tmp > /app/nginx.conf && \
|
||||
rm /app/api.conf.tmp
|
||||
COPY ./docker/api/local_settings.py.keep ./tacticalrmm/local_settings.py.tmp
|
||||
@@ -41,5 +43,4 @@ RUN tar -xzf /tmp/go1.15.linux-amd64.tar.gz -C /tmp && \
|
||||
rm -rf /tmp/go
|
||||
|
||||
RUN /usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo && \
|
||||
cp ./api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/ && \
|
||||
chmod +x /usr/local/bin/goversioninfo
|
||||
|
||||
@@ -13,11 +13,6 @@ export default {
|
||||
<style lang="sass">
|
||||
.tabs-tbl-sticky
|
||||
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #f5f4f2
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
@@ -27,11 +22,6 @@ export default {
|
||||
.remote-bg-tbl-sticky
|
||||
max-height: 70vh
|
||||
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #f5f4f2
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
@@ -41,11 +31,6 @@ export default {
|
||||
.audit-mgr-tbl-sticky
|
||||
max-height: 75vh
|
||||
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #fffefe
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
@@ -55,11 +40,6 @@ export default {
|
||||
.settings-tbl-sticky
|
||||
max-height: 60vh
|
||||
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #CBCBCB
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
@@ -68,22 +48,32 @@ export default {
|
||||
|
||||
.agents-tbl-sticky
|
||||
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #f5f4f2
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
thead tr:first-child th
|
||||
top: 0
|
||||
|
||||
.table-bgcolor
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #f5f4f2
|
||||
|
||||
.table-bgcolor-dark
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th
|
||||
background-color: #1d1d1d
|
||||
|
||||
.highlight
|
||||
background-color: #c9e6ff
|
||||
|
||||
.highlight-dark
|
||||
background-color: #343434
|
||||
|
||||
.action-completed
|
||||
background-color: #baf5cc
|
||||
background-color: $positive
|
||||
|
||||
.agent-offline
|
||||
background: gray !important
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div style="width: 900px; max-width: 90vw;">
|
||||
<div style="width: 900px; max-width: 90vw">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-btn ref="refresh" @click="refresh" class="q-mr-sm" dense flat push icon="refresh" />User Administration
|
||||
@@ -10,17 +10,7 @@
|
||||
</q-bar>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
ref="new"
|
||||
label="New"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showAddUserModal"
|
||||
/>
|
||||
<q-btn ref="new" label="New" dense flat push unelevated no-caps icon="add" @click="showAddUserModal" />
|
||||
<q-btn
|
||||
ref="edit"
|
||||
label="Edit"
|
||||
@@ -83,19 +73,20 @@
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
:class="{highlight: selected.length !== 0 && selected[0].id === props.row.id}"
|
||||
@click="editUserId = props.row.id; props.selected = true"
|
||||
@contextmenu="editUserId = props.row.id; props.selected = true"
|
||||
:class="rowSelectedClass(props.row.id, selected)"
|
||||
@click="
|
||||
editUserId = props.row.id;
|
||||
props.selected = true;
|
||||
"
|
||||
@contextmenu="
|
||||
editUserId = props.row.id;
|
||||
props.selected = true;
|
||||
"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showEditUserModal(selected[0])"
|
||||
id="context-edit"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="showEditUserModal(selected[0])" id="context-edit">
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
@@ -116,12 +107,7 @@
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="ResetPassword(props.row)"
|
||||
id="context-reset"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="ResetPassword(props.row)" id="context-reset">
|
||||
<q-item-section side>
|
||||
<q-icon name="autorenew" />
|
||||
</q-item-section>
|
||||
@@ -169,11 +155,7 @@
|
||||
|
||||
<!-- user reset password form modal -->
|
||||
<q-dialog v-model="showResetPasswordModal" @hide="closeResetPasswordModal">
|
||||
<UserResetPasswordForm
|
||||
:pk="resetUserId"
|
||||
:username="resetUserName"
|
||||
@close="closeResetPasswordModal"
|
||||
/>
|
||||
<UserResetPasswordForm :pk="resetUserId" :username="resetUserName" @close="closeResetPasswordModal" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -318,6 +300,9 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
rowSelectedClass(id, selected) {
|
||||
if (selected.length !== 0 && selected[0].id === id) return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<div class="q-pt-none q-pb-none q-pr-xs q-pl-xs">
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="agents-tbl-sticky"
|
||||
:style="{ 'max-height': agentTableHeight }"
|
||||
:data="filter"
|
||||
:filter="search"
|
||||
:filter-method="filterTable"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
row-key="id"
|
||||
@@ -69,7 +71,7 @@
|
||||
<q-tr
|
||||
@contextmenu="agentRowSelected(props.row.id, props.row.agent_id)"
|
||||
:props="props"
|
||||
:class="{ highlight: selectedRow === props.row.id }"
|
||||
:class="rowSelectedClass(props.row.id)"
|
||||
@click="agentRowSelected(props.row.id, props.row.agent_id)"
|
||||
@dblclick="rowDoubleClicked(props.row.id)"
|
||||
>
|
||||
@@ -84,7 +86,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 +257,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 +308,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 +373,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 +458,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;
|
||||
@@ -570,6 +632,9 @@ export default {
|
||||
menuMaintenanceText(mode) {
|
||||
return mode ? "Disable Maintenance Mode" : "Enable Maintenance Mode";
|
||||
},
|
||||
rowSelectedClass(id) {
|
||||
if (this.selectedRow === id) return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk", "agentTableHeight"]),
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="text-h6 q-pl-sm q-pt-sm">Filter Results</div>
|
||||
<div class="text-h6 q-pl-sm q-pt-sm">Filter</div>
|
||||
<div class="row">
|
||||
<div class="q-pa-sm col-2">
|
||||
<div class="q-pa-sm col-1">
|
||||
<q-option-group v-model="filterType" :options="filterTypeOptions" color="primary" @input="clear" />
|
||||
</div>
|
||||
<div class="q-pa-sm col-2" v-if="filterType === 'agents'">
|
||||
<q-select
|
||||
new-value-mode="add"
|
||||
multiple
|
||||
@@ -34,6 +37,20 @@
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="q-pa-sm col-2" v-if="filterType === 'clients'">
|
||||
<q-select
|
||||
clearable
|
||||
multiple
|
||||
filled
|
||||
dense
|
||||
v-model="clientFilter"
|
||||
fill-input
|
||||
label="Clients"
|
||||
map-options
|
||||
emit-value
|
||||
:options="clientsOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-pa-sm col-2">
|
||||
<q-select
|
||||
new-value-mode="add"
|
||||
@@ -58,6 +75,34 @@
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="q-pa-sm col-2">
|
||||
<q-select
|
||||
clearable
|
||||
filled
|
||||
multiple
|
||||
use-chips
|
||||
dense
|
||||
v-model="actionFilter"
|
||||
label="Action"
|
||||
emit-value
|
||||
map-options
|
||||
:options="actionOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-pa-sm col-2">
|
||||
<q-select
|
||||
clearable
|
||||
filled
|
||||
multiple
|
||||
use-chips
|
||||
dense
|
||||
v-model="objectFilter"
|
||||
label="Object"
|
||||
emit-value
|
||||
map-options
|
||||
:options="objectOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-pa-sm col-2">
|
||||
<q-select filled dense v-model="timeFilter" label="Time" emit-value map-options :options="timeOptions">
|
||||
<template v-slot:no-option>
|
||||
@@ -75,20 +120,28 @@
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="audit-mgr-tbl-sticky"
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
title="Audit Logs"
|
||||
:data="auditLogs"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:pagination.sync="pagination"
|
||||
:rows-per-page-options="[25, 50, 100, 500, 1000]"
|
||||
:no-data-label="noDataText"
|
||||
@row-click="showDetails"
|
||||
>
|
||||
<template v-slot:top-right>
|
||||
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportLog" />
|
||||
</template>
|
||||
<template v-slot:body-cell-action="props">
|
||||
<q-td :props="props">
|
||||
<div>
|
||||
<q-badge :color="actionColor(props.value)" :label="actionText(props.value)" />
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
@@ -132,9 +185,24 @@ export default {
|
||||
auditLogs: [],
|
||||
userOptions: [],
|
||||
agentOptions: [],
|
||||
agentFilter: [],
|
||||
agentFilter: null,
|
||||
userFilter: [],
|
||||
timeFilter: 30,
|
||||
actionFilter: null,
|
||||
clientsOptions: [],
|
||||
clientFilter: null,
|
||||
objectFilter: null,
|
||||
timeFilter: 7,
|
||||
filterType: "clients",
|
||||
filterTypeOptions: [
|
||||
{
|
||||
label: "Clients",
|
||||
value: "clients",
|
||||
},
|
||||
{
|
||||
label: "Agents",
|
||||
value: "agents",
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: "entry_time",
|
||||
@@ -147,25 +215,65 @@ export default {
|
||||
{ name: "username", label: "Username", field: "username", align: "left", sortable: true },
|
||||
{ name: "agent", label: "Agent", field: "agent", align: "left", sortable: true },
|
||||
{ name: "action", label: "Action", field: "action", align: "left", sortable: true },
|
||||
{ name: "object_type", label: "Object", field: "object_type", align: "left", sortable: true },
|
||||
{
|
||||
name: "object_type",
|
||||
label: "Object",
|
||||
field: "object_type",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (val, row) => this.formatObject(val),
|
||||
},
|
||||
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
|
||||
],
|
||||
actionOptions: [
|
||||
{ value: "agent_install", label: "Agent Installs" },
|
||||
{ value: "add", label: "Add Object" },
|
||||
{ value: "bulk_action", label: "Bulk Actions" },
|
||||
{ value: "check_run", label: "Check Run Results" },
|
||||
{ value: "execute_command", label: "Execute Command" },
|
||||
{ value: "execute_script", label: "Execute Script" },
|
||||
{ value: "delete", label: "Delete Object" },
|
||||
{ value: "failed_login", label: "Failed User login" },
|
||||
{ value: "login", label: "User Login" },
|
||||
{ value: "modify", label: "Modify Object" },
|
||||
{ value: "remote_session", label: "Remote Session" },
|
||||
{ value: "task_run", label: "Task Run Results" },
|
||||
],
|
||||
timeOptions: [
|
||||
{ value: 1, label: "1 Day Ago" },
|
||||
{ value: 7, label: "1 Week Ago" },
|
||||
{ value: 30, label: "30 Days Ago" },
|
||||
{ value: 90, label: "3 Months Ago" },
|
||||
{ value: 180, label: "6 Months Ago" },
|
||||
{ value: 365, label: "1 Year Ago" },
|
||||
{ value: 0, label: "Everything" },
|
||||
],
|
||||
objectOptions: [
|
||||
{ value: "agent", label: "Agent" },
|
||||
{ value: "automatedtask", label: "Automated Task" },
|
||||
{ value: "bulk", label: "Bulk Actions" },
|
||||
{ value: "coresettings", label: "Core Settings" },
|
||||
{ value: "check", label: "Check" },
|
||||
{ value: "client", label: "Client" },
|
||||
{ value: "policy", label: "Policy" },
|
||||
{ value: "site", label: "Site" },
|
||||
{ value: "script", label: "Script" },
|
||||
{ value: "user", label: "User" },
|
||||
{ value: "winupdatepolicy", label: "Patch Policy" },
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 50,
|
||||
rowsPerPage: 25,
|
||||
sortBy: "entry_time",
|
||||
descending: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getClients() {
|
||||
this.$axios.get("/clients/clients/").then(r => {
|
||||
this.clientsOptions = Object.freeze(r.data.map(client => ({ label: client.name, value: client.id })));
|
||||
});
|
||||
},
|
||||
getUserOptions(val, update, abort) {
|
||||
if (val.length < 2) {
|
||||
abort();
|
||||
@@ -182,10 +290,10 @@ 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.userOptions = Object.freeze(r.data.map(user => user.username));
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -209,10 +317,10 @@ 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.agentOptions = Object.freeze(r.data.map(agent => agent.hostname));
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -252,23 +360,18 @@ export default {
|
||||
this.searched = true;
|
||||
let data = {};
|
||||
|
||||
if (this.agentFilter.length > 0) {
|
||||
data["agentFilter"] = this.agentFilter;
|
||||
}
|
||||
if (!!this.agentFilter && this.agentFilter.length > 0) data["agentFilter"] = this.agentFilter;
|
||||
else if (!!this.clientFilter && this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter;
|
||||
if (!!this.userFilter && this.userFilter.length > 0) data["userFilter"] = this.userFilter;
|
||||
if (!!this.timeFilter) data["timeFilter"] = this.timeFilter;
|
||||
if (!!this.actionFilter && this.actionFilter.length > 0) data["actionFilter"] = this.actionFilter;
|
||||
if (!!this.objectFilter && this.objectFilter.length > 0) data["objectFilter"] = this.objectFilter;
|
||||
|
||||
if (this.userFilter.length > 0) {
|
||||
data["userFilter"] = this.userFilter;
|
||||
}
|
||||
|
||||
if (this.timeFilter) {
|
||||
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;
|
||||
this.auditLogs = Object.freeze(r.data);
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
@@ -282,11 +385,40 @@ export default {
|
||||
this.logDetails = null;
|
||||
this.showLogDetails = false;
|
||||
},
|
||||
actionColor(action) {
|
||||
if (action === "add") return "success";
|
||||
else if (action === "agent_install") return "success";
|
||||
else if (action === "modify") return "warning";
|
||||
else if (action === "delete") return "negative";
|
||||
else if (action === "failed_login") return "negative";
|
||||
else return "primary";
|
||||
},
|
||||
actionText(action) {
|
||||
if (action.includes("_")) {
|
||||
let text = action.split("_");
|
||||
return this.capitalize(text[0]) + " " + this.capitalize(text[1]);
|
||||
} else {
|
||||
return this.capitalize(action);
|
||||
}
|
||||
},
|
||||
formatObject(text) {
|
||||
if (text === "winupdatepolicy") return "Patch Policy";
|
||||
else if (text === "automatedtask") return "Automated Task";
|
||||
else if (text === "coresettings") return "Core Settings";
|
||||
else return this.capitalize(text);
|
||||
},
|
||||
clear() {
|
||||
this.clientFilter = null;
|
||||
this.agentFilter = null;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
noDataText() {
|
||||
return this.searched ? "No data found. Try to refine you search" : "Click search to find audit logs";
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getClients();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@
|
||||
<q-table
|
||||
dense
|
||||
class="tabs-tbl-sticky"
|
||||
:style="{'max-height': tabsTableHeight}"
|
||||
:style="{ 'max-height': tabsTableHeight }"
|
||||
:data="tasks"
|
||||
:columns="columns"
|
||||
:row-key="row => row.id"
|
||||
@@ -88,7 +88,7 @@
|
||||
</q-td>
|
||||
<!-- policy check icon -->
|
||||
<q-td v-if="props.row.managed_by_policy">
|
||||
<q-icon style="font-size: 1.3rem;" name="policy">
|
||||
<q-icon style="font-size: 1.3rem" name="policy">
|
||||
<q-tooltip>This task is managed by a policy</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
@@ -96,14 +96,14 @@
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td v-if="props.row.sync_status === 'notsynced'">Will sync on next agent checkin</q-td>
|
||||
<q-td v-else-if="props.row.sync_status === 'synced'">Synced with agent</q-td>
|
||||
<q-td
|
||||
v-else-if="props.row.sync_status === 'pendingdeletion'"
|
||||
>Pending deletion on agent</q-td>
|
||||
<q-td v-else-if="props.row.sync_status === 'pendingdeletion'">Pending deletion on agent</q-td>
|
||||
<q-td v-if="props.row.retcode !== null || props.row.stdout || props.row.stderr">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="scriptMoreInfo(props.row)"
|
||||
>output</span>
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-else>Awaiting output</q-td>
|
||||
<q-td v-if="props.row.last_run">{{ props.row.last_run }}</q-td>
|
||||
@@ -122,7 +122,13 @@
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showScriptOutput">
|
||||
<ScriptOutput @close="showScriptOutput = false; scriptInfo = {}" :scriptInfo="scriptInfo" />
|
||||
<ScriptOutput
|
||||
@close="
|
||||
showScriptOutput = false;
|
||||
scriptInfo = {};
|
||||
"
|
||||
:scriptInfo="scriptInfo"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -57,8 +57,9 @@
|
||||
<template v-else>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
:style="{'max-height': tabsTableHeight}"
|
||||
:style="{ 'max-height': tabsTableHeight }"
|
||||
:data="checks"
|
||||
:columns="columns"
|
||||
:row-key="row => row.id + row.check_type"
|
||||
@@ -142,12 +143,12 @@
|
||||
</q-td>
|
||||
<!-- policy check icon -->
|
||||
<q-td v-if="props.row.managed_by_policy">
|
||||
<q-icon style="font-size: 1.3rem;" name="policy">
|
||||
<q-icon style="font-size: 1.3rem" name="policy">
|
||||
<q-tooltip>This check is managed by a policy</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.overriden_by_policy">
|
||||
<q-icon style="font-size: 1.3rem;" name="remove_circle_outline">
|
||||
<q-icon style="font-size: 1.3rem" name="remove_circle_outline">
|
||||
<q-tooltip>This check is overriden by a policy</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
@@ -155,10 +156,10 @@
|
||||
<!-- status icon -->
|
||||
<q-td v-if="props.row.status === 'pending'"></q-td>
|
||||
<q-td v-else-if="props.row.status === 'passing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="positive" name="check_circle" />
|
||||
<q-icon style="font-size: 1.3rem" color="positive" name="check_circle" />
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.status === 'failing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="negative" name="error" />
|
||||
<q-icon style="font-size: 1.3rem" color="negative" name="error" />
|
||||
</q-td>
|
||||
<!-- check description -->
|
||||
<q-td>{{ props.row.readable_desc }}</q-td>
|
||||
@@ -173,30 +174,36 @@
|
||||
<!-- more info -->
|
||||
<q-td v-if="props.row.check_type === 'ping'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="pingInfo(props.row.readable_desc, props.row.more_info)"
|
||||
>output</span>
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.check_type === 'script'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="scriptMoreInfo(props.row)"
|
||||
>output</span>
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.check_type === 'eventlog'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="eventLogMoreInfo(props.row)"
|
||||
>output</span>
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-else-if="props.row.check_type === 'cpuload' || props.row.check_type === 'memory'"
|
||||
>{{ props.row.history_info }}</q-td>
|
||||
<q-td v-else-if="props.row.check_type === 'cpuload' || props.row.check_type === 'memory'">{{
|
||||
props.row.history_info
|
||||
}}</q-td>
|
||||
<q-td v-else>{{ props.row.more_info }}</q-td>
|
||||
<q-td>{{ props.row.last_run }}</q-td>
|
||||
<q-td
|
||||
v-if="props.row.assigned_task !== null && props.row.assigned_task.length > 1"
|
||||
>{{ props.row.assigned_task.length }} Tasks</q-td>
|
||||
<q-td v-if="props.row.assigned_task !== null && props.row.assigned_task.length > 1"
|
||||
>{{ props.row.assigned_task.length }} Tasks</q-td
|
||||
>
|
||||
<q-td v-else-if="props.row.assigned_task">{{ props.row.assigned_task.name }}</q-td>
|
||||
<q-td v-else></q-td>
|
||||
</q-tr>
|
||||
@@ -206,67 +213,41 @@
|
||||
</div>
|
||||
<!-- modals -->
|
||||
<q-dialog v-model="showDiskSpaceCheck">
|
||||
<DiskSpaceCheck
|
||||
@close="showDiskSpaceCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<DiskSpaceCheck @close="showDiskSpaceCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showMemCheck">
|
||||
<MemCheck
|
||||
@close="showMemCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<MemCheck @close="showMemCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showCpuLoadCheck">
|
||||
<CpuLoadCheck
|
||||
@close="showCpuLoadCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<CpuLoadCheck @close="showCpuLoadCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showPingCheck">
|
||||
<PingCheck
|
||||
@close="showPingCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<PingCheck @close="showPingCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showWinSvcCheck">
|
||||
<WinSvcCheck
|
||||
@close="showWinSvcCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<WinSvcCheck @close="showWinSvcCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEventLogCheck">
|
||||
<EventLogCheck
|
||||
@close="showEventLogCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<EventLogCheck @close="showEventLogCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showScriptCheck">
|
||||
<ScriptCheck
|
||||
@close="showScriptCheck = false"
|
||||
:agentpk="selectedAgentPk"
|
||||
:mode="mode"
|
||||
:checkpk="checkpk"
|
||||
/>
|
||||
<ScriptCheck @close="showScriptCheck = false" :agentpk="selectedAgentPk" :mode="mode" :checkpk="checkpk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showScriptOutput">
|
||||
<ScriptOutput @close="showScriptOutput = false; scriptInfo = {}" :scriptInfo="scriptInfo" />
|
||||
<ScriptOutput
|
||||
@close="
|
||||
showScriptOutput = false;
|
||||
scriptInfo = {};
|
||||
"
|
||||
:scriptInfo="scriptInfo"
|
||||
/>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEventLogOutput">
|
||||
<EventLogCheckOutput
|
||||
@close="showEventLogOutput = false; evtlogdata = {}"
|
||||
@close="
|
||||
showEventLogOutput = false;
|
||||
evtlogdata = {};
|
||||
"
|
||||
:evtlogdata="evtlogdata"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="audit-mgr-tbl-sticky"
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="remote-bg-tbl-sticky"
|
||||
:data="events"
|
||||
:columns="columns"
|
||||
@@ -32,21 +33,9 @@
|
||||
<template v-slot:top>
|
||||
<q-btn dense flat push @click="getEventLog" icon="refresh" />
|
||||
<q-space />
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="Application"
|
||||
label="Application"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-radio v-model="logType" color="cyan" val="Application" label="Application" @input="getEventLog" />
|
||||
<q-radio v-model="logType" color="cyan" val="System" label="System" @input="getEventLog" />
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="Security"
|
||||
label="Security"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-radio v-model="logType" color="cyan" val="Security" label="Security" @input="getEventLog" />
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
@@ -61,9 +50,9 @@
|
||||
<q-td>{{ props.row.eventID }}</q-td>
|
||||
<q-td>{{ props.row.time }}</q-td>
|
||||
<q-td @click.native="showFullMsg(props.row.message)">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
>{{ formatMessage(props.row.message) }}</span>
|
||||
<span style="cursor: pointer; text-decoration: underline" class="text-primary">{{
|
||||
formatMessage(props.row.message)
|
||||
}}</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="q-pa-md">
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="remote-bg-tbl-sticky"
|
||||
:data="procs"
|
||||
:columns="columns"
|
||||
@@ -13,15 +14,7 @@
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn v-if="poll" dense flat push @click="stopPoll" icon="stop" label="Stop Live Refresh" />
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
push
|
||||
@click="startPoll"
|
||||
icon="play_arrow"
|
||||
label="Resume Live Refresh"
|
||||
/>
|
||||
<q-btn v-else dense flat push @click="startPoll" icon="play_arrow" label="Resume Live Refresh" />
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog :value="toggleScriptManager" @hide="hideScriptManager" @show="getScripts">
|
||||
<q-card style="min-width: 70vw;">
|
||||
<q-card style="min-width: 70vw">
|
||||
<q-bar>
|
||||
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
|
||||
<q-space />
|
||||
@@ -19,7 +19,10 @@
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showScript('add'); clearRow()"
|
||||
@click="
|
||||
showScript('add');
|
||||
clearRow();
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
label="Edit"
|
||||
@@ -66,14 +69,11 @@
|
||||
@click="downloadScript"
|
||||
/>
|
||||
<q-space />
|
||||
<q-toggle
|
||||
:value="showBuiltIn"
|
||||
label="Show Community Scripts"
|
||||
@input="showBuiltIn = !showBuiltIn"
|
||||
/>
|
||||
<q-toggle :value="showBuiltIn" label="Show Community Scripts" @input="showBuiltIn = !showBuiltIn" />
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="settings-tbl-sticky"
|
||||
:data="visibleScripts"
|
||||
:columns="columns"
|
||||
@@ -88,16 +88,19 @@
|
||||
>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr
|
||||
:class="{highlight: scriptpk === props.row.id}"
|
||||
@click="scriptpk = props.row.id; filename = props.row.filename; code = props.row.code;"
|
||||
:class="rowSelectedClass(props.row.id)"
|
||||
@click="
|
||||
scriptpk = props.row.id;
|
||||
filename = props.row.filename;
|
||||
code = props.row.code;
|
||||
"
|
||||
>
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td>
|
||||
{{ truncateText(props.row.description) }}
|
||||
<q-tooltip
|
||||
v-if="props.row.description.length >= 60"
|
||||
content-style="font-size: 12px"
|
||||
>{{ props.row.description }}</q-tooltip>
|
||||
<q-tooltip v-if="props.row.description.length >= 60" content-style="font-size: 12px">{{
|
||||
props.row.description
|
||||
}}</q-tooltip>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.filename }}</q-td>
|
||||
<q-td>{{ props.row.shell }}</q-td>
|
||||
@@ -113,12 +116,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showScriptModal">
|
||||
<ScriptModal
|
||||
:mode="mode"
|
||||
:scriptpk="scriptpk"
|
||||
@close="showScriptModal = false"
|
||||
@uploaded="getScripts"
|
||||
/>
|
||||
<ScriptModal :mode="mode" :scriptpk="scriptpk" @close="showScriptModal = false" @uploaded="getScripts" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -256,6 +254,9 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
rowSelectedClass(id) {
|
||||
if (this.scriptpk === id) return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="q-pa-md">
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="remote-bg-tbl-sticky"
|
||||
:data="servicesData"
|
||||
:columns="columns"
|
||||
@@ -24,25 +25,13 @@
|
||||
<q-tr :props="props">
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'start', props.row.display_name)"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="serviceAction(props.row.name, 'start', props.row.display_name)">
|
||||
<q-item-section>Start</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'stop', props.row.display_name)"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="serviceAction(props.row.name, 'stop', props.row.display_name)">
|
||||
<q-item-section>Stop</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'restart', props.row.display_name)"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="serviceAction(props.row.name, 'restart', props.row.display_name)">
|
||||
<q-item-section>Restart</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
@@ -64,7 +53,7 @@
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="serviceDetailsModal">
|
||||
<q-card style="width: 600px; max-width: 80vw;">
|
||||
<q-card style="width: 600px; max-width: 80vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Service Details - {{ serviceData.DisplayName }}</div>
|
||||
</q-card-section>
|
||||
@@ -83,7 +72,7 @@
|
||||
<div class="row">
|
||||
<div class="col-3">Description:</div>
|
||||
<div class="col-9">
|
||||
<q-field outlined color="black">{{ serviceData.Description }}</q-field>
|
||||
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">{{ serviceData.Description }}</q-field>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
@@ -121,7 +110,7 @@
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
:text-color="$q.dark.isActive ? 'white' : 'black'"
|
||||
push
|
||||
label="Start"
|
||||
@click="serviceAction(serviceData.svc_name, 'start', serviceData.DisplayName)"
|
||||
@@ -129,7 +118,7 @@
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
:text-color="$q.dark.isActive ? 'white' : 'black'"
|
||||
push
|
||||
label="Stop"
|
||||
@click="serviceAction(serviceData.svc_name, 'stop', serviceData.DisplayName)"
|
||||
@@ -137,7 +126,7 @@
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
:text-color="$q.dark.isActive ? 'white' : 'black'"
|
||||
push
|
||||
label="Restart"
|
||||
@click="serviceAction(serviceData.svc_name, 'restart', serviceData.DisplayName)"
|
||||
@@ -146,7 +135,7 @@
|
||||
</div>
|
||||
</q-card-section>
|
||||
<hr />
|
||||
<q-card-actions align="left" class="bg-white text-teal">
|
||||
<q-card-actions align="left" :class="$q.dark.isActive ? 'text-teal' : 'bg-white text-teal'">
|
||||
<q-btn
|
||||
:disable="saveServiceDetailButton"
|
||||
dense
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
/>
|
||||
<q-btn dense flat push @click="refreshSoftware" icon="refresh" />
|
||||
<q-table
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
:style="{'max-height': tabsTableHeight}"
|
||||
:style="{ 'max-height': tabsTableHeight }"
|
||||
dense
|
||||
:data="software"
|
||||
:columns="columns"
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<q-btn label="Refresh" dense flat push @click="refreshUpdates(updates.pk)" icon="refresh" />
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
:style="{'max-height': tabsTableHeight}"
|
||||
:style="{ 'max-height': tabsTableHeight }"
|
||||
:data="sortedUpdates"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
@@ -22,36 +23,16 @@
|
||||
<q-tr :props="props">
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 100px">
|
||||
<q-item
|
||||
v-if="!props.row.installed"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editPolicy(props.row.id, 'inherit')"
|
||||
>
|
||||
<q-item v-if="!props.row.installed" clickable v-close-popup @click="editPolicy(props.row.id, 'inherit')">
|
||||
<q-item-section>Inherit</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="!props.row.installed"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editPolicy(props.row.id, 'approve')"
|
||||
>
|
||||
<q-item v-if="!props.row.installed" clickable v-close-popup @click="editPolicy(props.row.id, 'approve')">
|
||||
<q-item-section>Approve</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="!props.row.installed"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editPolicy(props.row.id, 'ignore')"
|
||||
>
|
||||
<q-item v-if="!props.row.installed" clickable v-close-popup @click="editPolicy(props.row.id, 'ignore')">
|
||||
<q-item-section>Ignore</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="!props.row.installed"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editPolicy(props.row.id, 'nothing')"
|
||||
>
|
||||
<q-item v-if="!props.row.installed" clickable v-close-popup @click="editPolicy(props.row.id, 'nothing')">
|
||||
<q-item-section>Do Nothing</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@@ -67,11 +48,7 @@
|
||||
<q-icon v-else-if="props.row.action === 'ignore'" name="fas fa-check" color="negative">
|
||||
<q-tooltip>Ignore</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else-if="props.row.action === 'inherit'"
|
||||
name="fiber_manual_record"
|
||||
color="accent"
|
||||
>
|
||||
<q-icon v-else-if="props.row.action === 'inherit'" name="fiber_manual_record" color="accent">
|
||||
<q-tooltip>Inherit</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
@@ -92,9 +69,9 @@
|
||||
<q-td>{{ props.row.severity }}</q-td>
|
||||
<q-td>{{ formatMessage(props.row.title) }}</q-td>
|
||||
<q-td @click.native="showFullMsg(props.row.title, props.row.description)">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
>{{ formatMessage(props.row.description) }}</span>
|
||||
<span style="cursor: pointer; text-decoration: underline" class="text-primary">{{
|
||||
formatMessage(props.row.description)
|
||||
}}</span>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.date_installed }}</q-td>
|
||||
</q-tr>
|
||||
|
||||
@@ -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
|
||||
@@ -48,6 +48,7 @@
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
dense
|
||||
:data="policies"
|
||||
:columns="columns"
|
||||
@@ -156,14 +157,16 @@
|
||||
<q-td>{{ props.row.desc }}</q-td>
|
||||
<q-td>
|
||||
<span
|
||||
style="cursor: pointer; color: blue; text-decoration: underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showRelationsModal(props.row)"
|
||||
>{{ `Show Relations (${props.row.agents_count}+)` }}</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<span
|
||||
style="cursor: pointer; color: blue; text-decoration: underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showEditPatchPolicyModal(props.row)"
|
||||
>{{ patchPolicyText(props.row) }}</span
|
||||
>
|
||||
@@ -208,9 +211,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 +280,6 @@ export default {
|
||||
pagination: {
|
||||
rowsPerPage: 9999,
|
||||
},
|
||||
thumbStyle: {
|
||||
right: "2px",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#027be3",
|
||||
width: "5px",
|
||||
opacity: 0.75,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<template>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
style="max-height: 35vh"
|
||||
:data="tasks"
|
||||
@@ -105,9 +106,9 @@
|
||||
<q-td>{{ props.row.schedule }}</q-td>
|
||||
<q-td>
|
||||
<span
|
||||
style="cursor: pointer; color: blue; text-decoration: underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
@click="showStatus(props.row)"
|
||||
class="status-cell"
|
||||
class="status-cell text-primary"
|
||||
>See Status</span
|
||||
>
|
||||
</q-td>
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<template>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
style="max-height: 35vh"
|
||||
:data="checks"
|
||||
@@ -109,23 +110,13 @@
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showEditDialog(props.row)"
|
||||
id="context-edit"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="showEditDialog(props.row)" id="context-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="deleteCheck(props.row)"
|
||||
id="context-delete"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="deleteCheck(props.row)" id="context-delete">
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
@@ -134,12 +125,7 @@
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showPolicyCheckStatusModal(props.row)"
|
||||
id="context-status"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="showPolicyCheckStatusModal(props.row)" id="context-status">
|
||||
<q-item-section side>
|
||||
<q-icon name="sync" />
|
||||
</q-item-section>
|
||||
@@ -171,14 +157,15 @@
|
||||
<q-td>{{ props.row.readable_desc }}</q-td>
|
||||
<q-td>
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
@click="showPolicyCheckStatusModal(props.row)"
|
||||
class="status-cell"
|
||||
>See Status</span>
|
||||
class="status-cell text-primary"
|
||||
>See Status</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-if="props.row.assignedtask !== null && props.row.assignedtask.length === 1"
|
||||
>{{ props.row.assignedtask[0].name }}</q-td>
|
||||
<q-td v-if="props.row.assignedtask !== null && props.row.assignedtask.length === 1">{{
|
||||
props.row.assignedtask[0].name
|
||||
}}</q-td>
|
||||
<q-td v-else-if="props.row.assignedtask">{{ props.row.assignedtask.length }} Tasks</q-td>
|
||||
<q-td v-else></q-td>
|
||||
</q-tr>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="tabs-tbl-sticky"
|
||||
style="max-height: 35vh"
|
||||
:data="tableData"
|
||||
@@ -35,10 +36,10 @@
|
||||
<!-- status icon -->
|
||||
<q-td v-if="props.row.status === 'pending'"></q-td>
|
||||
<q-td v-else-if="props.row.status === 'passing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="positive" name="check_circle" />
|
||||
<q-icon style="font-size: 1.3rem" color="positive" name="check_circle" />
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.status === 'failing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="negative" name="error" />
|
||||
<q-icon style="font-size: 1.3rem" color="negative" name="error" />
|
||||
</q-td>
|
||||
<q-td v-else></q-td>
|
||||
<!-- status text -->
|
||||
@@ -55,30 +56,33 @@
|
||||
<!-- more info -->
|
||||
<q-td v-if="props.row.check_type === 'ping'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
@click="pingInfo(props.row)"
|
||||
class="ping-cell"
|
||||
>output</span>
|
||||
class="ping-cell text-primary"
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-else-if="props.row.check_type === 'script' || props.row.retcode || props.row.stdout || props.row.stderr"
|
||||
>
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
@click="scriptMoreInfo(props.row)"
|
||||
class="script-cell"
|
||||
>output</span>
|
||||
class="script-cell text-primary"
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.check_type === 'eventlog'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
@click="eventLogMoreInfo(props.row)"
|
||||
class="eventlog-cell"
|
||||
>output</span>
|
||||
class="eventlog-cell text-primary"
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-else-if="props.row.check_type === 'cpuload' || props.row.check_type === 'memory'"
|
||||
>{{ props.row.history_info }}</q-td>
|
||||
<q-td v-else-if="props.row.check_type === 'cpuload' || props.row.check_type === 'memory'">{{
|
||||
props.row.history_info
|
||||
}}</q-td>
|
||||
<q-td v-else-if="props.row.more_info">{{ props.row.more_info }}</q-td>
|
||||
<q-td v-else>Awaiting Output</q-td>
|
||||
<!-- last run -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Download the agent then run the following command from an elevated command prompt on the device you want to add.
|
||||
</p>
|
||||
<p>
|
||||
<q-field outlined color="black">
|
||||
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">
|
||||
<code>{{ info.data.cmd }}</code>
|
||||
</q-field>
|
||||
</p>
|
||||
@@ -46,7 +46,9 @@
|
||||
</q-badge>
|
||||
<span>
|
||||
To skip downloading the Mesh Agent during the install. Download it
|
||||
<span style="cursor: pointer; color: blue; text-decoration: underline" @click="downloadMesh">here</span>
|
||||
<span style="cursor: pointer; text-decoration: underline" class="text-primary" @click="downloadMesh"
|
||||
>here</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
|
||||
@@ -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>
|
||||
@@ -9,6 +9,7 @@
|
||||
<div v-if="evtlogdata.extra_details !== null">
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="remote-bg-tbl-sticky"
|
||||
:data="evtlogdata.extra_details.log"
|
||||
:columns="columns"
|
||||
|
||||
@@ -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,12 +1,12 @@
|
||||
<template>
|
||||
<q-card style="width: 70vw; max-width: 90vw;">
|
||||
<q-card style="width: 70vw; max-width: 90vw">
|
||||
<q-bar>
|
||||
<span class="text-caption">{{ log.message }}</span>
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup />
|
||||
</q-bar>
|
||||
<q-card-section class="row">
|
||||
<div class="col-6">
|
||||
<q-card-section class="row scroll" style="max-height: 65vh">
|
||||
<div class="col-6" v-if="log.before_value !== null">
|
||||
<div class="text-h6">Before</div>
|
||||
<pre>{{ JSON.stringify(log.before_value, null, 4) }}</pre>
|
||||
</div>
|
||||
|
||||
@@ -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,98 @@
|
||||
<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
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
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 +121,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 +144,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 +157,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,38 +164,39 @@ export default {
|
||||
},
|
||||
clearRow() {
|
||||
this.selectedRow = null;
|
||||
this.selectedStatus = null;
|
||||
this.actionType = null;
|
||||
},
|
||||
rowClass(id, status) {
|
||||
if (this.selectedRow === id && status !== "completed") {
|
||||
return "highlight";
|
||||
return this.$q.dark.isActive ? "highlight-dark" : "highlight";
|
||||
} else if (status === "completed") {
|
||||
return "action-completed";
|
||||
}
|
||||
},
|
||||
},
|
||||
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,8 +1,9 @@
|
||||
<template>
|
||||
<q-card style="width: 50vw; max-width: 80vw;">
|
||||
<q-card style="width: 50vw; max-width: 80vw">
|
||||
<q-card-section>
|
||||
<q-table
|
||||
class="remote-bg-tbl-sticky"
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
title="Software"
|
||||
dense
|
||||
:data="chocos"
|
||||
@@ -36,7 +37,7 @@
|
||||
/>
|
||||
</q-td>
|
||||
<q-td @click="showDescription(props.row.name)">
|
||||
<span style="cursor:pointer;color:blue;text-decoration:underline">{{ props.row.name }}</span>
|
||||
<span style="cursor: pointer; text-decoration: underline" class="text-primary">{{ props.row.name }}</span>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.version }}</q-td>
|
||||
</q-tr>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user