feat(server): Verbindungstest mit Fähigkeitserkennung
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>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
benoetigteRechte,
|
||||
faehigkeitenAbleiten,
|
||||
rechtePruefen,
|
||||
versionLesen,
|
||||
type StorageInfo,
|
||||
} from "@/engine/proxmox/faehigkeiten";
|
||||
|
||||
const v = (text: string) => versionLesen(text)!;
|
||||
const storage = (name: string, inhalte: string[]): StorageInfo => ({
|
||||
storage: name,
|
||||
typ: "dir",
|
||||
inhalte,
|
||||
aktiv: true,
|
||||
geteilt: false,
|
||||
frei: null,
|
||||
gesamt: null,
|
||||
});
|
||||
|
||||
describe("PVE-Version", () => {
|
||||
it("liest übliche Schreibweisen", () => {
|
||||
expect(v("9.2.1")).toMatchObject({ major: 9, minor: 2, patch: 1 });
|
||||
expect(v("8.4")).toMatchObject({ major: 8, minor: 4, patch: 0 });
|
||||
expect(versionLesen("kaputt")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rolle ProxDeploy je Version", () => {
|
||||
const namen = (text: string) =>
|
||||
benoetigteRechte(v(text))
|
||||
.filter((r) => r.pflicht)
|
||||
.map((r) => r.recht);
|
||||
|
||||
it("PVE 9 nutzt die Guest-Agent-Rechte statt VM.Monitor", () => {
|
||||
expect(namen("9.2.1")).toEqual(
|
||||
expect.arrayContaining(["VM.GuestAgent.Audit", "VM.GuestAgent.Unrestricted"]),
|
||||
);
|
||||
expect(namen("9.2.1")).not.toContain("VM.Monitor");
|
||||
});
|
||||
|
||||
it("PVE 8 braucht VM.Monitor für den Guest-Agent", () => {
|
||||
expect(namen("8.4.1")).toContain("VM.Monitor");
|
||||
expect(namen("8.4.1")).not.toContain("VM.GuestAgent.Unrestricted");
|
||||
});
|
||||
|
||||
it("download-url: Sys.AccessNetwork ab 8.2, davor Sys.Modify", () => {
|
||||
expect(namen("8.2.0")).toContain("Sys.AccessNetwork");
|
||||
expect(namen("8.1.4")).toContain("Sys.Modify");
|
||||
expect(namen("8.1.4")).not.toContain("Sys.AccessNetwork");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rechteprüfung", () => {
|
||||
const version = v("9.2.1");
|
||||
const voll = Object.fromEntries(benoetigteRechte(version).map((r) => [r.recht, 1]));
|
||||
|
||||
it("meldet nichts, wenn alles da ist", () => {
|
||||
const rechte = {
|
||||
"/": voll,
|
||||
"/vms": voll,
|
||||
"/sdn/zones/localnetwork": voll,
|
||||
"/nodes/pve01": voll,
|
||||
"/storage/local-zfs": voll,
|
||||
};
|
||||
expect(rechtePruefen(version, rechte, { storages: ["local-zfs"], nodes: ["pve01"] })).toEqual([]);
|
||||
});
|
||||
|
||||
it("nennt fehlende Rechte mit Pfad und Zweck", () => {
|
||||
const ohneAgent = { ...voll, "VM.GuestAgent.Unrestricted": 0 };
|
||||
const rechte = {
|
||||
"/": voll,
|
||||
"/vms": ohneAgent,
|
||||
"/sdn/zones/localnetwork": voll,
|
||||
"/nodes/pve01": voll,
|
||||
"/storage/local-zfs": voll,
|
||||
};
|
||||
expect(rechtePruefen(version, rechte, { storages: ["local-zfs"], nodes: ["pve01"] })).toEqual([
|
||||
expect.objectContaining({ recht: "VM.GuestAgent.Unrestricted", pfad: "/vms", pflicht: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("Storage-Rechte: einer reicht; Node-Rechte: jeder Node braucht sie", () => {
|
||||
const rechte = {
|
||||
"/": voll,
|
||||
"/vms": voll,
|
||||
"/sdn/zones/localnetwork": voll,
|
||||
"/nodes/pve01": voll,
|
||||
"/nodes/pve02": {},
|
||||
"/storage/a": voll,
|
||||
"/storage/b": {},
|
||||
};
|
||||
const fehlend = rechtePruefen(version, rechte, { storages: ["a", "b"], nodes: ["pve01", "pve02"] });
|
||||
expect(fehlend.filter((f) => f.pflicht).map((f) => `${f.recht} ${f.pfad}`)).toEqual([
|
||||
"Sys.AccessNetwork /nodes/pve02",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Strategien", () => {
|
||||
const mitImport = [storage("local", ["iso", "import", "snippets"])];
|
||||
const ohneImport = [storage("local", ["iso", "snippets"])];
|
||||
|
||||
it("PVE 9.2 mit import-Storage: Template-Bau per API, auch ohne SSH", () => {
|
||||
const f = faehigkeitenAbleiten({
|
||||
version: v("9.2.1"),
|
||||
storages: mitImport,
|
||||
ssh: "aus",
|
||||
rechteFehlen: false,
|
||||
});
|
||||
expect(f.templateBau.weg).toBe("api");
|
||||
expect(f.vmProvisionierung.weg).toBe("api");
|
||||
expect(f.lxcProvisionierung).toMatchObject({ weg: "ssh", ueber: "gast" });
|
||||
expect(f.snippets.weg).toBe("nicht_moeglich");
|
||||
});
|
||||
|
||||
it("PVE 8.3: Template-Bau nur per SSH; ohne SSH mit Begründung unmöglich", () => {
|
||||
expect(
|
||||
faehigkeitenAbleiten({ version: v("8.3.5"), storages: mitImport, ssh: "ok", rechteFehlen: false })
|
||||
.templateBau.weg,
|
||||
).toBe("ssh");
|
||||
const ohne = faehigkeitenAbleiten({
|
||||
version: v("8.3.5"),
|
||||
storages: mitImport,
|
||||
ssh: "aus",
|
||||
rechteFehlen: false,
|
||||
});
|
||||
expect(ohne.templateBau).toMatchObject({ weg: "nicht_moeglich" });
|
||||
expect(ohne.templateBau.grund).toContain("8.4");
|
||||
});
|
||||
|
||||
it("PVE 8.4 ohne import-Storage nennt den Ausweg", () => {
|
||||
const f = faehigkeitenAbleiten({
|
||||
version: v("8.4.1"),
|
||||
storages: ohneImport,
|
||||
ssh: "aus",
|
||||
rechteFehlen: false,
|
||||
});
|
||||
expect(f.templateBau.grund).toMatch(/Inhaltstyp „Import“/);
|
||||
});
|
||||
|
||||
it("mit SSH: Snippets, pct exec und erweiterte LXC-Features", () => {
|
||||
const f = faehigkeitenAbleiten({
|
||||
version: v("9.2.1"),
|
||||
storages: mitImport,
|
||||
ssh: "ok",
|
||||
rechteFehlen: false,
|
||||
});
|
||||
expect(f.snippets.weg).toBe("ssh");
|
||||
expect(f.lxcProvisionierung).toMatchObject({ ueber: "node" });
|
||||
expect(f.lxcFeaturesErweitert.weg).toBe("ssh");
|
||||
});
|
||||
|
||||
it("unbestätigter Host-Key zählt nicht als SSH", () => {
|
||||
const f = faehigkeitenAbleiten({
|
||||
version: v("8.3.5"),
|
||||
storages: mitImport,
|
||||
ssh: "nicht_bestaetigt",
|
||||
rechteFehlen: false,
|
||||
});
|
||||
expect(f.templateBau.weg).toBe("nicht_moeglich");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user