Compare commits

..

7 Commits

Author SHA1 Message Date
d179f9c2f5 chore(boot-medium): Phase 4 - alte Ubiquity-Ära-Dateien entfernen, build.sh auf Fedora reduzieren
Entfernt (Historie bleibt ueber Git-Log erhalten, siehe ADR-0025 in
platform-docs fuer die vollstaendige Begruendung/den Gesamtrueckblick):
- backends/mint/ (Ubiquity-Preseed-Backend, backend_launch()-Pfad nicht
  mehr referenziert seit Phase 1, postinstall.sh dort bereits migriert)
- scripts/lib/initrd.sh, initrd-hooks/ (Casper-Bottom-Hook-Injection,
  nur fuer das Patchen einer fremden fertigen Mint-ISO noetig)
- grub/mint-boot-grub.cfg, grub/mint-isolinux-live.cfg
- live-updates/ (Cinnamon-Kiosk, AT-SPI-Autoklicker)

scripts/build.sh (das alte Test-Build-Skript) bedient jetzt nur noch das
Fedora-Backend (Kickstart/Anaconda, unveraendert, ausserhalb dieses
Plans) - Mint-Zweig entfernt (bake_test_preseed(), patch_initrd(),
initrd.sh-Source, mint-spezifische GRUB-Pfade). Die unbedingte
live-updates/-Kopie in prepare_updates() entfiel ebenfalls - sie wurde
fuer Fedora nie inhaltlich gebraucht (Anaconda/Kickstart kennt kein
Cinnamon-Kiosk/keinen AT-SPI-Autoklicker), nur bisher blind mitkopiert.

README.md um einen Abschnitt zum neuen Boot-Medium-Bauweg ergaenzt
(build_boot_medium.sh/build_customer_iso.sh vs. weiterhin
Fedora-spezifisches build.sh).
2026-09-01 09:22:19 +02:00
b9ccae6e85 fix(boot-medium): jq/curl/python3 im Zielsystem vor postinstall.sh sicherstellen
Fuenfter und letzter live gefundener Bug auf dem Weg zum ersten
vollstaendig erfolgreichen End-to-End-Deployment: die echte
Mint-Referenz-VM bringt jq nicht mit (anders als das Boot-Medium selbst,
das hilft dem chroot-Zielsystem aber nicht - komplett eigener Kontext).
postinstall.sh scheiterte deshalb an "jq: command not found". curl/python3
gleich mit abgesichert (python3 wird vom heruntergeladenen agent.py nach
dem naechsten Boot gebraucht), um nicht noch einen ganzen
Referenz-VM-Neupack-Zyklus wegen eines weiteren fehlenden Pakets zu
riskieren. apt-get update noetig, da package_golden_image.sh
/var/lib/apt/lists als Teil der Bereinigung leert.

Damit lief der komplette Self-Service-Flow zum ersten Mal vollstaendig
durch: Enrollment-Session -> personalisiertes ISO -> Boot -> Partition/
Format/Mount -> Golden-Image-Stream-Download+Extraktion -> chroot-Fixup
-> Bootloader -> Postinstall/Agent-Bootstrap -> Reboot -> echter
Linux-Mint-Cinnamon-Login-Bildschirm (Hostname "debian", deutsches
Tastaturlayout aktiv). agent_secret_issued_at in der Datenbank bestaetigt
die erfolgreiche Agent-Registrierung bei anode.

Damit sind alle vier Phasen des Boot-Medium-Plans live end-to-end
verifiziert. Voraussetzung dafuer: das golden-image-Archiv auf anode
wurde mit dem korrigierten package_golden_image.sh (vorheriger Commit)
neu gepackt (4,8 GB statt 2,3 GB, jetzt mit den vorher fehlenden
Kernelmodulen) und live ersetzt - alte Version als
linux-mint-22.3-cinnamon.tar.zst.defekt-31.08.bak gesichert.
2026-08-31 18:25:24 +02:00
27d6fa0f47 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.
2026-08-31 16:36:35 +02:00
95447f7486 feat(boot-medium): Begrüßungsbanner (Tuxflotte-Schriftzug + Flottengrafik)
80-Spalten-ASCII-Banner (pics/tuxflotte-terminal-80.txt, lose Asset-Datei,
nicht versioniert - pics/ ist kein Git-Repo) wird beim Boot vor
installer.sh angezeigt, sowohl im Dev- als auch im Produktiv-Build.
Schriftzug frisch per figlet (Font "big") erzeugt, passt sauber in 80
Spalten. Grafik-Teil aus der bestehenden 160-Spalten-Vorlage per
Spalten-Verdichtung (2:1) abgeleitet, da die Original-SVG-Logo-Konturen
(reine offene Pfade ohne Fill) bei 80 Spalten direkt aus dem Vektor
konvertiert nur noch vereinzelte Punkte ergeben - live geprueft, siehe
Konversation.

boot-autostart.sh zeigt das Banner nur wenn lesbar (if statt &&, sonst
wuerde ein fehlendes Banner das Skript unter set -e sofort beenden).

Live verifiziert in der enterprise-QEMU-VM: Banner erscheint korrekt vor
[1/12] Vorbereitung, passt vollstaendig in die 80-Spalten-Konsole.
2026-08-31 14:57:09 +02:00
bc13169418 feat(boot-medium): systemd-Autostart, Fortschrittsanzeige, SSH-Dev-Modus (Phase 2)
Neuer Service tuxflotte-installer.service startet installer.sh beim Boot
direkt auf tty1 (Type=idle, Conflicts=getty@tty1.service) - kein manuelles
Anmelden/sudo mehr noetig, egal ob Auto-Modus oder interaktiver
Test-/Entwicklungslauf. Wrapper boot-autostart.sh spiegelt die Ausgabe
zusaetzlich nach /var/log/tuxflotte-installer.log.

log_step() (scripts/lib/logging.sh) gibt vor jedem Modul einen klar
abgesetzten "[n/12] <Label>"-Block aus - gilt fuer Dev- UND Produktiv-Build
gleichermassen (die tty-Konsole ist die einzige UI, die eine Person vor
dem Geraet sieht, kein Kiosk-Browser mehr).

scripts/build_boot_medium.sh buendelt lb clean/config/build (loeste die
bisherigen Hand-Aufrufe ab) und steuert per --dev-Flag einen SSH-
Debug-Zugang (tuxflotte/test123) - ohne --dev bleibt SSH deaktiviert,
keine gebackenen Zugangsdaten (Produktiv-Default).

Zwei echte, live gefundene Bugs unterwegs behoben:
- Debians trixie-sshd_config deaktiviert PasswordAuthentication per
  Default - fuer den Dev-Testzugang explizit wieder aktiviert.
- Ein ExecStopPost, der getty@tty1.service nach Dienstende neu startet,
  ist unzuverlaessig (Race mit dem eigenen TTY-Teardown, Job wurde
  angestossen, blieb aber inactive/dead). Geloest durch einen
  deterministischen Fallback in boot-autostart.sh selbst: die Konsole
  faellt nach installer.sh in eine interaktive Shell statt auf einen
  eventuell nie zurueckkehrenden getty zu warten.

