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>
221 lines
9.2 KiB
SQL
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;
|