tuxflotte-installer/scripts/build_golden_image.sh
Thomas Stallinger 5845e0108a feat: Golden-Image-Deployment als Ersatz fuer Ubiquity-Automatisierung (Phase 0+1)
Neue Richtung nach ADR-0024-Diskussion: statt Distributions-Installer
(Ubiquity/Anaconda) zu automatisieren - strukturell fragil, siehe
ADR-0023-Nachtrag zum partman-rebuild-cache-Loop - wird das
curtin/FAI-Muster genutzt: Zieldatentraeger direkt partitionieren, ein
fertiges Root-Filesystem-Image entpacken, per chroot nacharbeiten.

- build_golden_image.sh: baut eine schlanke Debian-bookworm-Basis per
  debootstrap (Kernel, breites linux-firmware, NetworkManager,
  openssh-server) - KEINE Desktop-Umgebung, die kommt wie jedes andere
  Merkmal per Ansible-Blueprint nach dem ersten Boot (ADR-0002). Keine
  SSH-Hostkeys/kein Root-Passwort im Ergebnis - reale Zugangsdaten
  entstehen erst durch den Provisioning Agent pro Geraet.
- scripts/lib/image_deploy.sh: Kernmechanik als wiederverwendbare
  Funktionsbibliothek (partitionieren, formatieren, mounten, Image
  entpacken, fstab aus echten UUIDs, chroot-Fixup fuer machine-id/SSH-
  Hostkeys/initramfs, Bootloader-Installation UEFI+BIOS).
- scripts/image_deploy_test.sh: isolierter Testtreiber fuer Phase 1,
  ruft dieselben Funktionen auf, die spaeter backends/mint-image/
  backend.sh (Phase 2) nutzen wird.

Golden Image real gebaut und verifiziert (enterprise/LMDE 6, debootstrap
bookworm): 302 MB, Kernel vorhanden, keine SSH-Hostkeys, machine-id leer.
Deployment-Mechanik noch nicht live getestet - naechster Schritt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 22:26:37 +02:00

