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.
This commit is contained in:
Thomas Stallinger 2026-08-31 13:52:38 +02:00
parent ff4f48432e
commit 2190d6f403
27 changed files with 2961 additions and 45 deletions

4
.gitignore vendored
View File

@ -19,6 +19,10 @@ boot-medium/*.img
boot-medium/*.contents
boot-medium/*.files
boot-medium/*.packages
boot-medium/*.zsync
boot-medium/binary.modified_timestamps
boot-medium/chroot.packages.install
boot-medium/chroot.packages.live
# Editor/system files
*~

View File

@ -64,27 +64,27 @@ _mint_image_detect_target_disk() {
}
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 envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base)
command -v parted >/dev/null 2>&1 || live_packages_needed+=(parted)
command -v mkfs.vfat >/dev/null 2>&1 || live_packages_needed+=(dosfstools)
command -v mkfs.ext4 >/dev/null 2>&1 || live_packages_needed+=(e2fsprogs)
command -v zstd >/dev/null 2>&1 || live_packages_needed+=(zstd)
command -v curl >/dev/null 2>&1 || live_packages_needed+=(curl)
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)
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
[[ "${#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; }

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

@ -22,7 +22,7 @@ LB_DISTRIBUTION_BINARY="trixie"
LB_PARENT_DISTRIBUTION_BINARY="trixie"
# Select parent distribution for debian-installer to use
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION=""
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
# Select archive areas to use
LB_ARCHIVE_AREAS="main contrib non-free non-free-firmware"

View File

@ -0,0 +1,286 @@
#!/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"
GOLDEN_IMAGE_FILE="${RUNTIME_DIR}/golden-image.tar.zst"
# 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}" "${GOLDEN_IMAGE_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 "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}"
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
backend_log "Entpacke Golden Image"
image_deploy_extract_image "${GOLDEN_IMAGE_FILE}" "${TARGET_DIR}" || return 1
rm -f "${GOLDEN_IMAGE_FILE}"
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"
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,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

@ -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,256 @@
#!/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
}
# 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; }
}
# /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"
mount --bind /dev "${target}/dev" && stack_ref+=("${target}/dev")
mount -t proc proc "${target}/proc" && stack_ref+=("${target}/proc")
mount -t sysfs sysfs "${target}/sys" && stack_ref+=("${target}/sys")
mount -t devpts devpts "${target}/dev/pts" && stack_ref+=("${target}/dev/pts")
cp /etc/resolv.conf "${target}/etc/resolv.conf"
}
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,17 @@
#!/usr/bin/env bash
log_info() {
echo "[INFO ] $*"
}
log_warn() {
echo "[WARN ] $*" >&2
}
log_error() {
echo "[ERROR] $*" >&2
}
log_success() {
echo "[ OK ] $*"
}

View File

@ -0,0 +1,40 @@
#!/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_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

@ -3,22 +3,23 @@
# Tuxflotte Installer
# Phase 0 Preflight
#
# Stellt sicher, dass Werkzeuge vorhanden sind, die spaetere Module (ab
# 10_hardware.sh) brauchen, aber auf dem Live-Medium selbst (anders als im
# Zielsystem, siehe pkgsel/include in preseed.tpl) nicht vorinstalliert sind.
# 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.
#
# 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, das
# erste davon (10_hardware.sh) bereits deutlich vor 40_backend.sh, wo
# 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
# 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 bereits in 10_hardware.sh mit "Benoetigtes Programm nicht
# gefunden: jq" haengen, sichtbar nur in ~/.xsession-errors - manuelle Testlaeufe
# in dieser Session sind daran nie gescheitert, weil jq dabei stets vorab von
# Hand nachinstalliert wurde, bevor installer.sh gestartet wurde.
# 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##*/}"
@ -27,14 +28,10 @@ log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
if ! command -v jq >/dev/null 2>&1; then
log "jq fehlt auf dem Live-Medium, installiere nach."
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 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 ||
{ 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
command -v jq >/dev/null 2>&1 ||
fatal "jq fehlt auf dem Boot-Medium - Paketliste (tuxflotte.list.chroot) pruefen."