Merge pull request #22 from sadnub/feature-policies-alerts

Policy Check Finish
This commit is contained in:
wh1te909 2020-06-11 21:14:38 -07:00 committed by GitHub
commit 8dbf5d37c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
import automation
class Agent(models.Model):
version = models.CharField(default="0.1.0", max_length=255)
@ -50,9 +52,17 @@ class Agent(models.Model):
is_updating = models.BooleanField(default=False)
choco_installed = models.BooleanField(default=False)
wmi_detail = JSONField(null=True)
policies_pending = models.BooleanField(default=False)
time_zone = models.CharField(
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):
return self.hostname
@ -185,6 +195,21 @@ class Agent(models.Model):
except:
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
def get_login_token(self, key, user, action=3):
key = bytes.fromhex(key)

View File

@ -311,7 +311,11 @@ class CheckRunner(APIView):
def get(self, request, 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 = {
"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 .models import Policy
from .models import Policy, PolicyExclusions
admin.site.register(Policy)
admin.site.register(PolicyExclusions)

View File

@ -1,5 +1,9 @@
from django.apps import AppConfig
class AutomationConfig(AppConfig):
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)
desc = models.CharField(max_length=255)
active = models.BooleanField(default=False)
agents = models.ManyToManyField(Agent, related_name="policies")
sites = models.ManyToManyField(Site, related_name="policies")
clients = models.ManyToManyField(Client, related_name="policies")
enforced = models.BooleanField(default=False)
def __str__(self):
return self.name
@ -30,8 +28,160 @@ class Policy(models.Model):
for site in client.sites.all():
filtered_sites_ids.append(site.site)
site_agents = Agent.objects.filter(site__in=filtered_sites_ids)
client_agents = Agent.objects.filter(client__in=client_ids)
# Combine querysets and remove duplicates
return explicit_agents.union(site_agents, client_agents)
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()
@staticmethod
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 autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from autotasks.serializers import TaskSerializer
from checks.serializers import CheckSerializer
class PolicySerializer(serializers.ModelSerializer):
class PolicySerializer(ModelSerializer):
class Meta:
model = Policy
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:
model = Policy
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
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)

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("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
path("<int:policy>/policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
path("<int:policy>/policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
path("runwintask/<int:pk>/", views.RunPolicyTask.as_view()),
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
path("runwintask/<int:pk>/", views.PolicyAutoTask.as_view()),
]

View File

@ -20,8 +20,10 @@ from agents.serializers import AgentHostnameSerializer
from .serializers import (
PolicySerializer,
PolicyRelationSerializer,
AutoTaskPolicySerializer,
PolicyTableSerializer,
PolicyOverviewSerializer,
PolicyCheckStatusSerializer,
AutoTaskPolicySerializer
)
from checks.serializers import CheckSerializer
@ -31,30 +33,24 @@ class GetAddPolicies(APIView):
policies = Policy.objects.all()
return Response(PolicyRelationSerializer(policies, many=True).data)
return Response(PolicyTableSerializer(policies, many=True).data)
def post(self, request):
name = request.data["name"].strip()
desc = request.data["desc"].strip()
active = request.data["active"]
enforced = active = request.data["enforced"]
if Policy.objects.filter(name=name):
content = {"error": f"Policy {name} already exists"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
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:
content = {"error": "Policy name too long (max 255 chars)"}
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")
@ -63,7 +59,7 @@ class GetUpdateDeletePolicy(APIView):
policy = get_object_or_404(Policy, pk=pk)
return Response(PolicyRelationSerializer(policy).data)
return Response(PolicySerializer(policy).data)
def put(self, request, pk):
@ -72,24 +68,14 @@ class GetUpdateDeletePolicy(APIView):
policy.name = request.data["name"]
policy.desc = request.data["desc"]
policy.active = request.data["active"]
policy.enforced = request.data["enforced"]
try:
policy.save(update_fields=["name", "desc", "active"])
policy.save(update_fields=["name", "desc", "active", "enforced"])
except DataError:
content = {"error": "Policy name too long (max 255 chars)"}
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")
def delete(self, request, pk):
@ -109,26 +95,30 @@ class PolicyAutoTask(APIView):
# TODO pull agents and status for policy task
return Response(list())
class RunPolicyTask(APIView):
def get(self, request, pk):
# TODO: Run task for all Agents under policy
def put(self, request, pk):
return Response("ok")
class PolicyCheck(APIView):
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)
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())
class OverviewPolicy(APIView):
def get(self, request):
"""
clients = Client.objects.all()
response = {}
@ -154,8 +144,11 @@ class OverviewPolicy(APIView):
response[client.client] = client_sites
return Response(response)
"""
clients = Client.objects.all()
return Response(PolicyOverviewSerializer(clients, many=True).data)
class GetRelated(APIView):
def get(self, request, pk):
@ -182,35 +175,29 @@ class GetRelated(APIView):
def post(self, request):
# Update Agents, Clients, Sites to Policy
policies = request.data["policies"]
related_type = request.data["type"]
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":
client = get_object_or_404(Client, pk=pk)
client.policies.set(policies)
Client.objects.filter(pk=pk).update(policy=policy)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
site.policies.set(policies)
Site.objects.filter(pk=pk).update(policy=policy)
if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk)
agent.policies.set(policies)
Agent.objects.filter(pk=pk).update(policy=policy, policies_pending=True)
else:
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
client.policies.clear()
Client.objects.filter(pk=pk).update(policy=None)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
site.policies.clear()
Site.objects.filter(pk=pk).update(policy=None)
if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk)
agent.policies.clear()
Agent.objects.filter(pk=pk).update(policy=None, policies_pending=True)
return Response("ok")
@ -219,16 +206,16 @@ class GetRelated(APIView):
pk = request.data["pk"]
if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk)
return Response(PolicySerializer(agent.policies.all(), many=True).data)
policy = Policy.objects.filter(agents__pk=pk).first()
return Response(PolicySerializer(policy).data)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
return Response(PolicySerializer(site.policies.all(), many=True).data)
policy = Policy.objects.filter(sites__pk=pk).first()
return Response(PolicySerializer(policy).data)
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
return Response(PolicySerializer(client.policies.all(), many=True).data)
policy = Policy.objects.filter(clients__pk=pk).first()
return Response(PolicySerializer(policy).data)
content = {"error": "Data was submitted incorrectly"}
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
import agents
from .tasks import handle_check_email_alert_task
CHECK_TYPE_CHOICES = [
@ -66,6 +68,9 @@ class Check(models.Model):
blank=True,
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)
check_type = models.CharField(
max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace"
@ -212,6 +217,35 @@ class Check(models.Model):
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):
CORE = CoreSettings.objects.first()

