fix: /updates auf dem Live-System deployen + Kunden-ISO-Build (Phase 4)

Casper merged /updates nie automatisch auf das gebootete Live-System -
eine seit Projektbeginn unverifizierte Annahme, die erst beim echten
QEMU-Boot (UEFI, kompletter GRUB->Live-Desktop-Pfad) einer personalisierten
Kunden-ISO aufgefallen ist: /opt/tuxflotte fehlte trotz korrekt auf dem
Medium liegendem /cdrom/updates vollständig.

Fix per casper-bottom-Hook, eingebettet via Initrd-Cpio-Konkatenation
(scripts/lib/initrd.sh, derselbe in Phase 1 verifizierte Mechanismus wie
beim Kexec-Preseed). Wichtig dabei: ein komplett neuer Hook-Skriptname wird
nie ausgeführt, weil mkinitramfs eine statische ORDER-Datei mit der
Aufrufliste ins Initrd backt - stattdessen wird der Inhalt des bereits
gelisteten, garantiert letzten Skripts (99casperboot) überschrieben.

start-kiosk.sh erkennt jetzt TUXFLOTTE_AUTO_MODE und startet den Installer
automatisch statt der Kiosk-Startseite. build_customer_iso.sh baut daraus
personalisierte Kunden-ISOs (WLAN-Zugangsdaten + Enrollment-Session-Code).

Nebenbei zwei vorbestehende Bugs behoben: xorriso -osirrox übernimmt
Original-ISO-Dateirechte (oft 444/555, kein Write-Bit), was cp/rm -rf in
build.sh/build_customer_iso.sh bisher unbemerkt kaputt gemacht hat.

Real per QEMU verifiziert (echter GRUB-Boot einer gebauten Kunden-ISO).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-06 22:18:04 +02:00
parent 108444ecf7
commit 683cf70af5
5 changed files with 229 additions and 5 deletions

View File

@ -0,0 +1,38 @@
#! /bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
# get pre-requisites
prereqs)
prereqs
exit 0
;;
esac
# Ueberschreibt das originale 99casperboot (macht nur `touch
# /run/.casper-boot`) statt einen zusaetzlichen, neuen Hook-Skriptnamen
# einzufuehren: initramfs-tools generiert beim ISO-Bau eine ORDER-Datei
# (scripts/casper-bottom/ORDER) mit der fest verdrahteten Aufruf-Reihenfolge
# aller casper-bottom-Skripte - real durch Extraktion des echten initrd.lz
# mit unmkinitramfs bestaetigt. Ein per Initrd-Cpio-Layer NEU hinzugefuegtes
# Skript taucht in dieser ORDER-Datei nicht auf und wird nie aufgerufen,
# selbst wenn die Datei im initramfs physisch vorhanden ist (erste Version
# dieses Fixes ist daran gescheitert). Das Ueberschreiben eines bereits in
# ORDER referenzierten Pfads funktioniert dagegen zuverlaessig (spaetere
# cpio-Schichten ueberschreiben Dateiinhalte am selben Pfad, siehe
# scripts/lib/initrd.sh - derselbe Mechanismus, der schon fuer das
# Kexec-Preseed in Phase 1 verifiziert wurde). 99casperboot ist laut ORDER
# das letzte casper-bottom-Skript ueberhaupt - garantiert nach 05mountpoints,
# das /cdrom nach /root/cdrom umhaengt und damit die Voraussetzung fuer den
# folgenden Kopiervorgang schafft.
if [ -d /root/cdrom/updates ]; then
cp -a /root/cdrom/updates/. /root/
fi
touch /run/.casper-boot

View File

@ -30,4 +30,13 @@ dconf write /org/nemo/desktop/show-desktop-icons false
dconf write /org/cinnamon/enabled-applets "['panel1:left:0:window-list@cinnamon.org:1', 'panel1:right:0:power@cinnamon.org:2']" dconf write /org/cinnamon/enabled-applets "['panel1:left:0:window-list@cinnamon.org:1', 'panel1:right:0:power@cinnamon.org:2']"
mkdir -p /home/liveuser/.local/share/tuxflotte-kiosk mkdir -p /home/liveuser/.local/share/tuxflotte-kiosk
# Auto-Modus (personalisierte Kunden-ISO, siehe scripts/build_customer_iso.sh)
# startet den Installer sofort unbeaufsichtigt statt der Kiosk-Startseite -
# config/installer.conf wird nur bei personalisierten Abbildern mitgeliefert,
# das Standard-/Testabbild bleibt unverändert interaktiv.
if grep -qE '^\s*(export\s+)?TUXFLOTTE_AUTO_MODE=true\s*$' /opt/tuxflotte/config/installer.conf 2>/dev/null; then
exec sudo /opt/tuxflotte/scripts/installer.sh
fi
exec epiphany --profile=/home/liveuser/.local/share/tuxflotte-kiosk file:///opt/tuxflotte/kiosk/index.html exec epiphany --profile=/home/liveuser/.local/share/tuxflotte-kiosk file:///opt/tuxflotte/kiosk/index.html

