#!/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" # Von 10_hardware.sh im selben Live-Boot geschrieben (siehe dort) - Quelle # fuer den Fingerprint, der jetzt auch auf dem installierten Geraet selbst # hinterlegt wird (siehe backend_generate_config()/postinstall.sh). HARDWARE_FILE="/run/tuxflotte/hardware/hardware.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() { # jq/envsubst(gettext-base)/cpio/kexec-tools sind 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, zunächst nur für kexec-tools behoben, hier auf alle # vier Live-only-Werkzeuge ausgeweitet). base64 kommt aus coreutils und # ist auf jedem Debian-Derivat immer vorhanden, deshalb ohne Nachinstal- # lationspfad. local live_packages_needed=() command -v jq >/dev/null 2>&1 || live_packages_needed+=(jq) command -v envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base) command -v cpio >/dev/null 2>&1 || live_packages_needed+=(cpio) command -v kexec >/dev/null 2>&1 || live_packages_needed+=(kexec-tools) if [[ "${#live_packages_needed[@]}" -gt 0 ]]; then backend_log "Werkzeuge fehlen auf dem Live-Medium, installiere nach: ${live_packages_needed[*]}" 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."; return 1; } DEBIAN_FRONTEND=noninteractive apt-get install -y "${live_packages_needed[@]}" || { backend_fatal "Installation fehlender Werkzeuge fehlgeschlagen."; return 1; } fi command -v base64 >/dev/null 2>&1 || { backend_fatal "Benötigtes Werkzeug fehlt: base64"; return 1; } [[ -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 == "object") 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}'." } # Baut einen einzelnen partman-auto/expert_recipe-Partitionsblock als # EINZEILIGEN String (Felder durch Leerzeichen statt Zeilenumbrueche # getrennt) - Debconf-Preseed-Werte mit eingebetteten Zeilenumbruechen # brechen leicht lautlos (siehe base64-Kommentar bei success_command weiter # unten fuer denselben Fallstrick an anderer Stelle), partmans Parser selbst # ist bei Leerzeichen als Trenner tolerant. _tuxflotte_partman_stanza() { local size_mb="$1" local filesystem="$2" local mountpoint="$3" local extra_flags="${4:-}" printf '%s %s %s %s %s method{ format } format{ } use_filesystem{ } filesystem{ %s } mountpoint{ %s } . ' \ "${size_mb}" "${size_mb}" "${size_mb}" "${filesystem}" "${extra_flags}" "${filesystem}" "${mountpoint}" } # Ermittelt den ersten echten Datentraeger des Zielgeraets fuer die # Groessenberechnung bei prozentualer Partitionierung (Fedoras autopart macht # implizit dieselbe Ein-Datentraeger-Annahme, siehe fedora/backend.sh). # Groessenfilter (>0) und Namensausschluss noetig - reale Systeme koennen # nbd-/zram-/loop-Geraete mit type=="disk" aber ohne echte Speicherkapazitaet # auflisten, die sonst faelschlich vor dem echten Zieldatentraeger gewaehlt # wuerden (real beim Testen entdeckt). _tuxflotte_detect_target_disk() { lsblk --nodeps --noheadings --bytes --output NAME,TYPE,SIZE --paths | awk '$2 == "disk" && $3 > 0 && $1 !~ /(nbd|zram|loop)[0-9]*$/ { print $1; exit }' } # Baut den kompletten partman-auto/expert_recipe-Rezeptkoerper aus dem # installation_directives.partitioning-Objekt. Prozentangaben werden anhand # der realen Zieldatentraegergroesse (erst hier, live auf dem Zielgeraet, # bekannt - nicht beim ISO-Bau) in feste MB-Groessen umgerechnet. Bewusst # feste Groessen (min=priority=max) statt partmans eigener proportionaler # Prioritaets-Verteilung - deterministischer und leichter zu verifizieren. _tuxflotte_render_partman_recipe() { local partitioning_json="$1" local root_filesystem="$2" local scheme local disk_device local disk_size_mb local extra_count local extra_percent_sum=0 local root_mb local recipe_body="" local i local mountpoint local filesystem local percent local size_mb local esp_mb=0 local bios_grub_mb=0 scheme="$(jq --raw-output '.scheme // "single"' <<<"${partitioning_json}")" disk_device="$(_tuxflotte_detect_target_disk)" [[ -n "${disk_device}" ]] || { backend_fatal "Zieldatenträger konnte nicht ermittelt werden."; return 1; } disk_size_mb="$(( $(blockdev --getsize64 "${disk_device}") / 1024 / 1024 ))" # Die eingebauten partman-Recipes ("atomic" etc.) legen auf UEFI-Systemen # automatisch eine EFI-System-Partition an - ein eigenes expert_recipe # muss das selbst tun, sonst warnt/verweigert der Installer (real beim # Testen entdeckt: "No EFI System Partition was found"). 512 MB vorab # reserviert, vor der Prozentaufteilung der restlichen Platte. if [[ -d /sys/firmware/efi ]]; then # Exakte Stanza-Form aus der eingebauten "atomic"-Recipe uebernommen # (/usr/lib/partman/recipes-amd64-efi/30atomic auf dem Live-Medium # ausgelesen) - eine erste eigene Vermutung ohne $reusemethod{ } und # mit $bootable{ } wurde von partman zwar anstandslos geparst, aber # nicht als gueltige EFI-System-Partition erkannt ("No EFI System # Partition was found", real beim Testen entdeckt). esp_mb=512 disk_size_mb="$(( disk_size_mb - esp_mb ))" recipe_body="${esp_mb} ${esp_mb} ${esp_mb} fat32 \$reusemethod{ } \$primary{ } method{ efi } format{ } . " else # Analoges Pendant fuer reinen BIOS-Betrieb: partman legt den # Datentraeger auch ohne EFI offenbar als GPT an (real beim Testen # bestaetigt: freier Speicher vor Partition 1 und nach der letzten # Partition ist die GPT-Kopfdaten-Signatur, kein MSDOS-Layout). GPT + # BIOS-Boot braucht eine kleine unformatierte Boot-Partition fuer den # GRUB-Core, sonst schlaegt /usr/lib/partman/check.d/08biosgrub fehl # und partman-partitioning/no_bootable_biosgrub sorgt fuer eine # Endlosschleife zurueck ins choose_partition-Menue statt # abzuschliessen (real beim Testen entdeckt und via 08biosgrub- # Quelltext auf dem Live-Medium verifiziert - "true" bei dieser Frage # bedeutet dort "Problem besteht weiterhin", nicht "trotzdem # fortfahren", anders als bei den meisten uebrigen Boolean-Fragen in # diesem Rezept). "$iflabel{ gpt }" macht die Stanza auf einem # MSDOS-Datentraeger automatisch wirkungslos. bios_grub_mb=1 disk_size_mb="$(( disk_size_mb - bios_grub_mb ))" recipe_body="${bios_grub_mb} ${bios_grub_mb} ${bios_grub_mb} free \$iflabel{ gpt } \$reusemethod{ } method{ biosgrub } . " fi case "${scheme}" in single) root_mb="$(( disk_size_mb - 1024 ))" [[ "${root_mb}" -ge 2048 ]] || { backend_fatal "Zieldatenträger ist zu klein (${disk_size_mb} MB)."; return 1; } recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${root_mb}" "${root_filesystem}" "/" '$primary{ } $bootable{ }')" ;; custom) extra_count="$(jq '.extra_partitions | length' <<<"${partitioning_json}")" [[ "${extra_count}" -gt 0 ]] || { backend_fatal "scheme=custom ohne extra_partitions angegeben."; return 1; } for ((i = 0; i < extra_count; i++)); do mountpoint="$(jq --raw-output ".extra_partitions[${i}].mountpoint" <<<"${partitioning_json}")" filesystem="$(jq --raw-output ".extra_partitions[${i}].filesystem" <<<"${partitioning_json}")" percent="$(jq --raw-output ".extra_partitions[${i}].percent" <<<"${partitioning_json}")" case "${mountpoint}" in /home|/var) ;; *) backend_fatal "Nicht unterstützter Einhängepunkt: ${mountpoint}"; return 1 ;; esac case "${filesystem}" in ext4|btrfs) ;; *) backend_fatal "Nicht unterstütztes Dateisystem: ${filesystem}"; return 1 ;; esac extra_percent_sum="$(( extra_percent_sum + percent ))" done [[ "${extra_percent_sum}" -gt 0 && "${extra_percent_sum}" -lt 90 ]] || { backend_fatal "Summe der Partitions-Prozentangaben ist ungültig: ${extra_percent_sum}"; return 1; } root_mb="$(( disk_size_mb * (100 - extra_percent_sum) / 100 - 1024 ))" [[ "${root_mb}" -ge 2048 ]] || { backend_fatal "Root-Partition wäre bei dieser Aufteilung zu klein."; return 1; } recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${root_mb}" "${root_filesystem}" "/" '$primary{ } $bootable{ }')" for ((i = 0; i < extra_count; i++)); do mountpoint="$(jq --raw-output ".extra_partitions[${i}].mountpoint" <<<"${partitioning_json}")" filesystem="$(jq --raw-output ".extra_partitions[${i}].filesystem" <<<"${partitioning_json}")" percent="$(jq --raw-output ".extra_partitions[${i}].percent" <<<"${partitioning_json}")" size_mb="$(( disk_size_mb * percent / 100 ))" recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${size_mb}" "${filesystem}" "${mountpoint}")" done ;; *) backend_fatal "Nicht unterstütztes Partitionierungsschema: ${scheme}" return 1 ;; esac printf 'tuxflotte :: %s' "${recipe_body}" } backend_generate_config() { local hostname local device_id local partitioning_json local root_filesystem 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; } # Fuer die beidseitige Identifikation (Geraeteliste <-> Geraet selbst, # siehe postinstall.sh) - aus der bereits waehrend des Live-Boots # berechneten hardware.json, nicht aus der Serverantwort (die kennt nur # die zugewiesene device_id, nicht den urspruenglichen Hardware-Hash). local device_fingerprint [[ -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}")" secure_boot_required="$(jq --raw-output '.runtime_blueprint.installation_directives.secure_boot_required' "${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 partman_recipe="$(_tuxflotte_render_partman_recipe "${partitioning_json}" "${root_filesystem}")" || return 1 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}" \ 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 # 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." # Ubiquitys eigenes "noninteractive"-Frontend (ubiquity/frontend/ # noninteractive.py, ueber das gleichnamige Boot-Keyword ausgewaehlt) # arbeitet zwar rein ueber Debconf ohne je ein Fenster zu zeichnen, aber # dessen Seite fuer die gefuehrte Partitionierung (ubi-partman.py) haengt # sich bei einem vollstaendig vorbefuellten Rezept in einer echten # Endlosschleife auf (staendiges Neuaufbauen des choose_partition-Menues, # "partman/confirm" wird nie erreicht - real ueber >800 Wiederholungen # ohne Fortschritt bestaetigt, siehe ADR-0023-Nachtrag). Ursache: die # PageNoninteractive-Klasse liefert fuer etliche vom GTK-Codepfad # benoetigte Rueckfragen (get_autopartition_choice(), get_crypto_keys()) # nur "pass"/None statt echter Werte. # # Deshalb bewusst zurueck auf "automatic-ubiquity" (echte GTK-Oberflaeche, # PageGtk-Klasse - der von Ubiquity selbst getestete, produktiv genutzte # Codepfad, auch fuer die Partitionierung). Der GTK-Assistent fuellt jede # Seite aus dem Preseed vor, wartet aber weiterhin auf einen "Weiter"-Klick # pro Seite (real verifiziert, siehe ADR-0023-Nachtrag) - dafuer laeuft # zusaetzlich autoclicker.sh (live-updates/opt/tuxflotte/scripts/), per # systemd-Service und ausgeloest durch das eigene Boot-Keyword # "tuxflotte-autoclick" (harmlos bei jedem anderen Boot ohne dieses # Keyword). "debug-ubiquity" (setzt debug="-d") macht # /var/log/installer/debug ausfuehrlicher, hilfreich bei weiterer # Fehlersuche. kexec -l "${cdrom_vmlinuz}" \ --initrd="${custom_initrd}" \ --append="boot=casper automatic-ubiquity tuxflotte-autoclick debug-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)." }