Files
proxdeploy/tests/engine/erkundung.test.ts
florian.wildeandClaude Opus 5.5 21edff2141 feat(templates): Template-Bau ohne SSH über eine Bau-Adresse
Ohne SSH zum Node kommt der Guest-Agent jetzt über eine feste
Bau-Adresse ins Template: cloud-init setzt Adresse und den
Bereitstellungsschlüssel für den Standardbenutzer, ProxDeploy meldet
sich per SSH in der VM an, installiert den Agent und entfernt danach
Schlüssel, Adresse und cloud-init-Angaben.

Dabei korrigiert: der Verbindungstest meldete „Template-Bau per API“
auch ohne Weg, den Agent einzubauen. Im Protokoll erscheinen
unveränderte JSON-Felder nicht mehr als geändert.

Refs #021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:36:38 +02:00

187 lines
7.0 KiB
TypeScript

import { utils } from "ssh2";
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 } from "../ssh-mock";
const aufraeumen: Array<() => Promise<void>> = [];
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 = utils.generateKeyPairSync("ed25519");
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 = utils.generateKeyPairSync("ed25519");
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" });
});
});