/** * API-Anbindung von Virtualisierungsverbünden und Agenten. * * Beide teilen eine Eigenschaft, die die Masken prägt: **Geheimnisse gehen nur * hinein, nie heraus.** Ein API-Token eines Proxmox-Verbunds und das * Betriebstoken eines Agenten lassen sich nach dem Anlegen nicht mehr abrufen — * die API gibt sie nicht zurück, und das ist kein Mangel, sondern der Grund, * warum ein Lesezugriff auf die Konfiguration ungefährlich ist. * * Die Oberfläche muss das sagen, sonst schließt jemand das Fenster mit dem * einmalig angezeigten Aufnahme-Token. */ import { requestApi } from '../../api/client'; // --------------------------------------------------------------------------- // Proxmox-Verbünde // --------------------------------------------------------------------------- /** Zugriffsweg auf die Sicherungsarchive. */ export type TransportKind = 'local' | 'ssh'; /** Ein eingerichteter Verbund. */ export interface Cluster { id: string; name: string; api_endpoint: string; api_token_id: string; /** Fingerabdruck bei selbstsigniertem Zertifikat. */ tls_fingerprint?: string; backup_storage_id: string; archive_transport: TransportKind; archive_mount_roots?: Record; ssh_username?: string; ssh_port?: number; ssh_host_fingerprints?: Record; keep_archive_on_node: boolean; status: string; last_seen_at?: string; } /** Anfrage zum Anlegen eines Verbunds. */ export interface ClusterInput { name: string; api_endpoint: string; api_token_id: string; /** Wird verschlüsselt abgelegt und nie wieder ausgeliefert. */ api_token_secret: string; tls_fingerprint?: string; backup_storage_id: string; archive_transport: TransportKind; archive_mount_roots?: Record; ssh_username?: string; ssh_port?: number; ssh_private_key_pem?: string; /** Ohne hinterlegten Wirtsschlüssel kommt keine SSH-Verbindung zustande. */ ssh_host_fingerprints?: Record; keep_archive_on_node?: boolean; } /** Ein Knoten des Verbunds. */ export interface ClusterHost { id: string; cluster_id: string; node_name: string; status?: string; cpu_count?: number; memory_bytes?: number; } /** Ein Gast. */ export interface VirtualMachine { id: string; cluster_id: string; provider_vm_id: string; name: string; guest_kind: string; status?: string; cpu_count?: number; memory_bytes?: number; node_name?: string; disk_count: number; /** * Platten mit `backup=0`. * * Größer als null macht einen Sicherungslauf zum Teilfehler — nicht weil * etwas schiefging, sondern weil die Wiederherstellung sonst eine * unvollständige Maschine liefert, die jemand für vollständig hält. */ excluded_disk_count: number; /** * Zeitpunkt, seit dem der Gast im Bestand fehlt. * * Ein fehlender Gast wird nie gelöscht: Er könnte abgeschaltet oder * verschoben sein, und eine gelöschte Zeile nähme die Zuordnung zu * vorhandenen Backups mit — die man genau dann braucht, wenn die Maschine * weg ist. */ missing_since?: string; } /** Lädt die Verbünde. */ export async function listClusters(abortSignal?: AbortSignal): Promise { return requestApi( '/proxmox/clusters', abortSignal ? { signal: abortSignal } : {}, ); } /** Legt einen Verbund an. */ export async function createCluster(clusterInput: ClusterInput): Promise { return requestApi('/proxmox/clusters', { method: 'POST', body: clusterInput, idempotencyKey: true, }); } /** Löscht einen Verbund. */ export async function deleteCluster(clusterIdentifier: string): Promise { return requestApi(`/proxmox/clusters/${encodeURIComponent(clusterIdentifier)}`, { method: 'DELETE', idempotencyKey: true, }); } /** * Prüft die Verbindung. * * Ein nicht erreichbarer Verbund ist ein 503, kein 500 — und `unauthorized` ist * von `unreachable` getrennt, weil die Abhilfe eine völlig andere ist. */ export async function testCluster( clusterIdentifier: string, ): Promise<{ status: string; message?: string; version?: string }> { return requestApi(`/proxmox/clusters/${encodeURIComponent(clusterIdentifier)}/test`, { method: 'POST', }); } /** Erfasst den Bestand neu. */ export async function discoverCluster( clusterIdentifier: string, ): Promise<{ hosts_found?: number; guests_found?: number; summary?: string }> { return requestApi(`/proxmox/clusters/${encodeURIComponent(clusterIdentifier)}/discover`, { method: 'POST', idempotencyKey: true, }); } /** Lädt die Knoten eines Verbunds. */ export async function listClusterHosts( clusterIdentifier: string, abortSignal?: AbortSignal, ): Promise { return requestApi( `/proxmox/clusters/${encodeURIComponent(clusterIdentifier)}/hosts`, abortSignal ? { signal: abortSignal } : {}, ); } /** Lädt die Gäste eines Verbunds. */ export async function listClusterMachines( clusterIdentifier: string, abortSignal?: AbortSignal, ): Promise { return requestApi( `/proxmox/clusters/${encodeURIComponent(clusterIdentifier)}/vms`, abortSignal ? { signal: abortSignal } : {}, ); } /** Lädt alle bekannten Gäste, verbundsübergreifend. */ export async function listVirtualMachines( abortSignal?: AbortSignal, ): Promise { return requestApi( '/virtual-machines', abortSignal ? { signal: abortSignal } : {}, ); } // --------------------------------------------------------------------------- // Agenten // --------------------------------------------------------------------------- /** Ein aufgenommener Agent. */ export interface Agent { id: string; name: string; hostname?: string; platform?: string; architecture?: string; version?: string; status: string; last_heartbeat_at?: string; enrolled_at?: string; } /** Antwort auf die Erzeugung eines Aufnahme-Tokens. */ export interface EnrollmentToken { /** * Das Token im Klartext — **einmalig**. * * Es wird nur als Hash gespeichert und lässt sich nie wieder abrufen. Die * Oberfläche muss das sagen, sonst schließt jemand das Fenster. */ enrollment_token: string; agent_name: string; expires_at?: string; } /** Lädt die Agenten. */ export async function listAgents(abortSignal?: AbortSignal): Promise { return requestApi('/agents', abortSignal ? { signal: abortSignal } : {}); } /** * Erzeugt ein Aufnahme-Token. * * Der Name steht **im Token**, nicht in der Hand des Agenten: Sonst könnte er * sich als ein anderes System ausgeben. */ export async function createEnrollmentToken( agentName: string, validityMinutes = 60, ): Promise { return requestApi('/agents/enrollment-tokens', { method: 'POST', body: { agent_name: agentName, validity_minutes: validityMinutes }, idempotencyKey: true, }); } /** Sperrt einen Agenten. Sein Betriebstoken gilt sofort nicht mehr. */ export async function revokeAgent(agentIdentifier: string): Promise { return requestApi(`/agents/${encodeURIComponent(agentIdentifier)}/revoke`, { method: 'POST', idempotencyKey: true, }); } /** Erneuert das Betriebstoken eines Agenten. */ export async function rotateAgentCredentials( agentIdentifier: string, ): Promise<{ operational_token?: string }> { return requestApi( `/agents/${encodeURIComponent(agentIdentifier)}/rotate-credentials`, { method: 'POST', idempotencyKey: true }, ); } /** Lädt den Gesundheitszustand eines Agenten. */ export async function getAgentHealth( agentIdentifier: string, abortSignal?: AbortSignal, ): Promise<{ status: string; message?: string; last_heartbeat_at?: string }> { return requestApi( `/agents/${encodeURIComponent(agentIdentifier)}/health`, abortSignal ? { signal: abortSignal } : {}, ); }