Live verifiziert in der enterprise-QEMU-VM (zwei frische Enrollment-
Sessions, echte anode-Aktivierung, "Bekanntes Geraet erkannt"-Pfad):
Boot laeuft ohne jede manuelle Anmeldung direkt in installer.sh, alle
12 Fortschritts-Banner erscheinen korrekt nummeriert, Commit-Gate und
kontrollierter Abbruch funktionieren, SSH-Login mit Passwort
funktioniert, Fallback-Shell nach Programmende reagiert auf Eingaben.
2026-08-31 14:40:39 +02:00
2190d6f403 feat(boot-medium): Tuxflotte-Nutzlast ins Boot-Medium einbauen (Phase 1)
scripts/ (installer.sh, lib/*, modules/00-99) und backends/mint-image/
(inkl. echtem postinstall.sh statt Symlink auf backends/mint/) werden ueber
config/includes.chroot/opt/tuxflotte/ ins Image kopiert - bewusst nur die
zur Laufzeit benoetigten Dateien, nicht die Build-Host-Werkzeuge
(build_customer_iso.sh, build_golden_image.sh, package_golden_image.sh,
lib/initrd.sh etc. bleiben aussen vor).

00_preflight.sh und backend_init() (backends/mint-image/backend.sh)
verlieren ihre Laufzeit-apt-get-Nachinstallation (jq, parted, dosfstools,
e2fsprogs, zstd, btrfs-progs, gettext-base, curl) - alles bereits in
Phase 0 vorinstalliert. Werden durch reine Assertions ersetzt, die frueh
und klar melden, falls die Paketliste doch mal luecken sollte.

backends/mint/postinstall.sh nach backends/mint-image/postinstall.sh als
echte Datei verschoben (war Symlink) - noetig, weil backends/mint/ in
Phase 4 komplett entfernt wird.

Live verifiziert in der enterprise-QEMU-VM: installer.sh von Hand
gestartet, Module 00/05/10/12/15/17 liefen sauber durch, inkl. echtem
Server-Kontakt zu anode und echter Geraeteregistrierung (Liebherr-Org),
Commit-Gate erschien interaktiv wie erwartet, kontrollierter Abbruch ohne
jede destruktive Aktion.
2026-08-31 13:52:38 +02:00
ff4f48432e feat(boot-medium): live-build-Grundgerüst für unabhängiges Boot-Medium (Phase 0)
lb config für Debian trixie/amd64, iso-hybrid, GRUB-EFI+syslinux. Paketliste
(tuxflotte.list.chroot) übernimmt die Hardware-/WLAN-Abdeckung aus
build_golden_image.sh, ergänzt um btrfs-progs sowie die bisher erst zur
Laufzeit nachinstallierten Werkzeuge (jq, gettext-base, dosfstools,
e2fsprogs, zstd).

Ersetzt langfristig das Patchen der echten Mint-Live-ISO
(build_customer_iso.sh + Casper-Hook-Injection) - siehe Plan
~/.claude/plans/inherited-floating-treasure.md und Memory
project_independent_boot_medium.md. Auslöser: ein live gefundener Bug, bei
dem statt der direkten Partitionier-Mechanik unerwartet Ubiquity hochkam,
weil das Boot-Medium bislang weiterhin die echte, Ubiquity-tragende
Mint-ISO war.

Noch kein lauffähiges Image - nur das live-build-Grundgerüst, Bau + Boot-
Test folgt.
2026-08-31 12:40:19 +02:00
94 changed files with 4027 additions and 2029 deletions

18
.gitignore vendored
View File

@ -7,6 +7,24 @@ work/
output/ output/
build/ build/
# live-build (boot-medium/) own working/cache/output directories - only
# boot-medium/config/ and boot-medium/auto/ are actual source, everything else
# is regenerated by `lb build`
boot-medium/.build/
boot-medium/cache/
boot-medium/chroot/
boot-medium/binary*/
boot-medium/*.iso
boot-medium/*.img
boot-medium/*.contents
boot-medium/*.files
boot-medium/*.packages
boot-medium/*.zsync
boot-medium/binary.modified_timestamps
boot-medium/config/includes.chroot/etc/tuxflotte-dev-build
boot-medium/chroot.packages.install
boot-medium/chroot.packages.live
# Editor/system files # Editor/system files
*~ *~
.DS_Store .DS_Store

View File

@ -14,3 +14,24 @@ Ziele:
- lokaler Festplattenstart - lokaler Festplattenstart
Die eigentliche Provisionierung erfolgt über den Tuxflotte Provisioning Server. Die eigentliche Provisionierung erfolgt über den Tuxflotte Provisioning Server.
## Boot-Medium (Mint)
Das Mint-Boot-Medium ist ein eigenständiges, per Debian `live-build`
gebautes Image (`boot-medium/`, siehe ADR-0025) - kein gepatchter
Distributions-Installer mehr. Es enthält nur Tuxflottes eigenen,
headless-tauglichen Code (Module `scripts/modules/00`-`40`,
`backends/mint-image/`) und bootet direkt in einen systemd-Dienst, der
`installer.sh` startet.
- `scripts/build_boot_medium.sh [--dev]` baut das Basis-Image
(`boot-medium/live-image-amd64.hybrid.iso`). `--dev` aktiviert einen
SSH-Debug-Zugang (`tuxflotte`/`test123`); ohne `--dev` bleibt SSH
deaktiviert (Produktiv-Default).
- `scripts/build_customer_iso.sh <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]`
personalisiert ein bereits gebautes Basis-Image für eine konkrete
Kundenorganisation (Aktivierungscode + WLAN-Zugangsdaten).
Fedora nutzt weiterhin das ältere Kickstart/Anaconda-Muster
(`scripts/build.sh <source.iso> fedora`) - noch nicht auf dasselbe
Boot-Medium-Muster umgestellt.

View File

@ -28,7 +28,6 @@ HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
RUNTIME_DIR="/run/tuxflotte/backend" RUNTIME_DIR="/run/tuxflotte/backend"
CONFIG_FILE="${RUNTIME_DIR}/config.json" 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() # Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch()
# und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe # und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe
@ -64,27 +63,27 @@ _mint_image_detect_target_disk() {
} }
backend_init() { backend_init() {
local live_packages_needed=() # 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 || live_packages_needed+=(jq) command -v jq >/dev/null 2>&1 || missing+=(jq)
command -v envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base) command -v envsubst >/dev/null 2>&1 || missing+=(gettext-base)
command -v parted >/dev/null 2>&1 || live_packages_needed+=(parted) command -v parted >/dev/null 2>&1 || missing+=(parted)
command -v mkfs.vfat >/dev/null 2>&1 || live_packages_needed+=(dosfstools) command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools)
command -v mkfs.ext4 >/dev/null 2>&1 || live_packages_needed+=(e2fsprogs) command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs)
command -v zstd >/dev/null 2>&1 || live_packages_needed+=(zstd) command -v mkfs.btrfs >/dev/null 2>&1 || missing+=(btrfs-progs)
command -v curl >/dev/null 2>&1 || live_packages_needed+=(curl) command -v zstd >/dev/null 2>&1 || missing+=(zstd)
command -v curl >/dev/null 2>&1 || missing+=(curl)
if [[ "${#live_packages_needed[@]}" -gt 0 ]]; then [[ "${#missing[@]}" -eq 0 ]] ||
backend_log "Werkzeuge fehlen auf dem Live-Medium, installiere nach: ${live_packages_needed[*]}" { backend_fatal "Werkzeuge fehlen auf dem Boot-Medium (Paketliste pruefen): ${missing[*]}"; return 1; }
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
[[ -r "${IMAGE_DEPLOY_LIB}" ]] || [[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; } { backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
@ -100,7 +99,7 @@ backend_init() {
--group=root \ --group=root \
"${RUNTIME_DIR}" "${RUNTIME_DIR}"
rm -f -- "${CONFIG_FILE}" "${GOLDEN_IMAGE_FILE}" rm -f -- "${CONFIG_FILE}"
backend_log "Initialisiert." backend_log "Initialisiert."
} }
@ -203,11 +202,6 @@ backend_launch() {
root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")" 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}" backend_log "Partitioniere ${disk}"
read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" || read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" ||
return 1 return 1
@ -218,9 +212,15 @@ backend_launch() {
backend_log "Mounte unter ${TARGET_DIR}" backend_log "Mounte unter ${TARGET_DIR}"
image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1 image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1
backend_log "Entpacke Golden Image" # Live gefunden (31.08.2026, erster echter End-to-End-Lauf mit dem vollen
image_deploy_extract_image "${GOLDEN_IMAGE_FILE}" "${TARGET_DIR}" || return 1 # Referenz-VM-Archiv): "erst nach /run/tuxflotte/... herunterladen, dann
rm -f "${GOLDEN_IMAGE_FILE}" # 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" backend_log "Schreibe fstab"
image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1 image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1
@ -270,6 +270,31 @@ backend_postinstall() {
printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh" printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh"
chmod 0700 "${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." backend_log "Führe Postinstall-Skript im chroot aus."
chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh || chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh ||
{ backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; } { backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; }

View File

@ -1 +0,0 @@
../mint/postinstall.sh

View File

@ -0,0 +1,72 @@
#!/bin/bash
# Lesbare Referenzfassung des Agent-Bootstraps, den backend_generate_config()
# in backend.sh zur Laufzeit envsubst-auflöst und anschließend base64-kodiert
# in preseed.tpls ubiquity/success_command einsetzt (siehe backend.sh). Diese
# Datei selbst wird nie direkt ausgeführt - sie existiert, damit der Code
# lesbar bleibt statt nur als Base64-Blob im Preseed zu existieren.
#
# Inhaltlich das Bash-Pendant zu backends/fedora/kickstart.tpl %post: gleiche
# curl/jq-Aufrufe, nur eingebettet über ubiquity/success_command (in-target,
# chrooted) statt Kickstart %post.
tuxflotte_agent_fatal() {
echo "tuxflotte: Provisioning-Agent-Einrichtung fehlgeschlagen: $*" >> /var/log/tuxflotte-postinstall.log
exit 1
}
ANODE_URL="https://anode.tuxflotte.de"
AGENT_REPO_RAW="https://git.tuxflotte.de/admin/provisioning-agent/raw/branch/main"
install -d -m 0700 /etc/tuxflotte ||
tuxflotte_agent_fatal "Verzeichnis /etc/tuxflotte konnte nicht angelegt werden."
# Identifikation soll in beide Richtungen moeglich sein: die Geraeteliste
# zeigt den Fingerprint bereits an (siehe geraete_liste.html), aber bislang
# gab es auf dem installierten Geraet selbst keine Datei, um ihn mit einem
# einfachen "cat" gegenzupruefen - build_device_fingerprint() (10_hardware.sh)
# berechnet ihn nur einmalig waehrend des Live-Boots und haelt ihn sonst
# nirgends fest. Absichtlich Klartext, kein Secret - reiner Hardware-Hash,
# kein chmod 0600 noetig wie bei agent.credentials.
echo "${TUXFLOTTE_DEVICE_FINGERPRINT}" > /etc/tuxflotte/device_fingerprint ||
tuxflotte_agent_fatal "device_fingerprint konnte nicht abgelegt werden."
cat > /etc/tuxflotte/runtime_blueprint.json <<'RUNTIME_BLUEPRINT_EOF'
${TUXFLOTTE_BLUEPRINTS_JSON}
RUNTIME_BLUEPRINT_EOF
install -d /opt/tuxflotte/agent ||
tuxflotte_agent_fatal "Verzeichnis /opt/tuxflotte/agent konnte nicht angelegt werden."
curl --silent --show-error --fail --location \
--output /opt/tuxflotte/agent/agent.py \
"${AGENT_REPO_RAW}/agent.py" ||
tuxflotte_agent_fatal "agent.py konnte nicht von ${AGENT_REPO_RAW} geladen werden."
curl --silent --show-error --fail --location \
--output /etc/systemd/system/tuxflotte-agent.service \
"${AGENT_REPO_RAW}/tuxflotte-agent.service" ||
tuxflotte_agent_fatal "tuxflotte-agent.service konnte nicht von ${AGENT_REPO_RAW} geladen werden."
AGENT_BOOTSTRAP_RESPONSE="$(
curl --silent --show-error --fail --location \
--header 'Content-Type: application/json' \
--data-binary "{\"device_id\": \"${TUXFLOTTE_DEVICE_ID}\"}" \
"${ANODE_URL}/api/v1/agent/bootstrap"
)" ||
tuxflotte_agent_fatal "Bootstrap-Aufruf gegen ${ANODE_URL} ist fehlgeschlagen."
jq --exit-status '.success == true' <<<"${AGENT_BOOTSTRAP_RESPONSE}" >/dev/null ||
tuxflotte_agent_fatal "Server hat den Bootstrap abgelehnt: ${AGENT_BOOTSTRAP_RESPONSE}"
jq --null-input \
--arg device_id "${TUXFLOTTE_DEVICE_ID}" \
--argjson response "${AGENT_BOOTSTRAP_RESPONSE}" \
'{device_id: $device_id, agent_secret: $response.agent_secret}' \
> /etc/tuxflotte/agent.credentials ||
tuxflotte_agent_fatal "Credentials-Datei konnte nicht erzeugt werden."
chmod 0600 /etc/tuxflotte/agent.credentials
systemctl enable tuxflotte-agent.service ||
tuxflotte_agent_fatal "systemd-Dienst tuxflotte-agent konnte nicht aktiviert werden."
echo "tuxflotte: Runtime Blueprint unter /etc/tuxflotte/runtime_blueprint.json hinterlegt." >> /var/log/tuxflotte-postinstall.log
echo "tuxflotte: Provisioning-Agent installiert, registriert und für den ersten Boot aktiviert." >> /var/log/tuxflotte-postinstall.log

View File

@ -1,454 +0,0 @@
#!/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
local root_extra_flags
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{ } . "
# Root NICHT zusaetzlich $bootable{ } markieren wie im BIOS-Zweig
# unten (real beim Testen entdeckt, 29.08.2026): auf UEFI traegt
# bereits die ESP-Stanza oben die eigentliche Boot-Kennzeichnung
# (method{ efi }) - das im Original-Rezept
# /usr/lib/partman/recipes-amd64-efi/30atomic (Quelle der
# ESP-Stanza) uebernommene Wurzel-Partitionsschema markiert die
# Root-Partition dort ebenfalls NICHT als bootable. Ein
# zusaetzliches $bootable{ } auf der Root-Partition scheint partman
# in einen Zustand zu bringen, den es nach dem Commit staendig neu
# bewerten will: der Installer klickte "Jetzt installieren"
# nachweislich korrekt (Debug-Log erreichte sogar
# "grub-installer/bootdev seen"), sprang aber danach immer wieder
# zurueck auf dieselbe Partitionierungsseite (ubi-partman.py:
# rebuild_cache(), ausgeloest durch die wiederholt gestellte
# Debconf-Frage "ubiquity/partman-rebuild-cache" aus
# /lib/partman/update.d/99signal_ubiquity). Noch nicht abschliessend
# verifiziert, ob dies die alleinige Ursache ist - siehe
# ADR-0023-Nachtrag.
root_extra_flags='$primary{ }'
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 } . "
# Im BIOS-Zweig (anders als EFI oben) bleibt $bootable{ } auf der
# Root-Partition noetig - hier gibt es keine ESP, die diese Rolle
# uebernimmt.
root_extra_flags='$primary{ } $bootable{ }'
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}" "/" "${root_extra_flags}")"
;;
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}" "/" "${root_extra_flags}")"
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.
# username=/hostname=mint bewusst ergaenzt (real beim Testen entdeckt,
# 29.08.2026): ohne diese beiden Parameter faellt casper auf dem
# kexec-Boot auf einen anderen Live-Account-Zustand zurueck als beim
# ersten Boot (der die grub.cfg-Vorlage explizit mit "username=mint
# hostname=mint" startet) - konkret verlangt der Konsolenlogin auf
# diesem zweiten Boot ein echtes Passwort statt des sonst leeren
# Live-Session-Passworts. Fuer den eigentlichen Auto-Install-Ablauf
# (GTK-Assistent unter ubiquity-dm, kein Konsolenlogin noetig)
# folgenlos, aber inkonsistent gegenueber dem ersten Boot und erschwert
# die Fehlersuche via Konsole unnoetig - deshalb hier angeglichen.
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 username=mint hostname=mint 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)."
}

View File

@ -1,29 +0,0 @@
#!/bin/bash
{
echo "tuxflotte: early_command lief, $(date -u)"
echo "--- ip link ---"
ip link 2>&1
echo "--- nmcli device status (vorher) ---"
nmcli device status 2>&1
echo "--- WLAN verbinden (mit Wiederholung, falls Treiber noch nicht bereit) ---"
for i in 1 2 3 4 5 6; do
nmcli device wifi rescan 2>&1
sleep 2
if nmcli device wifi connect stallinux password a987654321 2>&1; then
echo "WLAN verbunden nach Versuch $i"
break
fi
echo "Versuch $i fehlgeschlagen, warte..."
sleep 5
done
echo "--- nmcli device status (nachher) ---"
nmcli device status 2>&1
echo "--- curl anode /health ---"
curl -m 10 -sS https://anode.tuxflotte.de/health 2>&1
echo
echo "--- debconf-set-selections Test (simuliert dynamisch von anode geholten Wert) ---"
echo "d-i netcfg/get_hostname string tuxflotte-early-test" | debconf-set-selections
echo "debconf-set-selections exit: $?"
echo "--- Verifikation per debconf-communicate ---"
echo "GET netcfg/get_hostname" | debconf-communicate 2>&1
} >> /var/log/tuxflotte-early-command.log 2>&1

View File

@ -1,137 +0,0 @@
### Tuxflotte Auto-Install Preseed fuer Linux Mint (Ubiquity/Debian-Installer).
### Liegt direkt auf dem Medium (file=/cdrom/preseed/tuxflotte.seed), keine
### Netz-Zustellung noetig -- analog zu Fedoras inst.ks=cdrom:/ks.cfg.
###
### d-i preseed/early_command wird bewusst NICHT verwendet -- unter Ubiquity
### bestaetigt wirkungslos (Ubiquity nutzt eigene Python-Plugins statt der
### klassischen Debian-Installer-Komponenten, an die early_command haengt).
d-i debian-installer/locale string de_DE.UTF-8
d-i keyboard-configuration/xkb-keymap select de
d-i keyboard-configuration/layoutcode string de
d-i netcfg/get_hostname string ${TUXFLOTTE_HOSTNAME}
d-i netcfg/get_domain string unassigned-domain
# Lab-Bootstrap-Zugangsdaten. Ersetzt ein noch fehlendes Secret-Reference-Modell
# (siehe 09-data-model-v1.md) und darf nicht als Produktionsmechanismus gelten.
d-i passwd/user-fullname string Tuxflotte
d-i passwd/username string tuxflotte
d-i passwd/user-password password test123
d-i passwd/user-password-again password test123
d-i user-setup/allow-password-weak boolean true
d-i clock-setup/utc boolean true
d-i time/zone string Europe/Berlin
d-i clock-setup/ntp boolean true
d-i partman-auto/method string regular
d-i partman-auto/expert_recipe string ${TUXFLOTTE_PARTMAN_RECIPE}
d-i partman-auto/choose_recipe select tuxflotte
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman-partitioning/confirm_new_label boolean true
# "partman/choose_partition select finish" wurde bewusst entfernt: das ist
# die Frage des MANUELLEN/erweiterten Partitionierers ("Menu" -> "Finish
# partitioning"), keine des gefuehrten/automatischen Ablaufs. Direktes
# Preseeden ohne echten Seitenaufbau (weder im GTK- noch im
# noninteractive-Frontend) fuehrt in ubi-partman.py real reproduzierbar zu
# einer Endlosschleife im internen "building_cache"-Zustandsautomaten der
# choose_partition-Verarbeitung (ueber 100.000 Debconf-Zeilen in wenigen
# Sekunden ohne echten Fortschritt, sowohl mit automatic-ubiquity/GTK als
# auch mit dem verworfenen noninteractive-Frontend - siehe ADR-0023-Nachtrag).
# Ohne diese Zeile uebernehmen partman-auto/method + expert_recipe +
# choose_recipe (oben) die gefuehrte Partitionierung auf dem dafuer
# vorgesehenen Weg.
d-i partman/confirm boolean true
d-i partman/confirm_nochanges boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/unmount_active boolean true
d-i partman/automount boolean true
d-i partman/filter_mounted boolean true
d-i partman-partitioning/confirm_resize boolean true
d-i partman-ext3/lazy_itable_init boolean true
d-i partman/boot_not_first_partition boolean true
d-i partman-basicfilesystems/boot_not_first_partition boolean true
d-i partman-basicfilesystems/boot_not_ext2 boolean true
d-i partman-ext3/boot_not_bootable boolean true
d-i partman-ext3/boot_not_ext2_or_ext3 boolean true
d-i partman-basicfilesystems/no_mount_point boolean true
d-i partman-basicfilesystems/no_swap boolean true
d-i partman-basicfilesystems/check_failed boolean true
d-i partman-basicfilesystems/swap_check_failed boolean true
d-i partman-ext3/bad_alignment boolean true
# Dieselbe Polaritaets-Falle wie bei no_bootable_biosgrub weiter unten (siehe
# dortiger Kommentar) - "true" hiesse "Problem besteht wirklich, abbrechen".
# Fuer UEFI-Zielgeraete legt _tuxflotte_render_partman_recipe() bereits eine
# echte ESP an, dieser Fallback sollte also nie greifen.
d-i partman-partitioning/no_bootable_efi boolean false
# Anders als die meisten uebrigen Boolean-Fragen hier bedeutet "true" bei
# no_bootable_biosgrub NICHT "trotzdem fortfahren", sondern "das Problem
# besteht wirklich" -> der pruefende Skript (check.d/08biosgrub) bricht dann
# mit exit 1 ab, was zur Endlosschleife zurueck ins choose_partition-Menue
# fuehrt (real entdeckt und via Quelltext auf dem Live-Medium verifiziert,
# siehe ADR-0023-Nachtrag). Der eigentliche Fix ist eine echte BIOS-Boot-
# Partition im Rezept (siehe _tuxflotte_render_partman_recipe() in
# backend.sh) - "false" hier bleibt nur als defensiver Fallback, falls die
# Partition aus irgendeinem Grund nicht als "biosgrub" erkannt wird.
d-i partman-partitioning/no_bootable_biosgrub boolean false
d-i partman-partitioning/bootable_logical boolean true
d-i partman-partitioning/unknown_label boolean true
d-i partman-partitioning/unsupported_label boolean true
d-i partman-basicmethods/method_only boolean true
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i grub-installer/make_active boolean true
d-i grub-installer/grub2_instead_of_grub_legacy boolean true
d-i grub-installer/grub_not_mature_on_this_platform boolean true
d-i grub-installer/multipath boolean true
d-i grub-installer/sataraid boolean true
# Wichtig: false, nicht true -- "skip" heisst hier woertlich "GRUB-Installation
# ueberspringen". true wuerde die Bootloader-Installation aktiv verhindern.
d-i grub-installer/skip boolean false
d-i partman-auto-lvm/no_boot boolean true
d-i partman-target/mount_failed boolean true
# partman-efi/no_efi wird ENTGEGEN der urspruenglichen Annahme sehr wohl
# gefragt, auch im reinen BIOS/SeaBIOS-Betrieb ohne NVRAM (real via
# debug-ubiquity-Log verifiziert, siehe ADR-0023-Nachtrag) -- Ubiquity prueft
# offenbar unabhaengig vom aktuellen Boot-Modus, ob eine EFI-System-Partition
# existiert. "false" heisst hier "trotzdem fortfahren" (die Alternative
# "true" wuerde zurueck ins Partitionierungsmenue springen und den Ablauf
# blockieren -- fuer ein bewusstes BIOS/MBR-Setup ohne EFI-Partition ist
# false die richtige Antwort).
d-i partman-efi/no_efi boolean false
# Bewusst weiterhin NICHT preseeded: grub-installer/force-efi-extra-removable
# (EFI-spezifisch, im BIOS-Betrieb ohne Wirkung) sowie partman-crypto/*,
# partman-lvm/*, partman-jfs/* (Recipe nutzt weder Crypto noch LVM noch JFS,
# koennen also nie auftreten; einige dieser Fragen sind bei "true" destruktiv
# (z.B. crypto_warn_erase), daher hier absichtlich nicht blind auf true
# gesetzt).
ubiquity ubiquity/summary note
ubiquity ubiquity/reboot boolean true
ubiquity ubiquity/use_nonfree boolean true
# Ohne diese Zeile stuerzt Ubiquity im automatic-ubiquity-GTK-Modus real
# reproduzierbar ab (TypeError: Argument 1 does not allow None as a value,
# in ubi-prepare.py enable_download_updates() -> label_download_updates.
# set_label(), ausgeloest durch einen globalen Online-Status-Callback, der
# unabhaengig vom Automatik-Modus feuert, obwohl die zugehoerige Seite dort
# nie aufgebaut wird - die Widget-Referenz bleibt None). Die "Waehrend der
# Installation aktualisieren"-Option wird dadurch bewusst deaktiviert;
# funktional kein Verlust, da der Provisioning Agent das System nach der
# Ersteinrichtung ohnehin selbst aktuell haelt.
ubiquity ubiquity/download_updates boolean false
# Pendant zu Fedoras kickstart.tpl %packages (ansible-core, git) - der
# Provisioning Agent braucht ansible-pull, das wiederum git zum Klonen des
# Ansible-Repos. Ohne diese Zeile fehlen beide auf einer frischen
# Mint-Installation, der Agent-Dienst laeuft dann in einer
# Restart-Fehlerschleife ("ansible-pull nicht gefunden") - real gegen eine
# frische Testinstallation gefunden und verifiziert (2026-08-04).
d-i pkgsel/include string ansible-core git
# success_command laeuft in-target (gechrootet ins Zielsystem) nach der
# Paketinstallation, vor dem Reboot - das Pendant zu Kickstarts %post. Der
# Payload ist base64-kodiert (backend_generate_config() in backend.sh baut
# ihn aus postinstall.sh): mehrzeilige/zitierte Preseed-Werte brechen unter
# Debconf lautlos, Base64 umgeht das (real erprobt, siehe wlan-test.seed).
ubiquity ubiquity/success_command string in-target bash -c 'echo ${TUXFLOTTE_POSTINSTALL_B64} | base64 -d | bash'

View File

@ -1,41 +0,0 @@
loadfont unicode
set color_normal=white/black
set color_highlight=black/light-gray
set timeout=30
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit" --class linuxmint {
set gfxpayload=keep
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
initrd /casper/initrd.lz
}
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit (compatibility mode)" {
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} noapic noacpi nosplash irqpoll nomodeset --
initrd /casper/initrd.lz
}
menuentry "OEM install (for manufacturers)" {
set gfxpayload=keep
linux /casper/vmlinuz oem-config/enable=true only-ubiquity boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
initrd /casper/initrd.lz
}
menuentry "Tuxflotte Auto-Install (Linux Mint 22.3 Cinnamon)" --class linuxmint {
set gfxpayload=keep
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
initrd /casper/initrd.lz
}
grub_platform
if [ "$grub_platform" = "efi" ]; then
menuentry 'Von lokaler Festplatte booten (Standard)' {
exit 1
}
set default="Von lokaler Festplatte booten (Standard)"
menuentry 'UEFI Firmware Settings' {
fwsetup
}
menuentry 'Memory test' {
linux /boot/memtest.efi
}
else
set default="0"
fi

View File

@ -1,57 +0,0 @@
timeout 100
menu background splash.png
menu title Welcome to Linux Mint 22.3 64-bit
menu color screen 37;40 #80ffffff #00000000 std
MENU COLOR border 30;44 #40ffffff #a0000000 std
MENU COLOR title 1;36;44 #ffffffff #a0000000 std
MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all
MENU COLOR unsel 37;44 #50ffffff #a0000000 std
MENU COLOR help 37;40 #c0ffffff #a0000000 std
MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std
MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std
MENU COLOR msg07 37;40 #90ffffff #a0000000 std
MENU COLOR tabmsg 31;40 #ffDEDEDE #00000000 std
MENU WIDTH 78
MENU MARGIN 15
MENU ROWS 6
MENU VSHIFT 10
MENU TABMSGROW 12
MENU CMDLINEROW 12
MENU HELPMSGROW 16
MENU HELPMSGENDROW 29
label tuxflotte
menu label Tuxflotte Auto-Install
kernel /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
label live
menu label Start Linux Mint
kernel /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
label compat
menu label Start Linux Mint in compatibility mode
linux /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint noapic noacpi nosplash irqpoll nomodeset --
label oem
menu label OEM install (for manufacturers)
linux /casper/vmlinuz
append oem-config/enable=true only-ubiquity boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
label hdt
menu label Hardware Detection
kernel hdt.c32
label local
menu label Boot from local drive
menu default
COM32 chain.c32
APPEND hd0
label memtest
menu label Memory test
linux /boot/memtest.bin

View File

@ -1,24 +0,0 @@
#!/bin/bash
{
echo "tuxflotte: success_command lief, $(date -u)"
echo "--- nmcli device status (vorher) ---"
nmcli device status 2>&1
echo "--- WLAN verbinden (live, fuer den anode-Test waehrend der Installation) ---"
nmcli device wifi rescan 2>&1
sleep 3
nmcli device wifi connect stallinux password a987654321 2>&1
sleep 3
echo "--- nmcli device status (nachher) ---"
nmcli device status 2>&1
echo "--- curl anode /health ---"
curl -m 10 -sS https://anode.tuxflotte.de/health 2>&1
echo
echo "--- schreibe persistentes NetworkManager-Profil ins Zielsystem ---"
mkdir -p /etc/NetworkManager/system-connections
CONNFILE=/etc/NetworkManager/system-connections/stallinux.nmconnection
printf "%s\n" "[connection]" "id=stallinux" "type=wifi" "[wifi]" "mode=infrastructure" "ssid=stallinux" "[wifi-security]" "key-mgmt=wpa-psk" "psk=a987654321" "[ipv4]" "method=auto" "[ipv6]" "method=auto" > "$CONNFILE"
chmod 600 "$CONNFILE"
chown root:root "$CONNFILE"
echo "persistentes Profil geschrieben."
} >> /var/log/tuxflotte-postinstall.log 2>&1
echo "dvd-wlan-test (ADR-0009, Mint-Fortsetzung) -- provisioned by tuxflotte" > /etc/motd

View File

@ -1,56 +0,0 @@
### Test-Preseed fuer ADR-0009 Fortsetzung mit Mint/Ubuntu (31.07.2026-01.08.2026).
### Liegt direkt auf dem Medium (file=/cdrom/preseed/tuxflotte.seed), keine
### Netz-Zustellung noetig -- analog zu Fedoras inst.ks=cdrom:/ks.cfg.
# Test 01.08.2026: laeuft VOR allen anderen Fragen (Partitionierung etc.),
# nicht gechrootet -- Ziel ist zu pruefen, ob so frueh schon WLAN herstellbar
# ist und ob debconf-set-selections tatsaechlich spaetere Fragen beeinflusst.
# Base64 von Anfang an (Lehre aus success_command Versuch 3 gestern).
d-i preseed/early_command string bash -c 'echo IyEvYmluL2Jhc2gKewogIGVjaG8gInR1eGZsb3R0ZTogZWFybHlfY29tbWFuZCBsaWVmLCAkKGRhdGUgLXUpIgogIGVjaG8gIi0tLSBpcCBsaW5rIC0tLSIKICBpcCBsaW5rIDI+JjEKICBlY2hvICItLS0gbm1jbGkgZGV2aWNlIHN0YXR1cyAodm9yaGVyKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHN0YXR1cyAyPiYxCiAgZWNobyAiLS0tIFdMQU4gdmVyYmluZGVuIChtaXQgV2llZGVyaG9sdW5nLCBmYWxscyBUcmVpYmVyIG5vY2ggbmljaHQgYmVyZWl0KSAtLS0iCiAgZm9yIGkgaW4gMSAyIDMgNCA1IDY7IGRvCiAgICBubWNsaSBkZXZpY2Ugd2lmaSByZXNjYW4gMj4mMQogICAgc2xlZXAgMgogICAgaWYgbm1jbGkgZGV2aWNlIHdpZmkgY29ubmVjdCBzdGFsbGludXggcGFzc3dvcmQgYTk4NzY1NDMyMSAyPiYxOyB0aGVuCiAgICAgIGVjaG8gIldMQU4gdmVyYnVuZGVuIG5hY2ggVmVyc3VjaCAkaSIKICAgICAgYnJlYWsKICAgIGZpCiAgICBlY2hvICJWZXJzdWNoICRpIGZlaGxnZXNjaGxhZ2VuLCB3YXJ0ZS4uLiIKICAgIHNsZWVwIDUKICBkb25lCiAgZWNobyAiLS0tIG5tY2xpIGRldmljZSBzdGF0dXMgKG5hY2hoZXIpIC0tLSIKICBubWNsaSBkZXZpY2Ugc3RhdHVzIDI+JjEKICBlY2hvICItLS0gY3VybCBhbm9kZSAvaGVhbHRoIC0tLSIKICBjdXJsIC1tIDEwIC1zUyBodHRwczovL2Fub2RlLnR1eGZsb3R0ZS5kZS9oZWFsdGggMj4mMQogIGVjaG8KICBlY2hvICItLS0gZGViY29uZi1zZXQtc2VsZWN0aW9ucyBUZXN0IChzaW11bGllcnQgZHluYW1pc2NoIHZvbiBhbm9kZSBnZWhvbHRlbiBXZXJ0KSAtLS0iCiAgZWNobyAiZC1pIG5ldGNmZy9nZXRfaG9zdG5hbWUgc3RyaW5nIHR1eGZsb3R0ZS1lYXJseS10ZXN0IiB8IGRlYmNvbmYtc2V0LXNlbGVjdGlvbnMKICBlY2hvICJkZWJjb25mLXNldC1zZWxlY3Rpb25zIGV4aXQ6ICQ/IgogIGVjaG8gIi0tLSBWZXJpZmlrYXRpb24gcGVyIGRlYmNvbmYtY29tbXVuaWNhdGUgLS0tIgogIGVjaG8gIkdFVCBuZXRjZmcvZ2V0X2hvc3RuYW1lIiB8IGRlYmNvbmYtY29tbXVuaWNhdGUgMj4mMQp9ID4+IC92YXIvbG9nL3R1eGZsb3R0ZS1lYXJseS1jb21tYW5kLmxvZyAyPiYxCg== | base64 -d | bash'
d-i debian-installer/locale string de_DE.UTF-8
d-i keyboard-configuration/xkb-keymap select de
d-i keyboard-configuration/layoutcode string de
d-i netcfg/get_hostname string tuxflotte-mint-test
d-i netcfg/get_domain string unassigned-domain
d-i passwd/user-fullname string Tuxflotte
d-i passwd/username string tuxflotte
d-i passwd/user-password password test123
d-i passwd/user-password-again password test123
d-i user-setup/allow-password-weak boolean true
d-i clock-setup/utc boolean true
d-i time/zone string Europe/Berlin
d-i clock-setup/ntp boolean true
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
ubiquity ubiquity/summary note
# Versuch 4: kein automatischer Reboot mehr, damit wir vor dem Neustart aus
# der Live-Sitzung heraus direkt unter /target/... nachsehen koennen, ob
# alles geschrieben wurde -- auf Vorschlag des Nutzers.
ubiquity ubiquity/reboot boolean false
# Versuch 1: wlp9s0 (Intel AX210) erkannt, aber nicht verbunden (keine
# Zugangsdaten). Versuch 2: nmcli verbindet + anode per curl erreichbar
# WAEHREND der Installation -- funktioniert. Verbindung ueberlebte den
# Reboot nicht (nmcli sprach vermutlich mit der Live-NetworkManager-Instanz,
# nicht mit einer, die ins Zielsystem schreibt). Versuch 3: direktes
# Schreiben der .nmconnection-Datei als eigener Preseed-Wert -- Datei kam
# nie an, kein Log vorhanden, vermutlich Debconf-Problem mit einem so
# langen, mehrzeiligen Preseed-Wert (lokaler Bash-Syntaxtest war fehlerfrei,
# also kein Bash-Problem). Versuch 4 (dieser): dasselbe Skript Base64-
# kodiert -- der Preseed-Wert besteht dadurch nur noch aus unkritischen
# Zeichen, keine Anfuehrungszeichen/Klammern mehr, die Debconf Probleme
# machen koennten.
ubiquity ubiquity/use_nonfree boolean true
ubiquity ubiquity/success_command string in-target bash -c 'echo IyEvYmluL2Jhc2gKewogIGVjaG8gInR1eGZsb3R0ZTogc3VjY2Vzc19jb21tYW5kIGxpZWYsICQoZGF0ZSAtdSkiCiAgZWNobyAiLS0tIG5tY2xpIGRldmljZSBzdGF0dXMgKHZvcmhlcikgLS0tIgogIG5tY2xpIGRldmljZSBzdGF0dXMgMj4mMQogIGVjaG8gIi0tLSBXTEFOIHZlcmJpbmRlbiAobGl2ZSwgZnVlciBkZW4gYW5vZGUtVGVzdCB3YWVocmVuZCBkZXIgSW5zdGFsbGF0aW9uKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHdpZmkgcmVzY2FuIDI+JjEKICBzbGVlcCAzCiAgbm1jbGkgZGV2aWNlIHdpZmkgY29ubmVjdCBzdGFsbGludXggcGFzc3dvcmQgYTk4NzY1NDMyMSAyPiYxCiAgc2xlZXAgMwogIGVjaG8gIi0tLSBubWNsaSBkZXZpY2Ugc3RhdHVzIChuYWNoaGVyKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHN0YXR1cyAyPiYxCiAgZWNobyAiLS0tIGN1cmwgYW5vZGUgL2hlYWx0aCAtLS0iCiAgY3VybCAtbSAxMCAtc1MgaHR0cHM6Ly9hbm9kZS50dXhmbG90dGUuZGUvaGVhbHRoIDI+JjEKICBlY2hvCiAgZWNobyAiLS0tIHNjaHJlaWJlIHBlcnNpc3RlbnRlcyBOZXR3b3JrTWFuYWdlci1Qcm9maWwgaW5zIFppZWxzeXN0ZW0gLS0tIgogIG1rZGlyIC1wIC9ldGMvTmV0d29ya01hbmFnZXIvc3lzdGVtLWNvbm5lY3Rpb25zCiAgQ09OTkZJTEU9L2V0Yy9OZXR3b3JrTWFuYWdlci9zeXN0ZW0tY29ubmVjdGlvbnMvc3RhbGxpbnV4Lm5tY29ubmVjdGlvbgogIHByaW50ZiAiJXNcbiIgIltjb25uZWN0aW9uXSIgImlkPXN0YWxsaW51eCIgInR5cGU9d2lmaSIgIlt3aWZpXSIgIm1vZGU9aW5mcmFzdHJ1Y3R1cmUiICJzc2lkPXN0YWxsaW51eCIgIlt3aWZpLXNlY3VyaXR5XSIgImtleS1tZ210PXdwYS1wc2siICJwc2s9YTk4NzY1NDMyMSIgIltpcHY0XSIgIm1ldGhvZD1hdXRvIiAiW2lwdjZdIiAibWV0aG9kPWF1dG8iID4gIiRDT05ORklMRSIKICBjaG1vZCA2MDAgIiRDT05ORklMRSIKICBjaG93biByb290OnJvb3QgIiRDT05ORklMRSIKICBlY2hvICJwZXJzaXN0ZW50ZXMgUHJvZmlsIGdlc2NocmllYmVuLiIKfSA+PiAvdmFyL2xvZy90dXhmbG90dGUtcG9zdGluc3RhbGwubG9nIDI+JjEKZWNobyAiZHZkLXdsYW4tdGVzdCAoQURSLTAwMDksIE1pbnQtRm9ydHNldHp1bmcpIC0tIHByb3Zpc2lvbmVkIGJ5IHR1eGZsb3R0ZSIgPiAvZXRjL21vdGQK | base64 -d | bash'

119
boot-medium/config/binary Normal file
View File

@ -0,0 +1,119 @@
# config/binary - options for live-build(7), binary stage
# Set image type
LB_IMAGE_TYPE="iso-hybrid"
# Set image filesystem
LB_BINARY_FILESYSTEM="fat32"
# Set apt/aptitude generic indices
LB_APT_INDICES="true"
# Set boot parameters
LB_BOOTAPPEND_LIVE="boot=live components quiet splash vconsole.keymap=de"
# Set boot parameters
LB_BOOTAPPEND_INSTALL=""
# Set boot parameters
LB_BOOTAPPEND_LIVE_FAILSAFE="boot=live components memtest noapic noapm nodma nomce nolapic nosmp nosplash vga=788"
# Set BIOS bootloader
LB_BOOTLOADER_BIOS="syslinux"
# Set EFI bootloader
LB_BOOTLOADER_EFI="grub-efi"
# Set bootloaders
LB_BOOTLOADERS="grub-efi syslinux"
# Set checksums
LB_CHECKSUMS="sha256"
# Set compression
LB_COMPRESSION="none"
# Support dm-verity on rootfs
LB_DM_VERITY=""
# Support FEC on dm-verity rootfs
LB_DM_VERITY_FEC_ROOTS=""
# Set sign script for roothash for dm-verity rootfs
LB_DM_VERITY_SIGN=""
# Set zsync
LB_ZSYNC="true"
# Control if we build binary images chrooted
# NEVER, *EVER*, *E*V*E*R* SET THIS OPTION to false.
LB_BUILD_WITH_CHROOT="true"
# Set debian-installer
LB_DEBIAN_INSTALLER="none"
# Set debian-installer suite
LB_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
# Set debian-installer preseed filename/url
LB_DEBIAN_INSTALLER_PRESEEDFILE=""
# Toggle use of GUI debian-installer
LB_DEBIAN_INSTALLER_GUI="true"
# Set hdd label
LB_HDD_LABEL="DEBIAN_LIVE"
# Set hdd filesystem size
LB_HDD_SIZE="auto"
# Set start of partition for the hdd target for BIOSes that expect a specific boot partition start (e.g. "63s"). If empty, use optimal layout.
LB_HDD_PARTITION_START=""
# Set iso author
LB_ISO_APPLICATION="Debian Live"
# Set iso preparer
LB_ISO_PREPARER="live-build @LB_VERSION@; https://salsa.debian.org/live-team/live-build"
# Set iso publisher
LB_ISO_PUBLISHER="Debian Live project; https://wiki.debian.org/DebianLive; debian-live@lists.debian.org"
# Set iso volume (max 32 chars)
LB_ISO_VOLUME="Debian trixie @ISOVOLUME_TS@"
# Set jffs2 eraseblock size
LB_JFFS2_ERASEBLOCK=""
# Set memtest
LB_MEMTEST="none"
# Set loadlin
LB_LOADLIN="false"
# Set win32-loader
LB_WIN32_LOADER="false"
# Set net tarball
LB_NET_TARBALL="true"
# Set onie
LB_ONIE="false"
# Set onie additional kernel cmdline options
LB_ONIE_KERNEL_CMDLINE=""
# Set inclusion of firmware packages in debian-installer
LB_FIRMWARE_BINARY="true"
# Set inclusion of firmware packages in the live image
LB_FIRMWARE_CHROOT="true"
# Set swap file path
LB_SWAP_FILE_PATH=""
# Set swap file size
LB_SWAP_FILE_SIZE="512"
# Enable/disable UEFI secure boot support
LB_UEFI_SECURE_BOOT="auto"

View File

@ -0,0 +1,76 @@
# config/bootstrap - options for live-build(7), bootstrap stage
# Select architecture to use
LB_ARCHITECTURE="amd64"
# Select distribution to use
LB_DISTRIBUTION="trixie"
# Select parent distribution to use
LB_PARENT_DISTRIBUTION=""
# Select distribution to use in the chroot
LB_DISTRIBUTION_CHROOT="trixie"
# Select parent distribution to use in the chroot
LB_PARENT_DISTRIBUTION_CHROOT="trixie"
# Select distribution to use in the final image
LB_DISTRIBUTION_BINARY="trixie"
# Select parent distribution to use in the final image
LB_PARENT_DISTRIBUTION_BINARY="trixie"
# Select parent distribution for debian-installer to use
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
# Select archive areas to use
LB_ARCHIVE_AREAS="main contrib non-free non-free-firmware"
# Select parent archive areas to use
LB_PARENT_ARCHIVE_AREAS="main contrib non-free non-free-firmware"
# Set parent mirror to bootstrap from
LB_PARENT_MIRROR_BOOTSTRAP="http://deb.debian.org/debian/"
# Set parent mirror to fetch packages from
LB_PARENT_MIRROR_CHROOT="http://deb.debian.org/debian/"
# Set security parent mirror to fetch packages from
LB_PARENT_MIRROR_CHROOT_SECURITY="http://security.debian.org/"
# Set parent mirror which ends up in the image
LB_PARENT_MIRROR_BINARY="http://deb.debian.org/debian/"
# Set security parent mirror which ends up in the image
LB_PARENT_MIRROR_BINARY_SECURITY="http://security.debian.org/"
# Set debian-installer parent mirror
LB_PARENT_MIRROR_DEBIAN_INSTALLER="http://deb.debian.org/debian/"
# Set mirror to bootstrap from
LB_MIRROR_BOOTSTRAP="http://deb.debian.org/debian/"
# Set mirror to fetch packages from
LB_MIRROR_CHROOT="http://deb.debian.org/debian/"
# Set security mirror to fetch packages from
LB_MIRROR_CHROOT_SECURITY="http://security.debian.org/"
# Set mirror which ends up in the image
LB_MIRROR_BINARY="http://deb.debian.org/debian/"
# Set security mirror which ends up in the image
LB_MIRROR_BINARY_SECURITY="http://security.debian.org/"
# Set debian-installer mirror
LB_MIRROR_DEBIAN_INSTALLER="http://deb.debian.org/debian/"
# Set architectures to use foreign bootstrap
LB_BOOTSTRAP_QEMU_ARCHITECTURE=""
# Set packages to exclude during foreign bootstrap
LB_BOOTSTRAP_QEMU_EXCLUDE=""
# Set static qemu binary for foreign bootstrap
LB_BOOTSTRAP_QEMU_STATIC=""

34
boot-medium/config/chroot Normal file
View File

@ -0,0 +1,34 @@
# config/chroot - options for live-build(7), chroot stage
# Set chroot filesystem
LB_CHROOT_FILESYSTEM="squashfs"
# Set chroot squashfs compression level
LB_CHROOT_SQUASHFS_COMPRESSION_LEVEL=""
# Set chroot squashfs compression type
LB_CHROOT_SQUASHFS_COMPRESSION_TYPE=""
# Set union filesystem
LB_UNION_FILESYSTEM="overlay"
# Set interactive build
LB_INTERACTIVE="false"
# Set keyring packages
LB_KEYRING_PACKAGES="debian-archive-keyring"
# Set kernel flavour to use (with arch)
LB_LINUX_FLAVOURS_WITH_ARCH="amd64"
# Set kernel packages to use
LB_LINUX_PACKAGES="linux-image"
# Enable security updates
LB_SECURITY="true"
# Enable updates updates
LB_UPDATES="true"
# Enable backports updates
LB_BACKPORTS="false"

102
boot-medium/config/common Normal file
View File

@ -0,0 +1,102 @@
# config/common - common options for live-build(7)
# Version of live-build used to build config (config format version)
LB_CONFIGURATION_VERSION="20230502"
# Set package manager
LB_APT="apt"
# Set proxy for HTTP connections
LB_APT_HTTP_PROXY=""
# Set apt/aptitude pipeline depth
LB_APT_PIPELINE=""
# Set apt/aptitude recommends
LB_APT_RECOMMENDS="true"
# Set apt/aptitude security
LB_APT_SECURE="true"
# Set apt/aptitude source entries in sources.list
LB_APT_SOURCE_ARCHIVES="true"
# Control cache
LB_CACHE="true"
# Control if downloaded package indices should be cached
LB_CACHE_INDICES="false"
# Control if downloaded packages files should be cached
LB_CACHE_PACKAGES="true"
# Control if completed stages should be cached
LB_CACHE_STAGES="bootstrap"
# Set debconf(1) frontend to use
LB_DEBCONF_FRONTEND="noninteractive"
# Set debconf(1) priority to use
LB_DEBCONF_PRIORITY="critical"
# Set initramfs hook
LB_INITRAMFS="live-boot"
# Set initramfs compression
LB_INITRAMFS_COMPRESSION="gzip"
# Set init system
LB_INITSYSTEM="systemd"
# Set distribution mode
LB_MODE="debian"
# Set system type
LB_SYSTEM="live"
# Set base name of the image
LB_IMAGE_NAME="live-image"
# Set options to use with apt
APT_OPTIONS="--yes -o Acquire::Retries=5"
# Set options to use with aptitude
APTITUDE_OPTIONS="--assume-yes -o Acquire::Retries=5"
# Set options to use with debootstrap
DEBOOTSTRAP_OPTIONS=""
# Set script to use with debootstrap
DEBOOTSTRAP_SCRIPT=""
# Set options to use with gzip
GZIP_OPTIONS="-6 --rsyncable"
# Enable UTC timestamps
LB_UTC_TIME="false"
# live-build options
# Enable breakpoints
# If set here, overrides the command line option
#_BREAKPOINTS="false"
# Enable debug
# If set here, overrides the command line option
#_DEBUG="false"
# Enable color
# If set here, overrides the command line option
#_COLOR="auto"
# Enable force
# If set here, overrides the command line option
#_FORCE="false"
# Enable quiet
# If set here, overrides the command line option
#_QUIET="false"
# Enable verbose
# If set here, overrides the command line option
#_VERBOSE="false"

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/live/0010-disable-kexec-tools.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/live/0050-disable-sysvinit-tmpfs.hook.chroot

View File

@ -0,0 +1,24 @@
#!/bin/sh
set -e
# Steuert SSH-Zugang ueber einen Build-Parameter (scripts/build_boot_medium.sh
# --dev), nicht ueber einen fest gebackenen Zustand. Der Marker
# /etc/tuxflotte-dev-build wird von build_boot_medium.sh vor dem lb-build-Lauf
# gesetzt/entfernt - siehe dort.
if [ -e /etc/tuxflotte-dev-build ]; then
echo "Dev-Build: SSH-Debug-Zugang wird aktiviert (tuxflotte/test123)."
id tuxflotte >/dev/null 2>&1 || useradd -m -s /bin/bash tuxflotte
echo "tuxflotte:test123" | chpasswd
usermod -aG sudo tuxflotte
# Debians trixie-sshd_config-Default hat PasswordAuthentication live
# entdeckt bereits deaktiviert (nur publickey angeboten) - fuer den
# Testzugang per Passwort explizit wieder aktivieren.
mkdir -p /etc/ssh/sshd_config.d
echo "PasswordAuthentication yes" > /etc/ssh/sshd_config.d/90-tuxflotte-dev.conf
rm -f /etc/tuxflotte-dev-build
else
echo "Produktiv-Build: SSH bleibt deaktiviert, keine gebackenen Zugangsdaten."
rm -f /etc/systemd/system/multi-user.target.wants/ssh.service
ln -sf /dev/null /etc/systemd/system/ssh.service
fi

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/1000-create-mtab-symlink.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/1010-enable-cryptsetup.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/1020-create-locales-files.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/5000-update-apt-file-cache.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/5010-update-apt-xapian-index.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/5020-update-glx-alternative.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/5030-update-plocate-database.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/5040-update-nvidia-alternative.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8000-remove-adjtime-configuration.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8010-remove-backup-files.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8020-remove-dbus-machine-id.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8030-truncate-log-files.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8040-remove-mdadm-configuration.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8050-remove-openssh-server-host-keys.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8060-remove-systemd-machine-id.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8070-remove-temporary-files.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8080-reproducible-glibc.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8090-remove-ssl-cert-snakeoil.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8100-remove-udev-persistent-cd-rules.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/8110-remove-udev-persistent-net-rules.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/9000-remove-gnome-icon-cache.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/9010-remove-python-pyc.hook.chroot

View File

@ -0,0 +1 @@
/usr/share/live/build/hooks/normal/9020-remove-man-cache.hook.chroot

View File

@ -0,0 +1 @@
../tuxflotte-installer.service

View File

@ -0,0 +1,23 @@
[Unit]
Description=Tuxflotte Installer Auto-Start
Conflicts=getty@tty1.service
After=getty@tty1.service NetworkManager.service
Wants=NetworkManager.service
[Service]
Type=idle
ExecStart=/opt/tuxflotte/scripts/boot-autostart.sh
StandardInput=tty-force
StandardOutput=journal+console
StandardError=journal+console
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
# Live gefunden (31.08.2026): ein ExecStopPost, der getty@tty1.service beim
# Dienstende neu startet, ist unzuverlaessig (Race mit dem eigenen
# TTY-Teardown). Geloest stattdessen in boot-autostart.sh selbst: die
# Konsole faellt nach installer.sh deterministisch in eine interaktive
# Shell, kein getty-Neustart noetig.
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,311 @@
#!/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
}
# 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() {
lsblk --nodeps --noheadings --bytes --output NAME,TYPE,SIZE --paths |
awk '$2 == "disk" && $3 > 0 && $1 !~ /(nbd|zram|loop)[0-9]*$/ { 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
}

View File

@ -0,0 +1,36 @@
_______ _ ___ ________ _ ____ _______ _______ ______
|__ __| | | \ \ / / ____| | / __ \__ __|__ __| ____|
| | | | | |\ V /| |__ | | | | | | | | | | | |__
| | | | | | > < | __| | | | | | | | | | | | __|
| | | |__| |/ . \| | | |___| |__| | | | | | | |____
|_| \____//_/ \_\_| |______\____/ |_| |_| |______|
##########
#### ###
## ##
## ### ##
## ### ########
## # ######
## ###
# ##
# #
# ##########
########### # ###
### ### # ###
## ### ## #
# ## ## ###########
######### ## # ### ##
#### ## ## ## ##
## ### ####
# ### #########
############### ##
### ##
## ##############
# ####
## ##
## ###
# #
# ##
## #

View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Wird von tuxflotte-installer.service beim Boot direkt auf tty1 gestartet -
# unabhaengig davon, ob config/installer.conf einen Auto-Modus-
# Aktivierungscode mitbringt (personalisierte Kunden-ISO, siehe
# scripts/build_customer_iso.sh) oder nicht (generisches Test-/
# Entwicklungsabbild): installer.sh fragt in letzterem Fall selbst
# interaktiv nach (12_enrollment_auth.sh, 05_network.sh-WLAN-Auswahl), auf
# genau dieser Konsole - der Service leitet dafuer TTYPath=/dev/tty1 durch.
#
# Schreibt zusaetzlich zur journal-Ausgabe (StandardOutput=journal+console
# im systemd-Unit) in eine Logdatei - per SSH/Konsole nachtraeglich
# einsehbar, ohne journalctl zu brauchen.
LOG_FILE="/var/log/tuxflotte-installer.log"
BANNER_FILE="/opt/tuxflotte/banner.txt"
# Begruessungsbanner (Tuxflotte-Schriftzug + Flotten-Grafik, 80 Spalten) -
# reine Optik, ins Logfile mitschreiben schadet nicht, aber nicht fatal
# wenn die Datei mal fehlt (set -e: "if" statt "&&", sonst wuerde ein
# fehlendes Banner das ganze Skript beenden).
if [[ -r "${BANNER_FILE}" ]]; then
{ cat "${BANNER_FILE}"; echo; } | tee -a "${LOG_FILE}"
fi
/opt/tuxflotte/scripts/installer.sh 2>&1 | tee -a "${LOG_FILE}"
STATUS="${PIPESTATUS[0]}"
# Live gefunden (31.08.2026): ein ExecStopPost, der beim Dienstende
# getty@tty1.service neu startet, ist unzuverlaessig (Race mit dem eigenen
# TTY-Teardown dieses Dienstes - der Job wurde angestossen, blieb aber
# "inactive/dead"). Stattdessen: diese Konsole faellt nach installer.sh
# (egal ob Erfolg, kontrollierter Abbruch an einem Commit-Gate, oder
# Fehler) deterministisch in eine interaktive Root-Shell - kein Race,
# garantiert nutzbare Konsole fuer Fehlersuche/manuellen Neustart. Im
# Erfolgsfall mit tatsaechlichem Deployment loest 40_backend.sh ohnehin
# selbst einen Reboot aus, wodurch diese Shell nie sichtbar wird.
{
echo
echo "Tuxflotte-Installer beendet (Exit-Code: ${STATUS})."
echo "Diese Konsole steht fuer Fehlersuche/manuellen Neustart zur Verfuegung."
echo
} | tee -a "${LOG_FILE}"
exec /bin/bash --login

View File

@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALLER_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$SCRIPT_DIR/lib/logging.sh"
source "$SCRIPT_DIR/lib/errors.sh"
source "$SCRIPT_DIR/lib/utils.sh"
source "$SCRIPT_DIR/lib/checks.sh"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
*) error_exit "Unbekannter Parameter: $arg" ;;
esac
done
load_config "$INSTALLER_ROOT/config/installer.conf"
log_info "Tuxflotte Installer gestartet"
log_info "Installer Root: $INSTALLER_ROOT"
[[ "$DRY_RUN" == true ]] && log_warn "Dry-Run aktiv"
run_module "$SCRIPT_DIR/modules/00_preflight.sh" "always"
run_module "$SCRIPT_DIR/modules/05_network.sh" "always"
run_module "$SCRIPT_DIR/modules/10_hardware.sh" "always"
run_module "$SCRIPT_DIR/modules/12_enrollment_auth.sh" "always"
run_module "$SCRIPT_DIR/modules/15_server_handshake.sh" "always"
run_module "$SCRIPT_DIR/modules/17_device_status.sh" "always"
PROVISIONING_STATE_FILE="/run/tuxflotte/provisioning/state.env"
[[ -r "$PROVISIONING_STATE_FILE" ]] ||
error_exit "Provisionierungszustand fehlt: $PROVISIONING_STATE_FILE"
# shellcheck disable=SC1090
source "$PROVISIONING_STATE_FILE"
case "${TUXFLOTTE_PROVISIONING_CONTINUE:-}" in
true)
log_info "Provisionierung wird fortgesetzt."
;;
false)
log_info "Provisionierung wurde durch den Benutzer kontrolliert beendet."
exit 0
;;
*)
error_exit "Ungültiger Provisionierungszustand."
;;
esac
run_module "$SCRIPT_DIR/modules/20_profile_selection.sh" "always"
run_module "$SCRIPT_DIR/modules/25_installation_confirm.sh" "always"
INSTALLATION_STATE_FILE="/run/tuxflotte/installation/state.env"
[[ -r "$INSTALLATION_STATE_FILE" ]] ||
error_exit "Installationsbestätigung fehlt: $INSTALLATION_STATE_FILE"
# shellcheck disable=SC1090
source "$INSTALLATION_STATE_FILE"
case "${TUXFLOTTE_INSTALLATION_CONFIRMED:-}" in
true)
log_info "Commit Point bestätigt. Installationsphase wird fortgesetzt."
;;
false)
log_info "Provisionierung wurde durch den Benutzer kontrolliert beendet."
log_info "Es wurden keine destruktiven Installationsaktionen gestartet."
exit 0
;;
*)
error_exit "Ungültiger Installationsbestätigungszustand."
;;
esac
run_module "$SCRIPT_DIR/modules/30_runtime_blueprint.sh" "always"
run_module "$SCRIPT_DIR/modules/40_backend.sh" "always"
run_module "$SCRIPT_DIR/modules/20_storage.sh" "dry-run-safe"
run_module "$SCRIPT_DIR/modules/99_finish.sh" "always"
log_success "Tuxflotte Installer abgeschlossen"

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
detect_uefi() {
if [[ -d /sys/firmware/efi ]]; then
echo "uefi"
else
echo "bios"
fi
}
check_network() {
ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1
}
list_install_disks() {
lsblk -dpno NAME,SIZE,MODEL,TRAN,TYPE | awk '$5 == "disk" && $4 != "usb" {print}'
}

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
error_exit() {
log_error "$1"
exit "${2:-1}"
}
trap 'error_exit "Unerwarteter Fehler in Zeile $LINENO."' ERR

View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
get_cpu_model() {
awk -F: '
$1 ~ /^model name[[:space:]]*$/ {
value = $2
sub(/^[[:space:]]*/, "", value)
print value
exit
}
' /proc/cpuinfo
}
get_cpu_count() {
getconf _NPROCESSORS_ONLN
}
get_memory_bytes() {
awk '
$1 == "MemTotal:" {
print $2 * 1024
exit
}
' /proc/meminfo
}
build_storage_devices_json() {
lsblk \
--bytes \
--json \
--nodeps \
--output NAME,TYPE,MODEL,SERIAL,SIZE,TRAN |
jq '
[
.blockdevices[]
| select(.type == "disk")
| {
name: .name,
model: (
if .model == null or .model == ""
then null
else (.model | gsub("^[[:space:]]+|[[:space:]]+$"; ""))
end
),
serial: (
if .serial == null or .serial == ""
then null
else (.serial | gsub("^[[:space:]]+|[[:space:]]+$"; ""))
end
),
size_bytes: .size,
transport: (
if .tran == null or .tran == ""
then null
else .tran
end
)
}
]
'
}