154 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
# Baut das schlanke "Golden Image" fuer das Curtin/FAI-artige Deployment-
# Backend (siehe ADR-0024 und den Plan unter
# platform-docs/adr/0024-*.md) - eine reine, unbeaufsichtigte
# debootstrap-Basis OHNE Desktop-Umgebung. Cinnamon/Mint-Tools/Theming
# kommen NICHT hierher, sondern werden nach dem ersten Boot vom
# Provisioning Agent per Ansible-Blueprint ausgerollt (ADR-0002) - dieses
# Image muss nur ueberall booten und Netzwerk haben.
#
# Bewusst reines Debian (nicht Mints Ubuntu-Basis) als Zielsystem: die
# kundensichtbare Identitaet kommt komplett aus der Ansible-Schicht, die
# exakte Basis-Distribution des Images ist zweitrangig - siehe
# Plan-Kontext. Kein Root-Passwort, keine SSH-Hostkeys im Ergebnis (siehe
# Kommentare unten) - echte Zugangsdaten entstehen erst durch den
# Provisioning Agent nach der Installation, ein Golden Image mit
# eingebrannten Zugangsdaten waere ein Sicherheitsrisiko fuer JEDES damit
# ausgerollte Kundengeraet, nicht nur fuer einen einzelnen Testlauf.
#
# Laeuft auf jedem Debian-basierten Host mit debootstrap - keine VM
# noetig (real auf enterprise/LMDE 6 verifiziert, siehe Kommentar bei
# SUITE weiter unten).
usage() {
echo "Usage: $0 <output.tar.zst> [suite] [arch]" >&2
echo " suite: Debian-Suite fuer debootstrap (Standard: bookworm)" >&2
echo " arch: Zielarchitektur (Standard: amd64)" >&2
}
OUTPUT_FILE="${1:-}"
# bookworm = Debian 12, dieselbe Basis wie der Build-Host selbst (LMDE 6
# "faye"), dadurch keine Cross-Suite-Ueberraschungen bei apt/debootstrap.
SUITE="${2:-bookworm}"
ARCH="${3:-amd64}"
MIRROR="http://deb.debian.org/debian"
if [[ -z "${OUTPUT_FILE}" ]]; then
usage
exit 1
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "Fehler: muss als root laufen (debootstrap/chroot/mount)." >&2
exit 1
fi
for cmd in debootstrap chroot tar zstd; do
command -v "${cmd}" >/dev/null 2>&1 ||
{ echo "Fehler: benötigtes Werkzeug fehlt: ${cmd}" >&2; exit 1; }
done
WORK_DIR="$(mktemp -d /var/tmp/tuxflotte-golden-image.XXXXXX)"
readonly WORK_DIR
MOUNTED=()
cleanup() {
local mnt
# In umgekehrter Reihenfolge aushaengen - /dev/pts vor /dev usw.
for ((i = ${#MOUNTED[@]} - 1; i >= 0; i--)); do
mnt="${MOUNTED[i]}"
umount --recursive "${mnt}" 2>/dev/null ||
umount --lazy "${mnt}" 2>/dev/null || true
done
chmod -R u+w "${WORK_DIR}" 2>/dev/null || true
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
bind_mount() {
local target="$1"
mount --bind "${target}" "${WORK_DIR}${target}"
MOUNTED+=("${WORK_DIR}${target}")
}
echo "==> debootstrap ${SUITE}/${ARCH} nach ${WORK_DIR}"
debootstrap --arch="${ARCH}" "${SUITE}" "${WORK_DIR}" "${MIRROR}"
echo "==> apt-Quellen fuer non-free-firmware + backports einrichten"
# non-free-firmware ist seit Debian 12 eine eigene, standardmaessig NICHT
# aktive Komponente (anders als bei Ubuntu/Mint) - ohne sie fehlt auf
# vielen echten Geraeten die WLAN-/GPU-Firmware. Backports fuer einen
# neueren Kernel (bessere Hardwareunterstuetzung auf aktueller Hardware
# als der Suite-Standardkernel).
cat > "${WORK_DIR}/etc/apt/sources.list" <<EOF
deb ${MIRROR} ${SUITE} main contrib non-free-firmware
deb ${MIRROR} ${SUITE}-updates main contrib non-free-firmware
deb ${MIRROR}-security ${SUITE}-security main contrib non-free-firmware
deb ${MIRROR} ${SUITE}-backports main contrib non-free-firmware
EOF
echo "==> /dev, /proc, /sys, /dev/pts einbinden fuer chroot-Paketinstallation"
bind_mount /dev
mount -t proc proc "${WORK_DIR}/proc"
MOUNTED+=("${WORK_DIR}/proc")
mount -t sysfs sysfs "${WORK_DIR}/sys"
MOUNTED+=("${WORK_DIR}/sys")
mount -t devpts devpts "${WORK_DIR}/dev/pts"
MOUNTED+=("${WORK_DIR}/dev/pts")
# DNS-Aufloesung im chroot braucht eine eigene resolv.conf - der
# Netzwerk-Namespace wird zwar geteilt, aber /etc/resolv.conf ist eine
# normale Datei innerhalb des debootstrap-Baums, nicht automatisch mit
# der des Hosts identisch.
cp /etc/resolv.conf "${WORK_DIR}/etc/resolv.conf"
echo "==> Pakete im chroot installieren (Kernel, Firmware, Netzwerk, SSH)"
# Paketliste ist ein erster, plausibler Startpunkt - wird nach dem ersten
# echten Boot-Test (Phase 1 des Plans) voraussichtlich noch angepasst,
# je nachdem was auf der Testhardware/-VM tatsaechlich fehlt.
chroot "${WORK_DIR}" /bin/bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
linux-image-amd64 \
firmware-linux \
firmware-realtek \
firmware-iwlwifi \
firmware-atheros \
firmware-misc-nonfree \
network-manager \
openssh-server \
sudo \
locales \
curl \
ca-certificates \
parted
apt-get clean
'
echo "==> Golden Image von eingebrannten Zugangsdaten befreien"
# Debootstrap setzt kein Root-Passwort (Konto bleibt gesperrt) - das
# bleibt bewusst so. openssh-server generiert beim Postinst aber eigene
# Hostkeys, die JEDES aus diesem Image ausgerollte Geraet sonst identisch
# mit-ausliefern wuerde (Kollisionsrisiko + Sicherheitsproblem) - werden
# hier entfernt, die Deployment-Mechanik (Plan Phase 1) erzeugt sie beim
# eigentlichen Ausrollen pro Geraet frisch.
rm -f "${WORK_DIR}"/etc/ssh/ssh_host_*
: > "${WORK_DIR}/etc/machine-id"
echo "==> Aushaengen"
for ((i = ${#MOUNTED[@]} - 1; i >= 0; i--)); do
umount --recursive "${MOUNTED[i]}"
done
MOUNTED=()
echo "==> Packe als ${OUTPUT_FILE}"
tar --numeric-owner --xattrs -C "${WORK_DIR}" -cf - . |
zstd -T0 -19 -o "${OUTPUT_FILE}"
echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))"