Der Test liest Version, Cluster, Nodes, Storages, Bridges, VNets und die effektiven Rechte des Tokens und leitet daraus ab, welcher Vorgang per API, per SSH oder gar nicht geht – je PVE-Version: Template-Bau per API erst ab 8.4 mit import-Storage, Snippets und LXC-Exec nur per SSH. Fehlende Rechte erscheinen einzeln mit Pfad und Zweck; die Rollenliste in docs/proxmox-rechte.md prüft ein Test gegen den Code. SSH-Host-Keys werden wie Zertifikate erst nach Bestätigung genutzt, und bestätigt werden kann nur ein Schlüssel, den der letzte Test gesehen hat. Statussymbole in der GUI sind jetzt gezeichnet: ◐ und ○ fehlen im Web-Subset von JetBrains Mono. Refs #003 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import type { AddressInfo } from "node:net";
|
|
import { Server, utils } from "ssh2";
|
|
import { hostSchluesselFingerabdruck } from "@/engine/ssh/verbindung";
|
|
|
|
/**
|
|
* Ein SSH-Server für Tests (ssh2 bringt ihn mit). Er nimmt genau einen öffentlichen Schlüssel an
|
|
* und beantwortet Befehle über eine frei definierbare Funktion.
|
|
*/
|
|
export class SshMock {
|
|
private server: Server | undefined;
|
|
port = 0;
|
|
hostSchluessel = "";
|
|
befehle: string[] = [];
|
|
|
|
constructor(
|
|
private readonly erlaubterSchluessel: string,
|
|
private readonly antwort: (befehl: string) => {
|
|
stdout?: string;
|
|
stderr?: string;
|
|
code?: number;
|
|
} = () => ({
|
|
stdout: "pve-manager/9.2.1/abcdef (running kernel: 6.14.8-2-pve)\n",
|
|
}),
|
|
) {}
|
|
|
|
async starten(): Promise<this> {
|
|
// ssh2 liest ed25519 nur im OpenSSH-Format, nicht als PKCS8.
|
|
const hostPem = utils.generateKeyPairSync("ed25519").private;
|
|
const host = utils.parseKey(hostPem);
|
|
if (host instanceof Error) throw host;
|
|
this.hostSchluessel = hostSchluesselFingerabdruck((Array.isArray(host) ? host[0]! : host).getPublicSSH());
|
|
const erlaubt = utils.parseKey(this.erlaubterSchluessel);
|
|
if (erlaubt instanceof Error) throw erlaubt;
|
|
const erlaubtSsh = (Array.isArray(erlaubt) ? erlaubt[0]! : erlaubt).getPublicSSH();
|
|
|
|
this.server = new Server({ hostKeys: [hostPem] }, (client) => {
|
|
client
|
|
.on("authentication", (kontext) => {
|
|
if (kontext.method === "publickey" && kontext.key.data.equals(erlaubtSsh)) kontext.accept();
|
|
else kontext.reject(["publickey"]);
|
|
})
|
|
.on("ready", () => {
|
|
client.on("session", (annehmen) => {
|
|
const sitzung = annehmen();
|
|
sitzung.on("exec", (annehmenExec, _ablehnen, info) => {
|
|
const kanal = annehmenExec();
|
|
this.befehle.push(info.command);
|
|
const ergebnis = this.antwort(info.command);
|
|
if (ergebnis.stdout) kanal.write(ergebnis.stdout);
|
|
if (ergebnis.stderr) kanal.stderr.write(ergebnis.stderr);
|
|
kanal.exit(ergebnis.code ?? 0);
|
|
kanal.end();
|
|
});
|
|
});
|
|
})
|
|
.on("error", () => undefined);
|
|
});
|
|
await new Promise<void>((erfuellen) => this.server!.listen(0, "127.0.0.1", erfuellen));
|
|
this.port = (this.server.address() as AddressInfo).port;
|
|
return this;
|
|
}
|
|
|
|
async stoppen(): Promise<void> {
|
|
await new Promise<void>((erfuellen) => this.server?.close(() => erfuellen()) ?? erfuellen());
|
|
}
|
|
}
|