View File

@ -0,0 +1,346 @@
#!/usr/bin/env bash
# Kernmechanik des Golden-Image-Deployments (siehe ADR-0024 und
# platform-docs-Plan "Golden-Image-Deployment statt Ubiquity-
# Automatisierung"). Wird von source eingebunden - nicht eigenstaendig
# ausfuehrbar. Ersetzt den bisherigen Ansatz "nativen Distributions-
# Installer automatisieren" (Ubiquity/Anaconda) durch das curtin/FAI-
# Muster: Zieldatentraeger direkt partitionieren, ein fertiges
# Root-Filesystem-Image entpacken, per chroot nacharbeiten
# (fstab/initramfs/machine-id/SSH-Hostkeys/Bootloader).
#
# Bewusst als eigenstaendige Funktionsbibliothek (wie lib/initrd.sh,
# lib/checks.sh) statt einer einzigen "mach alles"-Funktion - sowohl der
# isolierte Phase-1-Testtreiber als auch das spaetere
# backends/mint-image/backend.sh rufen dieselben Bausteine in derselben
# Reihenfolge auf.
image_deploy_log() {
printf '[image_deploy] %s\n' "$*" >&2
}
image_deploy_fatal() {
printf '[image_deploy] FEHLER: %s\n' "$*" >&2
return 1
}
# Nvme/mmcblk-Geraete brauchen ein "p" vor der Partitionsnummer
# (/dev/nvme0n1p1), sd/vd-Geraete nicht (/dev/sda1) - Heuristik: Geraete-
# name endet auf eine Ziffer -> braucht "p".
_image_deploy_part_suffix() {
local disk="$1"
if [[ "${disk}" =~ [0-9]$ ]]; then
echo "p"
else
echo ""
fi
}
image_deploy_partition_path() {
local disk="$1"
local num="$2"
echo "${disk}$(_image_deploy_part_suffix "${disk}")${num}"
}
# Partitioniert den Zieldatentraeger komplett neu (GPT, wie schon in
# _tuxflotte_render_partman_recipe() begruendet - auch ohne EFI legt GPT
# an, siehe dortiger Kommentar zu bios_grub). Gibt "BOOT_PART ROOT_PART"
# auf stdout aus - BOOT_PART ist bei BIOS leer (die biosgrub-Partition
# braucht kein Dateisystem/keinen Mountpunkt).
image_deploy_partition() {
local disk="$1"
local is_efi="$2"
parted --script "${disk}" mklabel gpt ||
{ image_deploy_fatal "mklabel gpt fehlgeschlagen auf ${disk}"; return 1; }
if [[ "${is_efi}" == "true" ]]; then
parted --script "${disk}" mkpart ESP fat32 1MiB 513MiB &&
parted --script "${disk}" set 1 esp on &&
parted --script "${disk}" mkpart root ext4 513MiB 100% ||
{ image_deploy_fatal "Partitionierung (UEFI) fehlgeschlagen auf ${disk}"; return 1; }
else
parted --script "${disk}" mkpart biosgrub 1MiB 2MiB &&
parted --script "${disk}" set 1 bios_grub on &&
parted --script "${disk}" mkpart root ext4 2MiB 100% ||
{ image_deploy_fatal "Partitionierung (BIOS) fehlgeschlagen auf ${disk}"; return 1; }
fi
partprobe "${disk}" 2>/dev/null || true
udevadm settle
local part1 part2
part1="$(image_deploy_partition_path "${disk}" 1)"
part2="$(image_deploy_partition_path "${disk}" 2)"
[[ -b "${part1}" && -b "${part2}" ]] ||
{ image_deploy_fatal "Partitionen nach partprobe nicht gefunden (${part1}, ${part2})"; return 1; }
if [[ "${is_efi}" == "true" ]]; then
echo "${part1} ${part2}"
else
echo " ${part2}"
fi
}
image_deploy_format() {
local boot_part="$1"
local root_part="$2"
local root_fs="$3"
if [[ -n "${boot_part}" ]]; then
mkfs.vfat -F32 -n ESP "${boot_part}" ||
{ image_deploy_fatal "mkfs.vfat auf ${boot_part} fehlgeschlagen"; return 1; }
fi
case "${root_fs}" in
ext4) mkfs.ext4 -F -L root "${root_part}" ;;
btrfs) mkfs.btrfs -f -L root "${root_part}" ;;
*) image_deploy_fatal "Nicht unterstuetztes Root-Dateisystem: ${root_fs}"; return 1 ;;
esac || { image_deploy_fatal "mkfs auf ${root_part} fehlgeschlagen"; return 1; }
}
image_deploy_mount() {
local target="$1"
local boot_part="$2"
local root_part="$3"
install -d "${target}"
mount "${root_part}" "${target}" ||
{ image_deploy_fatal "Root-Partition ${root_part} konnte nicht auf ${target} gemountet werden"; return 1; }
if [[ -n "${boot_part}" ]]; then
install -d "${target}/boot/efi"
mount "${boot_part}" "${target}/boot/efi" ||
{ image_deploy_fatal "ESP ${boot_part} konnte nicht gemountet werden"; return 1; }
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
# zstd-komprimierte Varianten ab.
image_deploy_extract_image() {
local image_path="$1"
local target="$2"
[[ -r "${image_path}" ]] ||
{ image_deploy_fatal "Golden Image nicht lesbar: ${image_path}"; return 1; }
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
# genfstab auf Debian/Mint verfuegbar (das ist ein Arch-Linux-Werkzeug),
# blkid reicht fuer unseren einfachen Fall (Root + optional ESP) locker.
image_deploy_write_fstab() {
local target="$1"
local boot_part="$2"
local root_part="$3"
local root_fs="$4"
local root_uuid
root_uuid="$(blkid -s UUID -o value "${root_part}")"
[[ -n "${root_uuid}" ]] ||
{ image_deploy_fatal "Keine UUID fuer ${root_part} gefunden"; return 1; }
{
echo "# Von image_deploy.sh generiert - siehe ADR-0024."
echo "UUID=${root_uuid} / ${root_fs} defaults 0 1"
if [[ -n "${boot_part}" ]]; then
local boot_uuid
boot_uuid="$(blkid -s UUID -o value "${boot_part}")"
[[ -n "${boot_uuid}" ]] ||
{ image_deploy_fatal "Keine UUID fuer ${boot_part} gefunden"; return 1; }
echo "UUID=${boot_uuid} /boot/efi vfat umask=0077 0 1"
fi
} > "${target}/etc/fstab"
}
# /dev, /proc, /sys, /dev/pts in den Zielbaum einbinden (fuer chroot-
# Operationen, die Geraeteknoten/Kernel-Interfaces brauchen - initramfs,
# grub-install) sowie eine funktionierende DNS-Aufloesung fuers
# apt-get-basierte Bootloader-Nachinstallieren. MOUNTED_STACK wird vom
# aufrufenden Skript vorgehalten (siehe image_deploy_unbind_mounts()).
image_deploy_bind_mounts() {
local target="$1"
local -n stack_ref="$2"
# 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() {
local -n stack_ref="$1"
local i mnt
for ((i = ${#stack_ref[@]} - 1; i >= 0; i--)); do
mnt="${stack_ref[i]}"
umount --recursive "${mnt}" 2>/dev/null || umount --lazy "${mnt}" 2>/dev/null || true
done
stack_ref=()
}
# machine-id + SSH-Hostkeys werden im Golden Image bewusst NICHT
# mitgeliefert (siehe build_golden_image.sh) - hier, pro tatsaechlich
# ausgerolltem Geraet, frisch erzeugt. update-initramfs regeneriert das
# initrd fuer die tatsaechliche Zielhardware (Golden Image wurde auf
# einem anderen System gebaut).
image_deploy_chroot_fixup() {
local target="$1"
chroot "${target}" systemd-machine-id-setup ||
{ image_deploy_fatal "machine-id konnte nicht erzeugt werden"; return 1; }
chroot "${target}" ssh-keygen -A ||
{ image_deploy_fatal "SSH-Hostkeys konnten nicht erzeugt werden"; return 1; }
chroot "${target}" update-initramfs -u -k all ||
{ image_deploy_fatal "initramfs-Regenerierung fehlgeschlagen"; return 1; }
}
# Grub wird bewusst NICHT ins Golden Image eingebaut (spart Platz, das
# Image kennt beim Bauen das Zielfirmware-Schema noch nicht) - stattdessen
# hier zur Deployment-Zeit nachinstalliert, wo bereits Netzwerk verfuegbar
# ist (Stufe-1-Umgebung hat sich schon fuer die Aktivierung verbunden).
image_deploy_install_bootloader() {
local target="$1"
local disk="$2"
local is_efi="$3"
if [[ "${is_efi}" == "true" ]]; then
chroot "${target}" /bin/bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y grub-efi-amd64
' || { image_deploy_fatal "grub-efi-amd64-Installation fehlgeschlagen"; return 1; }
chroot "${target}" grub-install --target=x86_64-efi \
--efi-directory=/boot/efi --bootloader-id=tuxflotte --recheck ||
{ image_deploy_fatal "grub-install (UEFI) fehlgeschlagen"; return 1; }
# Zusaetzlich auf den Standard-Fallback-Pfad (EFI/BOOT/BOOTX64.EFI)
# installieren - real beim Testen entdeckt: grub-install kann in
# verschachtelten/eingeschraenkten Umgebungen keinen NVRAM-
# Booteintrag setzen ("EFI variables are not supported on this
# system"), ohne --removable bleibt dann NUR der benannte
# /EFI/tuxflotte/-Pfad uebrig, den die Firmware ohne NVRAM-Eintrag
# nie findet ("No bootable option or device was found"). Der
# Fallback-Pfad wird von JEDER UEFI-Firmware ohne NVRAM-Eintrag
# automatisch versucht - robuster fuer heterogene Zielhardware
# generell, nicht nur fuer dieses Testszenario.
chroot "${target}" grub-install --target=x86_64-efi \
--efi-directory=/boot/efi --removable --recheck ||
{ image_deploy_fatal "grub-install (UEFI, removable-Fallback) fehlgeschlagen"; return 1; }
else
chroot "${target}" /bin/bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y grub-pc
' || { image_deploy_fatal "grub-pc-Installation fehlgeschlagen"; return 1; }
chroot "${target}" grub-install --target=i386-pc --recheck "${disk}" ||
{ image_deploy_fatal "grub-install (BIOS) fehlgeschlagen"; return 1; }
fi
chroot "${target}" update-grub ||
{ image_deploy_fatal "update-grub fehlgeschlagen"; return 1; }
}
image_deploy_set_hostname() {
local target="$1"
local hostname="$2"
echo "${hostname}" > "${target}/etc/hostname"
printf '127.0.1.1\t%s\n' "${hostname}" >> "${target}/etc/hosts"
}

View File

@ -0,0 +1,71 @@
#!/usr/bin/env bash
log_info() {
echo "[INFO ] $*"
}
log_warn() {
echo "[WARN ] $*" >&2
}
log_error() {
echo "[ERROR] $*" >&2
}
log_success() {
echo "[ OK ] $*"
}
# Klar abgesetzte Fortschrittsanzeige fuer die tty-Konsole des Boot-Mediums
# (kein Desktop, kein Kiosk-Browser - die Terminal-Ausgabe selbst ist die
# einzige UI, die eine Person vor dem Geraet sieht). Gilt fuer Dev- UND
# Produktiv-Build gleichermassen, nicht nur zum Debuggen - siehe Plan
# "Klare Fortschrittsanzeige auf der Konsole". Format/Stil bewusst an die
# bestehenden Banner in 17_device_status.sh angelehnt (Titel + "="-Linie
# gleicher Laenge, keine Sonderzeichen, die auf einer rohen VT-Konsole
# fehlen koennten).
declare -A TUXFLOTTE_STEP_LABELS=(
[00_preflight.sh]="Vorbereitung"
[05_network.sh]="Netzwerkverbindung wird hergestellt"
[10_hardware.sh]="Hardware wird erkannt"
[12_enrollment_auth.sh]="Aktivierungscode wird geprüft"
[15_server_handshake.sh]="Verbindung zu anode wird hergestellt"
[17_device_status.sh]="Gerätestatus wird abgerufen"
[20_profile_selection.sh]="Bereitstellungsvorlage wird ausgewählt"
[25_installation_confirm.sh]="Installationsbestätigung"
[30_runtime_blueprint.sh]="Installationsplan wird aufgelöst"
[40_backend.sh]="Golden Image wird geladen und installiert"
[20_storage.sh]="Speicher-Zusatzprüfung"
[99_finish.sh]="Abschluss"
)
readonly -a TUXFLOTTE_STEP_ORDER=(
00_preflight.sh 05_network.sh 10_hardware.sh 12_enrollment_auth.sh
15_server_handshake.sh 17_device_status.sh 20_profile_selection.sh
25_installation_confirm.sh 30_runtime_blueprint.sh 40_backend.sh
20_storage.sh 99_finish.sh
)
TUXFLOTTE_STEP_COUNT=0
log_step() {
local module_name="$1"
local label="${TUXFLOTTE_STEP_LABELS[$module_name]:-$module_name}"
local total="${#TUXFLOTTE_STEP_ORDER[@]}"
local bold="" reset=""
TUXFLOTTE_STEP_COUNT=$((TUXFLOTTE_STEP_COUNT + 1))
# Klartext-Fallback ohne Terminal (z.B. wenn stdout in eine Log-Datei
# umgeleitet wird) - kein tput-Aufruf, ncurses-bin ist bewusst nicht Teil
# des schlanken Paketsatzes, rohe ANSI-Codes reichen und sind ueberall
# ohne Zusatzpaket verfuegbar.
if [[ -t 1 ]]; then
bold=$'\033[1m'
reset=$'\033[0m'
fi
local title
title="[${TUXFLOTTE_STEP_COUNT}/${total}] ${label}"
printf '\n%s%s%s\n' "${bold}" "${title}" "${reset}"
printf '%s\n\n' "$(printf '=%.0s' $(seq 1 ${#title}))"
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
load_config() {
local config_file="$1"
if [[ -f "$config_file" ]]; then
# shellcheck disable=SC1090
source "$config_file"
log_info "Konfiguration geladen: $config_file"
else
log_warn "Keine Konfiguration gefunden: $config_file"
fi
}
run_module() {
local module="$1"
local mode="${2:-normal}"
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
log_step "$(basename "$module")"
log_info "Starte Modul: $(basename "$module")"
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then
log_warn "Dry-Run: Modul übersprungen: $module"
return 0
fi
# shellcheck disable=SC1090
"$module"
log_success "Modul abgeschlossen: $(basename "$module")"
}
require_root() {
[[ "$EUID" -eq 0 ]] || error_exit "Installer muss als root ausgeführt werden."
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Tuxflotte Installer
# Phase 0 Preflight
#
# Prueft, dass Werkzeuge vorhanden sind, die spaetere Module (ab
# 10_hardware.sh) brauchen. jq ist der einzige hier betroffene Fall:
# 10_hardware.sh, 12_enrollment_auth.sh, 15_server_handshake.sh,
# 17_device_status.sh, 20_profile_selection.sh, 25_installation_confirm.sh
# und 30_runtime_blueprint.sh nutzen es alle.
#
# Historisch (bis zum Umstieg auf das eigenstaendige, per live-build gebaute
# Boot-Medium) wurde jq hier noch zur Laufzeit per apt-get nachinstalliert,
# weil das damalige Boot-Medium (eine gepatchte Linux-Mint-Live-ISO) es nicht
# mitbrachte - real entdeckt: beim automatisierten Start ueber
# start-kiosk.sh (kein Terminal, keine sichtbare Fehlermeldung) blieb der
# Installer in 10_hardware.sh mit "Benoetigtes Programm nicht gefunden: jq"
# haengen, sichtbar nur in ~/.xsession-errors. Das eigenstaendige Boot-Medium
# bringt jq bereits im Paketsatz mit (siehe boot-medium/config/package-lists/
# tuxflotte.list.chroot) - diese Pruefung bleibt trotzdem als reine Assertion
# bestehen, damit ein kuenftiger Paketlisten-Fehler hier fruh und klar auffaellt,
# statt erst kryptisch in 10_hardware.sh.
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
command -v jq >/dev/null 2>&1 ||
fatal "jq fehlt auf dem Boot-Medium - Paketliste (tuxflotte.list.chroot) pruefen."

View File

@ -0,0 +1,544 @@
#!/usr/bin/env bash
# Tuxflotte Installer
# Phase 2 Netzwerkinitialisierung
#
# Unterstützt zunächst:
# - Ethernet über DHCP
# - bereits aktive NetworkManager-Verbindungen
# - WPA2/WPA3 Personal
# - interaktive WLAN-Auswahl
# - Prüfung des Tuxflotte-Servers
# - geschützte Runtime-Ablage des aktiven Netzwerkprofils
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly RUNTIME_DIR="/run/tuxflotte/network"
readonly STATE_FILE="${RUNTIME_DIR}/state.env"
readonly CONNECTION_EXPORT="${RUNTIME_DIR}/connection.nmconnection"
readonly SERVER_URL="${TUXFLOTTE_SERVER_URL:-https://anode.tuxflotte.de/health}"
readonly SERVER_TIMEOUT="${TUXFLOTTE_SERVER_TIMEOUT:-10}"
readonly NMCLI="${NMCLI:-nmcli}"
readonly CURL="${CURL:-curl}"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
warn() {
printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Netzwerkmodul muss als root ausgeführt werden."
fi
}
require_command() {
local command_name="$1"
command -v "${command_name}" >/dev/null 2>&1 ||
fatal "Benötigtes Programm nicht gefunden: ${command_name}"
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${STATE_FILE}" "${CONNECTION_EXPORT}"
}
networkmanager_is_running() {
"${NMCLI}" -t -f RUNNING general 2>/dev/null |
grep -qx 'running'
}
start_networkmanager_if_possible() {
if networkmanager_is_running; then
return 0
fi
log "NetworkManager ist noch nicht aktiv."
if command -v systemctl >/dev/null 2>&1; then
log "Versuche NetworkManager zu starten."
systemctl start NetworkManager.service 2>/dev/null || true
fi
networkmanager_is_running ||
fatal "NetworkManager konnte nicht verwendet werden."
}
enable_networking() {
"${NMCLI}" networking on >/dev/null 2>&1 || true
"${NMCLI}" radio wifi on >/dev/null 2>&1 || true
}
device_has_ipv4() {
local device="$1"
"${NMCLI}" -g IP4.ADDRESS device show "${device}" 2>/dev/null |
grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/'
}
get_active_device() {
local device
local type
while IFS=: read -r device type _; do
case "${type}" in
ethernet|wifi)
;;
*)
continue
;;
esac
[[ -e "/sys/class/net/${device}/device" ]] || continue
if device_has_ipv4 "${device}"; then
printf '%s\n' "${device}"
return 0
fi
done < <(
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status
)
return 1
}
get_active_connection_name() {
local device="$1"
"${NMCLI}" \
--get-values GENERAL.CONNECTION \
device show "${device}" 2>/dev/null |
head -n 1
}
get_device_type() {
local device="$1"
"${NMCLI}" \
--get-values GENERAL.TYPE \
device show "${device}" 2>/dev/null |
head -n 1
}
server_is_reachable() {
"${CURL}" \
--silent \
--show-error \
--fail \
--location \
--connect-timeout "${SERVER_TIMEOUT}" \
--max-time "${SERVER_TIMEOUT}" \
--output /dev/null \
"${SERVER_URL}"
}
check_existing_connection() {
local device
device="$(get_active_device || true)"
if [[ -z "${device}" ]]; then
return 1
fi
if ! device_has_ipv4 "${device}"; then
warn "Interface ${device} ist verbunden, besitzt aber keine IPv4-Adresse."
return 1
fi
log "Aktive Netzwerkverbindung über ${device} gefunden."
if server_is_reachable; then
log "Tuxflotte-Server ist erreichbar."
return 0
fi
warn "Netzwerk ist aktiv, aber der Tuxflotte-Server ist nicht erreichbar."
return 1
}
get_ethernet_devices() {
local device
while IFS=: read -r device type state; do
[[ "${type}" == "ethernet" ]] || continue
[[ "${state}" != "unavailable" ]] || continue
[[ -e "/sys/class/net/${device}/device" ]] || continue
printf '%s\n' "${device}"
done < <(
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status
)
}
try_ethernet() {
local device
while IFS= read -r device; do
[[ -n "${device}" ]] || continue
log "Prüfe Ethernet-Interface ${device}."
"${NMCLI}" device connect "${device}" >/dev/null 2>&1 || true
if device_has_ipv4 "${device}"; then
log "Ethernet-Verbindung über ${device} hergestellt."
if server_is_reachable; then
log "Tuxflotte-Server ist über Ethernet erreichbar."
return 0
fi
warn "Ethernet besitzt eine IP-Adresse, aber der Server ist nicht erreichbar."
fi
done < <(get_ethernet_devices)
return 1
}
get_wifi_device() {
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status |
awk -F: '$2 == "wifi" && $3 != "unavailable" { print $1; exit }'
}
scan_wifi_networks() {
local wifi_device="$1"
"${NMCLI}" device wifi rescan ifname "${wifi_device}" >/dev/null 2>&1 ||
true
sleep 2
"${NMCLI}" \
--terse \
--escape yes \
--fields SSID,SIGNAL,SECURITY \
device wifi list \
ifname "${wifi_device}" |
awk -F: '
$1 != "" && !seen[$1]++ {
printf "%s\t%s\t%s\n", $1, $2, $3
}
' |
sort -t $'\t' -k2,2nr
}
choose_wifi_ssid() {
local wifi_device="$1"
local -a networks=()
local entry
local choice
local index=1
while IFS= read -r entry; do
[[ -n "${entry}" ]] && networks+=("${entry}")
done < <(scan_wifi_networks "${wifi_device}")
if [[ "${#networks[@]}" -eq 0 ]]; then
warn "Keine sichtbaren WLAN-Netze gefunden."
read -r -p "Versteckte SSID manuell eingeben oder leer abbrechen: " WIFI_SSID
[[ -n "${WIFI_SSID}" ]]
return
fi
printf '\nVerfügbare WLAN-Netze:\n\n' >&2
for entry in "${networks[@]}"; do
IFS=$'\t' read -r ssid signal security <<<"${entry}"
printf ' %2d) %-32s Signal: %-3s Sicherheit: %s\n' \
"${index}" \
"${ssid}" \
"${signal}" \
"${security:-offen}" >&2
((index += 1))
done
printf '\n' >&2
read -r -p "WLAN auswählen [1-${#networks[@]}], m = manuell, q = abbrechen: " choice
case "${choice}" in
q|Q)
return 1
;;
m|M)
read -r -p "SSID: " WIFI_SSID
[[ -n "${WIFI_SSID}" ]]
;;
*)
if [[ ! "${choice}" =~ ^[0-9]+$ ]] ||
(( choice < 1 || choice > ${#networks[@]} )); then
warn "Ungültige Auswahl."
return 1
fi
IFS=$'\t' read -r WIFI_SSID _ _ <<<"${networks[choice - 1]}"
;;
esac
}
wifi_network_is_open() {
local wifi_device="$1"
local ssid="$2"
local security
security="$(
"${NMCLI}" \
--terse \
--escape no \
--fields SSID,SECURITY \
device wifi list \
ifname "${wifi_device}" |
awk -F: -v wanted="${ssid}" '
$1 == wanted {
print $2
exit
}
'
)"
[[ -z "${security}" || "${security}" == "--" ]]
}
connect_wifi() {
local wifi_device="$1"
choose_wifi_ssid "${wifi_device}" ||
return 1
log "Verbinde mit WLAN '${WIFI_SSID}'."
if wifi_network_is_open "${wifi_device}" "${WIFI_SSID}"; then
if ! "${NMCLI}" \
device wifi connect "${WIFI_SSID}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "Verbindung mit dem offenen WLAN konnte nicht hergestellt werden."
return 1
fi
else
log "NetworkManager fragt die WLAN-Zugangsdaten geschützt ab."
if ! "${NMCLI}" \
--ask \
device wifi connect "${WIFI_SSID}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "WLAN-Anmeldung ist fehlgeschlagen."
return 1
fi
fi
if ! device_has_ipv4 "${wifi_device}"; then
warn "WLAN-Verbindung besitzt keine IPv4-Adresse."
return 1
fi
log "WLAN-Verbindung wurde hergestellt."
if ! server_is_reachable; then
warn "WLAN ist verbunden, aber der Tuxflotte-Server ist nicht erreichbar."
return 1
fi
log "Tuxflotte-Server ist über WLAN erreichbar."
}
connect_wifi_noninteractive() {
local wifi_device="$1"
local ssid="${TUXFLOTTE_WIFI_SSID}"
log "Verbinde mit vorkonfiguriertem WLAN '${ssid}' (nicht-interaktiv)."
if [[ -n "${TUXFLOTTE_WIFI_PSK:-}" ]]; then
if ! "${NMCLI}" \
device wifi connect "${ssid}" \
password "${TUXFLOTTE_WIFI_PSK}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "WLAN-Anmeldung mit vorkonfigurierten Zugangsdaten ist fehlgeschlagen."
return 1
fi
else
if ! "${NMCLI}" \
device wifi connect "${ssid}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "Verbindung mit dem offenen, vorkonfigurierten WLAN konnte nicht hergestellt werden."
return 1
fi
fi
if ! device_has_ipv4 "${wifi_device}"; then
warn "WLAN-Verbindung besitzt keine IPv4-Adresse."
return 1
fi
log "WLAN-Verbindung wurde hergestellt."
if ! server_is_reachable; then
warn "WLAN ist verbunden, aber der Tuxflotte-Server ist nicht erreichbar."
return 1
fi
log "Tuxflotte-Server ist über WLAN erreichbar."
}
store_network_state() {
local device
local connection_name
local device_type
local connection_uuid=""
device="$(get_active_device)" ||
fatal "Kein aktives Provisionierungsinterface gefunden."
connection_name="$(
get_active_connection_name "${device}" || true
)"
device_type="$(get_device_type "${device}")"
if [[ -n "${connection_name}" && "${connection_name}" != "--" ]]; then
connection_uuid="$(
"${NMCLI}" \
--get-values connection.uuid \
connection show "${connection_name}" 2>/dev/null |
head -n 1
)"
else
connection_name=""
warn "Interface ${device} wird nicht durch ein aktives NetworkManager-Profil verwaltet."
fi
umask 077
{
printf 'TUXFLOTTE_NETWORK_DEVICE=%q\n' "${device}"
printf 'TUXFLOTTE_NETWORK_TYPE=%q\n' "${device_type}"
printf 'TUXFLOTTE_CONNECTION_NAME=%q\n' "${connection_name}"
printf 'TUXFLOTTE_CONNECTION_UUID=%q\n' "${connection_uuid}"
printf 'TUXFLOTTE_SERVER_URL=%q\n' "${SERVER_URL}"
} >"${STATE_FILE}"
chmod 0600 "${STATE_FILE}"
log "Netzwerkstatus wurde unter ${STATE_FILE} gespeichert."
}
export_connection_profile() {
local device
local connection_name
local source_file
device="$(get_active_device)" ||
fatal "Kein aktives Provisionierungsinterface gefunden."
connection_name="$(
get_active_connection_name "${device}" || true
)"
if [[ -z "${connection_name}" || "${connection_name}" == "--" ]]; then
warn "Für Interface ${device} existiert kein aktives NetworkManager-Profil."
return 0
fi
source_file="$(
"${NMCLI}" \
--get-values connection.filename \
connection show "${connection_name}" 2>/dev/null |
head -n 1 || true
)"
if [[ -z "${source_file}" || ! -f "${source_file}" ]]; then
warn "NetworkManager-Profil konnte nicht exportiert werden."
return 0
fi
install \
--mode=0600 \
--owner=root \
--group=root \
"${source_file}" \
"${CONNECTION_EXPORT}"
log "Aktives Verbindungsprofil wurde geschützt vorgemerkt."
}
main() {
require_root
require_command "${NMCLI}"
require_command "${CURL}"
prepare_runtime_directory
start_networkmanager_if_possible
enable_networking
log "Prüfe vorhandene Netzwerkverbindungen."
if check_existing_connection; then
:
elif try_ethernet; then
:
else
local wifi_device
wifi_device="$(get_wifi_device || true)"
if [[ -z "${wifi_device}" ]]; then
fatal "Keine funktionierende Ethernet-Verbindung und keine WLAN-Hardware gefunden."
fi
log "Ethernet ist nicht verfügbar. WLAN-Initialisierung wird gestartet."
if [[ -n "${TUXFLOTTE_WIFI_SSID:-}" ]]; then
connect_wifi_noninteractive "${wifi_device}" ||
fatal "Es konnte keine Verbindung zum Tuxflotte-Server hergestellt werden."
else
connect_wifi "${wifi_device}" ||
fatal "Es konnte keine Verbindung zum Tuxflotte-Server hergestellt werden."
fi
fi
store_network_state
export_connection_profile
log "Netzwerkinitialisierung erfolgreich abgeschlossen."
}
main "$@"

