Produkterkennung fuer PVE, PBS und PMG, JSON-Erzeugung ohne jq, Retry mit Backoff, Nachsenden aus dem Spool, Update und Uninstall. Das Dashboard liefert install.sh und agent.sh mit eingesetzter Adresse aus. 38 Tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
196 lines
7.2 KiB
TypeScript
196 lines
7.2 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { POST as enrollPost } from "@/app/api/agent/v1/enroll/route";
|
|
import { POST as reportPost } from "@/app/api/agent/v1/report/route";
|
|
import { agentRateLimit, enrollRateLimit } from "@/lib/ratelimit";
|
|
import { erzeugeToken } from "@/lib/tokens";
|
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
const AGENT = path.join(process.cwd(), "agent", "proxledger-agent.sh");
|
|
|
|
let stubVerzeichnis: string;
|
|
|
|
/** Legt ein ausführbares Skript an, das einen Proxmox-Befehl nachahmt. */
|
|
function befehlErsetzen(name: string, ausgabe: string) {
|
|
const datei = path.join(stubVerzeichnis, name);
|
|
writeFileSync(datei, `#!/usr/bin/env bash\ncat <<'ENDE'\n${ausgabe}\nENDE\n`);
|
|
chmodSync(datei, 0o755);
|
|
}
|
|
|
|
/** Führt den Agent mit den vorbereiteten Befehlen aus und liefert den Report. */
|
|
function reportErzeugen(): Record<string, never> | Record<string, unknown> {
|
|
const ausgabe = execFileSync("bash", [AGENT, "--dry-run"], {
|
|
env: {
|
|
...process.env,
|
|
PATH: `${stubVerzeichnis}:/usr/bin:/bin`,
|
|
PROXLEDGER_CONFIG: path.join(stubVerzeichnis, "gibt-es-nicht.conf"),
|
|
},
|
|
encoding: "utf8",
|
|
});
|
|
return JSON.parse(ausgabe) as Record<string, unknown>;
|
|
}
|
|
|
|
const PVE_SUBSCRIPTION = `key: pve1c-a6bd21fcb6
|
|
status: active
|
|
serverid: A1B2C3D4E5F60718
|
|
checktime: 1789610400
|
|
productname: Proxmox VE Community Subscription 1 CPU/year
|
|
regdate: 2025-09-24 00:00:00
|
|
nextduedate: 2026-09-24
|
|
sockets: 1
|
|
level: c`;
|
|
|
|
describe("Agent-Skript", () => {
|
|
beforeEach(() => {
|
|
stubVerzeichnis = mkdtempSync(path.join(tmpdir(), "proxledger-agent-test-"));
|
|
mkdirSync(stubVerzeichnis, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(stubVerzeichnis, { recursive: true, force: true });
|
|
});
|
|
|
|
it("erzeugt aus einer vollständigen PVE-Ausgabe einen gültigen Report", () => {
|
|
befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION);
|
|
befehlErsetzen("pveversion", "pve-manager/8.3.2/dc2b2f4e");
|
|
befehlErsetzen("lscpu", "Socket(s): 2\nModel name: Intel(R) Xeon(R) E-2388G");
|
|
befehlErsetzen("ip", "1: lo inet 127.0.0.1/8 scope host lo\n2: vmbr0 inet 192.168.10.11/24 scope global vmbr0");
|
|
befehlErsetzen("apt", "Auflistung...\nlibc6/stable 2.40 amd64 [upgradable from: 2.39]\nbash/stable 5.3 amd64 [upgradable from: 5.2]");
|
|
|
|
const report = reportErzeugen() as {
|
|
reportId: string;
|
|
agentVersion: string;
|
|
erhobenAm: string;
|
|
produkt: { typ: string; version: string };
|
|
subscription: Record<string, unknown>;
|
|
host: Record<string, unknown>;
|
|
};
|
|
|
|
expect(report.agentVersion).toBe("0.2.0");
|
|
expect(report.reportId).toMatch(/.{8,}/);
|
|
expect(report.erhobenAm).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
expect(report.produkt).toEqual({ typ: "pve", version: "pve-manager/8.3.2/dc2b2f4e" });
|
|
|
|
expect(report.subscription).toMatchObject({
|
|
key: "pve1c-a6bd21fcb6",
|
|
status: "active",
|
|
level: "c",
|
|
sockets: 1,
|
|
serverid: "A1B2C3D4E5F60718",
|
|
productname: "Proxmox VE Community Subscription 1 CPU/year",
|
|
nextduedate: "2026-09-24",
|
|
// Die Uhrzeit aus regdate wird abgeschnitten.
|
|
regdate: "2025-09-24",
|
|
checktime: "1789610400",
|
|
});
|
|
|
|
expect(report.host.sockets).toBe(2);
|
|
expect(report.host.cpuModell).toBe("Intel(R) Xeon(R) E-2388G");
|
|
expect(report.host.paketUpdates).toBe(2);
|
|
expect(report.host.kernel).toBeTypeOf("string");
|
|
});
|
|
|
|
it("liefert auch ohne Proxmox-Befehle gültiges JSON mit null-Werten", () => {
|
|
const report = reportErzeugen() as {
|
|
produkt: { typ: string | null };
|
|
subscription: Record<string, unknown>;
|
|
};
|
|
|
|
expect(report.produkt.typ).toBeNull();
|
|
expect(report.subscription.key).toBeNull();
|
|
expect(report.subscription.status).toBeNull();
|
|
expect(report.subscription.sockets).toBeNull();
|
|
});
|
|
|
|
it("maskiert Sonderzeichen aus Proxmox-Ausgaben korrekt", () => {
|
|
befehlErsetzen(
|
|
"pvesubscription",
|
|
`key: pve1c-test
|
|
status: active
|
|
productname: Proxmox "VE" \\ Subscription mit Tab`,
|
|
);
|
|
|
|
const report = reportErzeugen() as { subscription: { productname: string } };
|
|
expect(report.subscription.productname).toBe('Proxmox "VE" \\ Subscription\tmit Tab');
|
|
});
|
|
|
|
it("verarbeitet eine PBS-Ausgabe im JSON-Format", () => {
|
|
befehlErsetzen(
|
|
"proxmox-backup-manager",
|
|
`{"key":"pbsc-9d41be5507","status":"active","serverid":"C3D4E5F607182A3B","sockets":1,"nextduedate":"2027-03-02"}`,
|
|
);
|
|
|
|
const report = reportErzeugen() as {
|
|
produkt: { typ: string };
|
|
subscription: Record<string, unknown>;
|
|
};
|
|
|
|
expect(report.produkt.typ).toBe("pbs");
|
|
expect(report.subscription).toMatchObject({
|
|
key: "pbsc-9d41be5507",
|
|
status: "active",
|
|
serverid: "C3D4E5F607182A3B",
|
|
sockets: 1,
|
|
nextduedate: "2027-03-02",
|
|
});
|
|
});
|
|
|
|
it("gibt die Version aus", () => {
|
|
const ausgabe = execFileSync("bash", [AGENT, "--version"], { encoding: "utf8" }).trim();
|
|
expect(ausgabe).toBe("0.2.0");
|
|
});
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Zusammenspiel mit der Agent-API", () => {
|
|
afterAll(async () => {
|
|
await testPrisma.$disconnect();
|
|
});
|
|
|
|
it("wird vom Report-Endpunkt unverändert angenommen", async () => {
|
|
await datenbankLeeren();
|
|
agentRateLimit.zuruecksetzen();
|
|
enrollRateLimit.zuruecksetzen();
|
|
|
|
befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION);
|
|
befehlErsetzen("pveversion", "pve-manager/8.3.2/dc2b2f4e");
|
|
befehlErsetzen("lscpu", "Socket(s): 1\nModel name: Intel(R) Xeon(R) E-2388G");
|
|
|
|
const enrollToken = erzeugeToken("enrollment");
|
|
await testPrisma.enrollmentToken.create({
|
|
data: { tokenHash: enrollToken.hash, praefix: enrollToken.praefix, bezeichnung: "Agent-Test" },
|
|
});
|
|
|
|
const enrollAntwort = await enrollPost(
|
|
new Request("http://localhost:3000/api/agent/v1/enroll", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: `Bearer ${enrollToken.klartext}` },
|
|
body: JSON.stringify({
|
|
agentId: "machine-id-fuer-agent-test",
|
|
hostname: "pve-agent-test",
|
|
produkt: "pve",
|
|
}),
|
|
}),
|
|
);
|
|
const { token, hostId } = (await enrollAntwort.json()) as { token: string; hostId: string };
|
|
|
|
const report = reportErzeugen();
|
|
const antwort = await reportPost(
|
|
new Request("http://localhost:3000/api/agent/v1/report", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
body: JSON.stringify(report),
|
|
}),
|
|
);
|
|
|
|
expect(antwort.status).toBe(200);
|
|
const host = await testPrisma.host.findUniqueOrThrow({ where: { id: hostId } });
|
|
expect(host.subKey).toBe("pve1c-a6bd21fcb6");
|
|
expect(host.subStatus).toBe("ACTIVE");
|
|
expect(host.subNextduedate?.toISOString().slice(0, 10)).toBe("2026-09-24");
|
|
expect(host.subChecktime?.toISOString()).toBe("2026-09-17T02:00:00.000Z");
|
|
});
|
|
});
|
|
});
|