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:
@@ -32,7 +32,7 @@ async function mitSzenario(szenario: Szenario) {
|
||||
describe("Verbindungstest (Erkundung)", () => {
|
||||
it("PVE 9.2 Einzelhost: Nodes, Storages, Bridges, keine fehlenden Rechte, Template-Bau per API", async () => {
|
||||
const { client } = await mitSzenario({ version: "9.2.1", importStorage: true });
|
||||
const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null });
|
||||
const bericht = await erkunden(client, { serverHost: "127.0.0.1", ssh: null, bauNetz: true });
|
||||
expect(bericht.version).toBe("9.2.1");
|
||||
expect(bericht.cluster).toBeNull();
|
||||
expect(bericht.knoten).toHaveLength(1);
|
||||
|
||||
@@ -101,17 +101,27 @@ 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({
|
||||
it("PVE 9.2 mit import-Storage ohne SSH: Image ginge per API, der Guest-Agent aber nicht – außer mit Bau-Adresse", () => {
|
||||
const ohne = 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");
|
||||
expect(ohne.templateBau.weg).toBe("nicht_moeglich");
|
||||
expect(ohne.templateBau.grund).toMatch(/Bau-Adresse/);
|
||||
const mit = faehigkeitenAbleiten({
|
||||
version: v("9.2.1"),
|
||||
storages: mitImport,
|
||||
ssh: "aus",
|
||||
rechteFehlen: false,
|
||||
bauNetz: true,
|
||||
});
|
||||
expect(mit.templateBau).toMatchObject({ weg: "api" });
|
||||
expect(mit.templateBau.grund).toMatch(/Bau-Adresse/);
|
||||
expect(mit.vmProvisionierung.weg).toBe("api");
|
||||
expect(mit.lxcProvisionierung).toMatchObject({ weg: "ssh", ueber: "gast" });
|
||||
expect(mit.snippets.weg).toBe("nicht_moeglich");
|
||||
});
|
||||
|
||||
it("PVE 8.3: Template-Bau nur per SSH; ohne SSH mit Begründung unmöglich", () => {
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user