View File

@ -0,0 +1,550 @@
#!/usr/bin/env bash
# Tuxflotte Installer
# Phase 2 Hardware- und Geräteidentität
#
# Ermittelt:
# - DMI-/SMBIOS-Daten
# - System-UUID und Seriennummer
# - CPU-Architektur
# - physische Netzwerkinterfaces und MAC-Adressen
# - TPM-Verfügbarkeit
# - UEFI- und Secure-Boot-Status
#
# Ausgabe:
# /run/tuxflotte/hardware/hardware.json
set -Eeuo pipefail
SCRIPT_DIR="$(
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &&
pwd
)"
readonly SCRIPT_DIR
readonly COLLECTORS_FILE="${SCRIPT_DIR}/../lib/hardware_collectors.sh"
if [[ ! -r "${COLLECTORS_FILE}" ]]; then
printf '[%s] FEHLER: Collector-Library nicht gefunden: %s\n' \
"${0##*/}" \
"${COLLECTORS_FILE}" >&2
exit 1
fi
# shellcheck source=../lib/hardware_collectors.sh
source "${COLLECTORS_FILE}"
readonly SCRIPT_NAME="${0##*/}"
readonly RUNTIME_DIR="/run/tuxflotte/hardware"
readonly HARDWARE_FILE="${RUNTIME_DIR}/hardware.json"
readonly SYS_DMI_DIR="/sys/class/dmi/id"
readonly SYS_NET_DIR="/sys/class/net"
readonly EFI_VARS_DIR="/sys/firmware/efi/efivars"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
warn() {
printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Hardwaremodul muss als root ausgeführt werden."
fi
}
require_command() {
local command_name="$1"
command -v "${command_name}" >/dev/null 2>&1 ||
fatal "Benötigtes Programm nicht gefunden: ${command_name}"
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${HARDWARE_FILE}"
}
read_trimmed_file() {
local file="$1"
local value
if [[ ! -r "${file}" ]]; then
return 0
fi
value="$(tr -d '\000' <"${file}")"
value="$(
printf '%s' "${value}" |
sed \
-e 's/^[[:space:]]*//' \
-e 's/[[:space:]]*$//'
)"
printf '%s' "${value}"
}
read_dmi_value() {
local name="$1"
read_trimmed_file "${SYS_DMI_DIR}/${name}"
}
normalize_uuid() {
local value="$1"
value="${value,,}"
case "${value}" in
""|\
"none"|\
"not specified"|\
"to be filled by o.e.m."|\
"00000000-0000-0000-0000-000000000000"|\
"ffffffff-ffff-ffff-ffff-ffffffffffff")
return 0
;;
esac
printf '%s' "${value}"
}
normalize_serial() {
local value="$1"
local normalized
normalized="${value,,}"
case "${normalized}" in
""|\
"none"|\
"unknown"|\
"not specified"|\
"default string"|\
"system serial number"|\
"to be filled by o.e.m.")
return 0
;;
esac
printf '%s' "${value}"
}
get_machine_id() {
local candidate
for candidate in \
/etc/machine-id \
/var/lib/dbus/machine-id
do
if [[ -r "${candidate}" ]]; then
read_trimmed_file "${candidate}"
return 0
fi
done
}
get_architecture() {
uname -m
}
get_boot_mode() {
if [[ -d /sys/firmware/efi ]]; then
printf 'uefi'
else
printf 'bios'
fi
}
get_secure_boot_state() {
local secure_boot_file
local value
if [[ ! -d /sys/firmware/efi ]]; then
printf 'unsupported'
return 0
fi
secure_boot_file="$(
find "${EFI_VARS_DIR}" \
-maxdepth 1 \
-type f \
-name 'SecureBoot-*' \
-print \
-quit 2>/dev/null || true
)"
if [[ -z "${secure_boot_file}" || ! -r "${secure_boot_file}" ]]; then
printf 'unknown'
return 0
fi
value="$(
od \
--address-radix=n \
--format=u1 \
--skip-bytes=4 \
--read-bytes=1 \
"${secure_boot_file}" 2>/dev/null |
tr -d '[:space:]'
)"
case "${value}" in
1)
printf 'enabled'
;;
0)
printf 'disabled'
;;
*)
printf 'unknown'
;;
esac
}
get_tpm_version() {
if [[ ! -e /dev/tpm0 && ! -e /dev/tpmrm0 ]]; then
printf 'none'
return 0
fi
if [[ -r /sys/class/tpm/tpm0/tpm_version_major ]]; then
read_trimmed_file /sys/class/tpm/tpm0/tpm_version_major
return 0
fi
if [[ -r /sys/class/tpm/tpm0/device/description ]]; then
local description
description="$(
read_trimmed_file /sys/class/tpm/tpm0/device/description
)"
case "${description}" in
*2.0*)
printf '2'
;;
*1.2*)
printf '1.2'
;;
*)
printf 'unknown'
;;
esac
return 0
fi
printf 'unknown'
}
interface_is_physical() {
local interface="$1"
[[ "${interface}" != "lo" ]] || return 1
[[ -e "${SYS_NET_DIR}/${interface}/device" ]] || return 1
[[ -r "${SYS_NET_DIR}/${interface}/address" ]] || return 1
}
get_interface_type() {
local interface="$1"
if [[ -d "${SYS_NET_DIR}/${interface}/wireless" ]]; then
printf 'wifi'
else
printf 'ethernet'
fi
}
build_network_interfaces_json() {
local interface
local mac
local type
local -a interfaces=()
for interface_path in "${SYS_NET_DIR}"/*; do
[[ -e "${interface_path}" ]] || continue
interface="${interface_path##*/}"
interface_is_physical "${interface}" || continue
mac="$(read_trimmed_file "${interface_path}/address")"
type="$(get_interface_type "${interface}")"
[[ -n "${mac}" ]] || continue
interfaces+=("$(
jq \
--null-input \
--arg name "${interface}" \
--arg type "${type}" \
--arg mac "${mac,,}" \
'{
name: $name,
type: $type,
mac: $mac
}'
)")
done
if [[ "${#interfaces[@]}" -eq 0 ]]; then
printf '[]'
return 0
fi
printf '%s\n' "${interfaces[@]}" |
jq --slurp 'sort_by(.type, .name)'
}
build_device_fingerprint() {
local system_uuid="$1"
local system_serial="$2"
local board_serial="$3"
local interfaces_json="$4"
local identity_material
local mac_addresses
mac_addresses="$(
jq \
--raw-output \
'.[].mac // empty' \
<<<"${interfaces_json}" |
tr '[:upper:]' '[:lower:]' |
sort -u |
paste -sd ',' -
)"
identity_material="$(
printf 'system_uuid=%s\n' "${system_uuid,,}"
printf 'system_serial=%s\n' "${system_serial,,}"
printf 'board_serial=%s\n' "${board_serial,,}"
printf 'mac_addresses=%s\n' "${mac_addresses}"
)"
printf '%s' "${identity_material}" |
sha256sum |
awk '{ print $1 }'
}
build_hardware_json() {
local system_uuid
local system_serial
local machine_id
local manufacturer
local product_name
local product_version
local board_vendor
local board_name
local board_serial
local bios_vendor
local bios_version
local architecture
local boot_mode
local secure_boot
local tpm_version
local interfaces_json
local device_fingerprint
local cpu_model
local cpu_count
local memory_bytes
local storage_devices_json
system_uuid="$(normalize_uuid "$(read_dmi_value product_uuid)")"
system_serial="$(normalize_serial "$(read_dmi_value product_serial)")"
machine_id="$(get_machine_id)"
manufacturer="$(read_dmi_value sys_vendor)"
product_name="$(read_dmi_value product_name)"
product_version="$(read_dmi_value product_version)"
board_vendor="$(read_dmi_value board_vendor)"
board_name="$(read_dmi_value board_name)"
board_serial="$(normalize_serial "$(read_dmi_value board_serial)")"
bios_vendor="$(read_dmi_value bios_vendor)"
bios_version="$(read_dmi_value bios_version)"
architecture="$(get_architecture)"
boot_mode="$(get_boot_mode)"
secure_boot="$(get_secure_boot_state)"
tpm_version="$(get_tpm_version)"
cpu_model="$(get_cpu_model)"
cpu_count="$(get_cpu_count)"
memory_bytes="$(get_memory_bytes)"
interfaces_json="$(build_network_interfaces_json)"
storage_devices_json="$(build_storage_devices_json)"
device_fingerprint="$(
build_device_fingerprint \
"${system_uuid}" \
"${system_serial}" \
"${board_serial}" \
"${interfaces_json}"
)"
jq \
--null-input \
--arg schema_version "1" \
--arg device_fingerprint "${device_fingerprint}" \
--arg system_uuid "${system_uuid}" \
--arg system_serial "${system_serial}" \
--arg machine_id "${machine_id}" \
--arg manufacturer "${manufacturer}" \
--arg product_name "${product_name}" \
--arg product_version "${product_version}" \
--arg board_vendor "${board_vendor}" \
--arg board_name "${board_name}" \
--arg board_serial "${board_serial}" \
--arg bios_vendor "${bios_vendor}" \
--arg bios_version "${bios_version}" \
--arg architecture "${architecture}" \
--arg boot_mode "${boot_mode}" \
--arg secure_boot "${secure_boot}" \
--arg tpm_version "${tpm_version}" \
--arg cpu_model "${cpu_model}" \
--argjson cpu_count "${cpu_count}" \
--argjson memory_bytes "${memory_bytes}" \
--argjson network_interfaces "${interfaces_json}" \
--argjson storage_devices "${storage_devices_json}" \
'{
schema_version: ($schema_version | tonumber),
identity: {
device_fingerprint: $device_fingerprint,
system_uuid: (
if $system_uuid == "" then null else $system_uuid end
),
system_serial: (
if $system_serial == "" then null else $system_serial end
),
board_serial: (
if $board_serial == "" then null else $board_serial end
),
machine_id: (
if $machine_id == "" then null else $machine_id end
)
},
system: {
manufacturer: (
if $manufacturer == "" then null else $manufacturer end
),
product_name: (
if $product_name == "" then null else $product_name end
),
product_version: (
if $product_version == "" then null else $product_version end
),
architecture: $architecture,
cpu: {
model: (
if $cpu_model == "" then null else $cpu_model end
),
logical_count: $cpu_count
},
memory_bytes: $memory_bytes,
},
mainboard: {
vendor: (
if $board_vendor == "" then null else $board_vendor end
),
name: (
if $board_name == "" then null else $board_name end
)
},
firmware: {
bios_vendor: (
if $bios_vendor == "" then null else $bios_vendor end
),
bios_version: (
if $bios_version == "" then null else $bios_version end
),
boot_mode: $boot_mode,
secure_boot: $secure_boot
},
security: {
tpm_version: $tpm_version
},
network_interfaces: $network_interfaces,
storage_devices: $storage_devices
}'
}
validate_hardware_identity() {
local uuid
local serial
local board_serial
local mac_count
uuid="$(jq -r '.identity.system_uuid // empty' "${HARDWARE_FILE}")"
serial="$(jq -r '.identity.system_serial // empty' "${HARDWARE_FILE}")"
board_serial="$(jq -r '.identity.board_serial // empty' "${HARDWARE_FILE}")"
mac_count="$(jq '.network_interfaces | length' "${HARDWARE_FILE}")"
if [[ -z "${uuid}" &&
-z "${serial}" &&
-z "${board_serial}" &&
"${mac_count}" -eq 0 ]]; then
fatal "Es konnte kein stabiles Hardwaremerkmal ermittelt werden."
fi
if [[ -z "${uuid}" ]]; then
warn "Das Gerät stellt keine verwertbare System-UUID bereit."
fi
if [[ -z "${serial}" ]]; then
warn "Das Gerät stellt keine verwertbare Systemseriennummer bereit."
fi
}
main() {
require_root
require_command jq
require_command uname
require_command sed
require_command find
require_command od
require_command sha256sum
require_command sort
require_command paste
require_command tr
require_command awk
prepare_runtime_directory
log "Ermittle Hardware- und Geräteidentität."
umask 077
build_hardware_json >"${HARDWARE_FILE}"
chmod 0600 "${HARDWARE_FILE}"
jq --exit-status . "${HARDWARE_FILE}" >/dev/null ||
fatal "Die erzeugte Hardwaredatei enthält kein gültiges JSON."
validate_hardware_identity
log "Hardwareinformationen wurden unter ${HARDWARE_FILE} gespeichert."
}
main "$@"

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly RUNTIME_DIR="/run/tuxflotte/enrollment"
readonly AUTHORIZATION_FILE="${RUNTIME_DIR}/authorization.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Enrollment-Autorisierungsmodul muss als root ausgeführt werden."
fi
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${AUTHORIZATION_FILE}"
}
store_bootstrap_authorization() {
local activation_code
if [[ -n "${TUXFLOTTE_ACTIVATION_CODE:-}" ]]; then
activation_code="${TUXFLOTTE_ACTIVATION_CODE}"
else
printf '\n'
read -r -p "Temporären Aktivierungscode eingeben: " activation_code
fi
[[ -n "${activation_code}" ]] ||
fatal "Es wurde kein Aktivierungscode angegeben."
jq \
--null-input \
--arg activation_code "${activation_code}" \
'{
schema_version: 1,
authorization_type: "bootstrap_activation_code",
activation_code: $activation_code
}' >"${AUTHORIZATION_FILE}"
chmod 0600 "${AUTHORIZATION_FILE}"
}
validate_authorization() {
jq --exit-status '
.schema_version == 1
and .authorization_type == "bootstrap_activation_code"
and (.activation_code | type == "string")
and (.activation_code | length > 0)
' "${AUTHORIZATION_FILE}" >/dev/null ||
fatal "Enrollment-Autorisierung ist ungültig."
}
main() {
require_root
prepare_runtime_directory
store_bootstrap_authorization
validate_authorization
log "Temporäre Bootstrap-Autorisierung wurde vorbereitet."
}
main "$@"

