diff --git a/backends/mint-image/backend.sh b/backends/mint-image/backend.sh new file mode 100644 index 0000000..20a7883 --- /dev/null +++ b/backends/mint-image/backend.sh @@ -0,0 +1,284 @@ +#!/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=() + +# TODO(Phase 3): muss vor dem ersten echten Self-Service-Testlauf tatsaechlich +# auf anode bereitgestellt werden (siehe ADR-0024-Plan, "Ablage/ +# Bereitstellung: ueber anode per HTTPS als statische Datei") - aktuell nur +# lokal auf enterprise gebaut und getestet. +GOLDEN_IMAGE_URL="https://anode.tuxflotte.de/golden-images/debian-trixie-base.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() { + 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 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) + + 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 + + [[ -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 +} diff --git a/backends/mint-image/postinstall.sh b/backends/mint-image/postinstall.sh new file mode 120000 index 0000000..21b6b5b --- /dev/null +++ b/backends/mint-image/postinstall.sh @@ -0,0 +1 @@ +../mint/postinstall.sh \ No newline at end of file