ssh2.utils.generateKeyPairSync liefert in etwa 4 von 1000 Fällen einen privaten Schlüssel, an dem der eigene Parser scheitert. Ein so erzeugter Server-Schlüssel wäre dauerhaft unbrauchbar gewesen; in den Tests war es die Ursache des gelegentlich roten SSH-Tests. Refs #014 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
7.0 KiB
TypeScript
186 lines
7.0 KiB
TypeScript
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<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 = 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" });
|
|
});
|
|
});
|