View File

@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly NETWORK_STATE="/run/tuxflotte/network/state.env"
readonly HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
readonly RUNTIME_DIR="/run/tuxflotte/server"
readonly ACTIVATION_FILE="${RUNTIME_DIR}/activation.json"
readonly AUTHORIZATION_FILE="/run/tuxflotte/enrollment/authorization.json"
readonly RESPONSE_FILE="${RUNTIME_DIR}/response.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Server-Handshake-Modul muss als root ausgeführt werden."
fi
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${ACTIVATION_FILE}" "${RESPONSE_FILE}"
}
validate_inputs() {
[[ -r "${NETWORK_STATE}" ]] ||
fatal "Netzwerkstatus nicht gefunden: ${NETWORK_STATE}"
[[ -r "${HARDWARE_FILE}" ]] ||
fatal "Hardwareinformationen nicht gefunden: ${HARDWARE_FILE}"
jq --exit-status . "${HARDWARE_FILE}" >/dev/null ||
fatal "Hardwaredatei enthält kein gültiges JSON."
[[ -r "${AUTHORIZATION_FILE}" ]] ||
fatal "Enrollment-Autorisierung nicht gefunden: ${AUTHORIZATION_FILE}"
jq --exit-status '
.schema_version == 1
and .authorization_type == "bootstrap_activation_code"
and (.activation_code | type == "string")
and (.activation_code | length > 0)
' "${AUTHORIZATION_FILE}" >/dev/null ||
fatal "Enrollment-Autorisierung ist ungültig."
}
build_activation_request() {
local activation_code="$1"
local hostname
local machine_id
hostname="$(hostname)"
machine_id="$(
jq --raw-output \
'.identity.machine_id // empty' \
"${HARDWARE_FILE}"
)"
jq \
--null-input \
--arg activation_code "${activation_code}" \
--arg hostname "${hostname}" \
--arg machine_id "${machine_id}" \
--arg client_version "0.1.0" \
--slurpfile hardware "${HARDWARE_FILE}" \
'{
activation_code: $activation_code,
device_fingerprint: $hardware[0].identity.device_fingerprint,
hostname: $hostname,
machine_id: (
if $machine_id == ""
then null
else $machine_id
end
),
client_version: $client_version,
hardware: $hardware[0]
}'
}
send_activation_request() {
local server_url
# shellcheck disable=SC1090
source "${NETWORK_STATE}"
server_url="${TUXFLOTTE_SERVER_URL%/health}"
curl \
--silent \
--show-error \
--fail \
--location \
--header 'Content-Type: application/json' \
--data-binary "@${ACTIVATION_FILE}" \
--output "${RESPONSE_FILE}" \
"${server_url}/api/v1/activate" ||
fatal "Provisioning-Server konnte nicht erfolgreich kontaktiert werden."
chmod 0600 "${RESPONSE_FILE}"
jq --exit-status . "${RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält kein gültiges JSON."
jq --exit-status '.success == true' "${RESPONSE_FILE}" >/dev/null ||
fatal "Provisioning-Server hat die Aktivierung abgelehnt."
log "Provisioning-Handshake erfolgreich abgeschlossen."
}
main() {
require_root
prepare_runtime_directory
validate_inputs
log "Eingabedaten für den Provisioning-Handshake sind gültig."
local activation_code
local activation_code
activation_code="$(
jq --raw-output \
'.activation_code' \
"${AUTHORIZATION_FILE}"
)"
build_activation_request "${activation_code}" >"${ACTIVATION_FILE}"
chmod 0600 "${ACTIVATION_FILE}"
jq --exit-status . "${ACTIVATION_FILE}" >/dev/null ||
fatal "Aktivierungsrequest enthält kein gültiges JSON."
log "Aktivierungsrequest wurde unter ${ACTIVATION_FILE} gespeichert."
send_activation_request
}
main "$@"

View File

@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
readonly SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
readonly RUNTIME_DIR="/run/tuxflotte/provisioning"
readonly STATE_FILE="${RUNTIME_DIR}/state.env"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Gerätestatusmodul muss als root ausgeführt werden."
fi
}
prepare_runtime() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${STATE_FILE}"
}
store_provisioning_state() {
local continue_provisioning="$1"
umask 077
printf 'TUXFLOTTE_PROVISIONING_CONTINUE=%s\n' \
"${continue_provisioning}" >"${STATE_FILE}"
chmod 0600 "${STATE_FILE}"
}
validate_server_response() {
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"
jq --exit-status '
.success == true
and (.device | type == "object")
and (.device.registration_status == "existing"
or .device.registration_status == "registered")
and (.customer | type == "object")
' "${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält keinen gültigen Gerätestatus."
}
show_device_status() {
local registration_status
local hostname
local organization_name
registration_status="$(
jq -r '.device.registration_status' "${SERVER_RESPONSE_FILE}"
)"
hostname="$(
jq -r '.device.hostname // "unbekannt"' "${SERVER_RESPONSE_FILE}"
)"
organization_name="$(
jq -r '.customer.name // "unbekannt"' "${SERVER_RESPONSE_FILE}"
)"
printf '\n'
case "${registration_status}" in
existing)
printf 'Bekanntes Gerät erkannt\n'
printf '========================\n\n'
printf 'Gerät: %s\n' "${hostname}"
printf 'Organisation: %s\n' "${organization_name}"
printf '\n'
printf 'Das Gerät ist bereits registriert.\n'
;;
registered)
printf 'Neues Gerät registriert\n'
printf '=======================\n\n'
printf 'Gerät: %s\n' "${hostname}"
printf 'Organisation: %s\n' "${organization_name}"
printf '\n'
printf 'Hinweis:\n'
printf 'Die Registrierung erfolgte über den temporären Bootstrap-Aktivierungsmechanismus.\n'
;;
esac
}
confirm_provisioning() {
local answer
if [[ "${TUXFLOTTE_AUTO_MODE:-false}" == "true" ]]; then
store_provisioning_state true
log "Auto-Modus: Provisionierung wird ohne Rückfrage fortgesetzt."
return 0
fi
printf '\n'
read -r -p "Provisionierung fortsetzen? [j/N]: " answer
case "${answer}" in
j|J|ja|JA|Ja)
store_provisioning_state true
log "Provisionierung wird fortgesetzt."
;;
*)
store_provisioning_state false
log "Provisionierung wurde durch den Benutzer beendet."
;;
esac
}
main() {
require_root
prepare_runtime
validate_server_response
show_device_status
confirm_provisioning
}
main "$@"

View File

@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
readonly SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
readonly RUNTIME_DIR="/run/tuxflotte/assignment"
readonly TEMPLATE_FILE="${RUNTIME_DIR}/template.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*"
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Profilauswahlmodul muss als root ausgeführt werden."
fi
}
prepare_runtime() {
install -d -m 0700 "${RUNTIME_DIR}"
rm -f -- "${TEMPLATE_FILE}"
}
validate_server_response() {
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"
jq --exit-status . "${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält kein gültiges JSON."
jq --exit-status '
.success == true
and (.templates | type == "array")
and (.templates | length > 0)
' "${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält keine auswählbaren Bereitstellungsvorlagen."
}
show_templates() {
local index=1
local template
log "Verfügbare Bereitstellungsvorlagen:"
while IFS= read -r template; do
printf '\n'
printf ' %d) %s' \
"${index}" \
"$(jq -r '.label' <<<"${template}")"
if [[ "$(jq -r '.is_default' <<<"${template}")" == "true" ]]; then
printf ' (Standard)'
fi
printf '\n'
printf ' Workspace: %s\n' \
"$(jq -r '.workspace.name' <<<"${template}")"
printf ' Backend: %s %s\n' \
"$(jq -r '.backend.name' <<<"${template}")" \
"$(jq -r '.backend.version' <<<"${template}")"
((index += 1))
done < <(jq -c '.templates[]' "${SERVER_RESPONSE_FILE}")
}
select_template_auto() {
local default_index
# Kein spezifischer Server-Hinweis vorhanden (Enrollment Sessions legen
# serverseitig zwar schon eine Bereitstellungsvorlage fest, die
# Verbrauchslogik dafür existiert aber noch nicht - siehe Phase 3 im
# Plan) - bis dahin wird im Auto-Modus die als is_default markierte
# Vorlage gewählt.
default_index="$(
jq '[.templates[] | .is_default] | index(true)' "${SERVER_RESPONSE_FILE}"
)"
[[ "${default_index}" != "null" ]] ||
fatal "Auto-Modus: keine Standard-Bereitstellungsvorlage in der Serverantwort markiert."
jq \
--argjson index "${default_index}" \
'{
schema_version: 1,
template: .templates[$index]
}' \
"${SERVER_RESPONSE_FILE}" >"${TEMPLATE_FILE}"
log "Auto-Modus: Standard-Bereitstellungsvorlage automatisch gewählt."
}
select_template_interactive() {
local template_count
local selection
template_count="$(jq '.templates | length' "${SERVER_RESPONSE_FILE}")"
while true; do
printf '\n'
read -r -p "Bereitstellungsvorlage auswählen [1-${template_count}]: " selection
if [[ "${selection}" =~ ^[0-9]+$ ]] &&
((selection >= 1 && selection <= template_count)); then
break
fi
log "Ungültige Auswahl."
done
jq \
--argjson index "$((selection - 1))" \
'{
schema_version: 1,
template: .templates[$index]
}' \
"${SERVER_RESPONSE_FILE}" >"${TEMPLATE_FILE}"
}
select_template() {
if [[ "${TUXFLOTTE_AUTO_MODE:-false}" == "true" ]]; then
select_template_auto
else
select_template_interactive
fi
chmod 0600 "${TEMPLATE_FILE}"
jq --exit-status '
.schema_version == 1
and (.template | type == "object")
and (.template.id | type == "string")
and (.template.id | length > 0)
and (.template.workspace | type == "object")
and (.template.backend | type == "object")
' "${TEMPLATE_FILE}" >/dev/null ||
fatal "Ausgewählte Bereitstellungsvorlage konnte nicht gültig gespeichert werden."
log "Bereitstellungsvorlage $(jq -r '.template.id' "${TEMPLATE_FILE}") wurde ausgewählt."
log "Auswahl wurde unter ${TEMPLATE_FILE} gespeichert."
}
main() {
require_root
prepare_runtime
validate_server_response
show_templates
select_template
}
main "$@"

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
# shellcheck source=../lib/checks.sh
source "${SCRIPT_DIR}/../lib/checks.sh"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*"
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
main() {
log "Datenträger werden erkannt..."
local disks
disks="$(list_install_disks || true)"
[[ -n "${disks}" ]] ||
fatal "Keine geeigneten Datenträger erkannt."
printf '%s\n' "${disks}"
log "Phase 1: Datenträger werden nur angezeigt, nicht verändert."
log "Partitionierung ist noch deaktiviert."
}
main "$@"

View File

