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

WIP Alerts functionality (Stopped Error messages). Fix for policy checks/tasks
This commit is contained in:
wh1te909 2020-08-15 12:40:15 -07:00 committed by GitHub
commit f02fc950d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 258 additions and 71 deletions

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AlertsConfig(AppConfig):
name = 'alerts'

View File

@ -0,0 +1,28 @@
# Generated by Django 3.1 on 2020-08-15 15:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('agents', '0012_auto_20200810_0544'),
]
operations = [
migrations.CreateModel(
name='Alert',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.TextField(blank=True, null=True)),
('message', models.TextField(blank=True, null=True)),
('alert_time', models.DateTimeField(blank=True, null=True)),
('snooze_until', models.DateTimeField(blank=True, null=True)),
('resolved', models.BooleanField(default=False)),
('agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agent', to='agents.agent')),
],
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.1 on 2020-08-15 16:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='alert',
name='subject',
),
migrations.AddField(
model_name='alert',
name='severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=100),
),
]

View File

@ -0,0 +1,27 @@
from django.db import models
SEVERITY_CHOICES = [
("info", "Informational"),
("warning", "Warning"),
("error", "Error"),
]
class Alert(models.Model):
agent = models.ForeignKey(
"agents.Agent",
related_name="agent",
on_delete=models.CASCADE,
null=True,
blank=True,
)
message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(null=True, blank=True)
snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False)
severity = models.CharField(
max_length=100, choices=SEVERITY_CHOICES, default="info"
)
def __str__(self):
return self.message

View File

@ -0,0 +1,17 @@
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
)
from .models import Alert
class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname")
client = ReadOnlyField(source="agent.client")
site = ReadOnlyField(source="agent.site")
class Meta:
model = Alert
fields = "__all__"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path("alerts/", views.GetAddAlerts.as_view()),
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
]

View File

@ -0,0 +1,44 @@
from django.db import DataError
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Alert
from .serializers import AlertSerializer
class GetAddAlerts(APIView):
def get(self, request):
alerts = Alert.objects.all()
return Response(AlertSerializer(alerts, many=True).data)
def post(self, request):
serializer = AlertSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteAlert(APIView):
def get(self, request, pk):
alert = get_object_or_404(Alert, pk=pk)
return Response(AlertSerializer(alert).data)
def put(self, request, pk):
alert = get_object_or_404(Alert, pk=pk)
serializer = AlertSerializer(instance=alert, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
Alert.objects.get(pk=pk).delete()
return Response("ok")

View File

@ -17,22 +17,22 @@ class Policy(models.Model):
explicit_clients = self.clients.all()
explicit_sites = self.sites.all()
filtered_sites_ids = list()
client_ids = list()
filtered_sites_pks = list()
client_pks = list()
for site in explicit_sites:
if site.client not in explicit_clients:
filtered_sites_ids.append(site.site)
filtered_sites_pks.append(site.pk)
for client in explicit_clients:
client_ids.append(client.client)
client_pks.append(client.pk)
for site in client.sites.all():
filtered_sites_ids.append(site.site)
filtered_sites_pks.append(site.pk)
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)
| models.Q(pk__in=filtered_sites_pks)
| models.Q(pk__in=client_pks)
).distinct()
@staticmethod

View File

