873d15d25e
Fuer jeden nicht laufenden qemu/lxc (ohne Vorlage) liest der PVE-Durchlauf
onboot aus /nodes/{node}/{type}/{vmid}/config (nur GET, Deckel 40).
Befund in den Metriken (autostartStopped, autostartUnchecked); Karte wird
Warnung mit einer Zeile je Gast, Kachel zeigt 'gestoppt trotz Autostart'.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
/**
|
|
* Proxmox Module API client (260923-dhh). Konsumiert `/modules/proxmox/*`.
|
|
* Vorbild `dkv-api.ts`: `credentials: 'include'` fuer Cookie-Auth,
|
|
* `NEXT_PUBLIC_API_URL` als Basis.
|
|
*
|
|
* Sicherheit (T-DHH-01): keine Antwort dieses Clients enthaelt jemals ein
|
|
* Geheimnisfeld — der Server waehlt `encryptedTokenSecret`/`encryptedPassword`
|
|
* per `select` gar nicht erst aus.
|
|
*/
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
|
|
export type ProxmoxProductType = 'pve' | 'pbs' | 'pmg';
|
|
export type ProxmoxAuthMethod = 'token' | 'password';
|
|
export type ProxmoxErrorKind =
|
|
| 'netz'
|
|
| 'zugang'
|
|
| 'rechte'
|
|
| 'zertifikat'
|
|
| 'antwortform'
|
|
| 'server'
|
|
| 'unbekannt';
|
|
|
|
export interface ProxmoxPveNodeMetric {
|
|
node: string;
|
|
cpu: number | null;
|
|
maxcpu: number | null;
|
|
mem: number | null;
|
|
maxmem: number | null;
|
|
}
|
|
|
|
export interface ProxmoxPveStorageMetric {
|
|
storage: string;
|
|
node: string;
|
|
disk: number | null;
|
|
maxdisk: number | null;
|
|
}
|
|
|
|
/** Gast, der nicht laeuft, obwohl Autostart aktiv ist (quick-261005). */
|
|
export interface ProxmoxPveAutostartGuest {
|
|
vmid: number;
|
|
name: string | null;
|
|
type: 'qemu' | 'lxc';
|
|
node: string;
|
|
status: string;
|
|
}
|
|
|
|
export interface ProxmoxPveMetrics {
|
|
productType: 'pve';
|
|
nodeCount: number;
|
|
guestsRunning: number;
|
|
guestsStopped: number;
|
|
nodes: ProxmoxPveNodeMetric[];
|
|
storages: ProxmoxPveStorageMetric[];
|
|
/** Fehlt in Zwischenlagerstaenden von vor quick-261005. */
|
|
autostartStopped?: ProxmoxPveAutostartGuest[];
|
|
/** Gestoppte Gaeste, deren Autostart nicht gelesen werden konnte. */
|
|
autostartUnchecked?: number;
|
|
}
|
|
|
|
export interface ProxmoxPbsDatastoreMetric {
|
|
name: string;
|
|
total: number | null;
|
|
used: number | null;
|
|
free: number | null;
|
|
lastBackupAt: number | null;
|
|
lastVerifyState: string | null;
|
|
}
|
|
|
|
export interface ProxmoxPbsMetrics {
|
|
productType: 'pbs';
|
|
datastores: ProxmoxPbsDatastoreMetric[];
|
|
}
|
|
|
|
export interface ProxmoxPmgMetrics {
|
|
productType: 'pmg';
|
|
countIn: number | null;
|
|
countOut: number | null;
|
|
spamCount: number | null;
|
|
virusCount: number | null;
|
|
}
|
|
|
|
export type ProxmoxMetrics = ProxmoxPveMetrics | ProxmoxPbsMetrics | ProxmoxPmgMetrics;
|
|
|
|
export interface ProxmoxServerStatus {
|
|
id: string;
|
|
serverId: string;
|
|
lastPolledAt: string | null;
|
|
lastOkAt: string | null;
|
|
reachable: boolean;
|
|
errorKind: ProxmoxErrorKind | null;
|
|
errorDetail: string | null;
|
|
metrics: ProxmoxMetrics | null;
|
|
rawSample: unknown;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface ProxmoxServer {
|
|
id: string;
|
|
tenantId: string;
|
|
name: string;
|
|
productType: ProxmoxProductType;
|
|
baseUrl: string;
|
|
authMethod: ProxmoxAuthMethod;
|
|
tokenId: string | null;
|
|
username: string | null;
|
|
tlsRejectUnauthorized: boolean;
|
|
isActive: boolean;
|
|
pollIntervalMin: number;
|
|
position: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
status: ProxmoxServerStatus | null;
|
|
}
|
|
|
|
export interface CreateProxmoxServerPayload {
|
|
name: string;
|
|
productType: ProxmoxProductType;
|
|
baseUrl: string;
|
|
authMethod: ProxmoxAuthMethod;
|
|
tokenId?: string;
|
|
tokenSecret?: string;
|
|
username?: string;
|
|
password?: string;
|
|
tlsRejectUnauthorized?: boolean;
|
|
pollIntervalMin?: number;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export type UpdateProxmoxServerPayload = Partial<CreateProxmoxServerPayload>;
|
|
|
|
export interface ProxmoxTestResult {
|
|
reachable: boolean;
|
|
errorKind: ProxmoxErrorKind | null;
|
|
errorDetail: string | null;
|
|
metrics: ProxmoxMetrics | null;
|
|
rawSample: unknown;
|
|
}
|
|
|
|
/** Liest die NestJS-Fehlermeldung aus dem Antwortkoerper, faellt sonst auf einen Standardtext zurueck. */
|
|
async function readErrorMessage(res: Response, fallback: string): Promise<string> {
|
|
try {
|
|
const body = await res.json();
|
|
if (typeof body?.message === 'string') return body.message;
|
|
if (Array.isArray(body?.message) && body.message.length > 0) return String(body.message[0]);
|
|
} catch {
|
|
/* Antwort war kein JSON — Standardtext bleibt */
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/** GET /modules/proxmox/servers — Serverliste samt Zwischenlager. */
|
|
export async function listServers(): Promise<ProxmoxServer[]> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw new Error('Failed to fetch proxmox servers');
|
|
return res.json();
|
|
}
|
|
|
|
/** POST /modules/proxmox/servers — Server anlegen (ADMIN/SUPER_ADMIN). */
|
|
export async function createServer(
|
|
payload: CreateProxmoxServerPayload,
|
|
): Promise<ProxmoxServer> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to create proxmox server'));
|
|
return res.json();
|
|
}
|
|
|
|
/** PUT /modules/proxmox/servers/:id — Server bearbeiten (ADMIN/SUPER_ADMIN). */
|
|
export async function updateServer(
|
|
id: string,
|
|
payload: UpdateProxmoxServerPayload,
|
|
): Promise<ProxmoxServer> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to update proxmox server'));
|
|
return res.json();
|
|
}
|
|
|
|
/** DELETE /modules/proxmox/servers/:id — Server samt Zwischenlagerzeile loeschen (ADMIN/SUPER_ADMIN). */
|
|
export async function deleteServer(id: string): Promise<void> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to delete proxmox server'));
|
|
}
|
|
|
|
/** POST /modules/proxmox/servers/:id/poll — sofortige Abfrage (ADMIN/SUPER_ADMIN). */
|
|
export async function pollServer(id: string): Promise<ProxmoxTestResult> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}/poll`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to poll proxmox server'));
|
|
return res.json();
|
|
}
|
|
|
|
/**
|
|
* POST /modules/proxmox/servers/:id/test — Verbindungstest fuer einen
|
|
* gespeicherten Server, schreibt NICHT ins Zwischenlager. `payload` traegt
|
|
* den aktuellen Formularstand (Nachbesserung Befund 1): der Test prueft
|
|
* damit, was im Formular steht, statt blind den gespeicherten Stand — ein
|
|
* leer gelassenes Geheimnisfeld (`tokenSecret`/`password: undefined`) laesst
|
|
* den Server serverseitig auf den gespeicherten Wert zurueckfallen.
|
|
*/
|
|
export async function testServer(
|
|
id: string,
|
|
payload: UpdateProxmoxServerPayload,
|
|
): Promise<ProxmoxTestResult> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}/test`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to test proxmox server'));
|
|
return res.json();
|
|
}
|
|
|
|
/**
|
|
* POST /modules/proxmox/servers/test — Verbindungstest waehrend der
|
|
* Neuanlage (Nachbesserung Befund 1): es gibt noch keinen gespeicherten
|
|
* Server, `payload` ist deshalb die einzige Quelle.
|
|
*/
|
|
export async function testDraftServer(
|
|
payload: UpdateProxmoxServerPayload,
|
|
): Promise<ProxmoxTestResult> {
|
|
const res = await fetch(`${API_URL}/modules/proxmox/servers/test`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to test proxmox server'));
|
|
return res.json();
|
|
}
|