@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
readonly TEMPLATE_FILE="/run/tuxflotte/assignment/template.json"
readonly RUNTIME_DIR="/run/tuxflotte/installation"
readonly STATE_FILE="${RUNTIME_DIR}/state.env"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Installationsbestätigungsmodul muss als root ausgeführt werden."
fi
}
prepare_runtime() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${STATE_FILE}"
}
validate_template() {
[[ -r "${TEMPLATE_FILE}" ]] ||
fatal "Bereitstellungsvorlage nicht gefunden: ${TEMPLATE_FILE}"
jq --exit-status '
.schema_version == 1
and (.template | type == "object")
and (.template.id | type == "string")
and (.template.label | type == "string")
and (.template.workspace.name | type == "string")
and (.template.backend.name | type == "string")
and (.template.backend.version | type == "string")
' "${TEMPLATE_FILE}" >/dev/null ||
fatal "Bereitstellungsvorlage enthält keine gültige Auswahl."
}
show_installation_plan() {
local label
local workspace
local backend_name
local backend_version
label="$(jq -r '.template.label' "${TEMPLATE_FILE}")"
workspace="$(jq -r '.template.workspace.name' "${TEMPLATE_FILE}")"
backend_name="$(jq -r '.template.backend.name' "${TEMPLATE_FILE}")"
backend_version="$(jq -r '.template.backend.version' "${TEMPLATE_FILE}")"
printf '\n'
printf 'Geplanter Installationsvorgang\n'
printf '==============================\n\n'
printf 'Bereitstellungsvorlage: %s\n' "${label}"
printf 'Workspace: %s\n' "${workspace}"
printf 'Backend: %s %s\n' "${backend_name}" "${backend_version}"
printf '\n'
printf 'Die eigentliche Installation kann lokale Datenträger verändern.\n'
printf 'Bis zu dieser Bestätigung wurden keine destruktiven Installationsaktionen gestartet.\n'
printf '\n'
}
store_confirmation_state() {
local confirmed="$1"
umask 077
printf 'TUXFLOTTE_INSTALLATION_CONFIRMED=%s\n' \
"${confirmed}" >"${STATE_FILE}"
chmod 0600 "${STATE_FILE}"
}
confirm_installation() {
local answer
if [[ "${TUXFLOTTE_AUTO_MODE:-false}" == "true" ]]; then
store_confirmation_state true
log "Auto-Modus: Installation wird ohne Rückfrage gestartet."
return 0
fi
read -r -p "Installation jetzt starten? [j/N]: " answer
case "${answer}" in
j|J|ja|JA|Ja)
store_confirmation_state true
log "Installation wurde durch den Benutzer bestätigt."
;;
*)
store_confirmation_state false
log "Installation wurde durch den Benutzer abgebrochen."
log "Es wurden keine destruktiven Installationsaktionen gestartet."
;;
esac
}
main() {
require_root
prepare_runtime
validate_template
show_installation_plan
confirm_installation
}
main "$@"

View File

@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
readonly NETWORK_STATE="/run/tuxflotte/network/state.env"
readonly SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
readonly TEMPLATE_FILE="/run/tuxflotte/assignment/template.json"
readonly RUNTIME_DIR="/run/tuxflotte/runtime"
readonly RESOLVE_REQUEST_FILE="${RUNTIME_DIR}/resolve_request.json"
readonly BLUEPRINT_FILE="${RUNTIME_DIR}/runtime_blueprint.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Runtime-Blueprint-Modul muss als root ausgeführt werden."
fi
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${RESOLVE_REQUEST_FILE}" "${BLUEPRINT_FILE}"
}
validate_inputs() {
[[ -r "${NETWORK_STATE}" ]] ||
fatal "Netzwerkstatus nicht gefunden: ${NETWORK_STATE}"
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"
jq --exit-status '.device.id | type == "string" and length > 0' \
"${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält keine gültige Geräte-ID."
[[ -r "${TEMPLATE_FILE}" ]] ||
fatal "Bereitstellungsvorlage nicht gefunden: ${TEMPLATE_FILE}"
jq --exit-status '.template.id | type == "string" and length > 0' \
"${TEMPLATE_FILE}" >/dev/null ||
fatal "Bereitstellungsvorlage enthält keine gültige ID."
}
build_resolve_request() {
jq \
--null-input \
--slurpfile response "${SERVER_RESPONSE_FILE}" \
'{ device_id: $response[0].device.id }' \
>"${RESOLVE_REQUEST_FILE}"
chmod 0600 "${RESOLVE_REQUEST_FILE}"
}
send_resolve_request() {
local server_url
local template_id
# shellcheck disable=SC1090
source "${NETWORK_STATE}"
server_url="${TUXFLOTTE_SERVER_URL%/health}"
template_id="$(jq --raw-output '.template.id' "${TEMPLATE_FILE}")"
curl \
--silent \
--show-error \
--fail \
--location \
--header 'Content-Type: application/json' \
--data-binary "@${RESOLVE_REQUEST_FILE}" \
--output "${BLUEPRINT_FILE}" \
"${server_url}/api/v1/templates/${template_id}/resolve" ||
fatal "Runtime Blueprint konnte nicht aufgelöst werden."
chmod 0600 "${BLUEPRINT_FILE}"
jq --exit-status . "${BLUEPRINT_FILE}" >/dev/null ||
fatal "Antwort auf die Runtime-Blueprint-Anfrage enthält kein gültiges JSON."
jq --exit-status '.success == true' "${BLUEPRINT_FILE}" >/dev/null ||
fatal "$(jq -r '.message // "Provisioning-Server hat die Auflösung abgelehnt."' "${BLUEPRINT_FILE}")"
jq --exit-status '
.runtime_blueprint
| (.workspace_id | type == "string")
and (.backend_id | type == "string")
and (.blueprints | type == "array")
and (.installation_directives | type == "object")
' "${BLUEPRINT_FILE}" >/dev/null ||
fatal "Runtime Blueprint enthält keine gültige Zielbeschreibung."
log "Runtime Blueprint für Backend $(jq -r '.runtime_blueprint.backend_id' "${BLUEPRINT_FILE}") erzeugt."
log "Runtime Blueprint wurde unter ${BLUEPRINT_FILE} gespeichert."
}
main() {
require_root
prepare_runtime_directory
validate_inputs
build_resolve_request
send_resolve_request
}
main "$@"

View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
BACKENDS_DIR="$(cd "${SCRIPT_DIR}/../../backends" && pwd)"
readonly BACKENDS_DIR
readonly ORCHESTRATOR_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Backend-Modul muss als root ausgeführt werden."
fi
}
load_backend() {
local backend_id
local backend_script
[[ -r "${ORCHESTRATOR_BLUEPRINT_FILE}" ]] ||
fatal "Runtime Blueprint nicht gefunden: ${ORCHESTRATOR_BLUEPRINT_FILE}"
backend_id="$(jq --raw-output '.runtime_blueprint.backend_id // empty' "${ORCHESTRATOR_BLUEPRINT_FILE}")"
[[ -n "${backend_id}" ]] ||
fatal "Runtime Blueprint enthält keine gültige Backend-ID."
backend_script="${BACKENDS_DIR}/${backend_id}/backend.sh"
[[ -r "${backend_script}" ]] ||
fatal "Kein Backend für '${backend_id}' gefunden: ${backend_script}"
log "Lade Backend '${backend_id}' aus ${backend_script}"
# shellcheck disable=SC1090
source "${backend_script}"
}
run_lifecycle() {
local step
for step in backend_init backend_validate backend_generate_config backend_launch backend_postinstall; do
declare -f "${step}" >/dev/null ||
fatal "Backend implementiert erforderliche Funktion nicht: ${step}"
log "Führe ${step}() aus."
"${step}" ||
fatal "${step}() ist fehlgeschlagen."
done
}
main() {
require_root
load_backend
run_lifecycle
}
main "$@"

View File

@ -0,0 +1,4 @@
live-boot
live-config
live-config-systemd
systemd-sysv

View File

@ -0,0 +1,33 @@
# Tuxflotte Boot-Medium Paketliste (Phase 0 des unabhängigen Boot-Mediums)
# Basis: scripts/build_golden_image.sh (Zeilen 151-169), erweitert um Werkzeuge, die
# bisher erst zur Laufzeit per apt-get nachinstalliert wurden (00_preflight.sh,
# backend_init() in backends/mint-image/backend.sh), sowie btrfs-progs auf Nutzerwunsch.
linux-image-amd64
firmware-linux
firmware-realtek
firmware-iwlwifi
firmware-atheros
firmware-brcm80211
firmware-misc-nonfree
wireless-regdb
wpasupplicant
iw
rfkill
network-manager
openssh-server
sudo
locales
curl
ca-certificates
parted
# Deployment-Mechanik (scripts/lib/image_deploy.sh, backends/mint-image/backend.sh) -
# vorinstalliert statt Laufzeit-apt-get, damit 00_preflight.sh keine Netzwerk-vor-Preflight-
# Annahme mehr braucht
jq
gettext-base
dosfstools
e2fsprogs
zstd
btrfs-progs

View File

@ -0,0 +1,7 @@
# config/source - options for live-build(7), source stage
# Set source option
LB_SOURCE="false"
# Set image type
LB_SOURCE_IMAGES="tar"

View File

@ -1,44 +0,0 @@
loadfont unicode
set color_normal=white/black
set color_highlight=black/light-gray
set timeout=30
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit" --class linuxmint {
set gfxpayload=keep
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
initrd /casper/initrd.lz
}
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit (compatibility mode)" {
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} noapic noacpi nosplash irqpoll nomodeset --
initrd /casper/initrd.lz
}
menuentry "OEM install (for manufacturers)" {
set gfxpayload=keep
linux /casper/vmlinuz oem-config/enable=true only-ubiquity boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
initrd /casper/initrd.lz
}
menuentry "Tuxflotte Auto-Install (Linux Mint 22.3 Cinnamon)" --class linuxmint {
set gfxpayload=keep
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt priority=critical debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
initrd /casper/initrd.lz
}
grub_platform
if [ "$grub_platform" = "efi" ]; then
menuentry 'Von lokaler Festplatte booten (Standard)' {
exit 1
}
set default="Von lokaler Festplatte booten (Standard)"
menuentry 'UEFI Firmware Settings' {
fwsetup
}
menuentry 'Memory test' {
linux /boot/memtest.efi
}
else
menuentry 'Von lokaler Festplatte booten (Standard)' {
exit 1
}
set default="Von lokaler Festplatte booten (Standard)"
fi

View File

@ -1,57 +0,0 @@
timeout 100
menu background splash.png
menu title Welcome to Linux Mint 22.3 64-bit
menu color screen 37;40 #80ffffff #00000000 std
MENU COLOR border 30;44 #40ffffff #a0000000 std
MENU COLOR title 1;36;44 #ffffffff #a0000000 std
MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all
MENU COLOR unsel 37;44 #50ffffff #a0000000 std
MENU COLOR help 37;40 #c0ffffff #a0000000 std
MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std
MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std
MENU COLOR msg07 37;40 #90ffffff #a0000000 std
MENU COLOR tabmsg 31;40 #ffDEDEDE #00000000 std
MENU WIDTH 78
MENU MARGIN 15
MENU ROWS 6
MENU VSHIFT 10
MENU TABMSGROW 12
MENU CMDLINEROW 12
MENU HELPMSGROW 16
MENU HELPMSGENDROW 29
label tuxflotte
menu label Tuxflotte Auto-Install
kernel /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt priority=critical debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
label live
menu label Start Linux Mint
kernel /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
label compat
menu label Start Linux Mint in compatibility mode
linux /casper/vmlinuz
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint noapic noacpi nosplash irqpoll nomodeset --
label oem
menu label OEM install (for manufacturers)
linux /casper/vmlinuz
append oem-config/enable=true only-ubiquity boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
label hdt
menu label Hardware Detection
kernel hdt.c32
label local
menu label Boot from local drive
menu default
COM32 chain.c32
APPEND hd0
label memtest
menu label Memory test
linux /boot/memtest.bin

View File

@ -1,71 +0,0 @@
#! /bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
# get pre-requisites
prereqs)
prereqs
exit 0
;;
esac
# Ueberschreibt das originale 99casperboot (macht nur `touch
# /run/.casper-boot`) statt einen zusaetzlichen, neuen Hook-Skriptnamen
# einzufuehren: initramfs-tools generiert beim ISO-Bau eine ORDER-Datei
# (scripts/casper-bottom/ORDER) mit der fest verdrahteten Aufruf-Reihenfolge
# aller casper-bottom-Skripte - real durch Extraktion des echten initrd.lz
# mit unmkinitramfs bestaetigt. Ein per Initrd-Cpio-Layer NEU hinzugefuegtes
# Skript taucht in dieser ORDER-Datei nicht auf und wird nie aufgerufen,
# selbst wenn die Datei im initramfs physisch vorhanden ist (erste Version
# dieses Fixes ist daran gescheitert). Das Ueberschreiben eines bereits in
# ORDER referenzierten Pfads funktioniert dagegen zuverlaessig (spaetere
# cpio-Schichten ueberschreiben Dateiinhalte am selben Pfad, siehe
# scripts/lib/initrd.sh - derselbe Mechanismus, der schon fuer das
# Kexec-Preseed in Phase 1 verifiziert wurde). 99casperboot ist laut ORDER
# das letzte casper-bottom-Skript ueberhaupt - garantiert nach 05mountpoints,
# das /cdrom nach /root/cdrom umhaengt und damit die Voraussetzung fuer den
# folgenden Kopiervorgang schafft.
if [ -d /root/cdrom/updates ]; then
cp -a /root/cdrom/updates/. /root/
fi
# Ubiquity stuerzt im automatic-ubiquity-GTK-Modus real reproduzierbar ab
# (TypeError: Argument 1 does not allow None as a value), sobald
# enable_download_updates(False) den Netzwerkstatus als "nicht verbunden"
# meldet: ubi-prepare.py uebergibt das Ergebnis von
# self.controller.get_string('ubiquity/text/label_download_updates_na')
# ungeprueft an GtkLabel.set_label() - fehlt fuer diese Vorlage/Sprache ein
# Uebersetzungsstring, liefert get_string() None statt eines leeren Strings,
# und GTK akzeptiert kein None als Label-Text (real ueber QEMU-Testlauf
# gefunden, siehe ADR-0023-Nachtrag). Gezielter Sed-Patch statt vollstaendigem
# Datei-Ersatz, um nicht die komplette Drittanbieter-Datei mitpflegen zu
# muessen - betrifft nur die eine Zeile, "or ''" faengt jeden None-Rueckgabewert
# ab, unabhaengig von der genauen Ursache der fehlenden Uebersetzung.
ubi_prepare="/root/usr/lib/ubiquity/plugins/ubi-prepare.py"
if [ -f "$ubi_prepare" ]; then
sed -i \
"s/self\.controller\.get_string(template))/self.controller.get_string(template) or '')/" \
"$ubi_prepare"
fi
# AT-SPI-Bruecke fuer den Autoklicker (atspi_click.py) muss VOR dem
# GTK-Start von Ubiquity aktiv sein - siehe live-updates/usr/share/ubiquity/
# tuxflotte-atspi-env.sh (dorthin per casper-bottom-Kopie oben bereits nach
# /root/usr/share/ubiquity/ ausgeliefert). Einzeilige Einbindung direkt nach
# dem fruehen Existenz-Check von ubiquity-dm - alles Weitere, inkl.
# Paketinstallation, uebernimmt die eingebundene Datei selbst.
start_ubiquity_dm="/root/usr/share/ubiquity/start-ubiquity-dm"
if [ -f "$start_ubiquity_dm" ]; then
sed -i \
'/^test -x \/usr\/bin\/ubiquity-dm || exit 0$/a\
. /usr/share/ubiquity/tuxflotte-atspi-env.sh' \
"$start_ubiquity_dm"
fi
touch /run/.casper-boot

View File

@ -1 +0,0 @@
mint ALL=(ALL) NOPASSWD: ALL

View File

@ -1 +0,0 @@
../tuxflotte-autoclicker.service

View File

@ -1,17 +0,0 @@
[Unit]
Description=Tuxflotte Autoklicker fuer Ubiquitys GTK-Installationsassistenten
# Nur bei automatisierter Installation relevant (siehe autoclicker.sh -
# prueft selbst auf das Boot-Keyword "tuxflotte-autoclick" und beendet sich
# sofort, wenn es fehlt). Bewusst kein "After=graphical.target" o.ae., da
# ubiquity-dm keine normale Display-Manager-Sitzung startet, deren
# Bereitschaft systemd auf ueblichem Weg erkennen wuerde - das Skript selbst
# wartet auf den X-Socket.
After=multi-user.target
[Service]
Type=simple
ExecStart=/opt/tuxflotte/scripts/autoclicker.sh
Restart=no
[Install]
WantedBy=multi-user.target

View File

@ -1,4 +0,0 @@
[Desktop Entry]
Type=Application
Name=SELinux Troubleshooter Notifier
Hidden=true

View File

@ -1,6 +0,0 @@
[Desktop Entry]
Type=Application
Name=Tuxflotte Provisioning
Exec=/opt/tuxflotte/kiosk/start-kiosk.sh
X-GNOME-Autostart-enabled=true
NoDisplay=true

View File

@ -1,33 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Tuxflotte Provisioning</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #ffffff;
color: #3465a4;
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
max-width: 60%;
margin-bottom: 2rem;
}
p {
font-size: 1.2rem;
}
</style>
</head>
<body>
<img src="logo.svg" alt="Tuxflotte">
<p>Kiosk-Oberfläche in Entwicklung.</p>
</body>
</html>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 77 KiB

View File

@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# German keyboard layout for the graphical session (vconsole.keymap=de on the
# kernel cmdline only covers the text console, not X11/Wayland input). Cinnamon
# ships its own parallel org.cinnamon.desktop.input-sources schema, separate
# from GNOME's org.gnome.desktop.input-sources — writing org.gnome alone had
# no effect (confirmed on real hardware), so both are set here.
dconf write /org/cinnamon/desktop/input-sources/sources "[('xkb', 'de')]"
dconf write /org/gnome/desktop/input-sources/sources "[('xkb', 'de')]"
# Suppresses Epiphany's "set as default browser?" first-run prompt (schema
# default is true, see /usr/share/glib-2.0/schemas/org.gnome.epiphany.gschema.xml).
dconf write /org/gnome/epiphany/ask-for-default false
# Kiosk cleanup: no desktop icons (Nemo). The panel itself is kept (removing
# the last panel via panels-enabled triggers Cinnamon's own "you don't have
# any panels, open panel settings?" confirmation dialog — hardcoded in
# js/ui/panel.js, no way to suppress it, so panels-enabled is deliberately
# left untouched). Instead, only the two useful applets are kept: a window
# list (to restore Epiphany if a user minimizes it) and power (shutdown/
# reboot) — everything else (menu, systray, clock, workspace switcher etc.)
# is removed. Deliberately the classic window-list@cinnamon.org, not
# grouped-window-list@cinnamon.org: the grouped variant ships default
# "pinned apps" launchers (firefox.desktop, org.gnome.Terminal.desktop,
# nemo.desktop — confirmed via its settings-schema.json) that showed up in
# the panel even with unrelated applets removed. Plain window-list has no
# pinned-apps concept at all, only ever shows actually open windows.
dconf write /org/nemo/desktop/show-desktop-icons false
dconf write /org/cinnamon/enabled-applets "['panel1:left:0:window-list@cinnamon.org:1', 'panel1:right:0:power@cinnamon.org:2']"
mkdir -p "${HOME}/.local/share/tuxflotte-kiosk"
# Auto-Modus (personalisierte Kunden-ISO, siehe scripts/build_customer_iso.sh)
# startet den Installer sofort unbeaufsichtigt statt der Kiosk-Startseite -
# config/installer.conf wird nur bei personalisierten Abbildern mitgeliefert,
# das Standard-/Testabbild bleibt unverändert interaktiv.
if grep -qE '^\s*(export\s+)?TUXFLOTTE_AUTO_MODE=true\s*$' /opt/tuxflotte/config/installer.conf 2>/dev/null; then
exec sudo /opt/tuxflotte/scripts/installer.sh
fi
exec epiphany --profile="${HOME}/.local/share/tuxflotte-kiosk" file:///opt/tuxflotte/kiosk/index.html

View File

