Thomas Stallinger d6a01462ef feat: backend_launch() für Mint real implementieren (Phase 1)
Startet die unbeaufsichtigte Ubiquity-Installation direkt aus dem
laufenden Live-System heraus per kexec - das personalisierte, erst
zur Laufzeit erzeugte Preseed (Hostname/Geräte-ID/Partitionierung
sind erst hier bekannt) wird dafür in eine zusätzliche Initrd-Schicht
eingebettet (aneinandergehängte cpio-Archive, vom Kernel nativ
unterstützt) statt per file=/cdrom/... (read-only Medium) oder
url=<lokaler Server> (Prozess-/Netzwerkzustand geht beim Kexec-Sprung
verloren).

backend_init() installiert kexec-tools jetzt bei Bedarf nach (auf dem
Live-Medium anders als im Zielsystem nicht vorinstalliert).

Real per QEMU verifiziert: kexec aus laufender Live-Sitzung heraus
funktioniert, automatic-ubiquity mit sowohl url= als auch
initrd-eingebettetem file= erreicht, komplette unbeaufsichtigte
Installation (Partitionierung, Paketinstallation, success_command)
bis zum funktionierenden Login-Bildschirm durchlaufen lassen. Danach
zusätzlich der tatsächliche, committete backend.sh-Code (nicht nur
die Spike-Annäherung) end-to-end über alle fünf Lifecycle-Funktionen
gegen ein reales runtime_blueprint.json/response.json bestätigt.

Bekannte Restlücke: jq/envsubst/base64 sind auf dem Live-Medium
ebenfalls nicht vorinstalliert (backend_init prüft nur, installiert
nicht nach) - vorbestehende Lücke, nicht Teil dieser Änderung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:35:11 +02:00

