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>
164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
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");
|
|
});
|
|
});
|