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 `), 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, 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 }; }