Thomas Stallinger fa54461cae mint-image: Boot-Medium-Platte von Zielplatten-Erkennung ausschliessen
Auf echter USB-Stick-Hardware (anders als in bisherigen QEMU-Tests mit
CD-ROM-Boot-Medium) enumeriert das Boot-Medium selbst als "disk"-Typ,
genau wie die interne Zielplatte - _mint_image_detect_target_disk()
konnte deshalb den eigenen Stick waehlen, dessen Partitionierung dann
immer mit dem "unable to inform the Kernel of the change"-Fehler
scheitert (man kann die Platte nicht partitionieren, von der man lebt).

_mint_image_boot_medium_disk() findet das Boot-Medium ueber
findmnt/lsblk (dem Live-Mountpoint /run/live/medium bis zur
Elternplatte folgend) und schliesst es aus der Kandidatenliste aus.

Verifiziert in QEMU: Boot-Medium-ISO absichtlich als device='disk' auf
hda (vor dem eigentlichen Ziel-qcow2 auf hdb) angehaengt, um die reale
Aufzaehlungsreihenfolge einer USB-Stick-Installation nachzustellen -
ohne diesen Fix waere hda (das Boot-Medium) gewaehlt worden. Mit dem
Fix: "Zieldatentraeger: /dev/sdb", Partitionierung/Formatierung ohne
Fehler, komplette Golden-Image-Bereitstellung durchgelaufen, Reboot in
den echten Linux-Mint-Cinnamon-Login-Bildschirm bestaetigt (Hostname
"debian", Login "tuxflotte", deutsches Tastaturlayout).
2026-09-01 13:29:45 +02:00

341 lines
15 KiB
Bash

#!/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"
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
}
# Live auf echter USB-Stick-Hardware gefunden (01.09.2026): auf einem
# CD-ROM (wie in allen bisherigen QEMU-Tests) ist das Boot-Medium ein
# eigener Geraetetyp ("rom"), der von der lsblk-Filterung unten schon
# ausgeschlossen wird - auf einem echten USB-Stick ist das Boot-Medium
# selbst aber ein ganz normales "disk"-Blockgeraet, genau wie die interne
# Zielplatte. Ohne Ausschluss kann die Erkennung darunter den Stick selbst
# waehlen (abhaengig von der Aufzaehlungsreihenfolge) - "parted mklabel"
# darauf schlaegt dann IMMER fehl ("...have been unable to inform the
# Kernel of the change, probably because it/they are in use"), weil man
# nicht die Platte partitionieren kann, von der man gerade lebt. Der
# vorherige Fix (Zielplatte freigeben + Neueinlesen wiederholen) half
# deshalb nicht - das war die falsche Diagnose fuer dieses Problem.
_mint_image_boot_medium_disk() {
local medium_source parent
medium_source="$(findmnt --noheadings --output SOURCE /run/live/medium 2>/dev/null)" || return 1
[[ -n "${medium_source}" ]] || return 1
parent="$(lsblk --noheadings --output PKNAME "${medium_source}" 2>/dev/null | head -n1)"
[[ -n "${parent}" ]] || return 1
printf '/dev/%s\n' "${parent}"
}
# Analog zu _tuxflotte_detect_target_disk() in backends/mint/backend.sh -
# bewusst hier dupliziert statt geteilt, um dieses Backend unabhaengig vom
# Mint-Referenzbackend zu halten (siehe Modul-Kommentar oben). Ein Umzug in
# eine gemeinsame lib waere ein sinnvolles spaeteres Aufraeumen, sobald
# mehr als zwei Backends dieselbe Logik brauchen.
_mint_image_detect_target_disk() {
local boot_medium_disk
boot_medium_disk="$(_mint_image_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 "${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 (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; }
backend_log "Starte neu - kein Rücksprung erwartet, ab hier läuft das frisch installierte System."
reboot
}