feat(cluster): Cluster-Knoten ohne Agent erkennen (#022, #018)

Feldtest auf zwei echten Proxmox-VE-9.2-Hosts (#018): Installation, Timer,
Zwischenspeicher und Nachsenden laufen wie vorgesehen. `pvesubscription get`
kennt kein JSON, ohne Subscription fehlen fast alle Felder, `pveversion`
enthaelt den Kernel doppelt - alles im Agent beruecksichtigt.

Dabei zeigte sich: in Proxmox hat jeder Knoten eine eigene Subscription.
Der Agent meldet darum die Cluster-Mitglieder aus `pvecm nodes`, und eine
elfte Warnregel meldet Knoten, von denen sich kein Agent meldet (#022).

Ausserdem behoben: die Bilder des Handbuchs zeigten allesamt die
Anmeldeseite, weil Firefox eine selbst erzeugte Cookie-Datei stillschweigend
verwirft. Das Skript nutzt jetzt einen lokalen Vermittler und bricht ab,
wenn alle Aufnahmen gleich gross sind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:05:08 +02:00
co-authored by Claude Opus 5
parent dc685980d8
commit 7728fcb565
39 changed files with 484 additions and 82 deletions
+23
View File
@@ -144,6 +144,29 @@ productname: Proxmox "VE" \\ Subscription mit Tab`,
});
});
it("meldet die Cluster-Mitglieder", () => {
befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION);
befehlErsetzen(
"pvecm",
`Membership information
----------------------
Nodeid Votes Name
1 1 hv-rz5-pve-01 (local)
2 1 hv-rz5-pve-02
3 1 hv-rz5-pve-03`,
);
const report = reportErzeugen() as { host: { clusterMitglieder: string[] } };
expect(report.host.clusterMitglieder).toEqual(["hv-rz5-pve-01", "hv-rz5-pve-02", "hv-rz5-pve-03"]);
});
it("meldet eine leere Liste, wenn der Host in keinem Cluster ist", () => {
befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION);
const report = reportErzeugen() as { host: { clusterMitglieder: string[] } };
expect(report.host.clusterMitglieder).toEqual([]);
});
it("gibt die Version aus", () => {
const ausgabe = execFileSync("bash", [AGENT, "--version"], { encoding: "utf8" }).trim();
expect(ausgabe).toBe("0.2.0");
+1 -1
View File
@@ -30,7 +30,7 @@ describe.runIf(datenbankVerfuegbar)("Abgleich der Warnungen", () => {
it("legt die Standard-Einstellungen für alle Regeln an", async () => {
const einstellungen = await testPrisma.warnregelEinstellung.findMany();
expect(einstellungen).toHaveLength(10);
expect(einstellungen).toHaveLength(11);
const ablauf = einstellungen.find((e) => e.regel === "subscription-laeuft-ab");
expect(ablauf?.schwellwerte).toEqual({ tage: [30, 14, 7] });
});
+62 -3
View File
@@ -18,6 +18,8 @@ function host(überschreibungen: Partial<HostDaten> = {}): HostDaten {
return {
id: "h1",
hostname: "pve-01",
clusterName: null,
clusterMitglieder: [],
kundeId: "k1",
kundeName: "Brandl Elektrotechnik GmbH",
subKey: "pve1c-aaaa",
@@ -70,9 +72,9 @@ function ausRegel(k: RegelKontext, name: string) {
}
describe("Warnregeln", () => {
it("prüft alle zehn Regeln, wenn nichts abgeschaltet ist", () => {
expect(REGELN).toHaveLength(10);
expect(new Set(REGELN.map((r) => r.name)).size).toBe(10);
it("prüft alle elf Regeln, wenn nichts abgeschaltet ist", () => {
expect(REGELN).toHaveLength(11);
expect(new Set(REGELN.map((r) => r.name)).size).toBe(11);
});
describe("Subscription läuft ab", () => {
@@ -297,6 +299,63 @@ describe("Warnregeln", () => {
});
});
describe("Cluster-Knoten ohne Agent", () => {
it("meldet einen Knoten, der im Cluster steht, sich aber nicht meldet", () => {
const k = kontext({
hosts: [
host({
id: "h1",
hostname: "pve-node-01",
clusterName: "rz5",
clusterMitglieder: ["pve-node-01", "pve-node-02", "pve-node-03"],
}),
host({ id: "h2", hostname: "pve-node-02", clusterName: "rz5", clusterMitglieder: [] }),
],
});
const kandidaten = ausRegel(k, "cluster-knoten-ohne-agent");
expect(kandidaten).toHaveLength(1);
expect(kandidaten[0].beschreibung).toContain("pve-node-03");
expect(kandidaten[0].fingerprint).toBe("cluster-knoten-ohne-agent:rz5:pve-node-03");
});
it("meldet nichts, wenn alle Knoten melden", () => {
const mitglieder = ["pve-node-01", "pve-node-02"];
const k = kontext({
hosts: [
host({ id: "h1", hostname: "pve-node-01", clusterName: "rz5", clusterMitglieder: mitglieder }),
host({ id: "h2", hostname: "pve-node-02", clusterName: "rz5", clusterMitglieder: mitglieder }),
],
});
expect(ausRegel(k, "cluster-knoten-ohne-agent")).toHaveLength(0);
});
it("beachtet Groß- und Kleinschreibung nicht", () => {
const k = kontext({
hosts: [
host({ id: "h1", hostname: "PVE-Node-01", clusterName: "rz5", clusterMitglieder: ["pve-node-01"] }),
],
});
expect(ausRegel(k, "cluster-knoten-ohne-agent")).toHaveLength(0);
});
it("meldet nichts für Hosts ohne Cluster", () => {
const k = kontext({ hosts: [host({ clusterName: null, clusterMitglieder: [] })] });
expect(ausRegel(k, "cluster-knoten-ohne-agent")).toHaveLength(0);
});
it("meldet jeden fehlenden Knoten nur einmal, auch wenn mehrere ihn melden", () => {
const mitglieder = ["pve-node-01", "pve-node-02", "pve-node-03"];
const k = kontext({
hosts: [
host({ id: "h1", hostname: "pve-node-01", clusterName: "rz5", clusterMitglieder: mitglieder }),
host({ id: "h2", hostname: "pve-node-02", clusterName: "rz5", clusterMitglieder: mitglieder }),
],
});
expect(ausRegel(k, "cluster-knoten-ohne-agent")).toHaveLength(1);
});
});
it("überspringt abgeschaltete Regeln", () => {
const k = kontext({
hosts: [host({ kundeId: null, kundeName: null })],