Thomas Stallinger 385a42034a device_fingerprint zusaetzlich lokal auf dem Geraet hinterlegen
Siehe ADR-0022 (platform-docs). Beide Backends lesen den bereits waehrend
des Live-Boots berechneten Fingerprint aus hardware.json und schreiben ihn
nach /etc/tuxflotte/device_fingerprint - ermoeglicht beidseitige
Identifikation (Geraeteliste <-> Geraet selbst), vorher nur einseitig
moeglich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:36:07 +02:00

379 lines
17 KiB
Bash

#!/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
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{ } . "
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."
kexec -l "${cdrom_vmlinuz}" \
--initrd="${custom_initrd}" \
--append="boot=casper automatic-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)."
}