feat(templates): Template-Bau ohne SSH über eine Bau-Adresse

Ohne SSH zum Node kommt der Guest-Agent jetzt über eine feste
Bau-Adresse ins Template: cloud-init setzt Adresse und den
Bereitstellungsschlüssel für den Standardbenutzer, ProxDeploy meldet
sich per SSH in der VM an, installiert den Agent und entfernt danach
Schlüssel, Adresse und cloud-init-Angaben.

Dabei korrigiert: der Verbindungstest meldete „Template-Bau per API“
auch ohne Weg, den Agent einzubauen. Im Protokoll erscheinen
unveränderte JSON-Felder nicht mehr als geändert.

Refs #021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 14:36:38 +02:00
co-authored by Claude Opus 5.5
parent 426d3ec95a
commit 21edff2141
14 changed files with 366 additions and 28 deletions
+71
View File
@@ -360,3 +360,74 @@ describe("Profile einbacken (#022)", () => {
]);
});
});
describe("Guest-Agent über die Bau-Adresse (#021)", () => {
it("ohne Node-SSH: feste Adresse und Bereitstellungsschlüssel per cloud-init, SSH in die VM, danach zurückgesetzt", async () => {
const { mock, client } = await aufbauen({ version: "9.2.1" });
mock.route("POST", /\/status\/shutdown$/, () => ({ data: UPID("qmshutdown") }));
const gast: Array<{ host: string; benutzer: string; skript: string }> = [];
const ergebnis = await phasenAusfuehren(
templateBauSchritte({
client,
distribution: await debian13(),
knoten: "pve01",
storage: "local-zfs",
bridge: "vmbr0",
ssh: null,
auftragKurzId: "t",
holen: async () => `${HASH} debian-13-genericcloud-amd64.qcow2`,
vmidReservieren: async () => 9000,
warten: async () => {},
bauNetz: { ip: "10.0.10.250/24", gateway: "10.0.10.1", dns: ["10.0.10.1"] },
bereitstellung: async () => ({
privat: "p",
oeffentlich: "ssh-ed25519 AAAAbau proxdeploy-bereitstellung",
}),
gastSsh: async (ziel, skript) => {
gast.push({ host: ziel.host, benutzer: ziel.benutzer, skript });
return 0;
},
}),
stumm,
);
expect(ergebnis).toEqual({ ok: true });
expect(gast).toEqual([
{
host: "10.0.10.250",
benutzer: "debian",
skript: expect.stringContaining("apt-get -y -q -o DPkg::Lock::Timeout=600 install qemu-guest-agent"),
},
]);
expect(gast[0]!.skript).toContain("rm -f /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys");
const configs = mock.anfragen.filter((a) => a.methode === "PUT").map((a) => Object.fromEntries(a.rumpf));
expect(configs[0]).toMatchObject({
ciuser: "debian",
ipconfig0: "ip=10.0.10.250/24,gw=10.0.10.1",
nameserver: "10.0.10.1",
});
expect(configs[0]!["sshkeys"]).toBe("ssh-ed25519%20AAAAbau%20proxdeploy-bereitstellung");
expect(configs.at(-1)).toEqual({ ipconfig0: "ip=dhcp", delete: "sshkeys,ciuser,nameserver" });
// Kein Snippet ohne Node-SSH.
expect(mock.anfragen.some((a) => a.rumpf.get("cicustom"))).toBe(false);
});
it("ohne Node-SSH und ohne Bau-Adresse sagt schon die Prüfung, was fehlt", async () => {
const { client } = await aufbauen({ version: "9.2.1" });
const ergebnis = await phasenAusfuehren(
templateBauSchritte({
client,
distribution: await debian13(),
knoten: "pve01",
storage: "local-zfs",
bridge: "vmbr0",
ssh: null,
auftragKurzId: "t",
holen: async () => "",
vmidReservieren: async () => 9000,
}),
stumm,
);
expect(ergebnis).toMatchObject({ ok: false, phase: "pruefungen" });
if (!ergebnis.ok) expect(ergebnis.fehler.hinweis).toMatch(/Bau-Adresse/);
});
});