Policy Check Finish

This commit is contained in:
Josh Krawczyk
2020-06-10 12:43:12 -04:00
parent e35406ccd5
commit eb50a8134c
36 changed files with 814 additions and 373 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-04 17:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0002_auto_20200531_2058'),
]
operations = [
migrations.AddField(
model_name='agent',
name='checks_last_generated',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.0.6 on 2020-06-04 17:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0003_agent_checks_last_generated'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='checks_last_generated',
),
migrations.AddField(
model_name='agent',
name='policies_pending',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.0.7 on 2020-06-09 16:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('automation', '0003_auto_20200609_1607'),
('agents', '0004_auto_20200604_1721'),
]
operations = [
migrations.AddField(
model_name='agent',
name='policy',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='automation.Policy'),
),
]

View File

@@ -16,6 +16,8 @@ from django.contrib.postgres.fields import JSONField
from core.models import TZ_CHOICES from core.models import TZ_CHOICES
import automation
class Agent(models.Model): class Agent(models.Model):
version = models.CharField(default="0.1.0", max_length=255) version = models.CharField(default="0.1.0", max_length=255)
@@ -50,9 +52,17 @@ class Agent(models.Model):
is_updating = models.BooleanField(default=False) is_updating = models.BooleanField(default=False)
choco_installed = models.BooleanField(default=False) choco_installed = models.BooleanField(default=False)
wmi_detail = JSONField(null=True) wmi_detail = JSONField(null=True)
policies_pending = models.BooleanField(default=False)
time_zone = models.CharField( time_zone = models.CharField(
max_length=255, choices=TZ_CHOICES, null=True, blank=True max_length=255, choices=TZ_CHOICES, null=True, blank=True
) )
policy = models.ForeignKey(
"automation.Policy",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
def __str__(self): def __str__(self):
return self.hostname return self.hostname
@@ -185,6 +195,21 @@ class Agent(models.Model):
except: except:
return [{"model": "unknown", "size": "unknown", "interfaceType": "unknown"}] return [{"model": "unknown", "size": "unknown", "interfaceType": "unknown"}]
def generate_checks_from_policies(self):
# Clear agent checks managed by policy
self.agentchecks.filter(managed_by_policy=True).delete()
# Clear agent checks that have overriden_by_policy set
self.agentchecks.update(overriden_by_policy=False)
# Generate checks based on policies
automation.models.Policy.generate_policy_checks(self)
# Set policies_pending to false to disable policy generation on next checkin
self.policies_pending = False
self.save()
# https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347 # https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347
def get_login_token(self, key, user, action=3): def get_login_token(self, key, user, action=3):
key = bytes.fromhex(key) key = bytes.fromhex(key)

View File

@@ -311,7 +311,11 @@ class CheckRunner(APIView):
def get(self, request, pk): def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
checks = Check.objects.filter(agent__pk=pk)
if agent.policies_pending:
agent.generate_checks_from_policies()
checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False)
ret = { ret = {
"agent": agent.pk, "agent": agent.pk,

View File

@@ -0,0 +1 @@
default_app_config = 'automation.apps.AutomationConfig'

View File

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

View File

@@ -1,5 +1,9 @@
from django.apps import AppConfig from django.apps import AppConfig
class AutomationConfig(AppConfig): class AutomationConfig(AppConfig):
name = "automation" name = "automation"
def ready(self):
# registering signals defined in signals.py
import automation.signals

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.0.6 on 2020-06-04 17:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('clients', '0002_auto_20200531_2058'),
('agents', '0003_agent_checks_last_generated'),
('automation', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='policy',
name='enforced',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='PolicyExclusions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('agents', models.ManyToManyField(related_name='policy_exclusions', to='agents.Agent')),
('clients', models.ManyToManyField(related_name='policy_exclusions', to='clients.Client')),
('policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exclusions', to='automation.Policy')),
('sites', models.ManyToManyField(related_name='policy_exclusions', to='clients.Site')),
],
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.0.7 on 2020-06-09 16:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('automation', '0002_auto_20200604_1713'),
]
operations = [
migrations.RemoveField(
model_name='policy',
name='agents',
),
migrations.RemoveField(
model_name='policy',
name='clients',
),
migrations.RemoveField(
model_name='policy',
name='sites',
),
migrations.RemoveField(
model_name='policyexclusions',
name='clients',
),
]

View File