222 lines
9.2 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
# Dieses Skript wird von einem Orchestrator-Modul (z.B. 40_backend.sh) per
# `source` in dessen Shell geladen. Variablen bleiben deshalb bewusst nicht
# readonly, um Namenskollisionen mit dem ladenden Modul zu vermeiden.
BACKEND_KEY="mint"
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRESEED_TEMPLATE="${BACKEND_DIR}/preseed.tpl"
POSTINSTALL_SCRIPT="${BACKEND_DIR}/postinstall.sh"
RUNTIME_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json"
SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
RUNTIME_DIR="/run/tuxflotte/backend"
CONFIG_FILE="${RUNTIME_DIR}/config"
backend_log() {
printf '[backend:%s] %s\n' "${BACKEND_KEY}" "$*" >&2
}
backend_fatal() {
printf '[backend:%s] FEHLER: %s\n' "${BACKEND_KEY}" "$*" >&2
return 1
}
backend_init() {
for cmd in jq envsubst base64; do
command -v "${cmd}" >/dev/null 2>&1 ||
{ backend_fatal "Benötigtes Werkzeug fehlt: ${cmd}"; return 1; }
done
# kexec-tools ist auf dem Live-Medium selbst (anders als im Zielsystem,
# siehe pkgsel/include in preseed.tpl) nicht vorinstalliert - real gegen
# das Live-Abbild verifiziert (Phase-1-Spike). Bis das Paket fest in die
# ISO gebacken wird (Folgeschritt), hier als Live-Nachinstallation.
if ! command -v kexec >/dev/null 2>&1; then
backend_log "kexec-tools fehlt auf dem Live-Medium, installiere nach."
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true
apt-get update -qq ||
{ backend_fatal "apt-get update fehlgeschlagen (kexec-tools)."; return 1; }
DEBIAN_FRONTEND=noninteractive apt-get install -y kexec-tools ||
{ backend_fatal "Installation von kexec-tools fehlgeschlagen."; return 1; }
fi
[[ -r "${PRESEED_TEMPLATE}" ]] ||
{ backend_fatal "Preseed-Template nicht gefunden: ${PRESEED_TEMPLATE}"; return 1; }
[[ -r "${POSTINSTALL_SCRIPT}" ]] ||
{ backend_fatal "Postinstall-Skript nicht gefunden: ${POSTINSTALL_SCRIPT}"; return 1; }
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${CONFIG_FILE}"
backend_log "Initialisiert."
}
backend_validate() {
[[ -r "${RUNTIME_BLUEPRINT_FILE}" ]] ||
{ backend_fatal "Runtime Blueprint nicht gefunden: ${RUNTIME_BLUEPRINT_FILE}"; return 1; }
jq --exit-status \
--arg backend_key "${BACKEND_KEY}" \
'.runtime_blueprint.backend_id == $backend_key' \
"${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
{ backend_fatal "Runtime Blueprint ist nicht für Backend '${BACKEND_KEY}' aufgelöst."; return 1; }
jq --exit-status '
.runtime_blueprint.installation_directives
| (.disk_encryption | type == "boolean")
and (.partitioning | type == "string")
and (.secure_boot_required | type == "boolean")
' "${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
{ backend_fatal "Installationszeitliche Vorgaben fehlen oder sind ungültig."; return 1; }
# disk_encryption wird für Mint (noch) nicht unterstützt - kein getesteter
# LUKS-Preseed-Mechanismus (anders als Fedoras "autopart --encrypted").
if [[ "$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" == "true" ]]; then
backend_fatal "disk_encryption=true wird vom Mint-Backend derzeit nicht unterstützt."
return 1
fi
backend_log "Runtime Blueprint ist gültig für Backend '${BACKEND_KEY}'."
}
backend_generate_config() {
local hostname
local device_id
local partitioning
local secure_boot_required
local partman_recipe
local blueprints_json
local postinstall_rendered
local postinstall_b64
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
{ backend_fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"; return 1; }
hostname="$(jq --raw-output '.device.hostname // empty' "${SERVER_RESPONSE_FILE}")"
[[ -n "${hostname}" ]] ||
{ backend_fatal "Kein Hostname in der Serverantwort gefunden."; return 1; }
device_id="$(jq --raw-output '.device.id // empty' "${SERVER_RESPONSE_FILE}")"
[[ -n "${device_id}" ]] ||
{ backend_fatal "Keine Geräte-ID in der Serverantwort gefunden."; return 1; }
partitioning="$(jq --raw-output '.runtime_blueprint.installation_directives.partitioning' "${RUNTIME_BLUEPRINT_FILE}")"
secure_boot_required="$(jq --raw-output '.runtime_blueprint.installation_directives.secure_boot_required' "${RUNTIME_BLUEPRINT_FILE}")"
case "${partitioning}" in
default)
partman_recipe="atomic"
;;
*)
backend_fatal "Nicht unterstützte Partitionierungsvorgabe: ${partitioning}"
return 1
;;
esac
if [[ "${secure_boot_required}" == "true" ]]; then
backend_log "Hinweis: secure_boot_required=true wird derzeit nicht in der Preseed-Konfiguration durchgesetzt (Phase 1)."
fi
blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")"
# Erste Stufe: postinstall.sh-Platzhalter auflösen.
postinstall_rendered="$(
TUXFLOTTE_DEVICE_ID="${device_id}" \
TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \
envsubst '${TUXFLOTTE_DEVICE_ID} ${TUXFLOTTE_BLUEPRINTS_JSON}' \
<"${POSTINSTALL_SCRIPT}"
)"
if grep -q '\${TUXFLOTTE_' <<<"${postinstall_rendered}"; then
backend_fatal "postinstall.sh enthält nach envsubst nicht aufgelöste Platzhalter."
return 1
fi
# base64-Kodierung: mehrzeilige/zitierte Preseed-Werte brechen unter
# Debconf lautlos (real erprobt, siehe backends/mint/wlan-test.seed) -
# als einzeiliger Base64-Blob besteht der success_command-Wert nur noch
# aus unkritischen Zeichen.
postinstall_b64="$(printf '%s' "${postinstall_rendered}" | base64 -w0)"
# Zweite Stufe: preseed.tpl mit allen Werten inkl. des fertigen Base64-Blobs auflösen.
TUXFLOTTE_HOSTNAME="${hostname}" \
TUXFLOTTE_PARTMAN_RECIPE="${partman_recipe}" \
TUXFLOTTE_POSTINSTALL_B64="${postinstall_b64}" \
envsubst '${TUXFLOTTE_HOSTNAME} ${TUXFLOTTE_PARTMAN_RECIPE} ${TUXFLOTTE_POSTINSTALL_B64}' \
<"${PRESEED_TEMPLATE}" >"${CONFIG_FILE}"
chmod 0600 "${CONFIG_FILE}"
[[ -s "${CONFIG_FILE}" ]] ||
{ backend_fatal "Erzeugte Konfigurationsdatei ist leer: ${CONFIG_FILE}"; return 1; }
if grep -q '\${TUXFLOTTE_' "${CONFIG_FILE}"; then
backend_fatal "Erzeugte Konfigurationsdatei enthält nicht aufgelöste Platzhalter."
return 1
fi
backend_log "Konfiguration erzeugt: ${CONFIG_FILE}"
}
backend_launch() {
local cdrom_vmlinuz="/cdrom/casper/vmlinuz"
local cdrom_initrd="/cdrom/casper/initrd.lz"
local extra_initrd_dir="${RUNTIME_DIR}/initrd-extra"
local extra_cpio="${RUNTIME_DIR}/extra.cpio.gz"
local custom_initrd="${RUNTIME_DIR}/initrd-custom.lz"
[[ -r "${cdrom_vmlinuz}" && -r "${cdrom_initrd}" ]] ||
{ backend_fatal "Casper-Kernel/-Initrd nicht gefunden unter /cdrom/casper."; return 1; }
# Das personalisierte Preseed (CONFIG_FILE, erst live auf diesem Gerät
# erzeugt - Hostname/Geräte-ID/Partitionierung sind erst hier bekannt,
# nicht schon beim ISO-Bau) kann nicht per file=/cdrom/... übergeben
# werden (read-only Medium, Inhalt seit ISO-Bau fixiert) und auch nicht
# per url= von einem selbst gestarteten lokalen Server - der komplette
# Prozess- und Netzwerkzustand dieser Sitzung geht beim Kexec-Sprung
# verloren, ein soeben gestarteter HTTP-Server koennte die neue
# Boot-Umgebung also nicht mehr bedienen. Stattdessen wird das Preseed in
# eine zusaetzliche Initrd-Schicht eingebettet: der Kernel unterstuetzt
# aneinandergehaengte cpio-Archive als initramfs (spaetere Archive
# ergaenzen fruehere), das uebersteht den Kexec-Uebergang unveraendert.
# Real gegen QEMU verifiziert (Phase-1-Spike, beide Ansaetze getestet).
rm -rf "${extra_initrd_dir}"
install -d --mode=0700 --owner=root --group=root "${extra_initrd_dir}"
cp "${CONFIG_FILE}" "${extra_initrd_dir}/preseed.cfg"
(cd "${extra_initrd_dir}" && find . | cpio -o -H newc 2>/dev/null | gzip) \
>"${extra_cpio}" ||
{ backend_fatal "Preseed-Initrd-Schicht konnte nicht gebaut werden."; return 1; }
cat "${cdrom_initrd}" "${extra_cpio}" >"${custom_initrd}" ||
{ backend_fatal "Initrd konnte nicht zusammengesetzt werden."; return 1; }
backend_log "Lade Kexec-Ziel fuer automatisierten Ubiquity-Start."
kexec -l "${cdrom_vmlinuz}" \
--initrd="${custom_initrd}" \
--append="boot=casper automatic-ubiquity noprompt file=/preseed.cfg debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash ---" ||
{ backend_fatal "kexec -l fehlgeschlagen."; return 1; }
backend_log "Starte unbeaufsichtigte Installation (kexec -e). Kein Ruecksprung erwartet - ab hier laeuft die eigentliche Installation im neuen Kernel weiter."
kexec -e
}
backend_postinstall() {
backend_log "Provisioning-Agent-Einrichtung erfolgt im ubiquity/success_command der Preseed-Konfiguration (Agent-Abruf, Bootstrap-Registrierung, systemd-Aktivierung)."
}