@ -1,308 +0,0 @@
#!/usr/bin/env python3
"""Klickt den primaeren "Weiter"-Button der aktuell sichtbaren Ubiquity-Seite
per AT-SPI (Linux-Barrierefreiheits-API) - wird von autoclicker.sh wiederholt
aufgerufen (ein Prozessstart pro Klickversuch, absichtlich einfach statt
dauerhaft im Hintergrund am Accessibility-Bus zu lauschen).
Warum AT-SPI statt Bildschirmkoordinaten: der urspruengliche xdotool-Ansatz
scheiterte daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher
Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - eine "groesstes
sichtbares Fenster"-Heuristik traf wiederholt das falsche Fenster (siehe
ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber seine
Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung
selbst - unabhaengig von Fenstergeometrie/-dekoration.
WICHTIG (real beim Testen entdeckt, 29.08.2026): trotz fest gesetztem
"debian-installer/language=de" sind NICHT alle Assistentenseiten auf
Deutsch beschriftet - die Mint-eigene "Multimedia-Codecs"-Seite zeigt reale
englische Buttons ("Quit"/"Back"/"Continue"), vermutlich dieselbe Ursache
wie der bereits gepatchte ubi-prepare.py-None-Bug (fehlender/unvollstaendiger
.mo-Uebersetzungskatalog fuer dieses Plugin auf dem Live-Medium). Eine reine
Werteliste bekannter deutscher Beschriftungen (fruehere Version dieses
Skripts) haette hier fuer immer haengen bleiben. Die Logik wurde deshalb auf
eine sprachunabhaengige Ausschluss-Strategie umgestellt: bekannte
rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden aus der
Kandidatenliste entfernt, der bzw. die verbleibenden Kandidaten werden
geklickt - siehe select_target() fuer die genaue Regel samt
Sicherheitsverhalten bei Mehrdeutigkeit.
Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed
grundsaetzlich auf de_DE gesetzt (siehe backend.sh), s.o. aber nicht
verlaesslich fuer jede einzelne Seite.
NACHTRAG (real beim Testen entdeckt, 29.08.2026): auf der manuellen
Partitionierungsseite ("Etwas Anderes") lief der Klick auf "Jetzt
installieren" nachweislich zuverlaessig (AT-SPI meldete jedes Mal Erfolg),
aber die Seite kam nie voran - stattdessen loeste jeder Klick erneut die
Debconf-Frage "ubiquity/partman-rebuild-cache" aus
(/lib/partman/update.d/99signal_ubiquity), was ubi-partman.py zurueck auf
dieselbe Seite wirft (siehe ADR-0023-Nachtrag). Ein gezielter Fix am
Partitionsrezept (kein zusaetzliches $bootable{ } auf der Root-Partition
unter UEFI) aenderte NICHTS - das deutet auf einen zeitlichen statt
strukturellen Fehler hin: die Seite zeigt waehrend dieser Phase ein
Status-Panel "Konfiguration der Installation wird ueberprueft ..." - klickt
der Autoklicker WAEHREND dieser Hintergrundpruefung noch laeuft, koennte
er sie unterbrechen/neu anstossen, statt ihr Zeit zum Abschliessen zu
geben. Dieses Skript wartet deshalb jetzt aktiv, solange dieser Text
sichtbar ist (siehe VALIDATION_TEXT_MARKERS/find_showing_text()), und
klickt erst, wenn er verschwunden ist.
Exit-Codes: 0 = ein Button wurde gefunden und geklickt,
1 = kein eindeutiger Button gefunden (z.B. Seite im Ladezustand, oder echte
Mehrdeutigkeit - dann bewusst NICHT geklickt, siehe select_target()),
2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv),
3 = Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar - bewusst
nicht geklickt, siehe Nachtrag oben.
"""
import sys
try:
import pyatspi
except ImportError:
print("pyatspi nicht verfuegbar", file=sys.stderr)
sys.exit(2)
# Zweisprachig, da nicht jede Seite verlaesslich deutsch beschriftet ist
# (siehe Modul-Docstring). Diese Namen werden aus der Kandidatenliste
# AUSGESCHLOSSEN, nie geklickt - unabhaengig davon, ob sie der einzige
# sichtbare Button waeren.
EXCLUDE_LABELS = {
"zurück",
"zurueck",
"back",
"abbrechen",
"abbruch",
"cancel",
"beenden",
"quit",
"exit",
"verwerfen",
}
# Bevorzugt geklickt, falls mehrere Kandidaten nach dem Ausschluss uebrig
# bleiben (siehe select_target()). Kein Ausschlusskriterium fuer
# Kandidaten, die HIER nicht drin stehen - die duerfen trotzdem als
# alleiniger Kandidat geklickt werden (Sole-Survivor-Regel), damit auch
# unbekannte/nicht uebersetzte Beschriftungen wie das reale "Continue"
# oben nicht zum Stillstand fuehren.
PREFER_LABELS = {
"weiter",
"continue",
"next",
"jetzt installieren",
"install now",
"fortfahren",
"proceed",
"installieren",
"install",
"neu starten",
"restart now",
"schließen",
"schliessen",
"close",
"finish",
"fertigstellen",
"done",
}
MAX_DEPTH = 40
# Zweisprachig vorsichtshalber (siehe Multimedia-Codecs-Erfahrung oben),
# auch wenn dieser konkrete Text bisher immer deutsch beobachtet wurde.
VALIDATION_TEXT_MARKERS = (
"wird überprüft",
"wird ueberprueft",
"being checked",
"checking the install",
)
def _node_text(node):
"""Sammelt allen aus einem Accessible auslesbaren Text: .name,
.description UND (falls unterstuetzt) das separate Text-Interface.
WICHTIG (real beim Testen entdeckt, 29.08.2026): ein einfaches GtkLabel
exponiert seinen sichtbaren Text ueblicherweise NICHT ueber .name/
.description (die bleiben dort oft leer) sondern ausschliesslich ueber
das AT-SPI-Text-Interface (queryText().getText(0, -1)) - ohne diesen
dritten Zugriffsweg fand find_showing_text() den sichtbar angezeigten
Status-Text nie, obwohl er laut Screenshot die ganze Zeit auf dem Schirm
stand."""
parts = []
try:
if node.name:
parts.append(node.name)
except Exception:
pass
try:
if node.description:
parts.append(node.description)
except Exception:
pass
try:
text_iface = node.queryText()
content = text_iface.getText(0, -1)
if content:
parts.append(content)
except NotImplementedError:
pass
except Exception:
pass
return " ".join(parts).strip().lower()
def find_showing_text(node, needle, depth=0):
"""Sucht ein sichtbares Accessible, dessen Text (siehe _node_text()) den
Suchtext enthaelt - unabhaengig von seiner Rolle (Label/Expander/...),
da Ubiquitys Status-Panel-Text nicht zuverlaessig unter derselben Rolle
exponiert sein muss. Bei Unsicherheit ueber den Sichtbarkeitsstatus
bewusst konservativ (lieber einen Zyklus zu viel warten als einen
Klick mitten in die Pruefung setzen)."""
if node is None or depth > MAX_DEPTH:
return False
if needle in _node_text(node):
try:
state = node.getState()
return state.contains(pyatspi.STATE_SHOWING)
except Exception:
return True
try:
count = node.childCount
except Exception:
return False
for i in range(count):
try:
child = node.getChildAtIndex(i)
except Exception:
continue
if find_showing_text(child, needle, depth + 1):
return True
return False
def collect_buttons(node, depth=0, acc=None):
"""Alle sichtbaren+aktivierten Push-Buttons im Baum einsammeln (Rolle,
Zustand). Absichtlich ALLE Kandidaten sammeln statt beim ersten Treffer
abzubrechen - select_target() braucht die vollstaendige Liste, um
Mehrdeutigkeit erkennen zu koennen."""
if acc is None:
acc = []
if node is None or depth > MAX_DEPTH:
return acc
try:
role = node.getRole()
except Exception:
return acc
if role == pyatspi.ROLE_PUSH_BUTTON:
try:
state = node.getState()
if state.contains(pyatspi.STATE_SENSITIVE) and state.contains(
pyatspi.STATE_SHOWING
):
acc.append(node)
except Exception:
pass
return acc
try:
count = node.childCount
except Exception:
return acc
for i in range(count):
try:
child = node.getChildAtIndex(i)
except Exception:
continue
collect_buttons(child, depth + 1, acc)
return acc
def select_target(buttons):
"""Waehlt aus allen sichtbaren+aktivierten Buttons genau einen aus, der
geklickt werden darf - oder None, wenn das nicht sicher moeglich ist.
Regel (bewusst sicherheitsorientiert, siehe Modul-Docstring):
1. Bekannte rueckwaerts-/abbrechende Beschriftungen (EXCLUDE_LABELS)
werden nie geklickt, unabhaengig vom Rest.
2. Von den verbleibenden Kandidaten wird bevorzugt, wer eine bekannte
vorwaerts-/abschliessende Beschriftung traegt (PREFER_LABELS).
3. Bleibt nach 1./2. KEIN bekannter Treffer, aber genau EIN Kandidat
uebrig, wird dieser geklickt (Sole-Survivor-Regel - deckt unbekannte/
nicht uebersetzte Beschriftungen wie das real beobachtete "Continue"
ab, siehe Modul-Docstring).
4. Bleiben mehrere unbekannte Kandidaten gleichzeitig uebrig, wird NICHT
geklickt (echte Mehrdeutigkeit - ein Fehlklick waere hier riskanter
als ein uebersprungener Zyklus, die Schleife in autoclicker.sh
versucht es 2 Sekunden spaeter erneut)."""
candidates = []
for btn in buttons:
name = (btn.name or "").strip().lower()
if name in EXCLUDE_LABELS:
continue
candidates.append(btn)
if not candidates:
return None
preferred = [b for b in candidates if (b.name or "").strip().lower() in PREFER_LABELS]
if preferred:
return preferred[0]
if len(candidates) == 1:
return candidates[0]
return None
def main():
try:
desktop = pyatspi.Registry.getDesktop(0)
except Exception as exc:
print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr)
return 2
apps = []
for i in range(desktop.childCount):
try:
app = desktop.getChildAtIndex(i)
except Exception:
continue
if app is not None:
apps.append(app)
for marker in VALIDATION_TEXT_MARKERS:
for app in apps:
if find_showing_text(app, marker):
print(
f"Hintergrundpruefung noch sichtbar ('{marker}'), warte statt zu klicken.",
file=sys.stderr,
)
return 3
all_buttons = []
for app in apps:
collect_buttons(app, 0, all_buttons)
target = select_target(all_buttons)
if target is None:
names = [repr((b.name or "").strip()) for b in all_buttons]
print(
"kein eindeutiger Button gefunden, sichtbare Buttons: "
+ (", ".join(names) if names else "(keine)"),
file=sys.stderr,
)
return 1
try:
action = target.queryAction()
action.doAction(0)
except Exception as exc:
print(f"Klick auf '{target.name}' fehlgeschlagen: {exc}", file=sys.stderr)
return 1
print(f"geklickt: {target.name}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,113 +0,0 @@
#!/bin/bash
### Klickt automatisiert durch Ubiquitys GTK-Installationsassistenten
### (automatic-ubiquity) - noetig, weil Ubiquitys eigenes "noninteractive"-
### Frontend die gefuehrte Partitionierung nicht zuverlaessig automatisieren
### kann (endlose choose_partition-Schleife in ubi-partman.py, real
### bestaetigt, siehe ADR-0023-Nachtrag). Der GTK-Assistent fuellt jede Seite
### bereits vollstaendig aus dem Preseed vor (siehe preseed.tpl) - es fehlt
### nur der "Weiter"/"Installieren"-Klick pro Seite.
###
### Klickt ueber AT-SPI (Barrierefreiheits-API), nicht mehr ueber
### Bildschirmkoordinaten/Enter-Taste (siehe atspi_click.py fuer die
### eigentliche Klicklogik). Der fruehere xdotool-Ansatz scheiterte real
### daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher
### Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - weder eine
### "groesstes sichtbares Fenster"-Heuristik noch die Enter-Taste (aktiviert
### auf der Partitionierungs-Uebersichtsseite einen Ausklapp-Pfeil statt des
### "Jetzt installieren"-Buttons) waren zuverlaessig genug (siehe
### ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber
### seine Rolle+seinen Namen im Accessibility-Baum der Anwendung selbst -
### unabhaengig von Fenstergeometrie/-dekoration.
###
### Die AT-SPI-Bruecke selbst (GTK_MODULES=gail:atk-bridge, Session-/A11y-
### Bus) wird NICHT hier, sondern bereits VOR dem GTK-Start von Ubiquity in
### tuxflotte-atspi-env.sh aktiviert (eingebunden von start-ubiquity-dm,
### siehe Sed-Patch in initrd-hooks/casper-bottom/99casperboot) - eine erst
### nachtraeglich aktivierte Bruecke wird von einer bereits laufenden
### GTK-Anwendung nicht mehr nachgeladen.
###
### Wird per systemd-Service (siehe ...service im selben Ausliefer-Layer) bei
### JEDEM Boot gestartet, bricht aber sofort ab, wenn das eigene Boot-Keyword
### "tuxflotte-autoclick" nicht in /proc/cmdline steht - siehe backend_launch()
### in backends/mint/backend.sh, wo dieses Keyword gesetzt wird. Auf einem
### normalen (nicht automatisierten) Boot also wirkungslos.
set -u
LOG=/var/log/tuxflotte-autoclicker.log
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >>"${LOG}"
}
grep -qw tuxflotte-autoclick /proc/cmdline || exit 0
log "tuxflotte-autoclick erkannt, starte Autoklicker."
export DISPLAY=:0
is_ubiquity_running() {
pgrep -f '/usr/bin/ubiquity' >/dev/null 2>&1
}
# ubiquity-dm startet Ubiquity frueh im Boot zu einem nicht exakt
# vorhersagbaren Zeitpunkt - auf den Prozess warten statt auf eine feste
# Wartezeit zu vertrauen. AT-SPI braucht anders als der fruehere
# xdotool-Ansatz keinen eigenen X-Socket-Check - der Zugriff laeuft ueber
# den Accessibility-Bus, nicht direkt ueber X11.
ubiquity_ready=0
for _ in $(seq 1 120); do
if is_ubiquity_running; then
ubiquity_ready=1
break
fi
sleep 1
done
if [ "${ubiquity_ready}" -ne 1 ]; then
log "ubiquity-Prozess nach 120s nicht erschienen, breche ab."
exit 1
fi
log "ubiquity-Prozess erkannt."
if ! python3 -c 'import pyatspi' >/dev/null 2>&1; then
log "pyatspi fehlt auf dem Live-Medium, installiere nach."
if ! apt-get install -y python3-pyatspi >>"${LOG}" 2>&1; then
log "pyatspi-Installation fehlgeschlagen, breche ab."
exit 1
fi
fi
ATSPI_CLICK=/opt/tuxflotte/scripts/atspi_click.py
log "Starte Klick-Schleife."
# Harte Obergrenze als Sicherheitsnetz (60 Minuten) - falls die Installation
# haengen bleibt, soll dieser Dienst nicht unbegrenzt weiterlaufen.
end=$((SECONDS + 3600))
clicks=0
misses=0
while [ "${SECONDS}" -lt "${end}" ]; do
if ! is_ubiquity_running; then
log "ubiquity-Prozess nicht mehr aktiv (vermutlich fertig oder abgestuerzt), beende Schleife nach ${clicks} Klick(s)."
break
fi
out="$(python3 "${ATSPI_CLICK}" 2>&1)"
rc=$?
if [ "${rc}" -eq 0 ]; then
clicks=$((clicks + 1))
misses=0
log "Klick ${clicks}: ${out}"
elif [ "${rc}" -eq 2 ]; then
log "AT-SPI-Bus nicht erreichbar, breche ab: ${out}"
break
elif [ "${rc}" -eq 3 ]; then
# Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar (siehe
# atspi_click.py-Nachtrag) - bewusst NICHT als Fehlversuch zaehlen,
# nur abwarten. Real beim Testen entdeckt: ein Klick auf "Jetzt
# installieren" WAEHREND dieser Pruefung fuehrte wiederholt in die
# partman-rebuild-cache-Schleife statt zum eigentlichen Commit.
log "wartet auf Hintergrundpruefung: ${out}"
else
misses=$((misses + 1))
log "kein Treffer (${misses}): ${out}"
fi
sleep 2
done
log "Autoklicker beendet (${clicks} Klick(s) insgesamt)."

View File

@ -1,54 +0,0 @@
#!/bin/sh
### Wird per Ein-Zeilen-Sed-Patch (siehe initrd-hooks/casper-bottom/99casperboot)
### GANZ AM ANFANG von start-ubiquity-dm eingebunden ("."), also BEVOR
### ubiquity-dm den X-Server startet und Ubiquity selbst exec't.
###
### Zweck: die AT-SPI-Barrierefreiheits-Bruecke (atk-bridge) fuer Ubiquitys
### GTK-Prozess aktivieren, damit atspi_click.py (siehe
### /opt/tuxflotte/scripts/) Buttons ueber ihre Rolle/ihren Namen im
### Accessibility-Baum anklicken kann statt ueber Bildschirmkoordinaten.
### Der xdotool-Koordinaten-Ansatz ist an ueberlappenden X11-Fenstern durch
### die Metacity-Fensterdekoration gescheitert (mehrere Fenster aehnlicher
### Groesse, "groesstes Fenster"-Heuristik traf wiederholt das falsche -
### siehe ADR-0023-Nachtrag).
###
### WICHTIG: das muss VOR dem GTK-Init von Ubiquity passieren - eine erst
### nachtraeglich verfuegbare Bruecke wird von einer bereits laufenden
### GTK-Anwendung nicht mehr nachgeladen (GTK_MODULES wird nur beim Start
### ausgewertet). Deshalb blockierend (kein Hintergrund-"&" fuer die
### Paketinstallation) und deshalb im Skript, das ubiquity-dm selbst
### aufruft, statt im spaeter laufenden Autoklicker-Service.
###
### Absichtlich EIN gemeinsamer apt-get-Aufruf fuer alle drei Pakete (statt
### getrennter Aufrufe je Skript) - der Autoklicker installiert xdotool
### zeitlich nah, ein zweiter paralleler apt-get/dpkg-Aufruf haette real
### beobachtete Lock-Konflikte riskiert (debconf: "config.dat is locked by
### another process").
for pkg in at-spi2-core dbus-x11 python3-pyatspi; do
dpkg -s "$pkg" >/dev/null 2>&1 && continue
if [ "${_tuxflotte_atspi_apt_updated:-}" != "1" ]; then
apt-get update -qq
_tuxflotte_atspi_apt_updated=1
fi
DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg"
done
export GTK_MODULES="gail:atk-bridge"
export NO_AT_BRIDGE=0
# Eigene Session-Bus-Instanz, falls keine geerbt wurde - die minimale
# ubiquity-dm-X-Sitzung startet anders als eine normale Desktop-Sitzung
# vermutlich keinen eigenen Session-Bus.
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then
eval "$(dbus-launch --sh-syntax)"
export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID
fi
if command -v at-spi-bus-launcher >/dev/null 2>&1; then
at-spi-bus-launcher --launch-immediately >/var/log/tuxflotte-atspi-bus.log 2>&1 &
# Kurz abwarten, bis der a11y-Bus unter org.a11y.Bus registriert ist,
# bevor ubiquity-dm/Ubiquity (naechste Zeile in start-ubiquity-dm)
# GTK initialisiert.
sleep 1
fi

45
scripts/boot-autostart.sh Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Wird von tuxflotte-installer.service beim Boot direkt auf tty1 gestartet -
# unabhaengig davon, ob config/installer.conf einen Auto-Modus-
# Aktivierungscode mitbringt (personalisierte Kunden-ISO, siehe
# scripts/build_customer_iso.sh) oder nicht (generisches Test-/
# Entwicklungsabbild): installer.sh fragt in letzterem Fall selbst
# interaktiv nach (12_enrollment_auth.sh, 05_network.sh-WLAN-Auswahl), auf
# genau dieser Konsole - der Service leitet dafuer TTYPath=/dev/tty1 durch.
#
# Schreibt zusaetzlich zur journal-Ausgabe (StandardOutput=journal+console
# im systemd-Unit) in eine Logdatei - per SSH/Konsole nachtraeglich
# einsehbar, ohne journalctl zu brauchen.
LOG_FILE="/var/log/tuxflotte-installer.log"
BANNER_FILE="/opt/tuxflotte/banner.txt"
# Begruessungsbanner (Tuxflotte-Schriftzug + Flotten-Grafik, 80 Spalten) -
# reine Optik, ins Logfile mitschreiben schadet nicht, aber nicht fatal
# wenn die Datei mal fehlt (set -e: "if" statt "&&", sonst wuerde ein
# fehlendes Banner das ganze Skript beenden).
if [[ -r "${BANNER_FILE}" ]]; then
{ cat "${BANNER_FILE}"; echo; } | tee -a "${LOG_FILE}"
fi
/opt/tuxflotte/scripts/installer.sh 2>&1 | tee -a "${LOG_FILE}"
STATUS="${PIPESTATUS[0]}"
# Live gefunden (31.08.2026): ein ExecStopPost, der beim Dienstende
# getty@tty1.service neu startet, ist unzuverlaessig (Race mit dem eigenen
# TTY-Teardown dieses Dienstes - der Job wurde angestossen, blieb aber
# "inactive/dead"). Stattdessen: diese Konsole faellt nach installer.sh
# (egal ob Erfolg, kontrollierter Abbruch an einem Commit-Gate, oder
# Fehler) deterministisch in eine interaktive Root-Shell - kein Race,
# garantiert nutzbare Konsole fuer Fehlersuche/manuellen Neustart. Im
# Erfolgsfall mit tatsaechlichem Deployment loest 40_backend.sh ohnehin
# selbst einen Reboot aus, wodurch diese Shell nie sichtbar wird.
{
echo
echo "Tuxflotte-Installer beendet (Exit-Code: ${STATUS})."
echo "Diese Konsole steht fuer Fehlersuche/manuellen Neustart zur Verfuegung."
echo
} | tee -a "${LOG_FILE}"
exec /bin/bash --login

View File

