import { afterEach, describe, expect, it } from "vitest"; import { ProxmoxClient } from "@/engine/proxmox/client"; import { erkunden } from "@/engine/proxmox/erkundung"; import { sshAusfuehren, SshFehler } from "@/engine/ssh/verbindung"; import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-mock/mock"; import { szenarioEinrichten, type Szenario } from "../proxmox-mock/szenario"; import { SshMock, testPaar } from "../ssh-mock"; const aufraeumen: Array<() => Promise> = []; afterEach(async () => { while (aufraeumen.length) await aufraeumen.pop()!(); }); async function mitSzenario(szenario: Szenario) { const mock = await new ProxmoxMock().starten(); szenarioEinrichten(mock, szenario); const client = new ProxmoxClient({ url: mock.url, tokenId: MOCK_TOKEN_ID, tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS, tlsPruefen: false, fingerabdruck: mock.fingerabdruck, }); aufraeumen.push(async () => { await client.schliessen(); await mock.stoppen(); }); return { mock, client }; } describe("Verbindungstest (Erkundung)", () => { it("PVE 9.2 Einzelhost: Nodes, Storages, Bridges, keine fehlenden Rechte, Template-Bau per API", async () => { const { client } = await mitSzenario({ version: "9.2.1", importStorage: true }); const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null, bauNetz: true }); expect(bericht.version).toBe("9.2.1"); expect(bericht.cluster).toBeNull(); expect(bericht.knoten).toHaveLength(1); expect(bericht.knoten[0]!.storages.map((s) => s.storage)).toEqual(["local", "local-zfs"]); expect(bericht.knoten[0]!.bridges).toEqual([ expect.objectContaining({ name: "vmbr0", vlanBewusst: true, cidr: "10.0.10.2/24", kommentar: "LAN" }), expect.objectContaining({ name: "vmbr1", vlanBewusst: false }), ]); expect(bericht.vnets).toEqual([{ name: "dmz", zone: "zone1", tag: 30, alias: "DMZ" }]); expect(bericht.fehlendeRechte.filter((r) => r.pflicht)).toEqual([]); expect(bericht.faehigkeiten.templateBau.weg).toBe("api"); expect(bericht.warnungen).toEqual([]); }); it("Cluster: Name, Quorum und Node-IPs", async () => { const { client } = await mitSzenario({ version: "9.2.1", cluster: { name: "rz1", nodes: [ { name: "pve01", ip: "10.0.0.1" }, { name: "pve02", ip: "10.0.0.2" }, { name: "pve03", ip: "10.0.0.3", online: false }, ], }, }); const bericht = await erkunden(client, { serverHost: "10.0.0.1", ssh: null }); expect(bericht.cluster).toEqual({ name: "rz1", quorate: true }); expect(bericht.knoten.map((k) => `${k.name}:${k.online}:${k.ip}`)).toEqual([ "pve01:true:10.0.0.1", "pve02:true:10.0.0.2", "pve03:false:10.0.0.3", ]); expect(bericht.knoten[2]!.storages).toEqual([]); }); it("PVE 8.3: Template-Bau braucht SSH, VM.Monitor wird verlangt", async () => { const { client } = await mitSzenario({ version: "8.3.5", importStorage: true, ohneRechte: ["VM.Monitor"], }); const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null }); expect(bericht.faehigkeiten.templateBau.weg).toBe("nicht_moeglich"); expect(bericht.fehlendeRechte).toContainEqual( expect.objectContaining({ recht: "VM.Monitor", pfad: "/vms" }), ); }); it("fehlende Leserechte werden zu Warnungen statt zum Abbruch", async () => { const { client } = await mitSzenario({ version: "9.2.1", ohneClusterStatus: true, ohneRechte: ["SDN.Audit"], }); const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null }); expect(bericht.knoten).toHaveLength(1); expect(bericht.vnets).toBeNull(); expect(bericht.warnungen.join("\n")).toMatch(/Cluster-Status.*Sys\.Audit/); }); it("warnt vor PVE 7", async () => { const { client } = await mitSzenario({ version: "7.4.17" }); const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null }); expect(bericht.warnungen.join()).toMatch(/nicht unterstützt/); }); }); describe("SSH", () => { const paar = testPaar(); async function sshServer() { const ssh = await new SshMock(paar.public).starten(); aufraeumen.push(() => ssh.stoppen()); return ssh; } it("Erkunden liefert den Host-Key; danach nur noch mit Pin", async () => { const ssh = await sshServer(); const ziel = { host: "127.0.0.1", port: ssh.port, benutzer: "root", privatSchluessel: paar.private, hostSchluessel: null, }; const erkundet = await sshAusfuehren(ziel, "pveversion", { erkunden: true }); expect(erkundet.hostSchluessel).toBe(ssh.hostSchluessel); expect(erkundet.stdout).toContain("pve-manager/9.2.1"); await expect(sshAusfuehren(ziel, "pveversion")).rejects.toMatchObject({ art: "host_schluessel" }); const mitPin = await sshAusfuehren({ ...ziel, hostSchluessel: ssh.hostSchluessel }, "echo hallo"); expect(mitPin.code).toBe(0); }); it("trennt bei geändertem Host-Key", async () => { const ssh = await sshServer(); const fehler = await sshAusfuehren( { host: "127.0.0.1", port: ssh.port, benutzer: "root", privatSchluessel: paar.private, hostSchluessel: "SHA256:falsch", }, "pveversion", ).catch((f: unknown) => f); expect(fehler).toBeInstanceOf(SshFehler); expect((fehler as SshFehler).art).toBe("host_schluessel"); expect(ssh.befehle).toEqual([]); }); it("meldet einen nicht hinterlegten Schlüssel als Anmeldefehler mit Hinweis", async () => { const ssh = await sshServer(); const fremd = testPaar(); const fehler = (await sshAusfuehren( { host: "127.0.0.1", port: ssh.port, benutzer: "root", privatSchluessel: fremd.private, hostSchluessel: ssh.hostSchluessel, }, "pveversion", ).catch((f: unknown) => f)) as SshFehler; expect(fehler.art).toBe("anmeldung"); expect(fehler.hinweis).toMatch(/authorized_keys/); }); it("Erkundung mit SSH: unbestätigt, dann bestätigt → Snippets und pct exec möglich", async () => { const ssh = await sshServer(); const { client } = await mitSzenario({ version: "8.3.5", importStorage: true }); const zugang = { benutzer: "root", port: ssh.port, privatSchluessel: paar.private, hostSchluessel: {} }; const erst = await erkunden(client, { serverHost: "127.0.0.1", ssh: zugang }); expect(erst.ssh).toBe("nicht_bestaetigt"); expect(erst.knoten[0]!.ssh).toMatchObject({ zustand: "nicht_bestaetigt", hostSchluessel: ssh.hostSchluessel, }); expect(erst.faehigkeiten.templateBau.weg).toBe("nicht_moeglich"); const dann = await erkunden(client, { serverHost: "127.0.0.1", ssh: { ...zugang, hostSchluessel: { pve01: ssh.hostSchluessel } }, }); expect(dann.ssh).toBe("ok"); expect(dann.faehigkeiten.templateBau.weg).toBe("ssh"); expect(dann.faehigkeiten.lxcProvisionierung).toMatchObject({ ueber: "node" }); }); });