@@ -6,9 +6,7 @@ class Policy(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
desc = models.CharField(max_length=255) desc = models.CharField(max_length=255)
active = models.BooleanField(default=False) active = models.BooleanField(default=False)
agents = models.ManyToManyField(Agent, related_name="policies") enforced = models.BooleanField(default=False)
sites = models.ManyToManyField(Site, related_name="policies")
clients = models.ManyToManyField(Client, related_name="policies")
def __str__(self): def __str__(self):
return self.name return self.name
@@ -30,8 +28,160 @@ class Policy(models.Model):
for site in client.sites.all(): for site in client.sites.all():
filtered_sites_ids.append(site.site) filtered_sites_ids.append(site.site)
site_agents = Agent.objects.filter(site__in=filtered_sites_ids) return Agent.objects.filter(models.Q(pk__in=explicit_agents.only("pk")) | models.Q(site__in=filtered_sites_ids) | models.Q(client__in=client_ids)).distinct()
client_agents = Agent.objects.filter(client__in=client_ids)
# Combine querysets and remove duplicates @staticmethod
return explicit_agents.union(site_agents, client_agents) def cascade_policy_checks(agent):
# Get checks added to agent directly
agent_checks = list(agent.agentchecks.filter(managed_by_policy=False))
# Get policies applied to agent and agent site and client
client_policy = Client.objects.get(client=agent.client).policy
site_policy = Site.objects.get(site=agent.site).policy
agent_policy = agent.policy
# Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first
enforced_checks = list()
policy_checks = list()
if agent_policy != None:
if agent_policy.active:
if agent_policy.enforced:
for check in agent_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in agent_policy.policychecks.all():
policy_checks.append(check)
if site_policy != None:
if site_policy.active:
if site_policy.enforced:
for check in site_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in site_policy.policychecks.all():
policy_checks.append(check)
if client_policy != None:
if client_policy.active:
if client_policy.enforced:
for check in client_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in client_policy.policychecks.all():
policy_checks.append(check)
# Sorted Checks already added
added_diskspace_checks = list()
added_ping_checks = list()
added_winsvc_checks = list()
added_script_checks = list()
added_eventlog_checks = list()
added_cpuload_checks = list()
added_memory_checks = list()
# Lists all agent and policy checks that will be created
diskspace_checks = list()
ping_checks = list()
winsvc_checks = list()
script_checks = list()
eventlog_checks = list()
cpuload_checks = list()
memory_checks = list()
# Loop over checks in with enforced policies first, then non-enforced policies
for check in enforced_checks + agent_checks + policy_checks:
if check.check_type == "diskspace":
# Check if drive letter was already added
if check.disk not in added_diskspace_checks:
added_diskspace_checks.append(check.disk)
# Dont create the check if it is an agent check
if check.agent == None:
diskspace_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "ping":
# Check if IP/host was already added
if check.ip not in added_ping_checks:
added_ping_checks.append(check.ip)
# Dont create the check if it is an agent check
if check.agent == None:
ping_checks.append(check)
added_ping_checks.append(check.ip)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "cpuload":
# Check if cpuload check exists
if len(added_cpuload_checks) == 0:
added_cpuload_checks.append(check)
# Dont create the check if it is an agent check
if check.agent == None:
cpuload_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "memory":
# Check if memory check exists
if len(added_memory_checks) == 0:
added_memory_checks.append(check)
# Dont create the check if it is an agent check
if check.agent == None:
memory_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "winsvc":
# Check if service name was already added
if check.svc_name not in added_winsvc_checks:
added_winsvc_checks.append(check.svc_name)
# Dont create the check if it is an agent check
if check.agent == None:
winsvc_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "script":
# Check if script id was already added
if check.script not in added_script_checks:
added_script_checks.append(check.script)
# Dont create the check if it is an agent check
if check.agent == None:
script_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
if check.check_type == "eventlog":
# Check if events were already added
if [check.log_name, check.event_id] not in added_eventlog_checks:
added_eventlog_checks.append([check.log_name, check.event_id])
if check.agent == None:
eventlog_checks.append(check)
elif check.agent != None:
check.overriden_by_policy = True
check.save()
return diskspace_checks + ping_checks + cpuload_checks + memory_checks + winsvc_checks + script_checks + eventlog_checks
@staticmethod
def generate_policy_checks(agent):
checks = Policy.cascade_policy_checks(agent)
if checks != None:
if len(checks) > 0:
for check in checks:
check.create_policy_check(agent)
class PolicyExclusions(models.Model):
policy = models.ForeignKey(Policy, related_name="exclusions", on_delete=models.CASCADE)
agents = models.ManyToManyField(Agent, related_name="policy_exclusions")
sites = models.ManyToManyField(Site, related_name="policy_exclusions")

View File

@@ -1,25 +1,73 @@
from rest_framework import serializers from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
StringRelatedField,
ReadOnlyField,
ValidationError
)
from .models import Policy from .models import Policy
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from autotasks.serializers import TaskSerializer from autotasks.serializers import TaskSerializer
from checks.serializers import CheckSerializer from checks.serializers import CheckSerializer
class PolicySerializer(serializers.ModelSerializer): class PolicySerializer(ModelSerializer):
class Meta: class Meta:
model = Policy model = Policy
fields = "__all__" fields = "__all__"
class PolicyRelationSerializer(serializers.ModelSerializer): class PolicyTableSerializer(ModelSerializer):
clients = StringRelatedField(many=True, read_only=True)
sites = StringRelatedField(many=True, read_only=True)
agents = StringRelatedField(many=True, read_only=True)
clients_count = SerializerMethodField(read_only=True)
sites_count = SerializerMethodField(read_only=True)
agents_count = SerializerMethodField(read_only=True)
class Meta: class Meta:
model = Policy model = Policy
fields = "__all__" fields = "__all__"
depth = 1
def get_clients_count(self, policy):
return policy.clients.count()
def get_sites_count(self, policy):
return policy.sites.count()
def get_agents_count(self, policy):
return policy.agents.count()
class PolicyOverviewSerializer(ModelSerializer):
class Meta:
model = Client
fields = (
"pk",
"client",
"sites",
"policy"
)
depth = 2 depth = 2
class AutoTaskPolicySerializer(serializers.ModelSerializer): class PolicyCheckStatusSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname")
class Meta:
model = Check
fields = "__all__"
class AutoTaskPolicySerializer(ModelSerializer):
autotasks = TaskSerializer(many=True, read_only=True) autotasks = TaskSerializer(many=True, read_only=True)

View File

@@ -0,0 +1,51 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from automation.models import Policy
from agents.models import Agent
def set_agent_policy_update_field(policy_list, many=False):
if many:
for policy in policy_list:
policy.related_agents().update(policies_pending=True)
else:
policy_list.related_agents().update(policies_pending=True)
@receiver(post_save, sender="checks.Check")
def post_save_check_handler(sender, instance, created, **kwargs):
# don't run when policy managed check is saved
if instance.managed_by_policy == True:
return
# For created checks
if created:
if instance.policy != None:
set_agent_policy_update_field(instance.policy)
elif instance.agent != None:
instance.agent.policies_pending=True
instance.agent.save()
# Checks that are updated except for agent
else:
if instance.policy != None:
set_agent_policy_update_field(instance.policy)
@receiver(post_delete, sender="checks.Check")
def post_delete_check_handler(sender, instance, **kwargs):
# don't run when policy managed check is saved
if instance.managed_by_policy == True:
return
if instance.policy != None:
set_agent_policy_update_field(instance.policy)
elif instance.agent != None:
instance.agent.policies_pending=True
instance.agent.save()
@receiver([post_save, post_delete], sender="automation.Policy")
def post_save_policy_handler(sender, instance, **kwargs):
set_agent_policy_update_field(instance)

View File

@@ -9,7 +9,7 @@ urlpatterns = [
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()), path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()), path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()), path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
path("<int:policy>/policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()), path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
path("<int:policy>/policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()), path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
path("runwintask/<int:pk>/", views.RunPolicyTask.as_view()), path("runwintask/<int:pk>/", views.PolicyAutoTask.as_view()),
] ]

View File

