Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
9.5 KiB
SQL
210 lines
9.5 KiB
SQL
-- Meldungen und Benachrichtigungen (Phase 14).
|
|
--
|
|
-- Das Meldungswesen hat einen Feind, und es ist nicht der fehlende Alarm: Es ist
|
|
-- der Alarm, den niemand mehr liest. Ein System, das jede Minute dieselbe
|
|
-- Meldung erzeugt, wird nach drei Tagen weggeklickt — und dann fehlt die eine,
|
|
-- auf die es ankam.
|
|
--
|
|
-- Das Schema ist deshalb um zwei Eigenschaften herum gebaut:
|
|
--
|
|
-- 1. **Eine Ursache, eine Meldung.** Der Fingerabdruck macht die Doppelung
|
|
-- unmoeglich, nicht die Anwendungslogik.
|
|
-- 2. **Meldungen loesen sich selbst auf.** Verschwindet die Ursache, schliesst
|
|
-- sich die Meldung. Was von Hand geschlossen werden muss, sammelt sich an.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Meldungen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE alerts (
|
|
-- id ist der oeffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- rule_name benennt die ausloesende Regel.
|
|
rule_name TEXT NOT NULL,
|
|
-- severity ist der Schweregrad.
|
|
severity TEXT NOT NULL,
|
|
-- status ist der Bearbeitungszustand.
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
-- fingerprint kennzeichnet die Ursache eindeutig.
|
|
--
|
|
-- Er entsteht aus Regel und betroffenem Gegenstand, nicht aus dem Zeitpunkt.
|
|
-- Genau deshalb erzeugt derselbe Fehlschlag beim zweiten Durchgang keine
|
|
-- zweite Meldung, sondern aktualisiert die erste.
|
|
fingerprint TEXT NOT NULL,
|
|
-- title ist die Ueberschrift.
|
|
title TEXT NOT NULL,
|
|
-- message erklaert den Befund und die naechste Handlung.
|
|
--
|
|
-- Eine Meldung ohne Handlungsanweisung ist eine Beunruhigung. Der Text nennt
|
|
-- deshalb immer, was zu tun ist.
|
|
message TEXT NOT NULL,
|
|
-- entity_type benennt die Art des betroffenen Gegenstands.
|
|
entity_type TEXT,
|
|
-- entity_id ist der betroffene Gegenstand.
|
|
entity_id UUID,
|
|
-- entity_name ist sein sprechender Name.
|
|
--
|
|
-- Fest gespeichert und nicht ueber einen Fremdschluessel gelesen: Die
|
|
-- Meldung muss auch dann noch lesbar sein, wenn der Auftrag geloescht wurde.
|
|
entity_name TEXT,
|
|
-- details traegt die Messwerte des Befunds.
|
|
details JSONB,
|
|
-- occurrence_count zaehlt, wie oft die Ursache seither auftrat.
|
|
--
|
|
-- Statt zehn gleicher Meldungen gibt es eine mit einem Zaehler. Die Zahl ist
|
|
-- die eigentliche Auskunft: Einmal ist ein Zwischenfall, zwanzigmal ein
|
|
-- Zustand.
|
|
occurrence_count INTEGER NOT NULL DEFAULT 1,
|
|
-- first_seen_at ist das erste Auftreten in UTC.
|
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- last_seen_at ist das letzte Auftreten in UTC.
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- acknowledged_at ist der Zeitpunkt der Kenntnisnahme in UTC.
|
|
acknowledged_at TIMESTAMPTZ,
|
|
-- acknowledged_by benennt den Bestaetigenden.
|
|
acknowledged_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- acknowledgement_note haelt eine Bemerkung des Bestaetigenden fest.
|
|
acknowledgement_note TEXT,
|
|
-- resolved_at ist der Zeitpunkt der Aufloesung in UTC.
|
|
resolved_at TIMESTAMPTZ,
|
|
-- resolved_by benennt den Aufloesenden; NULL bei automatischer Aufloesung.
|
|
--
|
|
-- Der Unterschied ist wichtig: Eine von selbst verschwundene Ursache ist
|
|
-- etwas anderes als eine weggeklickte Meldung.
|
|
resolved_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- resolution_note begruendet die Aufloesung.
|
|
resolution_note TEXT,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT alerts_severity_valid
|
|
CHECK (severity IN ('information', 'warning', 'high', 'critical')),
|
|
CONSTRAINT alerts_status_valid
|
|
CHECK (status IN ('open', 'acknowledged', 'resolved')),
|
|
-- Eine aufgeloeste Meldung braucht einen Zeitpunkt, sonst fehlt sie in jeder
|
|
-- Auswertung.
|
|
CONSTRAINT alerts_resolved_has_time CHECK (
|
|
status <> 'resolved' OR resolved_at IS NOT NULL
|
|
),
|
|
-- Eine bestaetigte Meldung braucht einen Bestaetigenden. „Jemand hat es
|
|
-- gesehen" ohne Namen ist keine Kenntnisnahme.
|
|
CONSTRAINT alerts_acknowledged_has_actor CHECK (
|
|
acknowledged_at IS NULL OR acknowledged_by IS NOT NULL
|
|
),
|
|
CONSTRAINT alerts_occurrence_positive CHECK (occurrence_count > 0)
|
|
);
|
|
|
|
-- **Der wichtigste Index dieser Migration.**
|
|
--
|
|
-- Er macht eine zweite offene Meldung zur selben Ursache unmoeglich. Ohne ihn
|
|
-- muesste die Anwendung vor jedem Schreiben nachsehen — und zwischen Nachsehen
|
|
-- und Schreiben passt ein zweiter Control-Server.
|
|
CREATE UNIQUE INDEX alerts_single_open_idx
|
|
ON alerts (fingerprint)
|
|
WHERE status IN ('open', 'acknowledged');
|
|
|
|
CREATE INDEX alerts_status_severity_idx ON alerts (status, severity, last_seen_at DESC);
|
|
CREATE INDEX alerts_entity_idx ON alerts (entity_type, entity_id);
|
|
CREATE INDEX alerts_created_idx ON alerts (created_at DESC);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Benachrichtigungskanaele
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE notification_channels (
|
|
-- id ist der oeffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- name ist die sprechende Bezeichnung.
|
|
name TEXT NOT NULL UNIQUE,
|
|
-- channel_type ist die Art der Zustellung.
|
|
channel_type TEXT NOT NULL,
|
|
-- enabled meldet einen aktiven Kanal.
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
-- minimum_severity ist der niedrigste zugestellte Schweregrad.
|
|
--
|
|
-- Ohne diese Grenze bekaeme der Bereitschaftsdienst nachts jede Information.
|
|
-- Nach einer Woche schaltet er die Benachrichtigungen ab, und dann kommt
|
|
-- auch die kritische nicht mehr an.
|
|
minimum_severity TEXT NOT NULL DEFAULT 'high',
|
|
-- configuration traegt die Zustelldaten.
|
|
--
|
|
-- Zugangsdaten stehen **nicht** hier, sondern verschluesselt in
|
|
-- credentials_ciphertext (PROMPT.md §140: keine Secrets im Klartext).
|
|
configuration JSONB NOT NULL,
|
|
-- credentials_ciphertext ist das verschluesselte Zugangsgeheimnis.
|
|
credentials_ciphertext BYTEA,
|
|
-- credentials_key_version benennt den verwendeten Schluessel.
|
|
credentials_key_version TEXT,
|
|
-- last_delivery_at ist der Zeitpunkt der letzten Zustellung in UTC.
|
|
last_delivery_at TIMESTAMPTZ,
|
|
-- last_delivery_error ist der Grund des letzten Fehlschlags.
|
|
--
|
|
-- Ein Kanal, der seit Wochen nichts zustellt, ist genauso schlimm wie eine
|
|
-- fehlende Meldung — und ohne dieses Feld faellt es niemandem auf.
|
|
last_delivery_error TEXT,
|
|
-- consecutive_failures zaehlt die Fehlschlaege in Folge.
|
|
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
-- created_by benennt den Anlegenden.
|
|
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- updated_at ist der Zeitpunkt der letzten Aenderung in UTC.
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT notification_channels_type_valid
|
|
CHECK (channel_type IN ('email', 'webhook')),
|
|
CONSTRAINT notification_channels_severity_valid
|
|
CHECK (minimum_severity IN ('information', 'warning', 'high', 'critical')),
|
|
CONSTRAINT notification_channels_failures_not_negative
|
|
CHECK (consecutive_failures >= 0),
|
|
-- Ein verschluesseltes Geheimnis ohne Schluesselversion liesse sich nicht
|
|
-- mehr entschluesseln.
|
|
CONSTRAINT notification_channels_credentials_have_version CHECK (
|
|
credentials_ciphertext IS NULL OR credentials_key_version IS NOT NULL
|
|
)
|
|
);
|
|
|
|
CREATE INDEX notification_channels_enabled_idx ON notification_channels (enabled)
|
|
WHERE enabled = true;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Zustellungen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Jede Zustellung wird festgehalten — auch die gescheiterte. Die Frage „ist die
|
|
-- Meldung von gestern Nacht rausgegangen?" muss beantwortbar sein, und zwar
|
|
-- ohne im Protokoll zu suchen.
|
|
CREATE TABLE notification_deliveries (
|
|
-- id ist der fortlaufende Bezeichner.
|
|
id BIGSERIAL PRIMARY KEY,
|
|
-- alert_id ist die zugestellte Meldung.
|
|
alert_id UUID NOT NULL REFERENCES alerts(id) ON DELETE CASCADE,
|
|
-- channel_id ist der verwendete Kanal.
|
|
channel_id UUID NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
|
|
-- status ist der Ausgang der Zustellung.
|
|
status TEXT NOT NULL,
|
|
-- attempt_number ist die Nummer des Versuchs.
|
|
attempt_number INTEGER NOT NULL DEFAULT 1,
|
|
-- error_message ist der Grund eines Fehlschlags.
|
|
error_message TEXT,
|
|
-- delivered_at ist der Zeitpunkt in UTC.
|
|
delivered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT notification_deliveries_status_valid
|
|
CHECK (status IN ('sent', 'failed', 'skipped')),
|
|
-- Ein Fehlschlag ohne Grund liesse sich nicht beheben.
|
|
CONSTRAINT notification_deliveries_failure_has_reason CHECK (
|
|
status <> 'failed' OR error_message IS NOT NULL
|
|
)
|
|
);
|
|
|
|
CREATE INDEX notification_deliveries_alert_idx ON notification_deliveries (alert_id);
|
|
CREATE INDEX notification_deliveries_channel_idx ON notification_deliveries (channel_id, delivered_at DESC);
|
|
|
|
-- Dieselbe Meldung wird ueber denselben Kanal nur einmal erfolgreich zugestellt.
|
|
-- Ohne diese Sperre erzeugte jeder Durchgang der Auswertung eine neue Mail zur
|
|
-- selben, weiterhin offenen Meldung.
|
|
CREATE UNIQUE INDEX notification_deliveries_once_idx
|
|
ON notification_deliveries (alert_id, channel_id)
|
|
WHERE status = 'sent';
|