Files
proxdeploy/src/engine/profile/rendern.ts
T
florian.wildeandClaude Opus 5.5 d25ec54ef9 feat(lxc): LXC-Container ausrollen, mit oder ohne Node-SSH
Container aus dem Proxmox-Template (neueste passende Version aus
aplinfo), unprivilegiert, nesting automatisch für docker. Weil Proxmox
keine Exec-API für Container hat, läuft die Provisionierung per SSH:
bevorzugt pct exec über den Node, sonst direkt in den Container mit
einem Bereitstellungsschlüssel, der am Ende wieder entfernt wird.

Auf pve-demo beide Wege geprüft: Debian 13 mit docker per pct exec
(hello-world läuft mit nesting allein) und Ubuntu 24.04 per SSH in den
Container. Dabei gefunden: chrony scheitert ohne CAP_SYS_TIME (jetzt
if: vm), sshd -t auf Ubuntu 24.04 ohne /run/sshd.

Closes #009

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:19:13 +02:00

119 lines
4.6 KiB
TypeScript

import type { FamilienAdapter } from "@/engine/adapter/schnittstelle";
import type { ProfilSchritt } from "@/engine/katalog/schema";
import type { AufgeloestesProfil } from "./aufloesen";
import { einsetzen, tiefEinsetzen, type VorlagenKontext } from "./vorlage";
/**
* Baut aus aufgelösten Profilen ein Provisionierungsskript.
*
* Zwischen den Profilen steht eine Markenzeile (`::proxdeploy:: profil <id>`), an der die Engine
* im Live-Log erkennt, welches Profil gerade läuft. SSH-Härtung wird ans Ende verschoben: Läuft die
* Provisionierung selbst über SSH als root (LXC ohne Node-SSH), schlösse sie sich sonst aus.
*/
export const MARKE = "::proxdeploy::";
export type RenderKontext = {
os: { id: string; version_id: string; codename?: string };
gast: { name: string; benutzer: string; typ?: "vm" | "lxc" };
};
/** Feste Bedingungen neben den Profil-Variablen: `if: vm` / `if: lxc` (bzw. `!vm`, `!lxc`). */
export const FESTE_BEDINGUNGEN = ["vm", "lxc"] as const;
export type Abschnitt = { profil: string; skript: string };
export type Gerendert = { skript: string; abschnitte: Abschnitt[] };
function bedingungErfuellt(
schritt: ProfilSchritt,
werte: Record<string, unknown>,
typ: "vm" | "lxc" | undefined,
): boolean {
if (!schritt.if) return true;
const verneint = schritt.if.startsWith("!");
const name = schritt.if.replace(/^!/, "");
// Ohne bekannten Typ (Rendern zum Ansehen) laufen typabhängige Schritte mit.
const wert = name === "vm" || name === "lxc" ? (typ ? typ === name : !verneint) : Boolean(werte[name]);
return verneint ? !wert : wert;
}
function schrittRendern(schritt: ProfilSchritt, adapter: FamilienAdapter, kontext: VorlagenKontext): string {
if ("update" in schritt) return adapter.aktualisieren();
if ("packages" in schritt)
return adapter.paketeInstallieren(schritt.packages.map((p) => einsetzen(p, kontext)));
if ("service" in schritt) {
return adapter.dienstAktivieren(einsetzen(schritt.service.name, kontext), {
starten: schritt.service.start,
neuStarten: schritt.service.restart,
});
}
if ("repo" in schritt) {
const r = tiefEinsetzen(schritt.repo, kontext);
return adapter.repoHinzufuegen({
name: r.name,
schluesselUrl: r.key_url,
uris: r.uris,
suites: r.suites,
komponenten: r.components,
});
}
if ("repo_package" in schritt)
return adapter.repoPaketInstallieren(einsetzen(schritt.repo_package, kontext));
if ("file" in schritt) {
const f = tiefEinsetzen(schritt.file, kontext);
return adapter.dateiSchreiben(f.path, f.content, f.mode);
}
if ("timezone" in schritt) return adapter.zeitzoneSetzen(einsetzen(schritt.timezone, kontext));
if ("locale" in schritt) return adapter.localeSetzen(einsetzen(schritt.locale, kontext));
if ("keyboard" in schritt) return adapter.tastaturSetzen(einsetzen(schritt.keyboard, kontext));
if ("guest_agent" in schritt) return adapter.gastAgentInstallieren();
if ("unattended_upgrades" in schritt) return adapter.automatischeUpdates(schritt.unattended_upgrades);
if ("run" in schritt) return einsetzen(schritt.run, kontext, "shell") + "\n";
if ("ssh_harden" in schritt) {
return adapter.sshHaerten({
passwortAnmeldung: schritt.ssh_harden.password_auth,
rootAnmeldung: schritt.ssh_harden.root_login,
});
}
const unbekannt: never = schritt;
throw new Error(`Unbekannter Schritt: ${JSON.stringify(unbekannt)}`);
}
export function profileRendern(
profile: AufgeloestesProfil[],
adapter: FamilienAdapter,
kontext: RenderKontext,
vorspann: string[] = [],
): Gerendert {
const abschnitte: Abschnitt[] = [];
const zumSchluss: string[] = [];
for (const { profil, werte } of profile) {
const vorlagenKontext: VorlagenKontext = {
var: werte,
os: { id: kontext.os.id, version_id: kontext.os.version_id, codename: kontext.os.codename ?? "" },
guest: { name: kontext.gast.name, user: kontext.gast.benutzer },
};
const teile: string[] = [];
for (const schritt of profil.families[adapter.familie] ?? []) {
if (!bedingungErfuellt(schritt, werte, kontext.gast.typ)) continue;
const text = schrittRendern(schritt, adapter, vorlagenKontext);
if ("ssh_harden" in schritt) zumSchluss.push(text);
else teile.push(text);
}
abschnitte.push({ profil: profil.id, skript: teile.join("") });
}
const skript = [
"#!/bin/bash",
"# Von ProxDeploy erzeugt",
adapter.kopf(),
...vorspann,
...abschnitte.map((a) => `echo '${MARKE} profil ${a.profil}'\n${a.skript}`),
...(zumSchluss.length > 0 ? [`echo '${MARKE} abschluss'\n${zumSchluss.join("")}`] : []),
`echo '${MARKE} fertig'\n`,
].join("\n");
return { skript, abschnitte };
}