@@ -20,8 +20,10 @@ from agents.serializers import AgentHostnameSerializer
from .serializers import ( from .serializers import (
PolicySerializer, PolicySerializer,
PolicyRelationSerializer, PolicyTableSerializer,
AutoTaskPolicySerializer, PolicyOverviewSerializer,
PolicyCheckStatusSerializer,
AutoTaskPolicySerializer
) )
from checks.serializers import CheckSerializer from checks.serializers import CheckSerializer
@@ -31,30 +33,24 @@ class GetAddPolicies(APIView):
policies = Policy.objects.all() policies = Policy.objects.all()
return Response(PolicyRelationSerializer(policies, many=True).data) return Response(PolicyTableSerializer(policies, many=True).data)
def post(self, request): def post(self, request):
name = request.data["name"].strip() name = request.data["name"].strip()
desc = request.data["desc"].strip() desc = request.data["desc"].strip()
active = request.data["active"] active = request.data["active"]
enforced = active = request.data["enforced"]
if Policy.objects.filter(name=name): if Policy.objects.filter(name=name):
content = {"error": f"Policy {name} already exists"} content = {"error": f"Policy {name} already exists"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
try: try:
policy = Policy.objects.create(name=name, desc=desc, active=active) policy = Policy.objects.create(name=name, desc=desc, active=active, enforced=enforced)
except DataError: except DataError:
content = {"error": "Policy name too long (max 255 chars)"} content = {"error": "Policy name too long (max 255 chars)"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
# Add Clients, Sites to Policy
if len(request.data["clients"]) > 0:
policy.clients.set(request.data["clients"])
if len(request.data["sites"]) > 0:
policy.sites.set(request.data["sites"])
return Response("ok") return Response("ok")
@@ -63,7 +59,7 @@ class GetUpdateDeletePolicy(APIView):
policy = get_object_or_404(Policy, pk=pk) policy = get_object_or_404(Policy, pk=pk)
return Response(PolicyRelationSerializer(policy).data) return Response(PolicySerializer(policy).data)
def put(self, request, pk): def put(self, request, pk):
@@ -72,24 +68,14 @@ class GetUpdateDeletePolicy(APIView):
policy.name = request.data["name"] policy.name = request.data["name"]
policy.desc = request.data["desc"] policy.desc = request.data["desc"]
policy.active = request.data["active"] policy.active = request.data["active"]
policy.enforced = request.data["enforced"]
try: try:
policy.save(update_fields=["name", "desc", "active"]) policy.save(update_fields=["name", "desc", "active", "enforced"])
except DataError: except DataError:
content = {"error": "Policy name too long (max 255 chars)"} content = {"error": "Policy name too long (max 255 chars)"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
# Update Clients, Sites to Policy
if len(request.data["clients"]) > 0:
policy.clients.set(request.data["clients"])
else:
policy.clients.clear()
if len(request.data["sites"]) > 0:
policy.sites.set(request.data["sites"])
else:
policy.sites.clear()
return Response("ok") return Response("ok")
def delete(self, request, pk): def delete(self, request, pk):
@@ -110,25 +96,29 @@ class PolicyAutoTask(APIView):
# TODO pull agents and status for policy task # TODO pull agents and status for policy task
return Response(list()) return Response(list())
class RunPolicyTask(APIView): def put(self, request, pk):
def get(self, request, pk):
# TODO: Run task for all Agents under policy
return Response("ok") return Response("ok")
class PolicyCheck(APIView): class PolicyCheck(APIView):
def get(self, request, pk): def get(self, request, pk):
checks = Check.objects.filter(policy__pk=pk) checks = Check.objects.filter(policy__pk=pk, agent=None)
return Response(CheckSerializer(checks, many=True).data) return Response(CheckSerializer(checks, many=True).data)
def patch(self, request, policy, check): def patch(self, request, check):
# TODO pull agents and status for policy check checks = Check.objects.filter(parent_check=check)
return Response(PolicyCheckStatusSerializer(checks, many=True).data)
def put(self, request):
# TODO run policy check manually for all agents
return Response(list()) return Response(list())
class OverviewPolicy(APIView): class OverviewPolicy(APIView):
def get(self, request): def get(self, request):
"""
clients = Client.objects.all() clients = Client.objects.all()
response = {} response = {}
@@ -154,8 +144,11 @@ class OverviewPolicy(APIView):
response[client.client] = client_sites response[client.client] = client_sites
return Response(response) return Response(response)
"""
clients = Client.objects.all()
return Response(PolicyOverviewSerializer(clients, many=True).data)
class GetRelated(APIView): class GetRelated(APIView):
def get(self, request, pk): def get(self, request, pk):
@@ -182,35 +175,29 @@ class GetRelated(APIView):
def post(self, request): def post(self, request):
# Update Agents, Clients, Sites to Policy # Update Agents, Clients, Sites to Policy
policies = request.data["policies"]
related_type = request.data["type"] related_type = request.data["type"]
pk = request.data["pk"] pk = request.data["pk"]
if len(policies) > 0: if request.data["policy"] != 0:
policy = Policy.objects.get(pk=request.data["policy"])
if related_type == "client": if related_type == "client":
client = get_object_or_404(Client, pk=pk) Client.objects.filter(pk=pk).update(policy=policy)
client.policies.set(policies)
if related_type == "site": if related_type == "site":
site = get_object_or_404(Site, pk=pk) Site.objects.filter(pk=pk).update(policy=policy)
site.policies.set(policies)
if related_type == "agent": if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk) Agent.objects.filter(pk=pk).update(policy=policy, policies_pending=True)
agent.policies.set(policies)
else: else:
if related_type == "client": if related_type == "client":
client = get_object_or_404(Client, pk=pk) Client.objects.filter(pk=pk).update(policy=None)
client.policies.clear()
if related_type == "site": if related_type == "site":
site = get_object_or_404(Site, pk=pk) Site.objects.filter(pk=pk).update(policy=None)
site.policies.clear()
if related_type == "agent": if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk) Agent.objects.filter(pk=pk).update(policy=None, policies_pending=True)
agent.policies.clear()
return Response("ok") return Response("ok")
@@ -219,16 +206,16 @@ class GetRelated(APIView):
pk = request.data["pk"] pk = request.data["pk"]
if related_type == "agent": if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk) policy = Policy.objects.filter(agents__pk=pk).first()
return Response(PolicySerializer(agent.policies.all(), many=True).data) return Response(PolicySerializer(policy).data)
if related_type == "site": if related_type == "site":
site = get_object_or_404(Site, pk=pk) policy = Policy.objects.filter(sites__pk=pk).first()
return Response(PolicySerializer(site.policies.all(), many=True).data) return Response(PolicySerializer(policy).data)
if related_type == "client": if related_type == "client":
client = get_object_or_404(Client, pk=pk) policy = Policy.objects.filter(clients__pk=pk).first()
return Response(PolicySerializer(client.policies.all(), many=True).data) return Response(PolicySerializer(policy).data)
content = {"error": "Data was submitted incorrectly"} content = {"error": "Data was submitted incorrectly"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-04 17:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='check',
name='managed_by_policy',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-04 17:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0002_check_managed_by_policy'),
]
operations = [
migrations.AddField(
model_name='check',
name='overriden_by_policy',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-07 00:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0003_check_overriden_by_policy'),
]
operations = [
migrations.AddField(
model_name='check',
name='parent_check',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@@ -10,6 +10,8 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from core.models import CoreSettings from core.models import CoreSettings
import agents
from .tasks import handle_check_email_alert_task from .tasks import handle_check_email_alert_task
CHECK_TYPE_CHOICES = [ CHECK_TYPE_CHOICES = [
@@ -66,6 +68,9 @@ class Check(models.Model):
blank=True, blank=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
managed_by_policy = models.BooleanField(default=False)
overriden_by_policy = models.BooleanField(default=False)
parent_check = models.PositiveIntegerField(null=True, blank=True)
name = models.CharField(max_length=255, null=True, blank=True) name = models.CharField(max_length=255, null=True, blank=True)
check_type = models.CharField( check_type = models.CharField(
max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace" max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace"
@@ -212,6 +217,35 @@ class Check(models.Model):
return default_services return default_services
def create_policy_check(self, agent):
Check.objects.create(
agent=agent,
policy=self.policy,
managed_by_policy=True,
parent_check=self.pk,
name=self.name,
check_type=self.check_type,
email_alert=self.email_alert,
text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details,
threshold=self.threshold,
disk=self.disk,
ip=self.ip,
script=self.script,
timeout=self.timeout,
svc_name=self.svc_name,
svc_display_name=self.svc_display_name,
pass_if_start_pending=self.pass_if_start_pending,
restart_if_stopped=self.restart_if_stopped,
svc_policy_mode=self.svc_policy_mode,
log_name=self.log_name,
event_id=self.event_id,
event_type=self.event_type,
fail_when=self.fail_when,
search_last_days=self.search_last_days,
)
def send_email(self): def send_email(self):
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()

View File

@@ -38,7 +38,7 @@ class CheckSerializer(serializers.ModelSerializer):
# disk checks # disk checks
# make sure no duplicate diskchecks exist for an agent/policy # make sure no duplicate diskchecks exist for an agent/policy
if check_type == "diskspace" and not self.instance: # only on create if check_type == "diskspace" and not self.instance: # only on create
checks = Check.objects.filter(**self.context).filter(check_type="diskspace") checks = Check.objects.filter(**self.context).filter(check_type="diskspace").exclude(managed_by_policy=True)
if checks: if checks:
for check in checks: for check in checks:
if val["disk"] in check.disk: if val["disk"] in check.disk:

View File

@@ -68,6 +68,7 @@ class GetUpdateDeleteCheck(APIView):
def delete(self, request, pk): def delete(self, request, pk):
check = get_object_or_404(Check, pk=pk) check = get_object_or_404(Check, pk=pk)
check.delete() check.delete()
return Response(f"{check.readable_desc} was deleted!") return Response(f"{check.readable_desc} was deleted!")

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.7 on 2020-06-09 16:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('automation', '0003_auto_20200609_1607'),
('clients', '0002_auto_20200531_2058'),
]
operations = [
migrations.AddField(
model_name='client',
name='policy',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='automation.Policy'),
),
migrations.AddField(
model_name='site',
name='policy',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='automation.Policy'),
),
]

View File

@@ -4,6 +4,13 @@ from agents.models import Agent
class Client(models.Model): class Client(models.Model):
client = models.CharField(max_length=255, unique=True) client = models.CharField(max_length=255, unique=True)
policy = models.ForeignKey(
"automation.Policy",
related_name="clients",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
def __str__(self): def __str__(self):
return self.client return self.client
@@ -22,6 +29,13 @@ class Client(models.Model):
class Site(models.Model): class Site(models.Model):
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE) client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
site = models.CharField(max_length=255) site = models.CharField(max_length=255)
policy = models.ForeignKey(
"automation.Policy",
related_name="sites",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
def __str__(self): def __str__(self):
return self.site return self.site

View File

@@ -6,7 +6,7 @@ services:
# Container that hosts Vue frontend # Container that hosts Vue frontend
app: app:
image: node:12 image: node:12
command: /bin/bash -c "npm install --force && npm run serve -- --host 0.0.0.0 --port 80 --public ${APP_HOST}" command: /bin/bash -c "npm install && npm run serve -- --host 0.0.0.0 --port 80 --public ${APP_HOST}"
working_dir: /home/node working_dir: /home/node
volumes: volumes:
- ../web:/home/node - ../web:/home/node

View File

@@ -91,29 +91,9 @@ This allows you to edit the files locally and those changes will be presented to
Files that need to be manually created are: Files that need to be manually created are:
- api/tacticalrmm/tacticalrmm/local_settings.py - api/tacticalrmm/tacticalrmm/local_settings.py
- web/.env.local - web/.env
For HMR to work with vue you may need to alter the web/vue.config.js file to with these changes For HMR to work with vue you can copy .env.example and modify the setting to fit your dev environment.
```
devServer: {
//host: "192.168.99.150",
disableHostCheck: true,
public: "YOUR_APP_URL"
},
```
Since this file is checked into git you can configure git to ignore it and the changes will stay intact
```
git update-index --assume-unchanged ./web/vue.config.js
```
To revert this run
```
git update-index --no-assume-unchanged ./web/vue.config.js
```
### Create Python Virtual Env ### Create Python Virtual Env

View File

@@ -83,6 +83,9 @@
<template v-slot:header-cell-statusicon="props"> <template v-slot:header-cell-statusicon="props">
<q-th auto-width :props="props"></q-th> <q-th auto-width :props="props"></q-th>
</template> </template>
<template v-slot:header-cell-policystatus="props">
<q-th auto-width :props="props"></q-th>
</template>
<!-- body slots --> <!-- body slots -->
<template slot="body" slot-scope="props" :props="props"> <template slot="body" slot-scope="props" :props="props">
<q-tr @contextmenu="checkpk = props.row.id"> <q-tr @contextmenu="checkpk = props.row.id">
@@ -128,6 +131,18 @@
v-model="props.row.email_alert" v-model="props.row.email_alert"
/> />
</q-td> </q-td>
<!-- policy check icon -->
<q-td v-if="props.row.managed_by_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-tooltip>This check is overriden by a policy</q-tooltip>
</q-icon>
</q-td>
<q-td v-else></q-td>
<!-- status icon --> <!-- status icon -->
<q-td v-if="props.row.status === 'pending'"></q-td> <q-td v-if="props.row.status === 'pending'"></q-td>
<q-td v-else-if="props.row.status === 'passing'"> <q-td v-else-if="props.row.status === 'passing'">
@@ -292,6 +307,7 @@ export default {
columns: [ columns: [
{ name: "smsalert", field: "text_alert", align: "left" }, { name: "smsalert", field: "text_alert", align: "left" },
{ name: "emailalert", field: "email_alert", align: "left" }, { name: "emailalert", field: "email_alert", align: "left" },
{ name: "policystatus", align: "left" },
{ name: "statusicon", align: "left" }, { name: "statusicon", align: "left" },
{ name: "desc", label: "Description", align: "left" }, { name: "desc", label: "Description", align: "left" },
{ name: "status", label: "Status", field: "status", align: "left" }, { name: "status", label: "Status", field: "status", align: "left" },

View File

@@ -62,19 +62,36 @@
class="settings-tbl-sticky" class="settings-tbl-sticky"
:data="policies" :data="policies"
:columns="columns" :columns="columns"
:visible-columns="visibleColumns"
:pagination.sync="pagination" :pagination.sync="pagination"
:selected.sync="selected" :selected.sync="selected"
@selection="policyRowSelected"
selection="single" selection="single"
@selection="policyRowSelected"
row-key="id" row-key="id"
binary-state-sort binary-state-sort
hide-bottom hide-bottom
flat
> >
<!-- header slots -->
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">{{ col.label }}</q-th> <template v-for="col in props.cols">
<q-th v-if="col.name === 'active'" auto-width :key="col.name">
<q-icon name="power_settings_new" size="1.5em">
<q-tooltip>Enable Policy</q-tooltip>
</q-icon>
</q-th>
<q-th v-else-if="col.name === 'enforced'" auto-width :key="col.name">
<q-icon name="security" size="1.5em">
<q-tooltip>Enforce Policy (Will override Agent checks)</q-tooltip>
</q-icon>
</q-th>
<q-th v-else :key="col.name" :props="props">
{{ col.label }}
</q-th>
</template>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
@@ -126,15 +143,30 @@
</q-item> </q-item>
</q-list> </q-list>
</q-menu> </q-menu>
<!-- enabled checkbox -->
<q-td>
<q-checkbox
dense
@input="toggleCheckbox(props.row, 'Active')"
v-model="props.row.active"
/>
</q-td>
<!-- enforced checkbox -->
<q-td>
<q-checkbox
dense
@input="toggleCheckbox(props.row, 'Enforced')"
v-model="props.row.enforced"
/>
</q-td>
<q-td>{{ props.row.name }}</q-td> <q-td>{{ props.row.name }}</q-td>
<q-td>{{ props.row.desc }}</q-td> <q-td>{{ props.row.desc }}</q-td>
<q-td>{{ props.row.active }}</q-td>
<q-td> <q-td>
<span <span
style="cursor:pointer;color:blue;text-decoration:underline" style="cursor:pointer;color:blue;text-decoration:underline"
@click="showRelationsModal(props.row)" @click="showRelationsModal(props.row)"
> >
{{ `Show Relations (${props.row.clients.length + props.row.sites.length + props.row.agents.length}+)` }} {{ `Show Relations (${props.row.clients_count + props.row.sites_count + props.row.agents_count}+)` }}
</span> </span>
</q-td> </q-td>
</q-tr> </q-tr>
@@ -196,13 +228,9 @@ export default {
policy: null, policy: null,
editPolicyId: null, editPolicyId: null,
selected: [], selected: [],
pagination: {
rowsPerPage: 0,
sortBy: "id",
descending: false
},
columns: [ columns: [
{ name: "id", label: "ID", field: "id" }, { name: "active", label: "Active", field: "active", align: "left" },
{ name: "enforced", label: "Enforced", field: "enforced", align: "left" },
{ {
name: "name", name: "name",
label: "Name", label: "Name",
@@ -215,24 +243,17 @@ export default {
label: "Description", label: "Description",
field: "desc", field: "desc",
align: "left", align: "left",
sortable: false
},
{
name: "active",
label: "Active",
field: "active",
align: "left",
sortable: true
}, },
{ {
name: "actions", name: "actions",
label: "Actions", label: "Actions",
field: "actions", field: "actions",
align: "left", align: "left",
sortable: false
} }
], ],
visibleColumns: ["name", "desc", "active", "actions"] pagination: {
rowsPerPage: 9999
},
}; };
}, },
methods: { methods: {
@@ -295,6 +316,32 @@ export default {
showPolicyOverview() { showPolicyOverview() {
this.showPolicyOverviewModal = true this.showPolicyOverviewModal = true
this.clearRow(); this.clearRow();
},
toggleCheckbox(policy, type) {
let text = "";
if (type === "Active") {
text = policy.active ? "Policy enabled successfully" : "Policy disabled successfully";
} else if (type === "Enforced") {
text = policy.enforced ? "Policy enforced successfully" : "Policy enforcement disabled";
}
const data ={
id: policy.id,
name: policy.name,
desc: policy.desc,
active: policy.active,
enforced: policy.enforced
}
this.$store
.dispatch("automation/editPolicy", data)
.then(response => {
this.$q.notify(notifySuccessConfig(text));
})
.catch(error => {
this.$q.notify(notifyErrorConfig("An Error occured while editing policy"));
});
} }
}, },
computed: { computed: {

View File

@@ -105,19 +105,18 @@ export default {
processTreeDataFromApi(data) { processTreeDataFromApi(data) {
/* Structure /* Structure
* [{ * [{
* "client_name_1": { * client: Client Name 1,
* "policies": [ * policy: {
* {
* id: 1, * id: 1,
* name: "Policy Name 1" * name: "Policy Name 1"
* },
* sites: [{
* name: "Site Name 1",
* policy: {
* id: 2,
* name: "Policy Name 2"
* } * }
* ], * }]
* sites: {
* "site_name_1": {
* "policies": []
* }
* }
* }
* }] * }]
*/ */
@@ -129,7 +128,7 @@ export default {
for (let client in data) { for (let client in data) {
var client_temp = {}; var client_temp = {};
client_temp["label"] = client; client_temp["label"] = data[client].client;
client_temp["id"] = unique_id; client_temp["id"] = unique_id;
client_temp["icon"] = "business"; client_temp["icon"] = "business";
client_temp["selectable"] = false; client_temp["selectable"] = false;
@@ -139,27 +138,25 @@ export default {
// Add any policies assigned to client // Add any policies assigned to client
if (data[client].policies.length > 0) { if (data[client].policy !== null) {
for (let policy in data[client].policies) {
let disabled = ""; let disabled = "";
// Indicate if the policy is active or not // Indicate if the policy is active or not
if (!data[client].policies[policy].active) { if (!data[client].policy.active) {
disabled = " (disabled)"; disabled = " (disabled)";
} }
client_temp["children"].push({ client_temp["children"].push({
label: data[client].policies[policy].name + disabled, label: data[client].policy.name + disabled,
icon: "policy", icon: "policy",
id: data[client].policies[policy].id id: data[client].policy.id
}); });
} }
}
// Iterate through Sites // Iterate through Sites
for (let site in data[client].sites) { for (let site in data[client].sites) {
var site_temp = {}; var site_temp = {};
site_temp["label"] = site; site_temp["label"] = data[client].sites[site].site;
site_temp["id"] = unique_id; site_temp["id"] = unique_id;
site_temp["icon"] = "apartment"; site_temp["icon"] = "apartment";
site_temp["selectable"] = false; site_temp["selectable"] = false;
@@ -167,23 +164,21 @@ export default {
unique_id--; unique_id--;
// Add any policies assigned to site // Add any policies assigned to site
if (data[client].sites[site].policies.length > 0) { if (data[client].sites[site].policy !== null) {
site_temp["children"] = []; site_temp["children"] = [];
for (let policy in data[client].sites[site].policies) {
// Indicate if the policy is active or not // Indicate if the policy is active or not
let disabled = ""; let disabled = "";
if (!data[client].sites[site].policies[policy].active) { if (!data[client].sites[site].policy.active) {
disabled = " (disabled)"; disabled = " (disabled)";
} }
site_temp["children"].push({ site_temp["children"].push({
label: data[client].sites[site].policies[policy].name + disabled, label: data[client].sites[site].policy.name + disabled,
icon: "policy", icon: "policy",
id: data[client].sites[site].policies[policy].id id: data[client].sites[site].policy.id
}); });
}
} }
// Add Site to Client children array // Add Site to Client children array

View File

@@ -1,7 +1,7 @@
<template> <template>
<q-card style="width: 60vw" > <q-card style="width: 60vw" >
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<div class="text-h6">Edit policies assigned to {{ type }}</div> <div class="text-h6">Edit policy assigned to {{ type }}</div>
<q-space /> <q-space />
<q-btn icon="close" flat round dense v-close-popup /> <q-btn icon="close" flat round dense v-close-popup />
</q-card-section> </q-card-section>
@@ -11,10 +11,9 @@
v-model="selected" v-model="selected"
:options="options" :options="options"
filled filled
multiple
use-chips
options-selected-class="text-green" options-selected-class="text-green"
dense dense
clearable
> >
<template v-slot:option="props"> <template v-slot:option="props">
<q-item <q-item
@@ -32,7 +31,7 @@
</q-select> </q-select>
</q-card-section> </q-card-section>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<q-btn label="Add Polices" color="primary" type="submit" /> <q-btn label="Add Policy" color="primary" type="submit" />
</q-card-section> </q-card-section>
</q-form> </q-form>
</q-card> </q-card>
@@ -57,7 +56,7 @@ export default {
}, },
data() { data() {
return { return {
selected: [], selected: null,
options: [] options: []
} }
}, },
@@ -73,7 +72,7 @@ export default {
let data = {}; let data = {};
data.pk = this.pk, data.pk = this.pk,
data.type = this.type; data.type = this.type;
data.policies = this.selected.map(policy => policy.value); data.policy = this.selected === null ? 0 : this.selected.value;
this.$store this.$store
.dispatch("automation/updateRelatedPolicies", data) .dispatch("automation/updateRelatedPolicies", data)
@@ -102,14 +101,16 @@ export default {
});; });;
}, },
getRelations(pk, type) { getRelation(pk, type) {
this.$store this.$store
.dispatch("automation/getRelatedPolicies", {pk, type}) .dispatch("automation/getRelatedPolicies", {pk, type})
.then(r => { .then(r => {
this.selected = r.data.map(item => ({ if (r.data.id !== undefined) {
label: item.name, this.selected = {
value: item.id label: r.data.name,
})) value: r.data.id
}
}
}) })
.catch(e => { .catch(e => {
this.$q.notify(notifyErrorConfig("Add error occured while loading")); this.$q.notify(notifyErrorConfig("Add error occured while loading"));
@@ -118,7 +119,7 @@ export default {
}, },
mounted() { mounted() {
this.getPolicies(); this.getPolicies();
this.getRelations(this.pk, this.type); this.getRelation(this.pk, this.type);
} }
} }
</script> </script>

View File

@@ -25,69 +25,9 @@
</div> </div>
</q-card-section> </q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-2">Clients:</div> <div class="col-2">Enforced:</div>
<div class="col-10"> <div class="col-10">
<q-select <q-toggle v-model="enforced" color="green" />
v-model="selectedClients"
:options="clientOptions"
filled
multiple
use-chips
options-selected-class="text-green"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">No Results</q-item-section>
</q-item>
</template>
<template v-slot:option="props">
<q-item
v-bind="props.itemProps"
v-on="props.itemEvents"
>
<q-item-section avatar>
<q-icon v-if="props.selected" name="check" />
</q-item-section>
<q-item-section>
<q-item-label v-html="props.opt.label" />
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Sites:</div>
<div class="col-10">
<q-select
v-model="selectedSites"
:options="siteOptions"
filled
multiple
use-chips
options-selected-class="text-green"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">No Results</q-item-section>
</q-item>
</template>
<template v-slot:option="props">
<q-item
v-bind="props.itemProps"
v-on="props.itemEvents"
>
<q-item-section avatar>
<q-icon v-if="props.selected" name="check" />
</q-item-section>
<q-item-section>
<!-- <q-item-label overline>{{ props.opt.client }}</q-item-label> -->
<q-item-label v-html="props.opt.label" />
<q-item-label caption>{{ props.opt.client }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
@@ -99,21 +39,17 @@
<script> <script>
import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins"; import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
import dropdown_formatter from "@/mixins/dropdown_formatter";
export default { export default {
name: "PolicyForm", name: "PolicyForm",
mixins: [mixins, dropdown_formatter], mixins: [mixins],
props: { pk: Number }, props: { pk: Number },
data() { data() {
return { return {
name: "", name: "",
desc: "", desc: "",
active: false, enforced: false,
selectedSites: [], active: false
selectedClients: [],
clientOptions: [],
siteOptions: []
}; };
}, },
computed: { computed: {
@@ -134,14 +70,7 @@ export default {
this.name = r.data.name; this.name = r.data.name;
this.desc = r.data.desc; this.desc = r.data.desc;
this.active = r.data.active; this.active = r.data.active;
this.selectedSites = r.data.sites.map(site => ({ this.enforced = r.data.enforced;
label: site.site,
value: site.id
}) );
this.selectedClients = r.data.clients.map(client => ({
label: client.client,
value: client.id
}) );
}); });
}, },
submit() { submit() {
@@ -157,8 +86,7 @@ export default {
name: this.name, name: this.name,
desc: this.desc, desc: this.desc,
active: this.active, active: this.active,
sites: this.selectedSites.map(site => site.value), enforced: this.enforced
clients: this.selectedClients.map(client => client.value)
}; };
if (this.pk) { if (this.pk) {
@@ -186,20 +114,6 @@ export default {
this.$q.notify(notifyErrorConfig(e.response.data)); this.$q.notify(notifyErrorConfig(e.response.data));
}); });
} }
},
getClients() {
this.$store
.dispatch("loadClients")
.then(r => {
this.clientOptions = this.formatClients(r.data);
});
},
getSites() {
this.$store
.dispatch("loadSites")
.then(r => {
this.siteOptions = this.formatSites(r.data);
});
} }
}, },
mounted() { mounted() {
@@ -207,9 +121,6 @@ export default {
if (this.pk) { if (this.pk) {
this.getPolicy(); this.getPolicy();
} }
this.getClients();
this.getSites();
} }
}; };
</script> </script>

View File

@@ -144,7 +144,7 @@ export default {
getCheckData() { getCheckData() {
this.$q.loading.show(); this.$q.loading.show();
this.$store this.$store
.dispatch("automation/loadCheckStatus", { policypk: this.item.policy, checkpk: this.item.id }) .dispatch("automation/loadCheckStatus", { checkpk: this.item.id })
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();
this.tableData = r.data this.tableData = r.data
@@ -157,7 +157,7 @@ export default {
getTaskData() { getTaskData() {
this.$q.loading.show(); this.$q.loading.show();
this.$store this.$store
.dispatch("automation/loadAutomatedTaskStatus", { policypk: this.item.policy, taskpk: this.item.id }) .dispatch("automation/loadAutomatedTaskStatus", { taskpk: this.item.id })
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();
this.tableData = r.data this.tableData = r.data
@@ -174,7 +174,23 @@ export default {
closeScriptOutput() { closeScriptOutput() {
this.showScriptOutput = false; this.showScriptOutput = false;
this.scriptInfo = {} this.scriptInfo = {}
} },
pingInfo(desc, output) {
this.$q.dialog({
title: desc,
style: "width: 50vw; max-width: 60vw",
message: `<pre>${output}</pre>`,
html: true
});
},
scriptMoreInfo(props) {
this.scriptInfo = props;
this.showScriptOutput = true;
},
eventLogMoreInfo(props) {
this.evtLogData = props;
this.showEventLogOutput = true;
},
}, },
mounted() { mounted() {
if (this.type === "task") { if (this.type === "task") {

View File

@@ -55,11 +55,11 @@ export default {
context.commit("setPolicyChecks", r.data); context.commit("setPolicyChecks", r.data);
}); });
}, },
loadCheckStatus(context, { policypk, checkpk }) { loadCheckStatus(context, { checkpk }) {
return axios.patch(`/automation/${policypk}/policycheckstatus/${checkpk}/check/`); return axios.patch(`/automation/policycheckstatus/${checkpk}/check/`);
}, },
loadAutomatedTaskStatus(context, { policypk, taskpk }) { loadAutomatedTaskStatus(context, { taskpk }) {
return axios.patch(`/automation/${policypk}/policyautomatedtaskstatus/${taskpk}/task/`); return axios.patch(`/automation/policyautomatedtaskstatus/${taskpk}/task/`);
}, },
loadPolicy(context, pk) { loadPolicy(context, pk) {
return axios.get(`/automation/policies/${pk}/`); return axios.get(`/automation/policies/${pk}/`);
@@ -76,7 +76,10 @@ export default {
}); });
}, },
runPolicyTask(context, pk) { runPolicyTask(context, pk) {
return axios.get(`/automation/runwintask/${pk}/`); return axios.put(`/automation/runwintask/${pk}/`);
},
runPolicyCheck(context, pk) {
return axios.put(`/automation/runpolicycheck/${pk}/`);
}, },
getRelated(context, pk) { getRelated(context, pk) {
return axios.get(`/automation/policies/${pk}/related/`); return axios.get(`/automation/policies/${pk}/related/`);

View File

@@ -241,6 +241,8 @@ describe("AutomationManager.vue", () => {
}); });
// TODO Test Checkboxes on table
// TODO: Test @close and @hide events // TODO: Test @close and @hide events
}); });

View File

@@ -8,48 +8,16 @@ const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
/*** TEST DATA ***/ /*** TEST DATA ***/
const clients = [
{
id: 1,
client: "Test Client"
},
{
id: 2,
client: "Test Client2"
},
{
id: 3,
client: "Test Client3"
}
];
const sites = [
{
id: 1,
site: "Site Name",
client_name: "Test Client"
},
{
id: 2,
site: "Site Name2",
client_name: "Test Client2"
}
];
const policy = { const policy = {
id: 1, id: 1,
name: "Test Policy", name: "Test Policy",
desc: "Test Desc", desc: "Test Desc",
active: true, enforced: false,
clients: [], active: true
sites: []
}; };
let actions, rootActions, store; let actions, rootActions, store;
beforeEach(() => { beforeEach(() => {
rootActions = {
loadClients: jest.fn(() => new Promise(res => res({ data: clients }))),
loadSites: jest.fn(() => new Promise(res => res({ data: sites }))),
};
actions = { actions = {
loadPolicy: jest.fn(() => new Promise(res => res({ data: policy }))), loadPolicy: jest.fn(() => new Promise(res => res({ data: policy }))),
@@ -87,8 +55,6 @@ describe("PolicyForm.vue when editting", () => {
/*** TESTS ***/ /*** TESTS ***/
it("calls vuex actions on mount with pk prop set", () => { it("calls vuex actions on mount with pk prop set", () => {
expect(rootActions.loadClients).toHaveBeenCalled();
expect(rootActions.loadSites).toHaveBeenCalled();
expect(actions.loadPolicy).toHaveBeenCalledWith(expect.anything(), 1); expect(actions.loadPolicy).toHaveBeenCalledWith(expect.anything(), 1);
}); });
@@ -127,24 +93,11 @@ describe("PolicyForm.vue when adding", () => {
/*** TESTS ***/ /*** TESTS ***/
it("calls vuex actions on mount", () => { it("calls vuex actions on mount", () => {
expect(rootActions.loadClients).toHaveBeenCalled();
expect(rootActions.loadSites).toHaveBeenCalled();
// Not called unless pk prop is set // Not called unless pk prop is set
expect(actions.loadPolicy).not.toHaveBeenCalled(); expect(actions.loadPolicy).not.toHaveBeenCalled();
}); });
it("Sets client and site options correctly", async () => {
// Make sure the promises are resolved
await flushPromises();
expect(wrapper.vm.clientOptions).toHaveLength(3);
expect(wrapper.vm.siteOptions).toHaveLength(2);
});
it("sends the correct add action on submit", async () => { it("sends the correct add action on submit", async () => {
wrapper.setData({name: "Test Policy"}); wrapper.setData({name: "Test Policy"});

View File

@@ -7,23 +7,21 @@ import "../../utils/quasar.js";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
const related = [ const related = {
{
id: 1, id: 1,
name: "Test Policy" name: "Test Policy"
}, };
{
id: 2,
name: "Test Policy 2"
}
];
let state, actions, getters, store; let state, actions, getters, store;
beforeEach(() => { beforeEach(() => {
state = { state = {
policies: [ policies: [
...related, related,
{
id: 2,
name: "Test Policy 2"
},
{ {
id: 3, id: 3,
name: "TestPolicy 3" name: "TestPolicy 3"
@@ -34,7 +32,7 @@ beforeEach(() => {
actions = { actions = {
updateRelatedPolicies: jest.fn(), updateRelatedPolicies: jest.fn(),
loadPolicies: jest.fn(), loadPolicies: jest.fn(),
getRelatedPolicies: jest.fn(() => new Promise(res => res({ data: related }))), getRelatedPolicies: jest.fn(() => Promise.resolve({ data: related })),
}; };
getters = { getters = {
@@ -86,7 +84,7 @@ describe.each([
it("renders title correctly", () => { it("renders title correctly", () => {
expect(wrapper.find(".text-h6").text()).toBe(`Edit policies assigned to ${type}`); expect(wrapper.find(".text-h6").text()).toBe(`Edit policy assigned to ${type}`);
}); });
it("renders correct amount of policies in dropdown", async () => { it("renders correct amount of policies in dropdown", async () => {
@@ -95,10 +93,10 @@ describe.each([
expect(wrapper.vm.options).toHaveLength(3); expect(wrapper.vm.options).toHaveLength(3);
}); });
it("renders correct amount of related policies in selected", async () => { it("renders correct policy in selected", async () => {
await flushpromises(); await flushpromises();
expect(wrapper.vm.selected).toHaveLength(2); expect(wrapper.vm.selected).toStrictEqual({label: related.name, value: related.id});
}); });
it("sends correct data on form submit", async () => { it("sends correct data on form submit", async () => {
@@ -109,7 +107,7 @@ describe.each([
await form.vm.$emit("submit"); await form.vm.$emit("submit");
expect(actions.updateRelatedPolicies).toHaveBeenCalledWith(expect.anything(), expect(actions.updateRelatedPolicies).toHaveBeenCalledWith(expect.anything(),
{ pk: pk, type: type, policies: [1,2] } { pk: pk, type: type, policy: related.id }
); );
}); });

View File

@@ -8,44 +8,44 @@ localVue.use(Vuex);
describe("PolicyOverview.vue", () => { describe("PolicyOverview.vue", () => {
const policyTreeData = { const policyTreeData = [
// node 0
"Client Name 1": {
policies: [
{ {
// node 0
client: "Client Name 1",
policy: {
id: 1, id: 1,
name: "Policy Name 1", name: "Policy Name 1",
active: true active: true
}
],
sites: {
// node -1
"Site Name 1": { policies: []}
}
}, },
// node -2 // node -1
"Client Name 2": { sites: [
policies: [
{ {
site: "Site Name 1",
policy: null
}
]
},
{
// node -2
client: "Client Name 2",
policy: {
id: 2, id: 2,
name: "Policy Name 2", name: "Policy Name 2",
active: true active: true
} },
], sites: [
sites: {
// node -3
"Site Name 2": {
policies: [
{ {
// node -3
site: "Site Name 2",
policy: {
id: 3, id: 3,
name: "Policy Name 3", name: "Policy Name 3",
active: false active: false
} }
}
] ]
} }
} ];
}
};
let wrapper, actions, mutations, store; let wrapper, actions, mutations, store;