-- 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';