diff --git a/api/tacticalrmm/alerts/__init__.py b/api/tacticalrmm/alerts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/tacticalrmm/alerts/admin.py b/api/tacticalrmm/alerts/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/api/tacticalrmm/alerts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/tacticalrmm/alerts/apps.py b/api/tacticalrmm/alerts/apps.py new file mode 100644 index 00000000..38c144bf --- /dev/null +++ b/api/tacticalrmm/alerts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AlertsConfig(AppConfig): + name = 'alerts' diff --git a/api/tacticalrmm/alerts/migrations/0001_initial.py b/api/tacticalrmm/alerts/migrations/0001_initial.py new file mode 100644 index 00000000..0a52b917 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/0002_auto_20200815_1618.py b/api/tacticalrmm/alerts/migrations/0002_auto_20200815_1618.py new file mode 100644 index 00000000..a24b4cfd --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0002_auto_20200815_1618.py @@ -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), + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/__init__.py b/api/tacticalrmm/alerts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/tacticalrmm/alerts/models.py b/api/tacticalrmm/alerts/models.py new file mode 100644 index 00000000..e89c2504 --- /dev/null +++ b/api/tacticalrmm/alerts/models.py @@ -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 \ No newline at end of file diff --git a/api/tacticalrmm/alerts/serializers.py b/api/tacticalrmm/alerts/serializers.py new file mode 100644 index 00000000..09c20d9b --- /dev/null +++ b/api/tacticalrmm/alerts/serializers.py @@ -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__" \ No newline at end of file diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/api/tacticalrmm/alerts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/tacticalrmm/alerts/urls.py b/api/tacticalrmm/alerts/urls.py new file mode 100644 index 00000000..de74e6aa --- /dev/null +++ b/api/tacticalrmm/alerts/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("alerts/", views.GetAddAlerts.as_view()), + path("alerts//", views.GetUpdateDeleteAlert.as_view()), +] diff --git a/api/tacticalrmm/alerts/views.py b/api/tacticalrmm/alerts/views.py new file mode 100644 index 00000000..ddf2d186 --- /dev/null +++ b/api/tacticalrmm/alerts/views.py @@ -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") diff --git a/api/tacticalrmm/automation/models.py b/api/tacticalrmm/automation/models.py index 82d04050..bbe8fc27 100644 --- a/api/tacticalrmm/automation/models.py +++ b/api/tacticalrmm/automation/models.py @@ -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 diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index e705a922..55bc6ad9 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -45,6 +45,7 @@ INSTALLED_APPS = [ "autotasks", "logs", "scripts", + "alerts", ] if not "TRAVIS" in os.environ and not "AZPIPELINE" in os.environ: diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 4a48b431..3b888ae4 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -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")), ] diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index e87e6aa7..fd329108 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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 diff --git a/docker/readme.md b/docker/readme.md index dd1c1f1f..21dc7550 100644 --- a/docker/readme.md +++ b/docker/readme.md @@ -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 diff --git a/web/src/components/AlertsIcon.vue b/web/src/components/AlertsIcon.vue index 253ec8ca..b0e2602e 100644 --- a/web/src/components/AlertsIcon.vue +++ b/web/src/components/AlertsIcon.vue @@ -1,22 +1,23 @@ \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertsOverview.vue b/web/src/components/modals/alerts/AlertsOverview.vue index db9d3ba5..554ce03c 100644 --- a/web/src/components/modals/alerts/AlertsOverview.vue +++ b/web/src/components/modals/alerts/AlertsOverview.vue @@ -10,40 +10,70 @@ All Alerts - + + + + {{ alert.client }} - {{ alert.hostname }} + + + {{ alert.message }} + + + + + {{ alert.timestamp }} + + + diff --git a/web/src/store/alerts.js b/web/src/store/alerts.js index a9cd02c4..23b13385 100644 --- a/web/src/store/alerts.js +++ b/web/src/store/alerts.js @@ -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}`); } } } \ No newline at end of file