Allow scheduling notification rules

This commit is contained in:
Mark Veidemanis 2023-01-14 16:36:22 +00:00
parent 9ee9c7abde
commit 2a1e6b3292
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 137 additions and 8 deletions

View File

@ -100,7 +100,7 @@ class NotificationRuleForm(RestrictedFormMixin, ModelForm):
"topic": "The topic to send notifications to. Leave blank for default.",
"enabled": "Whether the rule is enabled.",
"data": "The notification rule definition.",
"interval": "How often to run the search. On demand only evaluates messages as they are received.",
"interval": "How often to run the search. On demand evaluates messages as they are received, without running a scheduled search. The remaining options schedule a search of the database with the window below.",
"window": "Time window to search: 1d, 1h, 1m, 1s, etc.",
}

View File

@ -16,6 +16,10 @@ from core.util import logs
log = logs.get_logger("rules")
SECONDS_PER_UNIT = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
MAX_WINDOW = 2592000
class RuleParseError(Exception):
def __init__(self, message, field):
@ -111,10 +115,19 @@ class NotificationRuleData(object):
self.data = self.cleaned_data.get("data")
self.parsed = None
self.validate_user_permissions()
self.parse_data()
self.validate_permissions()
self.validate_time_fields()
def validate_user_permissions(self):
"""
Ensure the user can use notification rules.
"""
if not self.user.has_perm("core.use_rules"):
raise RuleParseError("User does not have permission to use rules", "data")
def validate_time_fields(self):
"""
Validate the interval and window fields.
@ -122,9 +135,33 @@ class NotificationRuleData(object):
"""
interval = self.cleaned_data.get("interval")
window = self.cleaned_data.get("window")
if interval == "ondemand" and window is not None:
if interval == 0 and window is not None:
raise RuleParseError(
"Window cannot be specified with ondemand interval", "window"
"Window cannot be specified with on-demand interval", "window"
)
if interval is not None and window is None:
raise RuleParseError(
"Window must be specified with non-on-demand interval", "window"
)
if window is not None:
window_number = window[:-1]
if not window_number.isdigit():
raise RuleParseError("Window prefix must be a number", "window")
window_number = int(window_number)
window_unit = window[-1]
if window_unit not in SECONDS_PER_UNIT:
raise RuleParseError(
f"Window unit must be one of {', '.join(SECONDS_PER_UNIT.keys())}, not '{window_unit}'",
"window",
)
window_seconds = window_number * SECONDS_PER_UNIT[window_unit]
print("Window seconds", window_seconds)
if window_seconds > MAX_WINDOW:
raise RuleParseError(
f"Window cannot be larger than {MAX_WINDOW} seconds (30 days)",
"window",
)
def validate_permissions(self):

View File

@ -0,0 +1,39 @@
from django.core.management.base import BaseCommand
from core.util import logs
from core.models import NotificationRule
from core.db.storage import db
import aioschedule as schedule
import asyncio
from time import sleep
log = logs.get_logger("scheduling")
# INTERVAL_CHOICES = (
# (0, "On demand"),
# (60, "Every minute"),
# (900, "Every 15 minutes"),
# (1800, "Every 30 minutes"),
# (3600, "Every hour"),
# (14400, "Every 4 hours"),
# (86400, "Every day"),
# )
INTERVALS = [60, 900, 1800, 3600, 14400, 86400]
def run_schedule(interval_seconds):
print("Running schedule", interval_seconds)
matching_rules = NotificationRule.objects.filter(
enabled=True, interval=interval_seconds
)
class Command(BaseCommand):
def handle(self, *args, **options):
for interval in INTERVALS:
schedule.every(interval).seconds.do(run_schedule, interval_seconds=interval)
loop = asyncio.get_event_loop()
while True:
loop.run_until_complete(schedule.run_pending())
sleep(10)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2023-01-14 14:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_notificationrule_interval_notificationrule_window'),
]
operations = [
migrations.AlterField(
model_name='notificationrule',
name='interval',
field=models.IntegerField(choices=[(0, 'On demand'), (60, 'Every minute'), (900, 'Every 15 minutes'), (1800, 'Every 30 minutes'), (3600, 'Every hour'), (14400, 'Every 4 hours'), (86400, 'Every day')], default=0),
),
]

View File

@ -1,4 +1,4 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from core.forms import NotificationRuleForm, NotificationSettingsForm
from core.models import NotificationRule, NotificationSettings
@ -7,7 +7,8 @@ from core.views.helpers import ObjectCreate, ObjectDelete, ObjectList, ObjectUpd
# Notifications - we create a new notification settings object if there isn't one
# Hence, there is only an update view, not a create view.
class NotificationsUpdate(LoginRequiredMixin, ObjectUpdate):
class NotificationsUpdate(LoginRequiredMixin, PermissionRequiredMixin, ObjectUpdate):
permission_required = "use_rules"
model = NotificationSettings
form_class = NotificationSettingsForm
@ -41,19 +42,22 @@ class RuleList(LoginRequiredMixin, ObjectList):
submit_url_name = "rule_create"
class RuleCreate(LoginRequiredMixin, ObjectCreate):
class RuleCreate(LoginRequiredMixin, PermissionRequiredMixin, ObjectCreate):
permission_required = "use_rules"
model = NotificationRule
form_class = NotificationRuleForm
submit_url_name = "rule_create"
class RuleUpdate(LoginRequiredMixin, ObjectUpdate):
class RuleUpdate(LoginRequiredMixin, PermissionRequiredMixin, ObjectUpdate):
permission_required = "use_rules"
model = NotificationRule
form_class = NotificationRuleForm
submit_url_name = "rule_update"
class RuleDelete(LoginRequiredMixin, ObjectDelete):
class RuleDelete(LoginRequiredMixin, PermissionRequiredMixin, ObjectDelete):
permission_required = "use_rules"
model = NotificationRule

View File

@ -58,6 +58,36 @@ services:
networks:
- default
scheduling:
image: pathogen/neptune:latest
container_name: scheduling_neptune
build:
context: .
args:
OPERATION: ${OPERATION}
command: sh -c '. /venv/bin/activate && python manage.py scheduling'
volumes:
- ${PORTAINER_GIT_DIR}:/code
- ${PORTAINER_GIT_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
- ${APP_LOCAL_SETTINGS}:/code/app/local_settings.py
- ${APP_DATABASE_FILE}:/code/db.sqlite3
- neptune_static:${STATIC_ROOT}
env_file:
- stack.env
volumes_from:
- tmp
depends_on:
redis:
condition: service_healthy
migration:
condition: service_started
collectstatic:
condition: service_started
networks:
- default
- pathogen
- elastic
migration:
image: pathogen/neptune:latest
container_name: migration_neptune

View File

@ -19,3 +19,4 @@ django-debug-toolbar
django-debug-toolbar-template-profiler
orjson
msgpack
aioschedule