Added frontrend and backend for alerts to stop error messages. Still WIP. Fixed related_agents function to help with policy check bugs.

This commit is contained in:
Josh Krawczyk 2020-08-15 14:13:24 -04:00
parent 111ccad70d
commit 4b366ad53a
19 changed files with 244 additions and 53 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

@ -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}`);
}
}
}