@ -30,7 +30,6 @@ def generate_agent_checks_from_policies_task(
@app.task
def generate_agent_checks_by_location_task(location, clear=False, parent_checks=[]):
# policy = Policy.objects.get(pk=policypk)
for agent in Agent.objects.filter(**location):
agent.generate_checks_from_policies(clear=clear, parent_checks=parent_checks)
@ -86,7 +85,6 @@ def generate_agent_tasks_from_policies_task(
@app.task
def generate_agent_tasks_by_location_task(location, clear=False, parent_tasks=[]):
# policy = Policy.objects.get(pk=policypk)
for agent in Agent.objects.filter(**location):
agent.generate_tasks_from_policies(clear=clear, parent_tasks=parent_tasks)

View File

@ -190,7 +190,7 @@ class TestPolicyViews(BaseTestCase):
resp = self.client.post(url, client_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_task.assert_called_with(location={"client": client.pk}, clear=True)
mock_task.assert_called_with(location={"client": client.client}, clear=True)
mock_task.reset_mock()
# test site add
@ -198,7 +198,7 @@ class TestPolicyViews(BaseTestCase):
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_task.assert_called_with(
location={"client": site.client.client, "site": site.pk}, clear=True
location={"client": site.client.client, "site": site.site}, clear=True
)
mock_task.reset_mock()
@ -236,7 +236,7 @@ class TestPolicyViews(BaseTestCase):
resp = self.client.post(url, client_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_task.assert_called_with(location={"client": client.pk}, clear=True)
mock_task.assert_called_with(location={"client": client.client}, clear=True)
mock_task.reset_mock()
# test site remove
@ -244,7 +244,7 @@ class TestPolicyViews(BaseTestCase):
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_task.assert_called_with(
location={"client": site.client.client, "site": site.pk}, clear=True
location={"client": site.client.client, "site": site.site}, clear=True
)
mock_task.reset_mock()

View File

@ -157,8 +157,10 @@ class GetRelated(APIView):
related_type = request.data["type"]
pk = request.data["pk"]
#If policy is set in request
if request.data["policy"] != 0:
policy = get_object_or_404(Policy, pk=request.data["policy"])
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
@ -167,10 +169,10 @@ class GetRelated(APIView):
client.policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.pk}, clear=True
location={"client": client.client}, clear=True
)
generate_agent_tasks_by_location_task.delay(
location={"client": client.pk}, clear=True
location={"client": client.client}, clear=True
)
if related_type == "site":
@ -181,11 +183,11 @@ class GetRelated(APIView):
site.policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.pk},
location={"client": site.client.client, "site": site.site},
clear=True,
)
generate_agent_tasks_by_location_task.delay(
location={"client": site.client.client, "site": site.pk},
location={"client": site.client.client, "site": site.site},
clear=True,
)
@ -207,15 +209,13 @@ class GetRelated(APIView):
# Check if policy is not none and update it to None
if client.policy:
# Get old policy pk to regenerate the checks
old_pk = client.policy.pk
client.policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"client": client.pk}, clear=True
location={"client": client.client}, clear=True
)
generate_agent_tasks_by_location_task.delay(
location={"client": client.pk}, clear=True
location={"client": client.client}, clear=True
)
if related_type == "site":
@ -224,16 +224,14 @@ class GetRelated(APIView):
# Check if policy is not none and update it to None
if site.policy:
# Get old policy pk to regenerate the checks
old_pk = site.policy.pk
site.policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"client": site.client.client, "site": site.pk},
location={"client": site.client.client, "site": site.site},
clear=True,
)
generate_agent_tasks_by_location_task.delay(
location={"client": site.client.client, "site": site.pk},
location={"client": site.client.client, "site": site.site},
clear=True,
)

View File

@ -45,6 +45,7 @@ INSTALLED_APPS = [
"autotasks",
"logs",
"scripts",
"alerts",
]
if not "TRAVIS" in os.environ and not "AZPIPELINE" in os.environ:

View File

@ -22,4 +22,5 @@ urlpatterns = [
path("tasks/", include("autotasks.urls")),
path("logs/", include("logs.urls")),
path("scripts/", include("scripts.urls")),
path("alerts/", include("alerts.urls")),
]

View File

@ -23,7 +23,7 @@ services:
# Container for Django backend
api:
image: python:3.8
command: /bin/bash -c "python manage.py collectstatic --clear --no-input && python manage.py migrate && sleep 10s && python manage.py initial_db_setup && python manage.py initial_mesh_setup && python manage.py load_chocos && python manage.py fix_salt_key && python manage.py runserver 0.0.0.0:80"
command: /bin/bash -c "python manage.py collectstatic --clear --no-input && python manage.py migrate && sleep 10s && python manage.py initial_db_setup && python manage.py initial_mesh_setup && python manage.py load_chocos && python manage.py runserver 0.0.0.0:80"
working_dir: /app
environment:
VIRTUAL_ENV: /app/env

View File

