syncova-backup/migrations/000012_proxmox.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

221 lines
9.2 KiB
SQL

-- Bestand der Virtualisierungsumgebungen (Phase 7).
--
-- Bis hierher konnte der Proxmox-Provider entdecken, sichern und
-- wiederherstellen — aber nur, wenn ihm jemand von Hand Zugangsdaten übergab.
-- Ein Sicherungsauftrag der Art 'proxmox_vm' ließ sich anlegen und scheiterte
-- beim Lauf, weil der Executor keinen Weg zu einem Verbund kannte.
--
-- Diese Migration schließt die Lücke: Ein Verbund wird einmal eingerichtet, sein
-- Bestand regelmäßig aufgenommen, und ein Auftrag verweist auf einen Gast
-- daraus.
-- ---------------------------------------------------------------------------
-- Verbünde
-- ---------------------------------------------------------------------------
CREATE TABLE proxmox_clusters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
api_endpoint TEXT NOT NULL,
-- Die Kennung des API-Tokens, etwa 'syncova@pve!backup'.
--
-- Im Klartext, weil sie kein Geheimnis ist: Sie steht in jeder
-- Proxmox-Oberfläche und wird zur Fehlersuche gebraucht. Das Geheimnis ist
-- ausschließlich der Wert daneben.
api_token_id TEXT NOT NULL,
-- Der verschlüsselte Tokenwert und die Version des verwendenden Schlüssels.
--
-- Niemals im Klartext (PROMPT.md §140). Die Spalte heißt _ciphertext, damit
-- an der Verwendungsstelle keine Verwechslung möglich ist.
api_token_ciphertext BYTEA NOT NULL,
api_token_key_version TEXT NOT NULL,
-- Fingerabdruck des Zertifikats bei selbstsignierter Anlage.
--
-- Die Bindung an genau ein Zertifikat ist strenger als eine CA-Prüfung —
-- und der einzige vertretbare Umgang mit dem Standardzertifikat von
-- Proxmox. Ein Schalter 'Zertifikat egal' existiert bewusst nicht.
tls_fingerprint TEXT,
-- Der Speicher, auf dem vzdump seine Archive ablegt.
backup_storage_id TEXT NOT NULL,
-- Wie Syncova an die Archivdateien kommt.
--
-- Die Proxmox-API kann eine Sicherung anstoßen, gibt die entstandene Datei
-- aber nicht heraus. Ohne einen dieser beiden Wege liefert eine Sicherung
-- kein einziges Byte — deshalb ist die Angabe verpflichtend und nicht
-- vorbelegt.
archive_transport TEXT NOT NULL
CHECK (archive_transport IN ('local', 'ssh')),
-- Zuordnung Proxmox-Speicherkennung -> lokaler Einhängepfad (Transport 'local').
--
-- Ausdrücklich statt geraten: Ein Speicher heißt auf dem Knoten anders als
-- auf dem Server, der ihn einhängt.
archive_mount_roots JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Zugang für den Transport 'ssh'.
ssh_username TEXT,
ssh_port INTEGER CHECK (ssh_port IS NULL OR (ssh_port > 0 AND ssh_port <= 65535)),
ssh_private_key_ciphertext BYTEA,
ssh_private_key_version TEXT,
-- Fingerabdrücke der Wirtsschlüssel je Knoten.
--
-- Ein Knoten ohne Eintrag wird abgelehnt. Ein Transport, der jeden
-- Wirtsschlüssel annimmt, macht aus einem Zwischenangriff eine Einladung:
-- Der Angreifer lieferte dann das Archiv, das Syncova für ein Backup hält.
ssh_host_fingerprints JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Lässt das vzdump-Archiv nach der Übernahme auf dem Knoten liegen.
keep_archive_on_node BOOLEAN NOT NULL DEFAULT FALSE,
status TEXT NOT NULL DEFAULT 'unknown'
CHECK (status IN ('unknown', 'reachable', 'unreachable', 'unauthorized')),
-- Ergebnis der letzten Verbindungsprüfung.
last_error TEXT,
last_seen_at TIMESTAMPTZ,
last_discovery_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Der SSH-Weg ohne Anmeldekonto oder Schlüssel wäre eine Einrichtung, die
-- erst um zwei Uhr nachts scheitert. Die Regel steht in der Datenbank, weil
-- im Code jede Stelle sie einhalten müsste — und eine vergisst es.
CONSTRAINT proxmox_clusters_ssh_complete CHECK (
archive_transport <> 'ssh' OR (
ssh_username IS NOT NULL AND ssh_username <> ''
AND ssh_private_key_ciphertext IS NOT NULL
AND ssh_private_key_version IS NOT NULL
)
),
-- Dasselbe für den lokalen Weg: ohne Zuordnung findet er keine Datei.
CONSTRAINT proxmox_clusters_local_complete CHECK (
archive_transport <> 'local' OR archive_mount_roots <> '{}'::jsonb
)
);
COMMENT ON TABLE proxmox_clusters IS
'Eingerichtete Proxmox-VE-Verbünde samt Zugang und Weg zu den Sicherungsarchiven.';
CREATE INDEX proxmox_clusters_status_idx ON proxmox_clusters (status);
-- ---------------------------------------------------------------------------
-- Knoten
-- ---------------------------------------------------------------------------
CREATE TABLE proxmox_hosts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES proxmox_clusters(id) ON DELETE CASCADE,
node_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'unknown',
cpu_count INTEGER,
memory_bytes BIGINT,
last_seen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (cluster_id, node_name)
);
COMMENT ON TABLE proxmox_hosts IS 'Knoten eines Verbunds, wie er zuletzt aufgenommen wurde.';
-- ---------------------------------------------------------------------------
-- Gäste
-- ---------------------------------------------------------------------------
CREATE TABLE virtual_machines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES proxmox_clusters(id) ON DELETE CASCADE,
host_id UUID REFERENCES proxmox_hosts(id) ON DELETE SET NULL,
-- Die Kennung beim Provider, etwa 'qemu/100' oder 'lxc/200'.
--
-- Die Art steht darin, weil eine reine Zahl mehrdeutig wäre: Eine VM 100
-- und ein Container 100 können nebeneinander existieren.
provider_vm_id TEXT NOT NULL,
name TEXT NOT NULL,
guest_kind TEXT NOT NULL DEFAULT 'qemu'
CHECK (guest_kind IN ('qemu', 'lxc')),
status TEXT,
cpu_count INTEGER,
memory_bytes BIGINT,
-- Die wortgetreue Konfiguration des Gasts.
--
-- Eine umgedeutete Fassung verlöre genau die Felder, die wir heute noch
-- nicht kennen — und die bei einer Wiederherstellung fehlten.
config_json JSONB,
-- Die Platten, wie sie zuletzt erkannt wurden.
--
-- Getrennt von config_json, weil die Auswertung Regeln kennt, die man dem
-- Rohtext nicht ansieht: backup=0 nimmt eine Platte aus, media=cdrom ist
-- keine, unused0 ist eine abgehängte Platte **mit** Daten.
disks_json JSONB,
-- Ob der Gastdienst antwortet.
--
-- Er entscheidet über die erreichbare Konsistenzstufe. Ohne ihn ergibt eine
-- Sicherung 'crash_consistent' — was nie beschönigt wird.
guest_agent_running BOOLEAN,
last_discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Ein Gast, der bei der letzten Aufnahme fehlte, wird nicht gelöscht.
--
-- Er könnte abgeschaltet, verschoben oder gelöscht worden sein — und
-- gelöschte Zeilen nähmen die Zuordnung zu vorhandenen Backups mit. Der
-- Zeitpunkt sagt, seit wann er fehlt.
missing_since TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (cluster_id, provider_vm_id)
);
COMMENT ON TABLE virtual_machines IS
'Aufgenommener Gästebestand. Der Bestand ist eine Momentaufnahme, keine Quelle der Wahrheit.';
CREATE INDEX virtual_machines_cluster_idx ON virtual_machines (cluster_id, name);
CREATE INDEX virtual_machines_missing_idx ON virtual_machines (missing_since)
WHERE missing_since IS NOT NULL;
-- ---------------------------------------------------------------------------
-- Bezug einer Sicherungsquelle auf einen Verbund
-- ---------------------------------------------------------------------------
-- Ohne diesen Bezug wüsste der Executor bei einer Quelle 'proxmox_vm' zwar,
-- welchen Gast er sichern soll, aber nicht, über welchen Verbund er ihn
-- erreicht. Bei genau einem eingerichteten Verbund ließe sich das raten — bei
-- zweien sicherte er die falsche Maschine.
ALTER TABLE backup_job_sources
ADD COLUMN cluster_id UUID REFERENCES proxmox_clusters(id) ON DELETE RESTRICT;
COMMENT ON COLUMN backup_job_sources.cluster_id IS
'Verbund einer Proxmox-Quelle; bei Dateisystem- und Agentenquellen NULL.';
-- Eine Proxmox-Quelle ohne Verbund ist eine Sicherung, die beim Lauf scheitert.
-- Sie beim Anlegen abzulehnen ist die einzige Stelle, an der es niemandem weh
-- tut.
ALTER TABLE backup_job_sources
ADD CONSTRAINT backup_job_sources_cluster_required CHECK (
source_type NOT IN ('proxmox_vm', 'proxmox_container') OR cluster_id IS NOT NULL
);
CREATE INDEX backup_job_sources_cluster_idx ON backup_job_sources (cluster_id)
WHERE cluster_id IS NOT NULL;