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>
487 lines
23 KiB
SQL
487 lines
23 KiB
SQL
-- Sicherungsaufträge, Läufe und Zeitsteuerung (Phase 8).
|
|
--
|
|
-- Zwei Grundsätze prägen dieses Schema:
|
|
--
|
|
-- 1. Die Datenbank hält Konfiguration und Verweise, niemals Sicherungsdaten
|
|
-- (PROMPT.md §138). Ein Manifest wird über manifest_ref adressiert; sein
|
|
-- Inhalt liegt im Repository.
|
|
-- 2. Ein Lauf ist kein Zustandsfeld am Auftrag, sondern eine eigene Zeile.
|
|
-- Ein Auftrag, dessen letzter Ausgang nur als Spalte am Auftrag stünde,
|
|
-- liesse keine Historie zu — und ohne Historie ist nicht feststellbar,
|
|
-- ob eine Sicherung regelmäßig gelingt.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Repositories
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE repositories (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- name ist die sprechende Bezeichnung.
|
|
name TEXT UNIQUE NOT NULL,
|
|
-- repository_type benennt die Ablageart, etwa local oder s3.
|
|
repository_type TEXT NOT NULL DEFAULT 'local',
|
|
-- location ist der Pfad oder die Adresse der Ablage.
|
|
location TEXT NOT NULL,
|
|
-- repository_uuid ist die im Repository selbst hinterlegte Kennung.
|
|
--
|
|
-- Sie wird gebraucht, um ein versehentlich vertauschtes Repository zu
|
|
-- erkennen: Der Katalog wird bei fremder Kennung neu aufgebaut, statt
|
|
-- falsche Einträge weiterzuverwenden.
|
|
repository_uuid TEXT,
|
|
-- status ist der Betriebszustand.
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
-- hardened meldet den gehärteten Modus.
|
|
hardened BOOLEAN NOT NULL DEFAULT false,
|
|
-- capacity_bytes ist der Gesamtplatz, sofern ermittelbar.
|
|
capacity_bytes BIGINT,
|
|
-- used_bytes ist der belegte Platz beim letzten Blick.
|
|
used_bytes BIGINT,
|
|
-- last_checked_at ist der Zeitpunkt der letzten Zustandsprüfung.
|
|
last_checked_at TIMESTAMPTZ,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT repositories_status_valid
|
|
CHECK (status IN ('active', 'read_only', 'unavailable', 'maintenance'))
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Aufbewahrungsregeln
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE retention_policies (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- name ist die sprechende Bezeichnung.
|
|
name TEXT UNIQUE NOT NULL,
|
|
-- rules beschreibt die Regeln als JSON.
|
|
--
|
|
-- Bewusst als JSON und nicht als Spaltensatz: Die Regeln (täglich,
|
|
-- wöchentlich, monatlich, jährlich, GFS) entwickeln sich weiter, und jede
|
|
-- Erweiterung wäre sonst eine Migration am Kern des Schemas.
|
|
rules JSONB NOT NULL,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Sicherungsaufträge
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backup_jobs (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- name ist die eindeutige, sprechende Bezeichnung.
|
|
name TEXT UNIQUE NOT NULL,
|
|
-- description erläutert den Zweck.
|
|
description TEXT,
|
|
-- status ist der Zustand des Auftrags.
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
-- priority ist die Dringlichkeit.
|
|
priority TEXT NOT NULL DEFAULT 'normal',
|
|
-- schedule_type benennt die Art des Zeitplans.
|
|
schedule_type TEXT NOT NULL,
|
|
-- schedule_config beschreibt den Zeitplan als JSON.
|
|
schedule_config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
-- repository_id ist das Ziel-Repository.
|
|
repository_id UUID REFERENCES repositories(id) ON DELETE RESTRICT,
|
|
-- retention_policy_id ist die Aufbewahrungsregel.
|
|
retention_policy_id UUID REFERENCES retention_policies(id) ON DELETE SET NULL,
|
|
-- encryption_policy_id ist die Verschlüsselungsregel.
|
|
encryption_policy_id UUID,
|
|
-- verification_policy_id ist die Prüfregel.
|
|
verification_policy_id UUID,
|
|
-- notification_policy_id ist die Benachrichtigungsregel.
|
|
notification_policy_id UUID,
|
|
-- rpo_seconds ist der zulässige Datenverlust in Sekunden.
|
|
rpo_seconds BIGINT,
|
|
-- rto_seconds ist die zulässige Wiederherstellungsdauer in Sekunden.
|
|
rto_seconds BIGINT,
|
|
-- bandwidth_limit_bps begrenzt den Durchsatz; NULL bedeutet unbegrenzt.
|
|
bandwidth_limit_bps BIGINT,
|
|
-- max_concurrency begrenzt gleichzeitige Läufe dieses Auftrags.
|
|
max_concurrency INTEGER NOT NULL DEFAULT 1,
|
|
-- retry_policy beschreibt das Wiederholungsverhalten als JSON.
|
|
retry_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
-- next_run_at ist der berechnete nächste Zeitpunkt in UTC.
|
|
--
|
|
-- Er wird gespeichert und nicht bei jedem Blick neu berechnet: Der
|
|
-- Scheduler fragt im Sekundentakt, welche Aufträge fällig sind. Eine
|
|
-- Berechnung über alle Aufträge bei jeder Abfrage wäre bei tausend
|
|
-- Aufträgen die teuerste Schleife der Anlage.
|
|
next_run_at TIMESTAMPTZ,
|
|
-- last_run_at ist der Beginn des letzten Laufs in UTC.
|
|
last_run_at TIMESTAMPTZ,
|
|
-- last_outcome ist der Ausgang des letzten Laufs.
|
|
--
|
|
-- Redundant zu backup_job_runs, aber die Übersicht braucht ihn ohne
|
|
-- Verbund über eine wachsende Historientabelle.
|
|
last_outcome TEXT,
|
|
-- paused_at ist der Zeitpunkt einer Aussetzung in UTC.
|
|
paused_at TIMESTAMPTZ,
|
|
-- paused_by benennt, wer ausgesetzt hat.
|
|
paused_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- created_by benennt den Anleger.
|
|
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 Änderung in UTC.
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- deleted_at ist der Zeitpunkt einer Löschung in UTC.
|
|
--
|
|
-- Soft-Delete, weil die Läufe eines gelöschten Auftrags im Audit erhalten
|
|
-- bleiben müssen. Ein hart gelöschter Auftrag risse die Historie mit.
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
CONSTRAINT backup_jobs_status_valid
|
|
CHECK (status IN ('active', 'paused', 'disabled', 'error')),
|
|
CONSTRAINT backup_jobs_priority_valid
|
|
CHECK (priority IN ('critical', 'high', 'normal', 'low')),
|
|
CONSTRAINT backup_jobs_schedule_type_valid
|
|
CHECK (schedule_type IN ('manual', 'interval', 'hourly', 'daily', 'weekly', 'monthly', 'cron')),
|
|
CONSTRAINT backup_jobs_concurrency_positive
|
|
CHECK (max_concurrency >= 1),
|
|
-- Eine Bandbreitengrenze von 0 wäre keine Grenze, sondern ein Stillstand.
|
|
CONSTRAINT backup_jobs_bandwidth_positive
|
|
CHECK (bandwidth_limit_bps IS NULL OR bandwidth_limit_bps > 0)
|
|
);
|
|
|
|
-- Der Scheduler fragt ausschließlich nach fälligen, aktiven Aufträgen. Der
|
|
-- Teilindex hält genau diese Menge klein — ausgesetzte und gelöschte Aufträge
|
|
-- stehen gar nicht erst darin.
|
|
CREATE INDEX backup_jobs_due_idx
|
|
ON backup_jobs (next_run_at)
|
|
WHERE status = 'active' AND deleted_at IS NULL;
|
|
|
|
CREATE INDEX backup_jobs_status_idx ON backup_jobs (status) WHERE deleted_at IS NULL;
|
|
CREATE INDEX backup_jobs_repository_idx ON backup_jobs (repository_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Quellen eines Auftrags
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backup_job_sources (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- job_id ist der zugehörige Auftrag.
|
|
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
|
|
-- source_type benennt die Art der Quelle.
|
|
source_type TEXT NOT NULL,
|
|
-- source_id ist die Kennung der Quelle innerhalb ihrer Art.
|
|
--
|
|
-- Bewusst Text und nicht UUID: Ein Proxmox-Gast heißt "qemu/100", ein
|
|
-- Dateipfad "/daten". Eine UUID erzwänge für jede Quelle einen eigenen
|
|
-- Datensatz, auch für einen bloßen Pfad.
|
|
source_id TEXT NOT NULL,
|
|
-- source_name ist die sprechende Bezeichnung.
|
|
source_name TEXT,
|
|
-- agent_id ist der ausführende Agent, sofern die Quelle einen braucht.
|
|
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
|
|
-- include_patterns beschränkt die Erfassung.
|
|
include_patterns JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
-- exclude_patterns nimmt Pfade aus.
|
|
exclude_patterns JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT backup_job_sources_type_valid
|
|
CHECK (source_type IN ('filesystem', 'proxmox_vm', 'proxmox_container', 'windows_system', 'linux_system')),
|
|
-- Dieselbe Quelle zweimal im selben Auftrag wäre ein Konfigurationsfehler,
|
|
-- der die Datenmenge verdoppelte.
|
|
CONSTRAINT backup_job_sources_unique UNIQUE (job_id, source_type, source_id)
|
|
);
|
|
|
|
CREATE INDEX backup_job_sources_job_idx ON backup_job_sources (job_id);
|
|
CREATE INDEX backup_job_sources_source_idx ON backup_job_sources (source_type, source_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Abhängigkeiten zwischen Aufträgen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backup_job_dependencies (
|
|
-- job_id ist der abhängige Auftrag.
|
|
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
|
|
-- depends_on_job_id ist der vorausgesetzte Auftrag.
|
|
depends_on_job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
PRIMARY KEY (job_id, depends_on_job_id),
|
|
-- Ein Auftrag, der von sich selbst abhängt, liefe nie an. Längere Ringe
|
|
-- muss die Anwendung erkennen — das kann eine Datenbank nicht.
|
|
CONSTRAINT backup_job_dependencies_no_self CHECK (job_id <> depends_on_job_id)
|
|
);
|
|
|
|
CREATE INDEX backup_job_dependencies_reverse_idx ON backup_job_dependencies (depends_on_job_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Wartungsfenster
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE maintenance_windows (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- name ist die sprechende Bezeichnung.
|
|
name TEXT NOT NULL,
|
|
-- window_kind benennt die Wirkung: sperrend oder erlaubend.
|
|
window_kind TEXT NOT NULL DEFAULT 'blackout',
|
|
-- starts_at ist der Beginn eines einmaligen Fensters in UTC.
|
|
starts_at TIMESTAMPTZ,
|
|
-- ends_at ist das Ende eines einmaligen Fensters in UTC.
|
|
ends_at TIMESTAMPTZ,
|
|
-- recurrence beschreibt ein wiederkehrendes Fenster als JSON.
|
|
recurrence JSONB,
|
|
-- enabled meldet ein wirksames Fenster.
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
-- created_by benennt den Anleger.
|
|
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 Änderung in UTC.
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT maintenance_windows_kind_valid
|
|
CHECK (window_kind IN ('blackout', 'allowed')),
|
|
-- Ein Fenster ist entweder einmalig oder wiederkehrend. Beides zugleich
|
|
-- wäre mehrdeutig, keines von beidem wirkungslos.
|
|
CONSTRAINT maintenance_windows_definition_complete CHECK (
|
|
(recurrence IS NOT NULL AND starts_at IS NULL AND ends_at IS NULL)
|
|
OR (recurrence IS NULL AND starts_at IS NOT NULL AND ends_at IS NOT NULL AND ends_at > starts_at)
|
|
)
|
|
);
|
|
|
|
CREATE INDEX maintenance_windows_enabled_idx ON maintenance_windows (enabled) WHERE enabled;
|
|
|
|
-- Zuordnung von Fenstern zu einzelnen Aufträgen. Fehlt ein Eintrag, gilt das
|
|
-- Fenster für alle Aufträge — das ist der sichere Standard.
|
|
CREATE TABLE maintenance_window_jobs (
|
|
-- window_id ist das Fenster.
|
|
window_id UUID NOT NULL REFERENCES maintenance_windows(id) ON DELETE CASCADE,
|
|
-- job_id ist der betroffene Auftrag.
|
|
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
|
|
|
|
PRIMARY KEY (window_id, job_id)
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Sicherungsketten
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backup_chains (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- source_reference benennt die gesicherte Quelle.
|
|
source_reference TEXT NOT NULL,
|
|
-- repository_id ist das Repository der Kette.
|
|
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT,
|
|
-- status ist der Zustand der Kette.
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT backup_chains_status_valid
|
|
CHECK (status IN ('active', 'broken', 'closed'))
|
|
);
|
|
|
|
CREATE INDEX backup_chains_source_idx ON backup_chains (source_reference, repository_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Läufe
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backup_job_runs (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- job_id ist der ausgeführte Auftrag.
|
|
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
|
|
-- status ist der Zustand des Laufs.
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
-- trigger benennt den Auslöser: Zeitplan, Anwender oder Wiederholung.
|
|
trigger TEXT NOT NULL DEFAULT 'schedule',
|
|
-- attempt_number ist die Nummer des Versuchs, beginnend bei 1.
|
|
attempt_number INTEGER NOT NULL DEFAULT 1,
|
|
-- scheduled_for ist der geplante Zeitpunkt in UTC.
|
|
--
|
|
-- Er steht neben started_at, weil die Abweichung die eigentliche Auskunft
|
|
-- ist: Ein Lauf, der regelmäßig eine Stunde zu spät beginnt, hat ein
|
|
-- Problem, das man ohne diesen Vergleich nicht sieht.
|
|
scheduled_for TIMESTAMPTZ,
|
|
-- started_at ist der tatsächliche Beginn in UTC.
|
|
started_at TIMESTAMPTZ,
|
|
-- completed_at ist das Ende in UTC.
|
|
completed_at TIMESTAMPTZ,
|
|
-- bytes_processed ist die gelesene Datenmenge.
|
|
bytes_processed BIGINT NOT NULL DEFAULT 0,
|
|
-- bytes_written ist die abgelegte Datenmenge.
|
|
bytes_written BIGINT NOT NULL DEFAULT 0,
|
|
-- bytes_transferred ist die über das Netz übertragene Menge.
|
|
bytes_transferred BIGINT NOT NULL DEFAULT 0,
|
|
-- throughput_bps ist der erreichte Durchsatz.
|
|
throughput_bps BIGINT,
|
|
-- files_processed ist die Zahl bearbeiteter Objekte.
|
|
files_processed BIGINT NOT NULL DEFAULT 0,
|
|
-- files_skipped ist die Zahl übergangener Objekte.
|
|
--
|
|
-- Ist sie größer als 0, ist der Lauf ein Teilfehler und niemals ein
|
|
-- Erfolg (PROMPT.md §138).
|
|
files_skipped BIGINT NOT NULL DEFAULT 0,
|
|
-- error_code ist die Fehlerkennung in SCREAMING_SNAKE_CASE.
|
|
error_code TEXT,
|
|
-- error_message ist die verständliche Fehlermeldung.
|
|
--
|
|
-- Sie enthält niemals Geheimnisse (PROMPT.md §141); die Redaktion
|
|
-- geschieht vor dem Schreiben.
|
|
error_message TEXT,
|
|
-- failure_class ordnet den Fehler ein und entscheidet über Wiederholung.
|
|
failure_class TEXT,
|
|
-- correlation_id verbindet den Lauf mit seinen Protokollzeilen.
|
|
correlation_id UUID NOT NULL,
|
|
-- triggered_by benennt den auslösenden Anwender bei manuellem Start.
|
|
triggered_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- scheduler_instance benennt den Control-Server, der den Lauf hält.
|
|
--
|
|
-- Nötig, um nach dem Absturz eines Servers dessen verwaiste Läufe zu
|
|
-- erkennen: Ein Lauf, der als laufend gilt, dessen Server aber verschwunden
|
|
-- ist, blockiert sonst den Auftrag für immer.
|
|
scheduler_instance TEXT,
|
|
-- heartbeat_at ist die letzte Lebendmeldung des Laufs in UTC.
|
|
heartbeat_at TIMESTAMPTZ,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT backup_job_runs_status_valid
|
|
CHECK (status IN ('queued', 'running', 'succeeded', 'partial_failure', 'failed', 'cancelled')),
|
|
CONSTRAINT backup_job_runs_trigger_valid
|
|
CHECK (trigger IN ('schedule', 'manual', 'retry', 'dependency')),
|
|
CONSTRAINT backup_job_runs_attempt_positive
|
|
CHECK (attempt_number >= 1),
|
|
-- Ein abgeschlossener Lauf ohne Endzeitpunkt wäre in jeder Auswertung eine
|
|
-- Lücke.
|
|
CONSTRAINT backup_job_runs_completion_consistent CHECK (
|
|
status IN ('queued', 'running') OR completed_at IS NOT NULL
|
|
),
|
|
-- Ein Lauf mit übergangenen Objekten darf nicht als Erfolg dastehen.
|
|
-- Die Regel gehört in die Datenbank, weil sie sonst von jeder Codestelle
|
|
-- einzeln eingehalten werden müsste — und eine davon vergisst es.
|
|
CONSTRAINT backup_job_runs_skipped_is_not_success CHECK (
|
|
files_skipped = 0 OR status <> 'succeeded'
|
|
)
|
|
);
|
|
|
|
CREATE INDEX backup_job_runs_job_started_idx ON backup_job_runs (job_id, started_at DESC);
|
|
CREATE INDEX backup_job_runs_status_idx ON backup_job_runs (status)
|
|
WHERE status IN ('queued', 'running');
|
|
CREATE INDEX backup_job_runs_correlation_idx ON backup_job_runs (correlation_id);
|
|
|
|
-- Ein Auftrag darf nur einen aktiven Lauf haben. Der Teilindex erzwingt das in
|
|
-- der Datenbank statt in der Anwendung: Zwei Control-Server, die gleichzeitig
|
|
-- denselben fälligen Auftrag sehen, würden ihn sonst beide starten und zwei
|
|
-- Sicherungen derselben Quelle erzeugen.
|
|
CREATE UNIQUE INDEX backup_job_runs_single_active_idx
|
|
ON backup_job_runs (job_id)
|
|
WHERE status IN ('queued', 'running');
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Backups
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE backups (
|
|
-- id ist der öffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- job_run_id ist der erzeugende Lauf.
|
|
job_run_id UUID REFERENCES backup_job_runs(id) ON DELETE SET NULL,
|
|
-- chain_id ist die Kette.
|
|
chain_id UUID REFERENCES backup_chains(id) ON DELETE SET NULL,
|
|
-- repository_id ist das Repository.
|
|
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT,
|
|
-- parent_backup_id ist das Elternbackup einer Zusatzsicherung.
|
|
parent_backup_id UUID REFERENCES backups(id) ON DELETE SET NULL,
|
|
-- backup_id_in_repository ist die Kennung innerhalb des Repositorys.
|
|
--
|
|
-- Sie ist die Brücke zur Ablage: Das Repository kennt keine UUIDs der
|
|
-- Datenbank, und es muss ohne sie auskommen (SYNCOVA_ARCHITECTURE.md §10).
|
|
backup_id_in_repository TEXT NOT NULL,
|
|
-- backup_type ist die Art des Backups.
|
|
backup_type TEXT NOT NULL,
|
|
-- consistency_level beschreibt die erreichte Konsistenz.
|
|
--
|
|
-- Sie wird niemals beschönigt: Eine anwendungskonsistente Sicherung ohne
|
|
-- Gastdienst gibt es nicht.
|
|
consistency_level TEXT,
|
|
-- status ist der Zustand des Backups.
|
|
status TEXT NOT NULL DEFAULT 'in_progress',
|
|
-- manifest_ref verweist auf das Manifest im Repository.
|
|
manifest_ref TEXT NOT NULL,
|
|
-- manifest_hash ist die Prüfsumme des Manifests.
|
|
manifest_hash TEXT,
|
|
-- logical_bytes ist die Menge der Ursprungsdaten.
|
|
logical_bytes BIGINT,
|
|
-- unique_bytes ist die nach Deduplizierung verbleibende Menge.
|
|
unique_bytes BIGINT,
|
|
-- compressed_bytes ist die Menge nach Kompression.
|
|
compressed_bytes BIGINT,
|
|
-- encrypted_bytes ist die abgelegte Menge.
|
|
encrypted_bytes BIGINT,
|
|
-- started_at ist der Beginn in UTC.
|
|
started_at TIMESTAMPTZ,
|
|
-- completed_at ist das Ende in UTC.
|
|
completed_at TIMESTAMPTZ,
|
|
-- integrity_status ist das Ergebnis der letzten Integritätsprüfung.
|
|
integrity_status TEXT,
|
|
-- verified_at ist der Zeitpunkt der letzten Prüfung in UTC.
|
|
verified_at TIMESTAMPTZ,
|
|
-- immutable_until ist das Ende der Aufbewahrungspflicht in UTC.
|
|
--
|
|
-- Bis dahin darf kein Löschvorgang das Backup entfernen — auch kein
|
|
-- Administrator (PROMPT.md: Unveränderlichkeit).
|
|
immutable_until TIMESTAMPTZ,
|
|
-- created_at ist der Anlagezeitpunkt in UTC.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT backups_type_valid
|
|
CHECK (backup_type IN ('full', 'incremental', 'differential', 'synthetic_full')),
|
|
CONSTRAINT backups_status_valid
|
|
CHECK (status IN ('in_progress', 'complete', 'incomplete', 'corrupt', 'deleted')),
|
|
CONSTRAINT backups_consistency_valid
|
|
CHECK (consistency_level IS NULL OR consistency_level IN
|
|
('crash_consistent', 'filesystem_consistent', 'application_consistent')),
|
|
-- Eine Zusatzsicherung ohne Elternbackup wäre eine Kette ohne Anfang.
|
|
CONSTRAINT backups_incremental_has_parent CHECK (
|
|
backup_type <> 'incremental' OR parent_backup_id IS NOT NULL
|
|
),
|
|
-- Dieselbe Kennung darf im selben Repository nur einmal vorkommen.
|
|
CONSTRAINT backups_repository_unique UNIQUE (repository_id, backup_id_in_repository)
|
|
);
|
|
|
|
CREATE INDEX backups_chain_idx ON backups (chain_id, started_at DESC);
|
|
CREATE INDEX backups_repository_status_idx ON backups (repository_id, status);
|
|
CREATE INDEX backups_job_run_idx ON backups (job_run_id);
|
|
-- Für die Aufbewahrung: Welche Backups dürfen noch nicht gelöscht werden?
|
|
CREATE INDEX backups_immutable_idx ON backups (immutable_until)
|
|
WHERE immutable_until IS NOT NULL;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Berechtigungen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- jobs.read, jobs.write und jobs.run stammen bereits aus 000002_identity und
|
|
-- werden hier NICHT erneut angelegt. Ein zweites INSERT verletzte die
|
|
-- Eindeutigkeit des Namens — die Datenbank hat das beim ersten Versuch
|
|
-- richtigerweise abgelehnt. Neu ist allein das Wartungsfenster: Es greift in
|
|
-- den Betrieb der gesamten Anlage ein und ist deshalb ein eigenes Recht.
|
|
INSERT INTO permissions (name, description) VALUES
|
|
('maintenance_windows.write', 'Wartungsfenster einrichten und ändern');
|
|
|
|
-- Ein Wartungsfenster unterdrückt Sicherungen. Wer es setzen darf, kann damit
|
|
-- den Schutz der ganzen Anlage aussetzen — das bleibt den Administratoren
|
|
-- vorbehalten.
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id FROM roles r, permissions p
|
|
WHERE r.name IN ('super_administrator', 'infrastructure_administrator')
|
|
AND p.name = 'maintenance_windows.write';
|