@ -1,6 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Nur noch fuer das Fedora-Backend (Kickstart/Anaconda) - das Mint-Backend
# ist auf das eigenstaendige, per live-build gebaute Boot-Medium umgezogen
# (siehe scripts/build_boot_medium.sh, boot-medium/). Der bisherige
# Mint-Zweig hier (Casper-Patchen einer echten Mint-Live-ISO + Ubiquity-
# Preseed) ist damit Geschichte - siehe ADR-0025 fuer die Begruendung.
SOURCE_ISO="${1:-}" SOURCE_ISO="${1:-}"
BACKEND="${2:-}" BACKEND="${2:-}"
@ -10,18 +16,8 @@ BUILD_DIR="$BASE_DIR/build"
WORK_DIR="$BUILD_DIR/work" WORK_DIR="$BUILD_DIR/work"
OUTPUT_DIR="$BUILD_DIR/output" OUTPUT_DIR="$BUILD_DIR/output"
# shellcheck source=lib/initrd.sh
source "$REPO_DIR/scripts/lib/initrd.sh"
# Test-Device fuer den in bake_test_preseed() eingebrannten, nicht
# personalisierten Test-Preseed (Mint) - dasselbe "Default Lab"-Testgeraet,
# das schon fuer die Fedora-Verifikation genutzt wurde. Echte Personalisierung
# pro Kunde ist nicht Teil dieses Build-Skripts (siehe ADR-0011-Kontext).
TEST_DEVICE_ID="a0238a0b-d2b5-4516-a6ce-da7170041d11"
TEST_HOSTNAME="tuxflotte-mint-test"
usage() { usage() {
echo "Usage: $0 /path/to/source.iso <fedora|mint>" echo "Usage: $0 /path/to/source.iso fedora"
} }
check_input() { check_input() {
@ -35,13 +31,10 @@ check_input() {
exit 1 exit 1
fi fi
case "$BACKEND" in if [[ "$BACKEND" != "fedora" ]]; then
fedora|mint) ;; echo "Error: unbekanntes Backend '$BACKEND' (erwartet: fedora)"
*) exit 1
echo "Error: unbekanntes Backend '$BACKEND' (erwartet: fedora oder mint)" fi
exit 1
;;
esac
} }
check_dependencies() { check_dependencies() {
@ -51,15 +44,6 @@ check_dependencies() {
exit 1 exit 1
fi fi
done done
if [[ "$BACKEND" == "mint" ]]; then
for cmd in jq envsubst base64 cpio; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: missing dependency: $cmd"
exit 1
fi
done
fi
} }
prepare_dirs() { prepare_dirs() {
@ -87,135 +71,39 @@ patch_grub() {
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf # -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim # Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
# Ueberschreiben mit "Keine Berechtigung" fehl. # Ueberschreiben mit "Keine Berechtigung" fehl.
if [[ "$BACKEND" == "fedora" ]]; then cp -f "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \
cp -f "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \ "$WORK_DIR/EFI/BOOT/grub.cfg"
"$WORK_DIR/EFI/BOOT/grub.cfg"
cp -f "$REPO_DIR/grub/boot-grub2-grub.cfg" \ cp -f "$REPO_DIR/grub/boot-grub2-grub.cfg" \
"$WORK_DIR/boot/grub2/grub.cfg" "$WORK_DIR/boot/grub2/grub.cfg"
else
# Mint hat, anders als Fedora, keine zweite ESP/FAT-Kopie des GRUB-Menüs,
# die separat gepatcht werden müsste (real gegen die vorhandene
# tuxflotte-mint-test.iso verifiziert - x86_64-efi/grub.cfg ist nur ein
# Loader-Stub, der per `source /boot/grub/grub.cfg` zurückverweist).
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"
fi
} }
verify_workdir() { verify_workdir() {
echo "Verifying workdir..." echo "Verifying workdir..."
if [[ "$BACKEND" == "fedora" ]]; then grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg"
grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg" grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg"
grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg"
else
grep -q "Tuxflotte" "$WORK_DIR/boot/grub/grub.cfg"
grep -q "Tuxflotte" "$WORK_DIR/isolinux/live.cfg"
fi
echo "GRUB verification passed." echo "GRUB verification passed."
} }
bake_test_preseed() {
[[ "$BACKEND" == "mint" ]] || return 0
echo "Rendering test preseed (nicht personalisiert, siehe TEST_DEVICE_ID)..."
local preseed_dir="$WORK_DIR/preseed"
local runtime_blueprint_file="$WORK_DIR/runtime_blueprint.json"
local server_response_file="$WORK_DIR/response.json"
mkdir -p "$preseed_dir"
cat > "$runtime_blueprint_file" <<EOF
{
"runtime_blueprint": {
"backend_id": "mint",
"blueprints": [],
"installation_directives": {
"disk_encryption": false,
"partitioning": {"scheme": "single", "root_filesystem": "ext4"},
"secure_boot_required": false
}
}
}
EOF
cat > "$server_response_file" <<EOF
{
"device": {"hostname": "${TEST_HOSTNAME}", "id": "${TEST_DEVICE_ID}"}
}
EOF
# backend.sh direkt wiederverwenden statt die Templating-Logik hier zu
# duplizieren - Variablen sind bewusst nicht readonly (siehe backend.sh),
# backend_init() wird übersprungen (braucht root für --owner/--group,
# hier nicht nötig, mkdir reicht für den Build-Kontext).
# shellcheck disable=SC1091
source "$REPO_DIR/backends/mint/backend.sh"
RUNTIME_BLUEPRINT_FILE="$runtime_blueprint_file"
SERVER_RESPONSE_FILE="$server_response_file"
RUNTIME_DIR="$preseed_dir"
CONFIG_FILE="$preseed_dir/tuxflotte.seed"
backend_validate
backend_generate_config
echo "Test-Preseed erzeugt: $CONFIG_FILE"
}
prepare_updates() { prepare_updates() {
echo "Assembling live-updates payload..." echo "Assembling updates payload..."
local updates_dir="$WORK_DIR/updates" local updates_dir="$WORK_DIR/updates"
rm -rf "$updates_dir" rm -rf "$updates_dir"
mkdir -p "$updates_dir/opt/tuxflotte" mkdir -p "$updates_dir/opt/tuxflotte"
cp -a "$REPO_DIR/live-updates/." "$updates_dir/"
cp -a "$REPO_DIR/scripts" "$updates_dir/opt/tuxflotte/" cp -a "$REPO_DIR/scripts" "$updates_dir/opt/tuxflotte/"
cp -a "$REPO_DIR/backends" "$updates_dir/opt/tuxflotte/" cp -a "$REPO_DIR/backends" "$updates_dir/opt/tuxflotte/"
cp -a "$REPO_DIR/config" "$updates_dir/opt/tuxflotte/" cp -a "$REPO_DIR/config" "$updates_dir/opt/tuxflotte/"
chmod 0440 "$updates_dir/etc/sudoers.d/90-tuxflotte"
}
patch_initrd() {
[[ "$BACKEND" == "mint" ]] || return 0
echo "Injecting casper-bottom updates-hook into initrd..."
inject_casper_bottom_hooks "$REPO_DIR" \
"$WORK_DIR/casper/initrd.lz" \
"$WORK_DIR/casper/initrd-tuxflotte.lz"
} }
create_iso() { create_iso() {
echo "Creating Tuxflotte ISO..." echo "Creating Tuxflotte ISO..."
local output_iso local output_iso="$OUTPUT_DIR/tuxflotte-provisioning-0.2.iso"
local -a grub_map_args
if [[ "$BACKEND" == "fedora" ]]; then
output_iso="$OUTPUT_DIR/tuxflotte-provisioning-0.2.iso"
grub_map_args=(
-map "$REPO_DIR/grub/EFI-BOOT-grub.cfg" /EFI/BOOT/grub.cfg
-map "$REPO_DIR/grub/boot-grub2-grub.cfg" /boot/grub2/grub.cfg
)
else
output_iso="$OUTPUT_DIR/tuxflotte-mint-provisioning-0.1.iso"
grub_map_args=(
-map "$REPO_DIR/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg
-map "$REPO_DIR/grub/mint-isolinux-live.cfg" /isolinux/live.cfg
-map "$WORK_DIR/preseed/tuxflotte.seed" /preseed/tuxflotte.seed
-map "$WORK_DIR/casper/initrd-tuxflotte.lz" /casper/initrd.lz
)
fi
rm -f "$output_iso" rm -f "$output_iso"
@ -224,7 +112,8 @@ create_iso() {
-outdev "$output_iso" \ -outdev "$output_iso" \
-compliance no_emul_toc \ -compliance no_emul_toc \
-volid TUXFLOTTE \ -volid TUXFLOTTE \
"${grub_map_args[@]}" \ -map "$REPO_DIR/grub/EFI-BOOT-grub.cfg" /EFI/BOOT/grub.cfg \
-map "$REPO_DIR/grub/boot-grub2-grub.cfg" /boot/grub2/grub.cfg \
-map "$WORK_DIR/updates" /updates \ -map "$WORK_DIR/updates" /updates \
-chown_r 0 /updates -- \ -chown_r 0 /updates -- \
-chgrp_r 0 /updates -- \ -chgrp_r 0 /updates -- \
@ -240,9 +129,7 @@ main() {
extract_iso extract_iso
patch_grub patch_grub
verify_workdir verify_workdir
bake_test_preseed
prepare_updates prepare_updates
patch_initrd
create_iso create_iso
echo echo

79
scripts/build_boot_medium.sh Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Baut das eigenstaendige Tuxflotte-Boot-Medium (Debian trixie, per
# Debian-live-build) - siehe Plan ~/.claude/plans/inherited-floating-treasure.md
# und Memory project_independent_boot_medium.md.
#
# Ersetzt langfristig scripts/build.sh/build_customer_iso.sh (Casper-Patchen
# einer echten Linux-Mint-Live-ISO): dieses Boot-Medium enthaelt nur
# Tuxflottes eigenen, headless-tauglichen Code (Module 00-40,
# backends/mint-image/) und beruehrt niemals Ubiquity/Casper/eine
# Distributions-Live-Session - macht die Bug-Klasse, die zu dieser
# Architektur gefuehrt hat (siehe Memory-Datei), strukturell unmoeglich.
#
# --dev aktiviert einen SSH-Debug-Zugang (Testzugang tuxflotte/test123,
# analog bestehender Test-Fixtures wie WLAN-Test-Org) - ohne --dev bleibt
# SSH deaktiviert, keine gebackenen Zugangsdaten (Produktiv-Default,
# konsistent damit, dass auch das Golden Image selbst keine Zugangsdaten
# enthaelt). Siehe boot-medium/config/hooks/live/0900-tuxflotte-ssh-mode.hook.chroot.
usage() {
echo "Usage: $0 [--dev]" >&2
echo " --dev SSH-Debug-Zugang aktivieren (tuxflotte/test123)" >&2
}
DEV_BUILD=false
for arg in "$@"; do
case "$arg" in
--dev) DEV_BUILD=true ;;
-h|--help) usage; exit 0 ;;
*) echo "Error: Unbekannter Parameter: $arg" >&2; usage; exit 1 ;;
esac
done
if [[ "${EUID}" -ne 0 ]]; then
echo "Error: muss als root laufen (live-build braucht root fuer chroot/mount)." >&2
exit 1
fi
for cmd in lb; do
command -v "${cmd}" >/dev/null 2>&1 ||
{ echo "Error: fehlendes Werkzeug: ${cmd} (apt-get install live-build)" >&2; exit 1; }
done
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BOOT_MEDIUM_DIR="${REPO_DIR}/boot-medium"
DEV_MARKER="${BOOT_MEDIUM_DIR}/config/includes.chroot/etc/tuxflotte-dev-build"
[[ -d "${BOOT_MEDIUM_DIR}/config" ]] ||
{ echo "Error: ${BOOT_MEDIUM_DIR}/config fehlt - lb config wurde noch nie ausgefuehrt." >&2; exit 1; }
cd "${BOOT_MEDIUM_DIR}"
if [[ "${DEV_BUILD}" == true ]]; then
echo "==> Dev-Build: SSH-Debug-Zugang wird aktiviert (tuxflotte/test123)."
mkdir -p "$(dirname "${DEV_MARKER}")"
touch "${DEV_MARKER}"
else
echo "==> Produktiv-Build: SSH bleibt deaktiviert."
rm -f "${DEV_MARKER}"
fi
echo "==> lb clean"
lb clean
echo "==> lb config"
lb config \
--distribution trixie \
--architectures amd64 \
--archive-areas "main contrib non-free non-free-firmware" \
--binary-images iso-hybrid \
--bootloaders "grub-efi,syslinux" \
--debian-installer none \
--bootappend-live "boot=live components quiet splash vconsole.keymap=de"
echo "==> lb build"
lb build
echo "==> Boot-Medium gebaut: ${BOOT_MEDIUM_DIR}/live-image-amd64.hybrid.iso"

View File

@ -1,16 +1,34 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Baut eine personalisierte Kunden-ISO fuer den Self-Service-Flow (Phase 4). # Personalisiert das eigenstaendige Boot-Medium (scripts/build_boot_medium.sh)
# Anders als build.sh (das fuer Testzwecke einen statischen Preseed mit # fuer eine bestimmte Kundenorganisation - befuellt config/installer.conf mit
# Fake-Geraete-ID einbrennt, siehe bake_test_preseed()) wird hier nur # Aktivierungscode + WLAN-Zugangsdaten. Hostname/Geraete-ID/Partitionierung
# config/installer.conf mit den echten, organisationsspezifischen Werten # werden weiterhin erst live auf dem Zielgeraet erzeugt (siehe
# befuellt - Hostname/Geraete-ID/Partitionierung werden weiterhin erst live # backend_generate_config() in backends/mint-image/backend.sh).
# auf dem Zielgeraet erzeugt (siehe backend_generate_config() in #
# backends/mint/backend.sh). Hier geht es nur um WLAN-Zugangsdaten + # Anders als das alte, Casper-basierte Verfahren (Xorriso-Extraktion der
# Enrollment-Session-Code als Boot-Konfiguration, die start-kiosk.sh dazu # ganzen ISO + Initrd-Hook-Injection, siehe scripts/lib/initrd.sh) kann hier
# bringt, den Installer beim Hochfahren automatisch zu starten statt der # NICHT einfach eine einzelne Datei direkt auf der ISO9660-Ebene ersetzt
# interaktiven Kiosk-Startseite. # 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() { usage() {
echo "Usage: $0 <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]" >&2 echo "Usage: $0 <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]" >&2
@ -28,20 +46,6 @@ WIFI_PSK="${5:-}"
# Aufrufe ohne sechsten Parameter (z.B. manuelle Tests). # Aufrufe ohne sechsten Parameter (z.B. manuelle Tests).
VOLID="${6:-TUXFLOTTE}" 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 if [[ -z "${SOURCE_ISO}" || -z "${OUTPUT_ISO}" || -z "${ACTIVATION_CODE}" ]]; then
usage usage
exit 1 exit 1
@ -50,51 +54,47 @@ fi
[[ -f "${SOURCE_ISO}" ]] || [[ -f "${SOURCE_ISO}" ]] ||
{ echo "Error: Quell-ISO nicht gefunden: ${SOURCE_ISO}" >&2; exit 1; } { 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 || command -v "${cmd}" >/dev/null 2>&1 ||
{ echo "Error: fehlendes Werkzeug: ${cmd}" >&2; exit 1; } { echo "Error: fehlendes Werkzeug: ${cmd}" >&2; exit 1; }
done done
echo "Extrahiere Quell-ISO..." WORK_DIR="$(mktemp -d)"
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}"
echo "Installiere Tuxflotte-GRUB-Konfiguration..." cleanup() {
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf # unsquashfs uebernimmt Original-Rechte (teils ohne Write-Bit) - ohne
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim # chmod schlaegt rm -rf darauf fehl.
# Ueberschreiben mit "Keine Berechtigung" fehl. chmod -R u+w "${WORK_DIR}" 2>/dev/null || true
cp -f "${REPO_DIR}/grub/mint-boot-grub.cfg" "${WORK_DIR}/boot/grub/grub.cfg" rm -rf "${WORK_DIR}"
cp -f "${REPO_DIR}/grub/mint-isolinux-live.cfg" "${WORK_DIR}/isolinux/live.cfg" }
trap cleanup EXIT
grep -q "Tuxflotte" "${WORK_DIR}/boot/grub/grub.cfg" echo "Extrahiere Squashfs aus Quell-ISO..."
grep -q "Tuxflotte" "${WORK_DIR}/isolinux/live.cfg" xorriso -indev "${SOURCE_ISO}" -osirrox on -extract /live/filesystem.squashfs \
"${WORK_DIR}/orig.squashfs" >/dev/null
echo "Baue personalisierte Boot-Konfiguration..." echo "Entpacke Squashfs..."
updates_dir="${WORK_DIR}/updates" unsquashfs -d "${WORK_DIR}/root" "${WORK_DIR}/orig.squashfs" >/dev/null
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 "Schreibe personalisierte installer.conf..."
mkdir -p "${WORK_DIR}/root/opt/tuxflotte/config"
{ {
echo "export TUXFLOTTE_AUTO_MODE=true" echo "export TUXFLOTTE_AUTO_MODE=true"
printf 'export TUXFLOTTE_ACTIVATION_CODE=%q\n' "${ACTIVATION_CODE}" printf 'export TUXFLOTTE_ACTIVATION_CODE=%q\n' "${ACTIVATION_CODE}"
[[ -n "${WIFI_SSID}" ]] && printf 'export TUXFLOTTE_WIFI_SSID=%q\n' "${WIFI_SSID}" [[ -n "${WIFI_SSID}" ]] && printf 'export TUXFLOTTE_WIFI_SSID=%q\n' "${WIFI_SSID}"
[[ -n "${WIFI_PSK}" ]] && printf 'export TUXFLOTTE_WIFI_PSK=%q\n' "${WIFI_PSK}" [[ -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 "Packe Squashfs neu (nur Kompression, kein apt/debootstrap)..."
mksquashfs "${WORK_DIR}/root" "${WORK_DIR}/new.squashfs" -comp xz -no-progress >/dev/null
echo "Injiziere casper-bottom Updates-Hook in die Initrd..." chmod 444 "${WORK_DIR}/new.squashfs"
custom_initrd="${WORK_DIR}/casper/initrd-tuxflotte.lz"
inject_casper_bottom_hooks "${REPO_DIR}" \
"${WORK_DIR}/casper/initrd.lz" \
"${custom_initrd}"
echo "Erzeuge Kunden-ISO..." echo "Erzeuge Kunden-ISO..."
rm -f "${OUTPUT_ISO}" rm -f "${OUTPUT_ISO}"
@ -102,14 +102,8 @@ rm -f "${OUTPUT_ISO}"
xorriso \ xorriso \
-indev "${SOURCE_ISO}" \ -indev "${SOURCE_ISO}" \
-outdev "${OUTPUT_ISO}" \ -outdev "${OUTPUT_ISO}" \
-compliance no_emul_toc \
-volid "${VOLID}" \ -volid "${VOLID}" \
-map "${REPO_DIR}/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg \ -update "${WORK_DIR}/new.squashfs" /live/filesystem.squashfs \
-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 -- \
-boot_image any replay -boot_image any replay
echo "Kunden-ISO erstellt: ${OUTPUT_ISO}" echo "Kunden-ISO erstellt: ${OUTPUT_ISO}"

View File

@ -115,6 +115,25 @@ image_deploy_mount() {
fi 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 # Entpackt das per build_golden_image.sh gebaute Golden Image direkt in
# den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt # den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt
# sowohl .tar.zst als auch (falls spaeter gewechselt) andere # sowohl .tar.zst als auch (falls spaeter gewechselt) andere
@ -128,6 +147,40 @@ image_deploy_extract_image() {
tar --zstd -xpf "${image_path}" -C "${target}" || tar --zstd -xpf "${image_path}" -C "${target}" ||
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; } { 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 # /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein
@ -166,11 +219,48 @@ image_deploy_bind_mounts() {
local target="$1" local target="$1"
local -n stack_ref="$2" local -n stack_ref="$2"
mount --bind /dev "${target}/dev" && stack_ref+=("${target}/dev") # Golden Image schliesst /dev, /proc, /sys bewusst aus
mount -t proc proc "${target}/proc" && stack_ref+=("${target}/proc") # (package_golden_image.sh --exclude=dev/sys/proc) - die Zielverzeichnisse
mount -t sysfs sysfs "${target}/sys" && stack_ref+=("${target}/sys") # existieren nach dem Entpacken deshalb noch nicht und muessen vor dem
mount -t devpts devpts "${target}/dev/pts" && stack_ref+=("${target}/dev/pts") # Bind-Mount erst angelegt werden. Live gefunden (31.08.2026, erster
cp /etc/resolv.conf "${target}/etc/resolv.conf" # 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() { image_deploy_unbind_mounts() {

View File

@ -1,42 +0,0 @@
#!/usr/bin/env bash
# Wird von build.sh und build_customer_iso.sh eingebunden (source), nicht
# eigenstaendig ausgefuehrt.
# Haengt initrd-hooks/casper-bottom/* als zusaetzliche cpio-Schicht an ein
# bestehendes Casper-Initrd an - derselbe Mechanismus, den
# backend_launch() (backends/mint/backend.sh) bereits fuer das
# Kexec-Preseed nutzt und der per QEMU verifiziert ist: der Kernel haengt
# spaeter aneinandergereihte cpio-Archive additiv an ein bestehendes
# initramfs an. Noetig, weil Casper keinen automatischen /updates-Merge
# kennt (real per QEMU-Boot widerlegt) - der Hook selbst kopiert
# /cdrom/updates beim Boot auf das Root-Dateisystem, siehe
# initrd-hooks/casper-bottom/21tuxflotte_updates.
inject_casper_bottom_hooks() {
local repo_dir="$1"
local src_initrd="$2"
local dest_initrd="$3"
local hook_dir="${repo_dir}/initrd-hooks"
local layer_dir
layer_dir="$(mktemp -d)"
# Ausserhalb von layer_dir, damit die Archiv-Datei sich beim Bauen nicht
# selbst als leeren Eintrag einfaengt (find laeuft im selben Moment, in
# dem die Shell-Redirection sie bereits anlegt).
local cpio_out
cpio_out="$(mktemp)"
cp -a "${hook_dir}/casper-bottom" "${layer_dir}/scripts-casper-bottom-tmp"
mkdir -p "${layer_dir}/scripts"
mv "${layer_dir}/scripts-casper-bottom-tmp" "${layer_dir}/scripts/casper-bottom"
chmod 0755 "${layer_dir}"/scripts/casper-bottom/*
(cd "${layer_dir}" && find . -mindepth 1 | cpio -o -H newc 2>/dev/null | gzip) \
>"${cpio_out}" ||
{ echo "Error: Casper-Bottom-Hook-Schicht konnte nicht gebaut werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
cat "${src_initrd}" "${cpio_out}" >"${dest_initrd}" ||
{ echo "Error: Initrd konnte nicht mit Hook-Schicht zusammengesetzt werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
rm -rf "${layer_dir}"
rm -f "${cpio_out}"
}

View File

@ -15,3 +15,57 @@ log_error() {
log_success() { log_success() {
echo "[ OK ] $*" echo "[ OK ] $*"
} }
# Klar abgesetzte Fortschrittsanzeige fuer die tty-Konsole des Boot-Mediums
# (kein Desktop, kein Kiosk-Browser - die Terminal-Ausgabe selbst ist die
# einzige UI, die eine Person vor dem Geraet sieht). Gilt fuer Dev- UND
# Produktiv-Build gleichermassen, nicht nur zum Debuggen - siehe Plan
# "Klare Fortschrittsanzeige auf der Konsole". Format/Stil bewusst an die
# bestehenden Banner in 17_device_status.sh angelehnt (Titel + "="-Linie
# gleicher Laenge, keine Sonderzeichen, die auf einer rohen VT-Konsole
# fehlen koennten).
declare -A TUXFLOTTE_STEP_LABELS=(
[00_preflight.sh]="Vorbereitung"
[05_network.sh]="Netzwerkverbindung wird hergestellt"
[10_hardware.sh]="Hardware wird erkannt"
[12_enrollment_auth.sh]="Aktivierungscode wird geprüft"
[15_server_handshake.sh]="Verbindung zu anode wird hergestellt"
[17_device_status.sh]="Gerätestatus wird abgerufen"
[20_profile_selection.sh]="Bereitstellungsvorlage wird ausgewählt"
[25_installation_confirm.sh]="Installationsbestätigung"
[30_runtime_blueprint.sh]="Installationsplan wird aufgelöst"
[40_backend.sh]="Golden Image wird geladen und installiert"
[20_storage.sh]="Speicher-Zusatzprüfung"
[99_finish.sh]="Abschluss"
)
readonly -a TUXFLOTTE_STEP_ORDER=(
00_preflight.sh 05_network.sh 10_hardware.sh 12_enrollment_auth.sh
15_server_handshake.sh 17_device_status.sh 20_profile_selection.sh
25_installation_confirm.sh 30_runtime_blueprint.sh 40_backend.sh
20_storage.sh 99_finish.sh
)
TUXFLOTTE_STEP_COUNT=0
log_step() {
local module_name="$1"
local label="${TUXFLOTTE_STEP_LABELS[$module_name]:-$module_name}"
local total="${#TUXFLOTTE_STEP_ORDER[@]}"
local bold="" reset=""
TUXFLOTTE_STEP_COUNT=$((TUXFLOTTE_STEP_COUNT + 1))
# Klartext-Fallback ohne Terminal (z.B. wenn stdout in eine Log-Datei
# umgeleitet wird) - kein tput-Aufruf, ncurses-bin ist bewusst nicht Teil
# des schlanken Paketsatzes, rohe ANSI-Codes reichen und sind ueberall
# ohne Zusatzpaket verfuegbar.
if [[ -t 1 ]]; then
bold=$'\033[1m'
reset=$'\033[0m'
fi
local title
title="[${TUXFLOTTE_STEP_COUNT}/${total}] ${label}"
printf '\n%s%s%s\n' "${bold}" "${title}" "${reset}"
printf '%s\n\n' "$(printf '=%.0s' $(seq 1 ${#title}))"
}

View File

@ -18,6 +18,7 @@ run_module() {
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module" [[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
log_step "$(basename "$module")"
log_info "Starte Modul: $(basename "$module")" log_info "Starte Modul: $(basename "$module")"
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then

View File

@ -3,22 +3,23 @@
# Tuxflotte Installer # Tuxflotte Installer
# Phase 0 Preflight # Phase 0 Preflight
# #
# Stellt sicher, dass Werkzeuge vorhanden sind, die spaetere Module (ab # Prueft, dass Werkzeuge vorhanden sind, die spaetere Module (ab
# 10_hardware.sh) brauchen, aber auf dem Live-Medium selbst (anders als im # 10_hardware.sh) brauchen. jq ist der einzige hier betroffene Fall:
# Zielsystem, siehe pkgsel/include in preseed.tpl) nicht vorinstalliert sind. # 10_hardware.sh, 12_enrollment_auth.sh, 15_server_handshake.sh,
# 17_device_status.sh, 20_profile_selection.sh, 25_installation_confirm.sh
# und 30_runtime_blueprint.sh nutzen es alle.
# #
# jq ist der einzige hier betroffene Fall: 10_hardware.sh, 12_enrollment_auth.sh, # Historisch (bis zum Umstieg auf das eigenstaendige, per live-build gebaute
# 15_server_handshake.sh, 17_device_status.sh, 20_profile_selection.sh, # Boot-Medium) wurde jq hier noch zur Laufzeit per apt-get nachinstalliert,
# 25_installation_confirm.sh und 30_runtime_blueprint.sh nutzen es alle, das # weil das damalige Boot-Medium (eine gepatchte Linux-Mint-Live-ISO) es nicht
# erste davon (10_hardware.sh) bereits deutlich vor 40_backend.sh, wo # mitbrachte - real entdeckt: beim automatisierten Start ueber
# backend_init() denselben Nachinstallations-Mechanismus fuer den Kexec-Pfad
# schon kennt (siehe backends/mint/backend.sh) - hier zu spaet fuer die
# frueheren Module. Real entdeckt: beim automatisierten Start ueber
# start-kiosk.sh (kein Terminal, keine sichtbare Fehlermeldung) blieb der # start-kiosk.sh (kein Terminal, keine sichtbare Fehlermeldung) blieb der
# Installer bereits in 10_hardware.sh mit "Benoetigtes Programm nicht # Installer in 10_hardware.sh mit "Benoetigtes Programm nicht gefunden: jq"
# gefunden: jq" haengen, sichtbar nur in ~/.xsession-errors - manuelle Testlaeufe # haengen, sichtbar nur in ~/.xsession-errors. Das eigenstaendige Boot-Medium
# in dieser Session sind daran nie gescheitert, weil jq dabei stets vorab von # bringt jq bereits im Paketsatz mit (siehe boot-medium/config/package-lists/
# Hand nachinstalliert wurde, bevor installer.sh gestartet wurde. # tuxflotte.list.chroot) - diese Pruefung bleibt trotzdem als reine Assertion
# bestehen, damit ein kuenftiger Paketlisten-Fehler hier fruh und klar auffaellt,
# statt erst kryptisch in 10_hardware.sh.
set -Eeuo pipefail set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}" readonly SCRIPT_NAME="${0##*/}"
@ -27,14 +28,10 @@ log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2 printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
} }
if ! command -v jq >/dev/null 2>&1; then fatal() {
log "jq fehlt auf dem Live-Medium, installiere nach." printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true command -v jq >/dev/null 2>&1 ||
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true fatal "jq fehlt auf dem Boot-Medium - Paketliste (tuxflotte.list.chroot) pruefen."
apt-get update -qq ||
{ log "FEHLER: apt-get update fehlgeschlagen."; exit 1; }
DEBIAN_FRONTEND=noninteractive apt-get install -y jq ||
{ log "FEHLER: Installation von jq fehlgeschlagen."; exit 1; }
fi

View File

@ -73,11 +73,26 @@ echo "==> Packe nach ${OUTPUT_FILE}"
# nicht absolute (/proc/...) - ein --exclude mit fuehrendem Slash matcht # nicht absolute (/proc/...) - ein --exclude mit fuehrendem Slash matcht
# dagegen NICHT (live am 31.08.2026 entdeckt: das lief zunaechst # dagegen NICHT (live am 31.08.2026 entdeckt: das lief zunaechst
# unbemerkt gegen die echte Referenz-VM und haette u.a. /proc/kcore mit # 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 \ tar --numeric-owner --xattrs \
--exclude=proc --exclude=sys --exclude=dev \ --exclude=./proc --exclude=./sys --exclude=./dev \
--exclude=run --exclude=tmp --exclude=var/tmp \ --exclude=./run --exclude=./tmp --exclude=./var/tmp \
--exclude="${OUTPUT_FILE#/}" \ --exclude="./${OUTPUT_FILE#/}" \
-C / -cf - . | zstd -f -T0 -19 -o "${OUTPUT_FILE}" -C / -cf - . | zstd -f -T0 -19 -o "${OUTPUT_FILE}"
echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))" echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))"