#!/usr/bin/env bash set -Eeuo pipefail # Dieses Skript wird von einem Orchestrator-Modul (40_backend.sh) per # `source` in dessen Shell geladen. Variablen bleiben deshalb bewusst nicht # readonly, um Namenskollisionen mit dem ladenden Modul zu vermeiden. # # Golden-Image-Deployment-Backend (siehe ADR-0024) - ersetzt die # Ubiquity-Automatisierung von backends/mint/ durch das curtin/FAI-Muster: # Zieldatentraeger direkt partitionieren, ein fertiges Root-Filesystem- # Image entpacken, per chroot nacharbeiten. Kein GUI-Installer, kein # Preseed/Kickstart mehr - die eigentliche Mechanik steckt in # scripts/lib/image_deploy.sh (Phase 1, isoliert live verifiziert). # # backends/mint/ bleibt unveraendert als Referenz bestehen - dieses # Backend ist ein bewusst NEUER backend_id ("mint-image"), nichts wird # live umgeschaltet. BACKEND_KEY="mint-image" BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "${BACKEND_DIR}/../.." && pwd)" POSTINSTALL_SCRIPT="${BACKEND_DIR}/postinstall.sh" IMAGE_DEPLOY_LIB="${REPO_DIR}/scripts/lib/image_deploy.sh" BOOT_MEDIUM_LIB="${REPO_DIR}/scripts/lib/boot_medium.sh" # Vom Netzwerkmodul geschuetzt abgelegtes WLAN-/Ethernet-Verbindungsprofil # (siehe export_connection_profile() in 05_network.sh) - existiert nur, # wenn das Provisionierungsinterface ueber ein aktives NetworkManager- # Profil lief (z.B. WLAN mit PSK). NETWORK_PROFILE_EXPORT="/run/tuxflotte/network/connection.nmconnection" RUNTIME_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json" SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json" HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json" RUNTIME_DIR="/run/tuxflotte/backend" CONFIG_FILE="${RUNTIME_DIR}/config.json" # Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch() # und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe # Shell/derselbe Prozess, siehe 40_backend.sh) sich denselben Baum teilen. TARGET_DIR="/target" declare -a MOUNT_STACK=() # Aus einer manuell in Proxmox installierten Referenz-VM gezogen (nicht # debootstrap - siehe ADR-0024-Nachtrag "Referenz-VM statt debootstrap", # 31.08.2026), bereinigt via scripts/package_golden_image.sh, gehostet # ueber die unauthentifizierte /golden-images/-Route in # provisioning-server (analog ks.cfg) - live verifiziert per Public-HTTPS- # Download (200, byte-exakte Groesse) am 31.08.2026. GOLDEN_IMAGE_URL="https://anode.tuxflotte.de/golden-images/linux-mint-22.3-cinnamon.tar.zst" backend_log() { printf '[backend:%s] %s\n' "${BACKEND_KEY}" "$*" >&2 } backend_fatal() { printf '[backend:%s] FEHLER: %s\n' "${BACKEND_KEY}" "$*" >&2 return 1 } # Zieldatentraeger-Erkennung - schliesst das Boot-Medium selbst aus (siehe # tuxflotte_boot_medium_disk() in scripts/lib/boot_medium.sh: auf echter # USB-Stick-Hardware enumeriert das Medium als normales "disk"-Blockgeraet, # genau wie die Zielplatte - ohne Ausschluss schlaegt "parted mklabel" # darauf IMMER fehl, weil man nicht die Platte partitionieren kann, von der # man gerade lebt). _mint_image_detect_target_disk() { local boot_medium_disk boot_medium_disk="$(tuxflotte_boot_medium_disk || true)" lsblk --nodeps --noheadings --bytes --output NAME,TYPE,SIZE --paths | awk -v exclude="${boot_medium_disk}" ' $2 == "disk" && $3 > 0 && $1 !~ /(nbd|zram|loop)[0-9]*$/ && $1 != exclude { print $1; exit } ' } backend_init() { # Historisch (bis zum Umstieg auf das eigenstaendige, per live-build # gebaute Boot-Medium) wurden diese Werkzeuge hier noch zur Laufzeit per # apt-get nachinstalliert, weil das damalige Boot-Medium (eine gepatchte # Linux-Mint-Live-ISO) sie nicht immer mitbrachte. Das eigenstaendige # Boot-Medium bringt sie bereits im Paketsatz mit (siehe # boot-medium/config/package-lists/tuxflotte.list.chroot) - hier bleibt # nur noch eine reine Assertion, damit ein kuenftiger Paketlisten-Fehler # fruh und klar auffaellt. local missing=() command -v jq >/dev/null 2>&1 || missing+=(jq) command -v envsubst >/dev/null 2>&1 || missing+=(gettext-base) command -v parted >/dev/null 2>&1 || missing+=(parted) command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools) command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs) command -v mkfs.btrfs >/dev/null 2>&1 || missing+=(btrfs-progs) command -v zstd >/dev/null 2>&1 || missing+=(zstd) command -v curl >/dev/null 2>&1 || missing+=(curl) [[ "${#missing[@]}" -eq 0 ]] || { backend_fatal "Werkzeuge fehlen auf dem Boot-Medium (Paketliste pruefen): ${missing[*]}"; return 1; } [[ -r "${IMAGE_DEPLOY_LIB}" ]] || { backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; } # shellcheck source=../../scripts/lib/image_deploy.sh source "${IMAGE_DEPLOY_LIB}" [[ -r "${BOOT_MEDIUM_LIB}" ]] || { backend_fatal "Boot-Medium-Bibliothek nicht gefunden: ${BOOT_MEDIUM_LIB}"; return 1; } # shellcheck source=../../scripts/lib/boot_medium.sh source "${BOOT_MEDIUM_LIB}" [[ -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 == "object") and (.secure_boot_required | type == "boolean") ' "${RUNTIME_BLUEPRINT_FILE}" >/dev/null || { backend_fatal "Installationszeitliche Vorgaben fehlen oder sind ungültig."; return 1; } # Phase 2 deckt bewusst nur das einfache Schema ab (ESP/biosgrub + # eine Root-Partition, siehe image_deploy_partition()) - "custom" mit # extra_partitions (/home, /var) ist noch nicht auf die neue # parted-basierte Mechanik uebertragen. Klarer Fehler statt stiller # Fehlinterpretation. local scheme scheme="$(jq --raw-output '.runtime_blueprint.installation_directives.partitioning.scheme // "single"' "${RUNTIME_BLUEPRINT_FILE}")" [[ "${scheme}" == "single" ]] || { backend_fatal "Partitionierungsschema '${scheme}' wird von diesem Backend noch nicht unterstützt (nur 'single')."; return 1; } if [[ "$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" == "true" ]]; then backend_fatal "disk_encryption=true wird von diesem 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 device_id device_fingerprint local root_filesystem partitioning_json blueprints_json [[ -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; } [[ -r "${HARDWARE_FILE}" ]] || { backend_fatal "Hardware-Erfassung nicht gefunden: ${HARDWARE_FILE}"; return 1; } device_fingerprint="$(jq --raw-output '.identity.device_fingerprint // empty' "${HARDWARE_FILE}")" [[ -n "${device_fingerprint}" ]] || { backend_fatal "Kein device_fingerprint in ${HARDWARE_FILE} gefunden."; return 1; } partitioning_json="$(jq --compact-output '.runtime_blueprint.installation_directives.partitioning' "${RUNTIME_BLUEPRINT_FILE}")" root_filesystem="$(jq --raw-output '.root_filesystem // "ext4"' <<<"${partitioning_json}")" case "${root_filesystem}" in ext4|btrfs) ;; *) backend_fatal "Nicht unterstütztes Root-Dateisystem: ${root_filesystem}"; return 1 ;; esac blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")" jq --null-input \ --arg hostname "${hostname}" \ --arg device_id "${device_id}" \ --arg device_fingerprint "${device_fingerprint}" \ --arg root_filesystem "${root_filesystem}" \ --argjson blueprints "${blueprints_json}" \ '{ hostname: $hostname, device_id: $device_id, device_fingerprint: $device_fingerprint, root_filesystem: $root_filesystem, blueprints: $blueprints }' > "${CONFIG_FILE}" || { backend_fatal "Konfigurationsdatei konnte nicht erzeugt werden."; return 1; } chmod 0600 "${CONFIG_FILE}" backend_log "Konfiguration erzeugt: ${CONFIG_FILE}" } backend_launch() { local disk is_efi root_fs boot_part root_part disk="$(_mint_image_detect_target_disk)" [[ -n "${disk}" ]] || { backend_fatal "Zieldatenträger konnte nicht ermittelt werden."; return 1; } [[ -d /sys/firmware/efi ]] && is_efi="true" || is_efi="false" backend_log "Zieldatenträger: ${disk} (Firmware: $([ "${is_efi}" = true ] && echo UEFI || echo BIOS))" root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")" backend_log "Partitioniere ${disk}" read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" || return 1 backend_log "Formatiere Partitionen" image_deploy_format "${boot_part}" "${root_part}" "${root_fs}" || return 1 backend_log "Mounte unter ${TARGET_DIR}" image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1 # Live gefunden (01.09.2026, echter Hardware-Test): export_connection_profile() # in 05_network.sh legt das aktive WLAN-/Ethernet-Profil zwar geschuetzt unter # /run/tuxflotte/network/connection.nmconnection ab, aber bis hierhin holte es # nie jemand von dort ab - das frisch installierte System stand deshalb beim # ersten Boot ohne gespeichertes WLAN-Profil da und fragte den PSK erneut ab. if [[ -f "${NETWORK_PROFILE_EXPORT}" ]]; then backend_log "Übernehme WLAN-/Netzwerkverbindungsprofil ins Zielsystem" image_deploy_install_network_profile "${TARGET_DIR}" "${NETWORK_PROFILE_EXPORT}" || return 1 fi # 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 backend_log "Binde /dev, /proc, /sys ein" image_deploy_bind_mounts "${TARGET_DIR}" MOUNT_STACK || return 1 backend_log "chroot-Fixup (machine-id, SSH-Hostkeys, initramfs)" image_deploy_chroot_fixup "${TARGET_DIR}" || return 1 backend_log "Installiere Bootloader" image_deploy_install_bootloader "${TARGET_DIR}" "${disk}" "${is_efi}" || return 1 local hostname hostname="$(jq --raw-output '.hostname' "${CONFIG_FILE}")" backend_log "Setze Hostname (${hostname})" image_deploy_set_hostname "${TARGET_DIR}" "${hostname}" || return 1 backend_log "Deployment abgeschlossen." } backend_postinstall() { local device_id device_fingerprint blueprints_json local postinstall_rendered device_id="$(jq --raw-output '.device_id' "${CONFIG_FILE}")" device_fingerprint="$(jq --raw-output '.device_fingerprint' "${CONFIG_FILE}")" blueprints_json="$(jq --compact-output '.blueprints' "${CONFIG_FILE}")" # Dasselbe Template wie backends/mint/postinstall.sh (per Symlink # geteilt, siehe Verzeichnis) - rein distributionsunabhaengiges # Bash-Skript (curl/jq gegen anode), hier per chroot statt per # ubiquity/success_command ausgefuehrt. postinstall_rendered="$( TUXFLOTTE_DEVICE_ID="${device_id}" \ TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \ TUXFLOTTE_DEVICE_FINGERPRINT="${device_fingerprint}" \ envsubst '${TUXFLOTTE_DEVICE_ID} ${TUXFLOTTE_BLUEPRINTS_JSON} ${TUXFLOTTE_DEVICE_FINGERPRINT}' \ <"${POSTINSTALL_SCRIPT}" )" if grep -q '\${TUXFLOTTE_' <<<"${postinstall_rendered}"; then backend_fatal "postinstall.sh enthält nach envsubst nicht aufgelöste Platzhalter." return 1 fi printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh" chmod 0700 "${TARGET_DIR}/tmp/postinstall.sh" # Live gefunden (31.08.2026, erster vollstaendig durchgelaufener # End-to-End-Test): postinstall.sh braucht jq (+curl), aber die echte # Mint-Referenz-VM bringt das nicht zwingend mit (anders als das # Boot-Medium selbst, das jq ja schon vorinstalliert hat - das hilft # dem ausgerollten Zielsystem hier nichts, das ist ein komplett # eigener chroot). python3 wird nicht von postinstall.sh selbst # gebraucht, aber vom heruntergeladenen agent.py nach dem naechsten # Boot - hier gleich mit absichern, um nicht noch einen ganzen # Referenz-VM-Neupack-Zyklus wegen eines einzelnen fehlenden Pakets zu # brauchen. package_golden_image.sh leert ausserdem /var/lib/apt/lists # als Teil der Bereinigung - "apt-get update" ist deshalb hier noetig, # bevor "apt-get install" ueberhaupt Pakete finden kann. Netzwerk ist # im chroot verfuegbar (resolv.conf wurde schon in # image_deploy_bind_mounts kopiert, dieselben Bind-Mounts sind noch # aktiv). chroot "${TARGET_DIR}" bash -c ' missing=() command -v jq >/dev/null 2>&1 || missing+=(jq) command -v curl >/dev/null 2>&1 || missing+=(curl) command -v python3 >/dev/null 2>&1 || missing+=(python3) [[ "${#missing[@]}" -eq 0 ]] && exit 0 apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}" ' || { backend_fatal "jq/curl/python3 konnten im Zielsystem nicht sichergestellt werden."; return 1; } backend_log "Führe Postinstall-Skript im chroot aus." chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh || { backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; } rm -f "${TARGET_DIR}/tmp/postinstall.sh" backend_log "Hänge Ziel-Dateisystem aus." image_deploy_unbind_mounts MOUNT_STACK umount --recursive "${TARGET_DIR}" || { backend_fatal "${TARGET_DIR} konnte nicht ausgehängt werden."; return 1; } # Nutzer-Feedback (01.09.2026, echter Hardware-Test): akustischer Hinweis # + klare Ansage, dass der USB-Stick jetzt gefahrlos gezogen werden kann # (ab hier wird nur noch von der Zielplatte gebootet) - Bestaetigung mit # ENTER, sonst automatischer Neustart nach 5 Minuten (kein Warten auf # eine Person, die vielleicht schon weitergegangen ist). "\a" (BEL) statt # eines externen Tonwerkzeugs - funktioniert auf jeder PC-Lautsprecher- # Hardware ohne zusaetzliches Paket. printf '\a\a\a' echo echo "Fertig! Sie können den USB-Stick jetzt entfernen." echo "Weiter mit ENTER, oder automatischer Neustart in 5 Minuten." read -r -t 300 _ || true backend_log "Starte neu - kein Rücksprung erwartet, ab hier läuft das frisch installierte System." reboot }