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