View File

@ -38,7 +38,7 @@ class CheckSerializer(serializers.ModelSerializer):
# disk checks
# make sure no duplicate diskchecks exist for an agent/policy
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:
for check in checks:
if val["disk"] in check.disk:

View File

@ -68,6 +68,7 @@ class GetUpdateDeleteCheck(APIView):
def delete(self, request, pk):
check = get_object_or_404(Check, pk=pk)
check.delete()
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):
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):
return self.client
@ -22,6 +29,13 @@ class Client(models.Model):
class Site(models.Model):
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
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):
return self.site

View File

@ -6,7 +6,7 @@ services:
# Container that hosts Vue frontend
app:
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
volumes:
- ../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:
- 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
```
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
```
For HMR to work with vue you can copy .env.example and modify the setting to fit your dev environment.
### Create Python Virtual Env

View File

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

View File

@ -62,21 +62,38 @@
class="settings-tbl-sticky"
:data="policies"
:columns="columns"
:visible-columns="visibleColumns"
:pagination.sync="pagination"
:selected.sync="selected"
@selection="policyRowSelected"
selection="single"
@selection="policyRowSelected"
row-key="id"
binary-state-sort
hide-bottom
flat
>
<!-- header slots -->
<template v-slot:header="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>
</template>
</template>
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @click="props.selected = true">
<!-- context menu -->
@ -126,15 +143,30 @@
</q-item>
</q-list>
</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.desc }}</q-td>
<q-td>{{ props.row.active }}</q-td>
<q-td>
<span
style="cursor:pointer;color:blue;text-decoration:underline"
@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>
</q-td>
</q-tr>
@ -196,13 +228,9 @@ export default {
policy: null,
editPolicyId: null,
selected: [],
pagination: {
rowsPerPage: 0,
sortBy: "id",
descending: false
},
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",
label: "Name",
@ -215,24 +243,17 @@ export default {
label: "Description",
field: "desc",
align: "left",
sortable: false
},
{
name: "active",
label: "Active",
field: "active",
align: "left",
sortable: true
},
{
name: "actions",
label: "Actions",
field: "actions",
align: "left",
sortable: false
}
],
visibleColumns: ["name", "desc", "active", "actions"]
pagination: {
rowsPerPage: 9999
},
};
},
methods: {
@ -295,6 +316,32 @@ export default {
showPolicyOverview() {
this.showPolicyOverviewModal = true
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: {

View File

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

View File

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

View File

@ -25,69 +25,9 @@
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Clients:</div>
<div class="col-2">Enforced:</div>
<div class="col-10">
<q-select
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>
<q-toggle v-model="enforced" color="green" />
</div>
</q-card-section>
<q-card-section class="row items-center">
@ -99,21 +39,17 @@
<script>
import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
import dropdown_formatter from "@/mixins/dropdown_formatter";
export default {
name: "PolicyForm",
mixins: [mixins, dropdown_formatter],
mixins: [mixins],
props: { pk: Number },
data() {
return {
name: "",
desc: "",
active: false,
selectedSites: [],
selectedClients: [],
clientOptions: [],
siteOptions: []
enforced: false,
active: false
};
},
computed: {
@ -134,14 +70,7 @@ export default {
this.name = r.data.name;
this.desc = r.data.desc;
this.active = r.data.active;
this.selectedSites = r.data.sites.map(site => ({
label: site.site,
value: site.id
}) );
this.selectedClients = r.data.clients.map(client => ({
label: client.client,
value: client.id
}) );
this.enforced = r.data.enforced;
});
},
submit() {
@ -157,8 +86,7 @@ export default {
name: this.name,
desc: this.desc,
active: this.active,
sites: this.selectedSites.map(site => site.value),
clients: this.selectedClients.map(client => client.value)
enforced: this.enforced
};
if (this.pk) {
@ -186,20 +114,6 @@ export default {
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() {
@ -207,9 +121,6 @@ export default {
if (this.pk) {
this.getPolicy();
}
this.getClients();
this.getSites();
}
};
</script>

View File

@ -144,7 +144,7 @@ export default {
getCheckData() {
this.$q.loading.show();
this.$store
.dispatch("automation/loadCheckStatus", { policypk: this.item.policy, checkpk: this.item.id })
.dispatch("automation/loadCheckStatus", { checkpk: this.item.id })
.then(r => {
this.$q.loading.hide();
this.tableData = r.data
@ -157,7 +157,7 @@ export default {
getTaskData() {
this.$q.loading.show();
this.$store
.dispatch("automation/loadAutomatedTaskStatus", { policypk: this.item.policy, taskpk: this.item.id })
.dispatch("automation/loadAutomatedTaskStatus", { taskpk: this.item.id })
.then(r => {
this.$q.loading.hide();
this.tableData = r.data
@ -174,7 +174,23 @@ export default {
closeScriptOutput() {
this.showScriptOutput = false;
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() {
if (this.type === "task") {

View File

@ -55,11 +55,11 @@ export default {
context.commit("setPolicyChecks", r.data);
});
},
loadCheckStatus(context, { policypk, checkpk }) {
return axios.patch(`/automation/${policypk}/policycheckstatus/${checkpk}/check/`);
loadCheckStatus(context, { checkpk }) {
return axios.patch(`/automation/policycheckstatus/${checkpk}/check/`);
},
loadAutomatedTaskStatus(context, { policypk, taskpk }) {
return axios.patch(`/automation/${policypk}/policyautomatedtaskstatus/${taskpk}/task/`);
loadAutomatedTaskStatus(context, { taskpk }) {
return axios.patch(`/automation/policyautomatedtaskstatus/${taskpk}/task/`);
},
loadPolicy(context, pk) {
return axios.get(`/automation/policies/${pk}/`);
@ -76,7 +76,10 @@ export default {
});
},
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) {
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
});

View File

@ -8,48 +8,16 @@ const localVue = createLocalVue();
localVue.use(Vuex);
/*** 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 = {
id: 1,
name: "Test Policy",
desc: "Test Desc",
active: true,
clients: [],
sites: []
enforced: false,
active: true
};
let actions, rootActions, store;
beforeEach(() => {
rootActions = {
loadClients: jest.fn(() => new Promise(res => res({ data: clients }))),
loadSites: jest.fn(() => new Promise(res => res({ data: sites }))),
};
actions = {
loadPolicy: jest.fn(() => new Promise(res => res({ data: policy }))),
@ -87,8 +55,6 @@ describe("PolicyForm.vue when editting", () => {
/*** TESTS ***/
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);
});
@ -127,24 +93,11 @@ describe("PolicyForm.vue when adding", () => {
/*** TESTS ***/
it("calls vuex actions on mount", () => {
expect(rootActions.loadClients).toHaveBeenCalled();
expect(rootActions.loadSites).toHaveBeenCalled();
// Not called unless pk prop is set
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 () => {
wrapper.setData({name: "Test Policy"});

View File

@ -7,23 +7,21 @@ import "../../utils/quasar.js";
const localVue = createLocalVue();
localVue.use(Vuex);
const related = [
{
const related = {
id: 1,
name: "Test Policy"
},
{
id: 2,
name: "Test Policy 2"
}
];
};
let state, actions, getters, store;
beforeEach(() => {
state = {
policies: [
...related,
related,
{
id: 2,
name: "Test Policy 2"
},
{
id: 3,
name: "TestPolicy 3"
@ -34,7 +32,7 @@ beforeEach(() => {
actions = {
updateRelatedPolicies: jest.fn(),
loadPolicies: jest.fn(),
getRelatedPolicies: jest.fn(() => new Promise(res => res({ data: related }))),
getRelatedPolicies: jest.fn(() => Promise.resolve({ data: related })),
};
getters = {
@ -86,7 +84,7 @@ describe.each([
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 () => {
@ -95,10 +93,10 @@ describe.each([
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();
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 () => {
@ -109,8 +107,8 @@ describe.each([
await form.vm.$emit("submit");
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", () => {
const policyTreeData = {
// node 0
"Client Name 1": {
policies: [
const policyTreeData = [
{
// node 0
client: "Client Name 1",
policy: {
id: 1,
name: "Policy Name 1",
active: true
},
// node -1
sites: [
{
id: 1,
name: "Policy Name 1",
active: true
site: "Site Name 1",
policy: null
}
],
sites: {
// node -1
"Site Name 1": { policies: []}
}
]
},
// node -2
"Client Name 2": {
policies: [
{
// node -2
client: "Client Name 2",
policy: {
id: 2,
name: "Policy Name 2",
active: true
},
sites: [
{
id: 2,
name: "Policy Name 2",
active: true
// node -3
site: "Site Name 2",
policy: {
id: 3,
name: "Policy Name 3",
active: false
}
}
],
sites: {
// node -3
"Site Name 2": {
policies: [
{
id: 3,
name: "Policy Name 3",
active: false
}
]
}
}
]
}
};
];
let wrapper, actions, mutations, store;