From 27d6fa0f47ad7022c8f363f1cbd199975eba75a9 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Mon, 31 Aug 2026 16:36:35 +0200 Subject: [PATCH] feat(boot-medium): build_customer_iso.sh ans neue Struktur anpassen (Phase 3) + vier echte Deployment-Bugs behoben Phase 3: Personalisierung (Aktivierungscode/WLAN) funktioniert jetzt ohne Xorriso-Extraktion der ganzen ISO / Casper-Hook-Injection. /opt/tuxflotte/ liegt innerhalb von /live/filesystem.squashfs (read-only) - ein direktes xorriso -update auf eine Datei darin traf live getestet ins Leere. Stattdessen: Squashfs entpacken (unsquashfs), installer.conf ersetzen, neu packen (mksquashfs -comp xz, ~1 Minute reine Kompression, kein debootstrap/apt), als Ganzes per xorriso -update einspielen. Muss als root laufen (Datei-Eigentuemer im Squashfs sonst falsch). Beim ersten echten End-to-End-Testlauf mit dem vollen Golden-Image-Archiv (vorher nie bis zum Ende gekommen - immer am Ubiquity-Bug oder an einem Commit-Gate gestoppt) vier echte, bisher unentdeckte Bugs gefunden und behoben: 1. Golden-Image-Download landete in /run/tuxflotte/backend/ (RAM-Tmpfs, hier 392 MB) statt auf echtem Speicher - "curl: (23) Failure writing output to destination" bei 2,3 GB Archiv, unabhaengig von RAM-Groesse. Fix: image_deploy_extract_image_from_url() streamt Download direkt in die Extraktion (curl | tar), kein Zwischenspeichern mehr noetig. backend_launch() partitioniert/formatiert/mountet jetzt VOR dem Download-Versuch statt danach. 2. /etc/resolv.conf im Zielbaum ist bei der echten Referenz-VM ein von NetworkManager verwalteter Symlink - "cp" verweigerte das Schreiben "through a dangling symlink". Fix: Ziel vor dem Kopieren explizit entfernen. 3. mount --bind auf /dev, /proc, /sys schlug fehl, weil diese Verzeichnisse nach dem Entpacken gar nicht existierten (package_golden_image.sh schliesst sie bewusst aus) - und zwar SILENT, weil das bisherige "cmd && stack_ref+=(...)"-Muster einen Fehlschlag unter "set -e" nicht als Statement-Fehler wertet. Fiel dadurch erst beim naechsten chroot-Aufruf auf ("ssh-keygen -A: Couldn't open /dev/null"), weit weg von der eigentlichen Ursache. Fix: neue image_deploy_restore_excluded_dirs() legt alle sechs von package_golden_image.sh ausgeschlossenen Verzeichnisse (proc/sys/dev/run/tmp/var-tmp) direkt nach der Extraktion wieder an; jeder Mount wird jetzt einzeln explizit geprueft statt auf &&-Verkettung zu vertrauen. 4. package_golden_image.sh: "--exclude=dev" (ohne "./"-Praefix) matcht bei GNU tar gegen JEDEN Verzeichnis-Basisnamen im ganzen Baum, nicht nur den Top-Level-Eintrag - hat dadurch auch kernel/drivers/net/can/dev/ (ein zufaellig gleichnamiger, voellig unverwandter Kernelmodul-Ordner) aus dem Archiv gerissen, update-initramfs scheiterte an fehlenden can-dev.ko.zst-Abhaengigkeiten. Lokal mit einem Wegwerf-Testbaum verifiziert (wie beim "run"-Fund vom selben Tag): "./"-Praefix verankert das Muster auf den exakten Top-Level-Pfad, gleichnamige verschachtelte Ordner bleiben erhalten. Betrifft das AKTUELL AUF ANODE GEHOSTETE Archiv - muss mit dem korrigierten Skript auf der Referenz-VM neu gepackt und hochgeladen werden (steht noch aus, Nutzer verschlankt die Referenz-VM gerade zusaetzlich). Live-Fortschritt in der enterprise-QEMU-VM: nach Fix 1-3 kam der Deployment-Versuch bis kurz vor update-initramfs durch (Partitionierung, Formatierung, Mounten, Download+Extraktion, chroot-Fixup bis machine-id + SSH-Hostkeys liefen alle sauber) - Fund 4 (das defekte Archiv) ist der letzte noch offene Blocker fuer einen vollstaendig erfolgreichen End-to-End-Durchlauf. --- backends/mint-image/backend.sh | 20 +-- .../tuxflotte/backends/mint-image/backend.sh | 20 +-- .../opt/tuxflotte/scripts/lib/image_deploy.sh | 100 ++++++++++++++- scripts/build_customer_iso.sh | 118 +++++++++--------- scripts/lib/image_deploy.sh | 100 ++++++++++++++- scripts/package_golden_image.sh | 23 +++- 6 files changed, 285 insertions(+), 96 deletions(-) diff --git a/backends/mint-image/backend.sh b/backends/mint-image/backend.sh index b4e9aa2..f16faf2 100644 --- a/backends/mint-image/backend.sh +++ b/backends/mint-image/backend.sh @@ -28,7 +28,6 @@ HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json" RUNTIME_DIR="/run/tuxflotte/backend" CONFIG_FILE="${RUNTIME_DIR}/config.json" -GOLDEN_IMAGE_FILE="${RUNTIME_DIR}/golden-image.tar.zst" # Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch() # und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe @@ -100,7 +99,7 @@ backend_init() { --group=root \ "${RUNTIME_DIR}" - rm -f -- "${CONFIG_FILE}" "${GOLDEN_IMAGE_FILE}" + rm -f -- "${CONFIG_FILE}" backend_log "Initialisiert." } @@ -203,11 +202,6 @@ backend_launch() { root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")" - backend_log "Lade Golden Image von ${GOLDEN_IMAGE_URL}" - curl --silent --show-error --fail --location \ - --output "${GOLDEN_IMAGE_FILE}" "${GOLDEN_IMAGE_URL}" || - { backend_fatal "Golden Image konnte nicht geladen werden: ${GOLDEN_IMAGE_URL}"; return 1; } - backend_log "Partitioniere ${disk}" read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" || return 1 @@ -218,9 +212,15 @@ backend_launch() { backend_log "Mounte unter ${TARGET_DIR}" image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1 - backend_log "Entpacke Golden Image" - image_deploy_extract_image "${GOLDEN_IMAGE_FILE}" "${TARGET_DIR}" || return 1 - rm -f "${GOLDEN_IMAGE_FILE}" + # Live gefunden (31.08.2026, erster echter End-to-End-Lauf mit dem vollen + # Referenz-VM-Archiv): "erst nach /run/tuxflotte/... herunterladen, dann + # entpacken" scheiterte an /run (RAM-Tmpfs, viel kleiner als das + # 2,3-GB-Archiv - "curl: (23) Failure writing output to destination"). + # Direktes Streamen in die Extraktion braucht nur ein paar MB Puffer, + # unabhaengig von der Archivgroesse - deshalb erst ab hier (nach + # Partitionieren/Formatieren/Mounten), kein Zwischenspeichern mehr. + backend_log "Lade und entpacke Golden Image von ${GOLDEN_IMAGE_URL}" + image_deploy_extract_image_from_url "${GOLDEN_IMAGE_URL}" "${TARGET_DIR}" || return 1 backend_log "Schreibe fstab" image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1 diff --git a/boot-medium/config/includes.chroot/opt/tuxflotte/backends/mint-image/backend.sh b/boot-medium/config/includes.chroot/opt/tuxflotte/backends/mint-image/backend.sh index b4e9aa2..f16faf2 100644 --- a/boot-medium/config/includes.chroot/opt/tuxflotte/backends/mint-image/backend.sh +++ b/boot-medium/config/includes.chroot/opt/tuxflotte/backends/mint-image/backend.sh @@ -28,7 +28,6 @@ HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json" RUNTIME_DIR="/run/tuxflotte/backend" CONFIG_FILE="${RUNTIME_DIR}/config.json" -GOLDEN_IMAGE_FILE="${RUNTIME_DIR}/golden-image.tar.zst" # Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch() # und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe @@ -100,7 +99,7 @@ backend_init() { --group=root \ "${RUNTIME_DIR}" - rm -f -- "${CONFIG_FILE}" "${GOLDEN_IMAGE_FILE}" + rm -f -- "${CONFIG_FILE}" backend_log "Initialisiert." } @@ -203,11 +202,6 @@ backend_launch() { root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")" - backend_log "Lade Golden Image von ${GOLDEN_IMAGE_URL}" - curl --silent --show-error --fail --location \ - --output "${GOLDEN_IMAGE_FILE}" "${GOLDEN_IMAGE_URL}" || - { backend_fatal "Golden Image konnte nicht geladen werden: ${GOLDEN_IMAGE_URL}"; return 1; } - backend_log "Partitioniere ${disk}" read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" || return 1 @@ -218,9 +212,15 @@ backend_launch() { backend_log "Mounte unter ${TARGET_DIR}" image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1 - backend_log "Entpacke Golden Image" - image_deploy_extract_image "${GOLDEN_IMAGE_FILE}" "${TARGET_DIR}" || return 1 - rm -f "${GOLDEN_IMAGE_FILE}" + # Live gefunden (31.08.2026, erster echter End-to-End-Lauf mit dem vollen + # Referenz-VM-Archiv): "erst nach /run/tuxflotte/... herunterladen, dann + # entpacken" scheiterte an /run (RAM-Tmpfs, viel kleiner als das + # 2,3-GB-Archiv - "curl: (23) Failure writing output to destination"). + # Direktes Streamen in die Extraktion braucht nur ein paar MB Puffer, + # unabhaengig von der Archivgroesse - deshalb erst ab hier (nach + # Partitionieren/Formatieren/Mounten), kein Zwischenspeichern mehr. + backend_log "Lade und entpacke Golden Image von ${GOLDEN_IMAGE_URL}" + image_deploy_extract_image_from_url "${GOLDEN_IMAGE_URL}" "${TARGET_DIR}" || return 1 backend_log "Schreibe fstab" image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1 diff --git a/boot-medium/config/includes.chroot/opt/tuxflotte/scripts/lib/image_deploy.sh b/boot-medium/config/includes.chroot/opt/tuxflotte/scripts/lib/image_deploy.sh index 7d3098a..efc732e 100644 --- a/boot-medium/config/includes.chroot/opt/tuxflotte/scripts/lib/image_deploy.sh +++ b/boot-medium/config/includes.chroot/opt/tuxflotte/scripts/lib/image_deploy.sh @@ -115,6 +115,25 @@ image_deploy_mount() { fi } +# package_golden_image.sh schliesst /proc, /sys, /dev, /run, /tmp, /var/tmp +# bewusst aus dem Archiv aus (Laufzeit-Zustand der Referenz-VM, nicht Teil +# eines portablen Images). Nach dem Entpacken fehlen diese Verzeichnisse +# deshalb komplett - live gefunden (31.08.2026, erster echter Lauf mit dem +# echten Referenz-VM-Archiv): erst "mount --bind /dev" ("mount point does +# not exist"), dann - nach Behebung dessen - "mktemp: failed to create +# directory via template '/var/tmp/mkinitramfs_XXXXXX'" bei +# update-initramfs. Statt jedes Fehlen einzeln zu jagen: alle sechs +# ausgeschlossenen Verzeichnisse direkt nach dem Entpacken einmalig mit +# sinnvollen Rechten wiederherstellen. /dev, /proc, /sys werden gleich +# danach ohnehin ueberbindmounted (image_deploy_bind_mounts) - schadet +# aber nicht, sie schon hier als leere Verzeichnisse anzulegen. +image_deploy_restore_excluded_dirs() { + local target="$1" + + mkdir -p "${target}"/{proc,sys,dev,run,tmp,var/tmp} + chmod 1777 "${target}/tmp" "${target}/var/tmp" +} + # Entpackt das per build_golden_image.sh gebaute Golden Image direkt in # den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt # sowohl .tar.zst als auch (falls spaeter gewechselt) andere @@ -128,6 +147,40 @@ image_deploy_extract_image() { tar --zstd -xpf "${image_path}" -C "${target}" || { image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; } + + image_deploy_restore_excluded_dirs "${target}" +} + +# Streamt das Golden Image direkt vom Server in die Extraktion, ohne es +# jemals als Ganzes zwischenzuspeichern - live gefunden (31.08.2026, +# erster echter End-to-End-Lauf mit dem vollen 2,3-GB-Referenz-VM-Archiv +# ueber das neue Boot-Medium): das bisherige "erst nach /run/tuxflotte/... +# herunterladen, dann entpacken" scheiterte an /run (RAM-Tmpfs, oft nur +# ein paar hundert MB gross, unabhaengig von der Geraete-RAM-Groesse) - +# "curl: (23) Failure writing output to destination". Direktes +# Zusammenrohren braucht dagegen nur ein paar MB Puffer, unabhaengig von +# der Archivgroesse. PIPESTATUS wird explizit geprueft statt sich auf +# "pipefail"-Semantik (rechteste fehlgeschlagene Stufe gewinnt) zu +# verlassen, damit die Fehlermeldung eindeutig zwischen Download- und +# Entpack-Fehler unterscheidet. +image_deploy_extract_image_from_url() { + local url="$1" + local target="$2" + local pipe_status + + # "|| true" verhindert, dass "set -e" hier sofort abbricht - der + # eigentliche Fehlerfall wird gleich anhand von PIPESTATUS gezielt + # ausgewertet, statt die Pipeline roh durchschlagen zu lassen. + curl --silent --show-error --fail --location "${url}" | + tar --zstd -xpf - -C "${target}" || true + pipe_status=("${PIPESTATUS[@]}") + + [[ "${pipe_status[0]}" -eq 0 ]] || + { image_deploy_fatal "Golden Image konnte nicht geladen werden: ${url}"; return 1; } + [[ "${pipe_status[1]}" -eq 0 ]] || + { image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; } + + image_deploy_restore_excluded_dirs "${target}" } # /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein @@ -166,11 +219,48 @@ image_deploy_bind_mounts() { local target="$1" local -n stack_ref="$2" - mount --bind /dev "${target}/dev" && stack_ref+=("${target}/dev") - mount -t proc proc "${target}/proc" && stack_ref+=("${target}/proc") - mount -t sysfs sysfs "${target}/sys" && stack_ref+=("${target}/sys") - mount -t devpts devpts "${target}/dev/pts" && stack_ref+=("${target}/dev/pts") - cp /etc/resolv.conf "${target}/etc/resolv.conf" + # Golden Image schliesst /dev, /proc, /sys bewusst aus + # (package_golden_image.sh --exclude=dev/sys/proc) - die Zielverzeichnisse + # existieren nach dem Entpacken deshalb noch nicht und muessen vor dem + # Bind-Mount erst angelegt werden. Live gefunden (31.08.2026, erster + # echter Lauf mit der echten Mint-Referenz-VM): ohne das schlaegt + # "mount --bind" mit "mount point does not exist" fehl - und zwar + # SILENT, weil das bisherige "cmd && stack_ref+=(...)"-Muster einen + # Fehlschlag von "cmd" unter "set -e" nicht als Statement-Fehler wertet + # (wohlbekannte Bash-Ausnahme fuer &&/||-Ketten) - der fehlende + # /dev-Bind-Mount fiel dann erst beim naechsten chroot-Aufruf auf + # ("ssh-keygen -A: Couldn't open /dev/null"), weit weg von der + # eigentlichen Ursache. Jetzt: Zielverzeichnisse anlegen, jeden + # Mount-Fehler explizit und einzeln pruefen statt auf &&-Verkettung + # zu vertrauen. + mkdir -p "${target}/dev" "${target}/proc" "${target}/sys" "${target}/dev/pts" + + mount --bind /dev "${target}/dev" || + { image_deploy_fatal "/dev konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/dev") + + mount -t proc proc "${target}/proc" || + { image_deploy_fatal "/proc konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/proc") + + mount -t sysfs sysfs "${target}/sys" || + { image_deploy_fatal "/sys konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/sys") + + mount -t devpts devpts "${target}/dev/pts" || + { image_deploy_fatal "/dev/pts konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/dev/pts") + + # Live gefunden (31.08.2026, erster echter Lauf mit der echten + # Mint-Referenz-VM statt eines debootstrap-Test-Images): dort ist + # /etc/resolv.conf ein von NetworkManager verwalteter Symlink (z.B. auf + # /run/NetworkManager/resolv.conf) - im gerade erst entpackten, + # ungebooteten Zielbaum zeigt der ins Leere, "cp" verweigert dann das + # Schreiben "through a dangling symlink". Ziel deshalb vorher entfernen + # (Symlink oder Datei, beides abgedeckt), erst dann kopieren. + rm -f "${target}/etc/resolv.conf" + cp /etc/resolv.conf "${target}/etc/resolv.conf" || + { image_deploy_fatal "resolv.conf konnte nicht nach ${target} kopiert werden"; return 1; } } image_deploy_unbind_mounts() { diff --git a/scripts/build_customer_iso.sh b/scripts/build_customer_iso.sh index 8e5e240..4a05de5 100755 --- a/scripts/build_customer_iso.sh +++ b/scripts/build_customer_iso.sh @@ -1,16 +1,34 @@ #!/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. +# Personalisiert das eigenstaendige Boot-Medium (scripts/build_boot_medium.sh) +# fuer eine bestimmte Kundenorganisation - befuellt config/installer.conf mit +# Aktivierungscode + WLAN-Zugangsdaten. Hostname/Geraete-ID/Partitionierung +# werden weiterhin erst live auf dem Zielgeraet erzeugt (siehe +# backend_generate_config() in backends/mint-image/backend.sh). +# +# Anders als das alte, Casper-basierte Verfahren (Xorriso-Extraktion der +# ganzen ISO + Initrd-Hook-Injection, siehe scripts/lib/initrd.sh) kann hier +# NICHT einfach eine einzelne Datei direkt auf der ISO9660-Ebene ersetzt +# werden: /opt/tuxflotte/ liegt innerhalb von /live/filesystem.squashfs +# (read-only, von live-boot gemountet). Ein direktes +# `xorriso -update datei /opt/tuxflotte/config/installer.conf` traf live +# getestet ins Leere - das gebootete System sah weiterhin die leere +# Platzhalter-Konfiguration aus dem Basis-Image, weil dieser Pfad ausserhalb +# des Squashfs liegt und live-boot ihn nie liest (31.08.2026, live in der +# enterprise-QEMU-VM gefunden: Aktivierungscode-Prompt erschien trotz +# TUXFLOTTE_AUTO_MODE=true in der vermeintlich aktualisierten ISO). +# +# Stattdessen: das Squashfs wird entpackt, installer.conf darin ersetzt, neu +# gepackt (mksquashfs -no-progress braucht dafuer nur reine Kompressionszeit, +# ~1 Minute - kein debootstrap/apt, kein Netzwerk) und als Ganzes per +# `xorriso -update` ins Ziel-ISO eingespielt (Boot-Sektoren/GRUB-Branding +# bleiben unangetastet, stecken schon im Basis-Image aus +# scripts/build_boot_medium.sh). mksquashfs kann ein bestehendes Squashfs +# zwar inkrementell erweitern, aber NICHT bereits vorhandene Dateien +# ueberschreiben (live getestet: identischer Dateiinhalt trotz +# augenscheinlich erfolgreichem Lauf) - deshalb der volle +# unsquashfs/mksquashfs-Zyklus statt eines inkrementellen Appends. usage() { echo "Usage: $0 [wifi_ssid] [wifi_psk] [volid]" >&2 @@ -28,20 +46,6 @@ WIFI_PSK="${5:-}" # Aufrufe ohne sechsten Parameter (z.B. manuelle Tests). VOLID="${6:-TUXFLOTTE}" -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 @@ -50,51 +54,47 @@ fi [[ -f "${SOURCE_ISO}" ]] || { echo "Error: Quell-ISO nicht gefunden: ${SOURCE_ISO}" >&2; exit 1; } -for cmd in xorriso cpio; do +if [[ "${EUID}" -ne 0 ]]; then + echo "Error: muss als root laufen (unsquashfs/mksquashfs brauchen root, um" >&2 + echo " Datei-Eigentuemer im Squashfs korrekt zu erhalten - ohne root" >&2 + echo " gehen z.B. root:shadow-Zuordnungen verloren, live gefunden)." >&2 + exit 1 +fi + +for cmd in xorriso unsquashfs mksquashfs; 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}" +WORK_DIR="$(mktemp -d)" -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" +cleanup() { + # unsquashfs uebernimmt Original-Rechte (teils ohne 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 -grep -q "Tuxflotte" "${WORK_DIR}/boot/grub/grub.cfg" -grep -q "Tuxflotte" "${WORK_DIR}/isolinux/live.cfg" +echo "Extrahiere Squashfs aus Quell-ISO..." +xorriso -indev "${SOURCE_ISO}" -osirrox on -extract /live/filesystem.squashfs \ + "${WORK_DIR}/orig.squashfs" >/dev/null -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 "Entpacke Squashfs..." +unsquashfs -d "${WORK_DIR}/root" "${WORK_DIR}/orig.squashfs" >/dev/null +echo "Schreibe personalisierte installer.conf..." +mkdir -p "${WORK_DIR}/root/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" +} >"${WORK_DIR}/root/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 "Packe Squashfs neu (nur Kompression, kein apt/debootstrap)..." +mksquashfs "${WORK_DIR}/root" "${WORK_DIR}/new.squashfs" -comp xz -no-progress >/dev/null +chmod 444 "${WORK_DIR}/new.squashfs" echo "Erzeuge Kunden-ISO..." rm -f "${OUTPUT_ISO}" @@ -102,14 +102,8 @@ rm -f "${OUTPUT_ISO}" xorriso \ -indev "${SOURCE_ISO}" \ -outdev "${OUTPUT_ISO}" \ - -compliance no_emul_toc \ -volid "${VOLID}" \ - -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 -- \ + -update "${WORK_DIR}/new.squashfs" /live/filesystem.squashfs \ -boot_image any replay echo "Kunden-ISO erstellt: ${OUTPUT_ISO}" diff --git a/scripts/lib/image_deploy.sh b/scripts/lib/image_deploy.sh index 7d3098a..efc732e 100644 --- a/scripts/lib/image_deploy.sh +++ b/scripts/lib/image_deploy.sh @@ -115,6 +115,25 @@ image_deploy_mount() { fi } +# package_golden_image.sh schliesst /proc, /sys, /dev, /run, /tmp, /var/tmp +# bewusst aus dem Archiv aus (Laufzeit-Zustand der Referenz-VM, nicht Teil +# eines portablen Images). Nach dem Entpacken fehlen diese Verzeichnisse +# deshalb komplett - live gefunden (31.08.2026, erster echter Lauf mit dem +# echten Referenz-VM-Archiv): erst "mount --bind /dev" ("mount point does +# not exist"), dann - nach Behebung dessen - "mktemp: failed to create +# directory via template '/var/tmp/mkinitramfs_XXXXXX'" bei +# update-initramfs. Statt jedes Fehlen einzeln zu jagen: alle sechs +# ausgeschlossenen Verzeichnisse direkt nach dem Entpacken einmalig mit +# sinnvollen Rechten wiederherstellen. /dev, /proc, /sys werden gleich +# danach ohnehin ueberbindmounted (image_deploy_bind_mounts) - schadet +# aber nicht, sie schon hier als leere Verzeichnisse anzulegen. +image_deploy_restore_excluded_dirs() { + local target="$1" + + mkdir -p "${target}"/{proc,sys,dev,run,tmp,var/tmp} + chmod 1777 "${target}/tmp" "${target}/var/tmp" +} + # Entpackt das per build_golden_image.sh gebaute Golden Image direkt in # den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt # sowohl .tar.zst als auch (falls spaeter gewechselt) andere @@ -128,6 +147,40 @@ image_deploy_extract_image() { tar --zstd -xpf "${image_path}" -C "${target}" || { image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; } + + image_deploy_restore_excluded_dirs "${target}" +} + +# Streamt das Golden Image direkt vom Server in die Extraktion, ohne es +# jemals als Ganzes zwischenzuspeichern - live gefunden (31.08.2026, +# erster echter End-to-End-Lauf mit dem vollen 2,3-GB-Referenz-VM-Archiv +# ueber das neue Boot-Medium): das bisherige "erst nach /run/tuxflotte/... +# herunterladen, dann entpacken" scheiterte an /run (RAM-Tmpfs, oft nur +# ein paar hundert MB gross, unabhaengig von der Geraete-RAM-Groesse) - +# "curl: (23) Failure writing output to destination". Direktes +# Zusammenrohren braucht dagegen nur ein paar MB Puffer, unabhaengig von +# der Archivgroesse. PIPESTATUS wird explizit geprueft statt sich auf +# "pipefail"-Semantik (rechteste fehlgeschlagene Stufe gewinnt) zu +# verlassen, damit die Fehlermeldung eindeutig zwischen Download- und +# Entpack-Fehler unterscheidet. +image_deploy_extract_image_from_url() { + local url="$1" + local target="$2" + local pipe_status + + # "|| true" verhindert, dass "set -e" hier sofort abbricht - der + # eigentliche Fehlerfall wird gleich anhand von PIPESTATUS gezielt + # ausgewertet, statt die Pipeline roh durchschlagen zu lassen. + curl --silent --show-error --fail --location "${url}" | + tar --zstd -xpf - -C "${target}" || true + pipe_status=("${PIPESTATUS[@]}") + + [[ "${pipe_status[0]}" -eq 0 ]] || + { image_deploy_fatal "Golden Image konnte nicht geladen werden: ${url}"; return 1; } + [[ "${pipe_status[1]}" -eq 0 ]] || + { image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; } + + image_deploy_restore_excluded_dirs "${target}" } # /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein @@ -166,11 +219,48 @@ image_deploy_bind_mounts() { local target="$1" local -n stack_ref="$2" - mount --bind /dev "${target}/dev" && stack_ref+=("${target}/dev") - mount -t proc proc "${target}/proc" && stack_ref+=("${target}/proc") - mount -t sysfs sysfs "${target}/sys" && stack_ref+=("${target}/sys") - mount -t devpts devpts "${target}/dev/pts" && stack_ref+=("${target}/dev/pts") - cp /etc/resolv.conf "${target}/etc/resolv.conf" + # Golden Image schliesst /dev, /proc, /sys bewusst aus + # (package_golden_image.sh --exclude=dev/sys/proc) - die Zielverzeichnisse + # existieren nach dem Entpacken deshalb noch nicht und muessen vor dem + # Bind-Mount erst angelegt werden. Live gefunden (31.08.2026, erster + # echter Lauf mit der echten Mint-Referenz-VM): ohne das schlaegt + # "mount --bind" mit "mount point does not exist" fehl - und zwar + # SILENT, weil das bisherige "cmd && stack_ref+=(...)"-Muster einen + # Fehlschlag von "cmd" unter "set -e" nicht als Statement-Fehler wertet + # (wohlbekannte Bash-Ausnahme fuer &&/||-Ketten) - der fehlende + # /dev-Bind-Mount fiel dann erst beim naechsten chroot-Aufruf auf + # ("ssh-keygen -A: Couldn't open /dev/null"), weit weg von der + # eigentlichen Ursache. Jetzt: Zielverzeichnisse anlegen, jeden + # Mount-Fehler explizit und einzeln pruefen statt auf &&-Verkettung + # zu vertrauen. + mkdir -p "${target}/dev" "${target}/proc" "${target}/sys" "${target}/dev/pts" + + mount --bind /dev "${target}/dev" || + { image_deploy_fatal "/dev konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/dev") + + mount -t proc proc "${target}/proc" || + { image_deploy_fatal "/proc konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/proc") + + mount -t sysfs sysfs "${target}/sys" || + { image_deploy_fatal "/sys konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/sys") + + mount -t devpts devpts "${target}/dev/pts" || + { image_deploy_fatal "/dev/pts konnte nicht eingebunden werden"; return 1; } + stack_ref+=("${target}/dev/pts") + + # Live gefunden (31.08.2026, erster echter Lauf mit der echten + # Mint-Referenz-VM statt eines debootstrap-Test-Images): dort ist + # /etc/resolv.conf ein von NetworkManager verwalteter Symlink (z.B. auf + # /run/NetworkManager/resolv.conf) - im gerade erst entpackten, + # ungebooteten Zielbaum zeigt der ins Leere, "cp" verweigert dann das + # Schreiben "through a dangling symlink". Ziel deshalb vorher entfernen + # (Symlink oder Datei, beides abgedeckt), erst dann kopieren. + rm -f "${target}/etc/resolv.conf" + cp /etc/resolv.conf "${target}/etc/resolv.conf" || + { image_deploy_fatal "resolv.conf konnte nicht nach ${target} kopiert werden"; return 1; } } image_deploy_unbind_mounts() { diff --git a/scripts/package_golden_image.sh b/scripts/package_golden_image.sh index 6baa771..83b10f7 100755 --- a/scripts/package_golden_image.sh +++ b/scripts/package_golden_image.sh @@ -73,11 +73,26 @@ echo "==> Packe nach ${OUTPUT_FILE}" # nicht absolute (/proc/...) - ein --exclude mit fuehrendem Slash matcht # dagegen NICHT (live am 31.08.2026 entdeckt: das lief zunaechst # unbemerkt gegen die echte Referenz-VM und haette u.a. /proc/kcore mit -# eingepackt). Ausschluesse deshalb ohne fuehrenden Slash angeben. +# eingepackt). +# +# ZWEITER FUND (31.08.2026, spaeter am selben Tag): ein --exclude OHNE +# fuehrenden Slash UND ohne "./" matcht bei GNU tar gegen den Basisnamen +# JEDER Datei/jedes Verzeichnisses im ganzen Baum, nicht nur den +# Top-Level-Eintrag - "--exclude=dev" hat dadurch auch +# kernel/drivers/net/can/dev/ (ein voellig unverwandter, aber zufaellig +# gleichnamiger Kernelmodul-Ordner) mit ausgeschlossen und damit das +# gesamte CAN-Bus-Treibermodul aus dem Archiv gerissen - live erst beim +# tatsaechlichen Deployment aufgefallen (update-initramfs scheiterte an +# fehlenden can-dev.ko.zst-Abhaengigkeiten). Ein Praefix "./" verankert +# das Muster auf den exakten Top-Level-Pfad (ein "/" im Muster laesst tar +# gegen den vollen Namen matchen statt nur den Basisnamen) - lokal mit +# einem Wegwerf-Testbaum verifiziert: ./dev/./proc/./run schliessen nur +# die Top-Level-Eintraege aus, kernel/drivers/net/can/dev/ und +# nested/proc/... bleiben erhalten. tar --numeric-owner --xattrs \ - --exclude=proc --exclude=sys --exclude=dev \ - --exclude=run --exclude=tmp --exclude=var/tmp \ - --exclude="${OUTPUT_FILE#/}" \ + --exclude=./proc --exclude=./sys --exclude=./dev \ + --exclude=./run --exclude=./tmp --exclude=./var/tmp \ + --exclude="./${OUTPUT_FILE#/}" \ -C / -cf - . | zstd -f -T0 -19 -o "${OUTPUT_FILE}" echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))"