@ -98,7 +98,7 @@ For HMR to work with vue you can copy .env.example and modify the setting to fit
Each python container shares the same virtual env to make spinning up faster. It is located in api/tacticalrmm/env.
There is a container dedicated to creating and keeping this up to date. Prior to spinning up the environment you can run `docker-compose -f docker-compose.dev.yml up venv` to make sure the virtual env is ready. Otherwise the api and celery containers will fail to start.
There is a container dedicated to creating and keeping this up to date. Prior to spinning up the environment you can run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up venv` to make sure the virtual env is ready. Otherwise the api and celery containers will fail to start.
### Spinup the environment

View File

@ -1,22 +1,23 @@
<template>
<q-btn dense flat icon="notifications">
<q-badge color="red" floating transparent>{{ test_alerts.length }}</q-badge>
<q-badge v-if="alerts.length !== 0" color="red" floating transparent>{{ alertsLengthText() }}</q-badge>
<q-menu>
<q-list separator>
<q-item v-for="alert in test_alerts" :key="alert.id">
<q-item v-if="alerts.length === 0">No Alerts</q-item>
<q-item v-for="alert in alerts" :key="alert.id">
<q-item-section>
<q-item-label>{{ alert.client }} - {{ alert.hostname }}</q-item-label>
<q-item-label caption>
<q-icon :class="`text-${alertColor(alert.type)}`" :name="alert.type"></q-icon>
<q-icon :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
{{ alert.message }}
</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label caption>{{ alert.timestamp }}</q-item-label>
<q-item-label caption>{{ alert.alert_time }}</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="showAlertsModal = true">View All Alerts ({{test_alerts.length}})</q-item>
<q-item clickable @click="showAlertsModal = true">View All Alerts</q-item>
</q-list>
</q-menu>
@ -32,38 +33,27 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
import mixins from "@/mixins/mixins"
import AlertsOverview from "@/components/modals/alerts/AlertsOverview";
export default {
name: "AlertsIcon",
components: { AlertsOverview },
mixins: [mixins],
data() {
return {
showAlertsModal: false,
test_alerts: [
{
id: 1,
client: "NMHSI",
site: "Default",
hostname: "NMSC-BACK01",
message: "HDD error. Stuff ain't working",
type: "error",
timestamp: "2 min ago"
},
{
id: 2,
client: "Dove IT",
site: "Default",
hostname: "NMSC-ANOTHER",
message: "Big error. Stuff still ain't working",
type: "warning",
timestamp: "5 hours ago"
}
]
};
},
methods: {
getAlerts() {
this.$store
.dispatch("alerts/getAlerts")
.catch(error => {
console.error(error)
});
},
alertColor(type) {
if (type === "error") {
return "red";
@ -71,12 +61,22 @@ export default {
if (type === "warning") {
return "orange";
}
},
alertsLengthText() {
if (this.alerts.length > 9) {
return "9+";
} else {
return this.alerts.length;
}
}
},
computed: {
...mapState("alerts/", {
alerts: state => state.alerts
...mapGetters({
alerts: "alerts/getNewAlerts"
})
},
mounted() {
this.getAlerts()
}
};
</script>

View File

@ -10,40 +10,70 @@
<q-separator />
<q-card-section>All Alerts</q-card-section>
<q-card-section>
<q-btn label="Update" color="primary" />
<q-list separator>
<q-item v-for="alert in alerts" :key="alert.id">
<q-item-section>
<q-item-label>{{ alert.client }} - {{ alert.hostname }}</q-item-label>
<q-item-label caption>
<q-icon :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
{{ alert.message }}
</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label caption>{{ alert.timestamp }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
export default {
name: "AlertsOverview",
mixins: [mixins],
data() {
return {
alerts: []
};
},
methods: {
getAlerts() {
this.$q.loading.show();
axios
.get("/alerts/")
.then(r => {
this.alerts = r.data.alerts;
this.$store
.dispatch("alerts/getAlerts")
.then(response => {
this.$q.loading.hide();
})
.catch(() => {
.catch(error => {
this.$q.loading.hide();
this.notifyError("Something went wrong");
});
}
},
alertColor(severity) {
if (severity === "error") {
return "red";
}
if (severity === "warning") {
return "orange";
}
if (severity === "info") {
return "blue";
}
},
},
computed: {},
created() {
this.getAlerts();
computed: {
...mapGetters({
alerts: "alerts/getAlerts"
})
},
mounted() {
this.getAlerts()
}
};
</script>

View File

@ -3,15 +3,15 @@ import axios from 'axios'
export default {
namespaced: true,
state: {
alerts: [],
alerts: []
},
getters: {
getAlerts(state) {
return state.alerts;
},
getUncheckedAlerts(state) {
//filter for non-dismissed active alerts
getNewAlerts(state) {
return state.alerts.filter(alert => !alert.resolved || alert.snoozed_until == undefined)
}
},
@ -23,9 +23,12 @@ export default {
actions: {
getAlerts(context) {
axios.get(`/alerts/getAlerts/`).then(r => {
axios.get("/alerts/alerts/").then(r => {
context.commit("SET_ALERTS", r.data);
});
},
editAlert(context, pk) {
return axios.put(`/alerts/alerts/${pk}`);
}
}
}