View File

@ -10,6 +10,9 @@ BUILD_DIR="$BASE_DIR/build"
WORK_DIR="$BUILD_DIR/work" WORK_DIR="$BUILD_DIR/work"
OUTPUT_DIR="$BUILD_DIR/output" OUTPUT_DIR="$BUILD_DIR/output"
# shellcheck source=lib/initrd.sh
source "$REPO_DIR/scripts/lib/initrd.sh"
# Test-Device fuer den in bake_test_preseed() eingebrannten, nicht # Test-Device fuer den in bake_test_preseed() eingebrannten, nicht
# personalisierten Test-Preseed (Mint) - dasselbe "Default Lab"-Testgeraet, # personalisierten Test-Preseed (Mint) - dasselbe "Default Lab"-Testgeraet,
# das schon fuer die Fedora-Verifikation genutzt wurde. Echte Personalisierung # das schon fuer die Fedora-Verifikation genutzt wurde. Echte Personalisierung
@ -50,7 +53,7 @@ check_dependencies() {
done done
if [[ "$BACKEND" == "mint" ]]; then if [[ "$BACKEND" == "mint" ]]; then
for cmd in jq envsubst base64; do for cmd in jq envsubst base64 cpio; do
if ! command -v "$cmd" >/dev/null 2>&1; then if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: missing dependency: $cmd" echo "Error: missing dependency: $cmd"
exit 1 exit 1
@ -61,6 +64,9 @@ check_dependencies() {
prepare_dirs() { prepare_dirs() {
mkdir -p "$BUILD_DIR" "$OUTPUT_DIR" mkdir -p "$BUILD_DIR" "$OUTPUT_DIR"
# xorriso extrahiert Verzeichnisse teils mit den Original-ISO-Rechten
# (z.B. 555, kein Write-Bit) - ohne chmod schlaegt rm -rf darauf fehl.
chmod -R u+w "$WORK_DIR" 2>/dev/null || true
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
} }
@ -68,26 +74,34 @@ prepare_dirs() {
extract_iso() { extract_iso() {
echo "Extracting ISO..." echo "Extracting ISO..."
xorriso -indev "$SOURCE_ISO" -osirrox on -extract / "$WORK_DIR" >/dev/null xorriso -indev "$SOURCE_ISO" -osirrox on -extract / "$WORK_DIR" >/dev/null
# xorriso uebernimmt die Original-ISO-Rechte (haeufig 555 auf
# Verzeichnissen, 444 auf Dateien) - ohne Write-Bit koennen weder
# spaetere Schritte (patch_grub) noch spaetere Aufraeumlaeufe
# (prepare_dirs) Dateien darin ersetzen/entfernen.
chmod -R u+w "$WORK_DIR"
} }
patch_grub() { patch_grub() {
echo "Installing Tuxflotte GRUB configuration..." echo "Installing Tuxflotte GRUB configuration..."
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
# Ueberschreiben mit "Keine Berechtigung" fehl.
if [[ "$BACKEND" == "fedora" ]]; then if [[ "$BACKEND" == "fedora" ]]; then
cp "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \ cp -f "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \
"$WORK_DIR/EFI/BOOT/grub.cfg" "$WORK_DIR/EFI/BOOT/grub.cfg"
cp "$REPO_DIR/grub/boot-grub2-grub.cfg" \ cp -f "$REPO_DIR/grub/boot-grub2-grub.cfg" \
"$WORK_DIR/boot/grub2/grub.cfg" "$WORK_DIR/boot/grub2/grub.cfg"
else else
# Mint hat, anders als Fedora, keine zweite ESP/FAT-Kopie des GRUB-Menüs, # Mint hat, anders als Fedora, keine zweite ESP/FAT-Kopie des GRUB-Menüs,
# die separat gepatcht werden müsste (real gegen die vorhandene # die separat gepatcht werden müsste (real gegen die vorhandene
# tuxflotte-mint-test.iso verifiziert - x86_64-efi/grub.cfg ist nur ein # tuxflotte-mint-test.iso verifiziert - x86_64-efi/grub.cfg ist nur ein
# Loader-Stub, der per `source /boot/grub/grub.cfg` zurückverweist). # Loader-Stub, der per `source /boot/grub/grub.cfg` zurückverweist).
cp "$REPO_DIR/grub/mint-boot-grub.cfg" \ cp -f "$REPO_DIR/grub/mint-boot-grub.cfg" \
"$WORK_DIR/boot/grub/grub.cfg" "$WORK_DIR/boot/grub/grub.cfg"
cp "$REPO_DIR/grub/mint-isolinux-live.cfg" \ cp -f "$REPO_DIR/grub/mint-isolinux-live.cfg" \
"$WORK_DIR/isolinux/live.cfg" "$WORK_DIR/isolinux/live.cfg"
fi fi
} }
@ -171,6 +185,16 @@ prepare_updates() {
chmod 0440 "$updates_dir/etc/sudoers.d/90-tuxflotte" chmod 0440 "$updates_dir/etc/sudoers.d/90-tuxflotte"
} }
patch_initrd() {
[[ "$BACKEND" == "mint" ]] || return 0
echo "Injecting casper-bottom updates-hook into initrd..."
inject_casper_bottom_hooks "$REPO_DIR" \
"$WORK_DIR/casper/initrd.lz" \
"$WORK_DIR/casper/initrd-tuxflotte.lz"
}
create_iso() { create_iso() {
echo "Creating Tuxflotte ISO..." echo "Creating Tuxflotte ISO..."
@ -189,6 +213,7 @@ create_iso() {
-map "$REPO_DIR/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg -map "$REPO_DIR/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg
-map "$REPO_DIR/grub/mint-isolinux-live.cfg" /isolinux/live.cfg -map "$REPO_DIR/grub/mint-isolinux-live.cfg" /isolinux/live.cfg
-map "$WORK_DIR/preseed/tuxflotte.seed" /preseed/tuxflotte.seed -map "$WORK_DIR/preseed/tuxflotte.seed" /preseed/tuxflotte.seed
-map "$WORK_DIR/casper/initrd-tuxflotte.lz" /casper/initrd.lz
) )
fi fi
@ -217,6 +242,7 @@ main() {
verify_workdir verify_workdir
bake_test_preseed bake_test_preseed
prepare_updates prepare_updates
patch_initrd
create_iso create_iso
echo echo

109
scripts/build_customer_iso.sh Executable file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
# Baut eine personalisierte Kunden-ISO fuer den Self-Service-Flow (Phase 4).
# Anders als build.sh (das fuer Testzwecke einen statischen Preseed mit
# Fake-Geraete-ID einbrennt, siehe bake_test_preseed()) wird hier nur
# config/installer.conf mit den echten, organisationsspezifischen Werten
# befuellt - Hostname/Geraete-ID/Partitionierung werden weiterhin erst live
# auf dem Zielgeraet erzeugt (siehe backend_generate_config() in
# backends/mint/backend.sh). Hier geht es nur um WLAN-Zugangsdaten +
# Enrollment-Session-Code als Boot-Konfiguration, die start-kiosk.sh dazu
# bringt, den Installer beim Hochfahren automatisch zu starten statt der
# interaktiven Kiosk-Startseite.
usage() {
echo "Usage: $0 <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk]" >&2
}
SOURCE_ISO="${1:-}"
OUTPUT_ISO="${2:-}"
ACTIVATION_CODE="${3:-}"
WIFI_SSID="${4:-}"
WIFI_PSK="${5:-}"
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK_DIR="$(mktemp -d)"
# shellcheck source=lib/initrd.sh
source "$REPO_DIR/scripts/lib/initrd.sh"
cleanup() {
# xorriso extrahiert Verzeichnisse teils mit den Original-ISO-Rechten
# (z.B. 555, kein Write-Bit) - ohne chmod schlaegt rm -rf darauf fehl.
chmod -R u+w "${WORK_DIR}" 2>/dev/null || true
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
if [[ -z "${SOURCE_ISO}" || -z "${OUTPUT_ISO}" || -z "${ACTIVATION_CODE}" ]]; then
usage
exit 1
fi
[[ -f "${SOURCE_ISO}" ]] ||
{ echo "Error: Quell-ISO nicht gefunden: ${SOURCE_ISO}" >&2; exit 1; }
for cmd in xorriso cpio; do
command -v "${cmd}" >/dev/null 2>&1 ||
{ echo "Error: fehlendes Werkzeug: ${cmd}" >&2; exit 1; }
done
echo "Extrahiere Quell-ISO..."
xorriso -indev "${SOURCE_ISO}" -osirrox on -extract / "${WORK_DIR}" >/dev/null
# xorriso uebernimmt die Original-ISO-Rechte (haeufig 555 auf
# Verzeichnissen, 444 auf Dateien) - ohne Write-Bit koennen weder
# spaetere Schritte noch der Cleanup-Trap Dateien darin ersetzen/entfernen.
chmod -R u+w "${WORK_DIR}"
echo "Installiere Tuxflotte-GRUB-Konfiguration..."
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
# Ueberschreiben mit "Keine Berechtigung" fehl.
cp -f "${REPO_DIR}/grub/mint-boot-grub.cfg" "${WORK_DIR}/boot/grub/grub.cfg"
cp -f "${REPO_DIR}/grub/mint-isolinux-live.cfg" "${WORK_DIR}/isolinux/live.cfg"
grep -q "Tuxflotte" "${WORK_DIR}/boot/grub/grub.cfg"
grep -q "Tuxflotte" "${WORK_DIR}/isolinux/live.cfg"
echo "Baue personalisierte Boot-Konfiguration..."
updates_dir="${WORK_DIR}/updates"
rm -rf "${updates_dir}"
mkdir -p "${updates_dir}"
cp -a "${REPO_DIR}/live-updates/." "${updates_dir}/"
cp -a "${REPO_DIR}/scripts" "${updates_dir}/opt/tuxflotte/"
cp -a "${REPO_DIR}/backends" "${updates_dir}/opt/tuxflotte/"
mkdir -p "${updates_dir}/opt/tuxflotte/config"
{
echo "export TUXFLOTTE_AUTO_MODE=true"
printf 'export TUXFLOTTE_ACTIVATION_CODE=%q\n' "${ACTIVATION_CODE}"
[[ -n "${WIFI_SSID}" ]] && printf 'export TUXFLOTTE_WIFI_SSID=%q\n' "${WIFI_SSID}"
[[ -n "${WIFI_PSK}" ]] && printf 'export TUXFLOTTE_WIFI_PSK=%q\n' "${WIFI_PSK}"
} >"${updates_dir}/opt/tuxflotte/config/installer.conf"
chmod 0440 "${updates_dir}/etc/sudoers.d/90-tuxflotte"
echo "Injiziere casper-bottom Updates-Hook in die Initrd..."
custom_initrd="${WORK_DIR}/casper/initrd-tuxflotte.lz"
inject_casper_bottom_hooks "${REPO_DIR}" \
"${WORK_DIR}/casper/initrd.lz" \
"${custom_initrd}"
echo "Erzeuge Kunden-ISO..."
rm -f "${OUTPUT_ISO}"
xorriso \
-indev "${SOURCE_ISO}" \
-outdev "${OUTPUT_ISO}" \
-compliance no_emul_toc \
-volid TUXFLOTTE \
-map "${REPO_DIR}/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg \
-map "${REPO_DIR}/grub/mint-isolinux-live.cfg" /isolinux/live.cfg \
-map "${custom_initrd}" /casper/initrd.lz \
-map "${updates_dir}" /updates \
-chown_r 0 /updates -- \
-chgrp_r 0 /updates -- \
-boot_image any replay
echo "Kunden-ISO erstellt: ${OUTPUT_ISO}"

42
scripts/lib/initrd.sh Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Wird von build.sh und build_customer_iso.sh eingebunden (source), nicht
# eigenstaendig ausgefuehrt.
# Haengt initrd-hooks/casper-bottom/* als zusaetzliche cpio-Schicht an ein
# bestehendes Casper-Initrd an - derselbe Mechanismus, den
# backend_launch() (backends/mint/backend.sh) bereits fuer das
# Kexec-Preseed nutzt und der per QEMU verifiziert ist: der Kernel haengt
# spaeter aneinandergereihte cpio-Archive additiv an ein bestehendes
# initramfs an. Noetig, weil Casper keinen automatischen /updates-Merge
# kennt (real per QEMU-Boot widerlegt) - der Hook selbst kopiert
# /cdrom/updates beim Boot auf das Root-Dateisystem, siehe
# initrd-hooks/casper-bottom/21tuxflotte_updates.
inject_casper_bottom_hooks() {
local repo_dir="$1"
local src_initrd="$2"
local dest_initrd="$3"
local hook_dir="${repo_dir}/initrd-hooks"
local layer_dir
layer_dir="$(mktemp -d)"
# Ausserhalb von layer_dir, damit die Archiv-Datei sich beim Bauen nicht
# selbst als leeren Eintrag einfaengt (find laeuft im selben Moment, in
# dem die Shell-Redirection sie bereits anlegt).
local cpio_out
cpio_out="$(mktemp)"
cp -a "${hook_dir}/casper-bottom" "${layer_dir}/scripts-casper-bottom-tmp"
mkdir -p "${layer_dir}/scripts"
mv "${layer_dir}/scripts-casper-bottom-tmp" "${layer_dir}/scripts/casper-bottom"
chmod 0755 "${layer_dir}"/scripts/casper-bottom/*
(cd "${layer_dir}" && find . -mindepth 1 | cpio -o -H newc 2>/dev/null | gzip) \
>"${cpio_out}" ||
{ echo "Error: Casper-Bottom-Hook-Schicht konnte nicht gebaut werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
cat "${src_initrd}" "${cpio_out}" >"${dest_initrd}" ||
{ echo "Error: Initrd konnte nicht mit Hook-Schicht zusammengesetzt werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
rm -rf "${layer_dir}"
rm -f "${cpio_out}"
}