feat(hosts): Doppelte Hosts erkennen und stilllegen (#041)
Zwei Eintraege desselben Namens beim selben Kunden entstehen, wenn sich die Kennung aus /etc/machine-id aendert - Neuinstallation, geklonter Container, zurueckgespielte Sicherung. Dieselbe Maschine meldet sich dann als neuer Host, der alte Eintrag bleibt stehen. Zwei getrennte Maschinen mit gleichem Namen sind aber genauso moeglich. Neue dreizehnte Warnregel meldet den Fall und nennt die IP-Adressen zum Unterscheiden; gleiche Namen bei verschiedenen Kunden bleiben still. Dazu die bis dahin fehlende Moeglichkeit, einen Host loszuwerden: Stilllegen blendet ihn aus Listen, Zahlen, Suche und Warnungen aus und schliesst seine offenen Warnungen, laesst Laufzeiten und Rechnungen aber unberuehrt. Endgueltig loeschen geht nur ohne Laufzeiten - Buchhaltung wirft man nicht weg. Kein automatisches Zusammenfuehren: Es muesste raten, welcher Eintrag welche Rechnung erbt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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(12);
|
||||
expect(einstellungen).toHaveLength(13);
|
||||
const ablauf = einstellungen.find((e) => e.regel === "subscription-laeuft-ab");
|
||||
expect(ablauf?.schwellwerte).toEqual({ tage: [30, 14, 7] });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/** Hosts stilllegen statt löschen – und löschen nur, wenn nichts daran hängt (#041). */
|
||||
vi.mock("@/server/auth/sitzungspflicht", () => ({
|
||||
sitzungErforderlich: async () => ({ benutzerId: "test", rolle: "ADMIN", kundeId: null }),
|
||||
sichtbarerKunde: async () => null,
|
||||
darfEinkaufSehen: async () => true,
|
||||
angemeldet: async () => null,
|
||||
}));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: () => {} }));
|
||||
vi.mock("next/navigation", () => ({ redirect: () => {} }));
|
||||
|
||||
const { hostArchivieren, hostLoeschen } = await import("@/server/actions/hosts");
|
||||
const { hostListe } = await import("@/server/queries/hostliste");
|
||||
|
||||
function formular(werte: Record<string, string>) {
|
||||
const daten = new FormData();
|
||||
for (const [name, wert] of Object.entries(werte)) daten.set(name, wert);
|
||||
return daten;
|
||||
}
|
||||
|
||||
async function host(hostname = "hv-pve-01") {
|
||||
return testPrisma.host.create({ data: { agentId: `a-${hostname}`, hostname, produkt: "PVE" } });
|
||||
}
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Hosts stilllegen", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
it("nimmt einen stillgelegten Host aus der Liste", async () => {
|
||||
const h = await host();
|
||||
expect((await hostListe({})).gesamt).toBe(1);
|
||||
|
||||
await hostArchivieren(formular({ hostId: h.id }));
|
||||
|
||||
expect((await hostListe({})).gesamt).toBe(0);
|
||||
expect((await hostListe({ archiviert: "ja" })).gesamt).toBe(1);
|
||||
});
|
||||
|
||||
it("schließt beim Stilllegen die offenen Warnungen", async () => {
|
||||
const h = await host();
|
||||
await testPrisma.alert.create({
|
||||
data: {
|
||||
regel: "status-nicht-aktiv",
|
||||
fingerprint: `status-nicht-aktiv:${h.id}`,
|
||||
schwere: "WARNUNG",
|
||||
titel: "Keine Subscription gefunden",
|
||||
beschreibung: "…",
|
||||
hostId: h.id,
|
||||
},
|
||||
});
|
||||
|
||||
await hostArchivieren(formular({ hostId: h.id }));
|
||||
|
||||
const alert = await testPrisma.alert.findFirstOrThrow({ where: { hostId: h.id } });
|
||||
expect(alert.erledigtAm).not.toBeNull();
|
||||
});
|
||||
|
||||
it("nimmt einen Host auf Wunsch wieder auf", async () => {
|
||||
const h = await host();
|
||||
await hostArchivieren(formular({ hostId: h.id }));
|
||||
await hostArchivieren(formular({ hostId: h.id }));
|
||||
|
||||
expect((await hostListe({})).gesamt).toBe(1);
|
||||
});
|
||||
|
||||
it("löscht einen Host ohne Laufzeiten", async () => {
|
||||
const h = await host();
|
||||
await hostLoeschen(formular({ hostId: h.id }));
|
||||
|
||||
expect(await testPrisma.host.count()).toBe(0);
|
||||
});
|
||||
|
||||
it("löscht keinen Host, an dem eine Laufzeit hängt", async () => {
|
||||
const h = await host();
|
||||
await testPrisma.laufzeit.create({
|
||||
data: {
|
||||
hostId: h.id,
|
||||
key: "pve1c-aaaa",
|
||||
start: new Date("2026-01-01"),
|
||||
ende: new Date("2026-12-31"),
|
||||
status: "BESTAETIGT",
|
||||
},
|
||||
});
|
||||
|
||||
await hostLoeschen(formular({ hostId: h.id }));
|
||||
|
||||
// Buchhaltung bleibt: Der Host ist noch da und lässt sich nur stilllegen.
|
||||
expect(await testPrisma.host.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ function host(überschreibungen: Partial<HostDaten> = {}): HostDaten {
|
||||
subLizenzierteSockets: 1,
|
||||
sockets: 1,
|
||||
letzterReportAm: new Date(JETZT.getTime() - 3_600_000),
|
||||
ips: ["10.0.0.10"],
|
||||
serverIds: [{ serverId: "AAAA", ersteMeldung: inTagen(-300), letzteMeldung: JETZT }],
|
||||
...überschreibungen,
|
||||
};
|
||||
@@ -72,9 +73,9 @@ function ausRegel(k: RegelKontext, name: string) {
|
||||
}
|
||||
|
||||
describe("Warnregeln", () => {
|
||||
it("prüft alle zwölf Regeln, wenn nichts abgeschaltet ist", () => {
|
||||
expect(REGELN).toHaveLength(12);
|
||||
expect(new Set(REGELN.map((r) => r.name)).size).toBe(12);
|
||||
it("prüft alle dreizehn Regeln, wenn nichts abgeschaltet ist", () => {
|
||||
expect(REGELN).toHaveLength(13);
|
||||
expect(new Set(REGELN.map((r) => r.name)).size).toBe(13);
|
||||
});
|
||||
|
||||
describe("Subscription läuft ab", () => {
|
||||
@@ -414,4 +415,57 @@ describe("Warnregeln", () => {
|
||||
expect(alleRegelnPruefen(k).filter((a) => a.regel === "kunde-bezahlt-einkauf-offen")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Gleicher Hostname beim selben Kunden", () => {
|
||||
it("meldet zwei gleichnamige Hosts desselben Kunden – mit IPs zum Unterscheiden", () => {
|
||||
const k = kontext({
|
||||
hosts: [
|
||||
host({ id: "h1", hostname: "hv-pve-01", ips: ["10.10.5.32"] }),
|
||||
host({ id: "h2", hostname: "hv-pve-01", ips: ["10.10.12.33", "10.10.5.11"] }),
|
||||
],
|
||||
});
|
||||
|
||||
const treffer = alleRegelnPruefen(k).filter((a) => a.regel === "hostname-doppelt");
|
||||
expect(treffer).toHaveLength(1);
|
||||
expect(treffer[0].titel).toBe("2 Hosts heißen „hv-pve-01“");
|
||||
expect(treffer[0].beschreibung).toContain("10.10.5.32");
|
||||
expect(treffer[0].beschreibung).toContain("10.10.12.33");
|
||||
});
|
||||
|
||||
it("schweigt bei gleichem Namen in verschiedenen Kundennetzen", () => {
|
||||
const k = kontext({
|
||||
hosts: [
|
||||
host({ id: "h1", hostname: "hv-pve-01", kundeId: "k1" }),
|
||||
host({ id: "h2", hostname: "hv-pve-01", kundeId: "k2" }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(alleRegelnPruefen(k).filter((a) => a.regel === "hostname-doppelt")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("schweigt bei Hosts ohne Kunde", () => {
|
||||
const k = kontext({
|
||||
hosts: [
|
||||
host({ id: "h1", hostname: "hv-pve-01", kundeId: null, kundeName: null }),
|
||||
host({ id: "h2", hostname: "hv-pve-01", kundeId: null, kundeName: null }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(alleRegelnPruefen(k).filter((a) => a.regel === "hostname-doppelt")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("erzeugt für drei gleichnamige Hosts nur eine Warnung", () => {
|
||||
const k = kontext({
|
||||
hosts: [
|
||||
host({ id: "h1", hostname: "hv-pve-01" }),
|
||||
host({ id: "h2", hostname: "hv-pve-01" }),
|
||||
host({ id: "h3", hostname: "HV-PVE-01" }),
|
||||
],
|
||||
});
|
||||
|
||||
const treffer = alleRegelnPruefen(k).filter((a) => a.regel === "hostname-doppelt");
|
||||
expect(treffer).toHaveLength(1);
|
||||
expect(treffer[0].titel).toContain("3 Hosts");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user