Files
ms 2c1c48df0c
PostgreSQL integration / tests (push) Has been cancelled
init
2026-11-09 13:15:48 +01:00

63 lines
3.9 KiB
Python

import logging
from datetime import timedelta
from celery import shared_task
from django.conf import settings
from django.core.mail import send_mail
from django.db import transaction
from django.utils import timezone
from .models import Server,Configuration,Alarm,Notification,Event,Task,Worker,AccessToken,Audit
log=logging.getLogger(__name__)
@shared_task
def evaluate_alarms():
cfg,_=Configuration.objects.get_or_create(pk=1)
now=timezone.now(); cutoff=now-timedelta(seconds=cfg.heartbeat_timeout)
for sid in Server.objects.values_list('pk',flat=True):
with transaction.atomic():
s=Server.objects.select_for_update().get(pk=sid)
desired={}
if s.enabled and not s.maintenance:
if not s.latest_sample_at or s.latest_sample_at<cutoff:
desired['heartbeat']=('critical','Aktuelle Host-Messdaten fehlen')
else:
for key,limit in [('cpu',cfg.cpu_warning),('ram',cfg.ram_warning),('disk',cfg.disk_warning)]:
if s.latest.get(key,0)>=limit: desired[key]=('warning',f'{key.upper()} über Schwellwert: {s.latest[key]:.1f} %')
for service,online in s.latest.get('services',{}).items():
if not online: desired['service:'+service]=('critical',f'Dienst nicht erreichbar: {service}')
for w in Worker.objects.filter(server=s):
if not w.online or w.last_seen<cutoff: desired['worker:'+str(w.pk)]=('warning',f'Worker-Heartbeat fehlt: {w.hostname}'[:400])
for kind in ['flower-status','collector-status']:
e=Event.objects.filter(server=s,kind=kind).order_by('-occurred_at').first()
if e and (not e.payload.get('online') or e.occurred_at<cutoff): desired[kind]=('warning',f'{kind}: Verbindung unterbrochen oder veraltet')
Alarm.objects.filter(server=s,resolved_at__isnull=True).exclude(code__in=desired).update(resolved_at=now)
for code,(severity,message) in desired.items():
a,created=Alarm.objects.get_or_create(server=s,code=code,resolved_at__isnull=True,defaults={'severity':severity,'message':message})
if created: Notification.objects.create(alarm=a,next_attempt=now)
@shared_task
def deliver_notifications():
if not settings.ALERT_RECIPIENTS: return
for nid in Notification.objects.filter(sent_at__isnull=True,next_attempt__lte=timezone.now(),alarm__resolved_at__isnull=True).values_list('pk',flat=True)[:50]:
with transaction.atomic():
n=Notification.objects.select_for_update().select_related('alarm__server').get(pk=nid)
if n.sent_at: continue
n.attempts+=1
try:
send_mail(f'[dilib Operations] {n.alarm.severity}: {n.alarm.server.name}',n.alarm.message,settings.DEFAULT_FROM_EMAIL,settings.ALERT_RECIPIENTS,fail_silently=False)
n.sent_at=timezone.now(); n.last_error=''
except Exception as exc:
# Do not store SMTP credentials or raw exception messages.
n.last_error=type(exc).__name__; n.next_attempt=timezone.now()+timedelta(seconds=min(3600,30*2**min(n.attempts,7)))
log.warning('Notification %s failed: %s',n.pk,n.last_error)
n.save()
@shared_task
def cleanup():
cfg,_=Configuration.objects.get_or_create(pk=1)
cutoff=timezone.now()-timedelta(days=cfg.retention_days)
# Bounded batches avoid one enormous transaction; Metric rows cascade.
for model,field in [(Event,'occurred_at'),(Task,'last_at'),(Worker,'last_seen'),(Audit,'at')]:
for _ in range(20):
ids=list(model.objects.filter(**{field+'__lt':cutoff}).values_list('pk',flat=True)[:1000])
if not ids: break
model.objects.filter(pk__in=ids).delete()
Alarm.objects.filter(resolved_at__lt=cutoff).delete()
AccessToken.objects.filter(expires_at__lt=timezone.now()).delete()