syncova-backup/migrations/000005_restores.up.sql
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

166 lines
8.1 KiB
SQL

-- Wiederherstellung (Phase 9).
--
-- Der Grundsatz aus PROMPT.md: Ein Backup gilt erst als vertrauenswuerdig, wenn
-- die Wiederherstellbarkeit nachgewiesen wurde. Daraus folgen zwei Dinge, die
-- dieses Schema traegt:
--
-- 1. Eine Wiederherstellung wird **vorab geprueft**, ohne etwas zu schreiben.
-- Das Ergebnis der Pruefung gehoert zum Auftrag, damit spaeter belegbar
-- ist, was man vor dem Start wusste.
-- 2. Eine Wiederherstellung hat eine **Sitzung mit Pruefpunkt**. Ein Abbruch
-- bei 90 Prozent darf nicht bedeuten, dass alles von vorn beginnt —
-- bei mehreren Terabyte waere das der Unterschied zwischen Stunden und
-- Tagen, und zwar in einer Lage, in der ohnehin schon etwas schiefging.
-- ---------------------------------------------------------------------------
-- Wiederherstellungsauftraege
-- ---------------------------------------------------------------------------
CREATE TABLE restore_jobs (
-- id ist der oeffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- backup_id ist das wiederherzustellende Backup.
--
-- ON DELETE RESTRICT: Ein Backup, aus dem gerade wiederhergestellt wird,
-- darf nicht verschwinden.
backup_id UUID NOT NULL REFERENCES backups(id) ON DELETE RESTRICT,
-- source_type benennt die Art der urspruenglichen Quelle.
source_type TEXT NOT NULL,
-- target_type benennt die Art des Ziels.
target_type TEXT NOT NULL,
-- target_ref ist der Zielpfad oder die Zielkennung.
target_ref TEXT NOT NULL,
-- path_prefix beschraenkt die Wiederherstellung auf einen Teilbaum.
--
-- Der haeufigste Fall im Betrieb ist nicht die vollstaendige
-- Wiederherstellung, sondern eine einzelne versehentlich geloeschte Datei.
path_prefix TEXT,
-- status ist der Zustand des Auftrags.
status TEXT NOT NULL DEFAULT 'queued',
-- overwrite_existing erlaubt das Ueberschreiben vorhandener Daten.
--
-- Der gefaehrlichste Schalter der ganzen Anlage: Er richtet sich gegen
-- Daten, die es noch gibt. Er wird niemals vorbelegt und immer auditiert
-- (PROMPT.md §140: destruktive Aktionen niemals still).
overwrite_existing BOOLEAN NOT NULL DEFAULT false,
-- restore_permissions setzt die urspruenglichen Rechte.
restore_permissions BOOLEAN NOT NULL DEFAULT true,
-- verify_content prueft jede Datei gegen ihre Pruefsumme.
verify_content BOOLEAN NOT NULL DEFAULT true,
-- validation_result haelt das Ergebnis der Vorabpruefung als JSON.
--
-- Es gehoert zum Auftrag, nicht in ein Protokoll: Spaeter muss belegbar
-- sein, was vor dem Start bekannt war — etwa dass ein Block fehlte und man
-- trotzdem begonnen hat.
validation_result JSONB,
-- started_at ist der Beginn in UTC.
started_at TIMESTAMPTZ,
-- completed_at ist das Ende in UTC.
completed_at TIMESTAMPTZ,
-- bytes_restored ist die zurueckgeschriebene Datenmenge.
bytes_restored BIGINT NOT NULL DEFAULT 0,
-- files_restored ist die Zahl zurueckgeschriebener Objekte.
files_restored BIGINT NOT NULL DEFAULT 0,
-- files_skipped ist die Zahl uebergangener Objekte.
--
-- Groesser als 0 bedeutet: Die Wiederherstellung ist unvollstaendig. Sie
-- darf dann nicht als Erfolg dastehen (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 verstaendliche Fehlermeldung.
error_message TEXT,
-- correlation_id verbindet den Auftrag mit seinen Protokollzeilen.
correlation_id UUID NOT NULL,
-- scheduler_instance benennt den ausfuehrenden Control-Server.
scheduler_instance TEXT,
-- heartbeat_at ist die letzte Lebendmeldung in UTC.
heartbeat_at TIMESTAMPTZ,
-- created_by benennt den Anfordernden.
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT restore_jobs_status_valid
CHECK (status IN ('queued', 'running', 'succeeded', 'partial_failure', 'failed', 'cancelled')),
CONSTRAINT restore_jobs_target_type_valid
CHECK (target_type IN ('filesystem', 'original_location', 'proxmox_vm', 'new_proxmox_vm')),
-- Ein abgeschlossener Auftrag ohne Endzeitpunkt waere in jeder Auswertung
-- eine Luecke.
CONSTRAINT restore_jobs_completion_consistent CHECK (
status IN ('queued', 'running') OR completed_at IS NOT NULL
),
-- Dieselbe Regel wie bei Sicherungen: Uebergangene Objekte sind kein Erfolg.
CONSTRAINT restore_jobs_skipped_is_not_success CHECK (
files_skipped = 0 OR status <> 'succeeded'
)
);
CREATE INDEX restore_jobs_backup_idx ON restore_jobs (backup_id, created_at DESC);
CREATE INDEX restore_jobs_status_idx ON restore_jobs (status) WHERE status IN ('queued', 'running');
CREATE INDEX restore_jobs_created_idx ON restore_jobs (created_at DESC);
-- Zwei gleichzeitige Wiederherstellungen in dasselbe Ziel schrieben sich
-- gegenseitig zu — und zwar ohne dass es auffiele, weil beide erfolgreich
-- endeten. Der Teilindex verhindert das in der Datenbank, nicht in der
-- Anwendung: Zwei Control-Server pruefen sonst beide erfolgreich.
CREATE UNIQUE INDEX restore_jobs_single_active_target_idx
ON restore_jobs (target_ref)
WHERE status IN ('queued', 'running');
-- ---------------------------------------------------------------------------
-- Sitzungen mit Pruefpunkt
-- ---------------------------------------------------------------------------
CREATE TABLE restore_sessions (
-- id ist der oeffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- restore_job_id ist der zugehoerige Auftrag.
restore_job_id UUID NOT NULL REFERENCES restore_jobs(id) ON DELETE CASCADE,
-- state ist der Zustand der Sitzung.
state TEXT NOT NULL DEFAULT 'active',
-- checkpoint haelt den Fortschritt als JSON.
--
-- Darin steht der zuletzt vollstaendig zurueckgeschriebene Pfad. Beim
-- Fortsetzen wird alles davor uebersprungen — die Reihenfolge der Objekte
-- im Manifest ist fest sortiert, deshalb ist ein einzelner Pfad als Marke
-- ausreichend und braucht keine Liste.
checkpoint JSONB NOT NULL DEFAULT '{}'::jsonb,
-- attempt_number zaehlt die Fortsetzungen, beginnend bei 1.
attempt_number INTEGER NOT NULL DEFAULT 1,
-- 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 restore_sessions_state_valid
CHECK (state IN ('active', 'suspended', 'completed', 'abandoned')),
CONSTRAINT restore_sessions_attempt_positive CHECK (attempt_number >= 1)
);
CREATE INDEX restore_sessions_job_idx ON restore_sessions (restore_job_id, created_at DESC);
-- Je Auftrag darf nur eine Sitzung aktiv sein. Zwei aktive Sitzungen schrieben
-- mit verschiedenen Pruefpunkten in dasselbe Ziel.
CREATE UNIQUE INDEX restore_sessions_single_active_idx
ON restore_sessions (restore_job_id)
WHERE state = 'active';
-- ---------------------------------------------------------------------------
-- Berechtigungen
-- ---------------------------------------------------------------------------
-- restores.read und restores.execute stammen bereits aus 000002_identity.
-- Neu ist allein das Ueberschreiben: Es richtet sich gegen Daten, die es noch
-- gibt, und ist damit etwas anderes als eine Wiederherstellung an einen leeren
-- Ort. Wer wiederherstellen darf, darf deshalb nicht automatisch ueberschreiben.
INSERT INTO permissions (name, description) VALUES
('restores.overwrite', 'Wiederherstellungen ausfuehren, die vorhandene Daten ueberschreiben');
-- Das Ueberschreiben produktiver Daten bleibt den Administratoren und dem
-- Restore Operator vorbehalten — den Rollen, die die Folgen verantworten.
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', 'restore_operator')
AND p.name = 'restores.overwrite';