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:
parent
ff4f48432e
commit
2190d6f403
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
|
||||
*~
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -1 +0,0 @@
|
||||
../mint/postinstall.sh
|
||||
72
backends/mint-image/postinstall.sh
Normal file
72
backends/mint-image/postinstall.sh
Normal 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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
84
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/installer.sh
Executable file
84
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/installer.sh
Executable 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"
|
||||
@ -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}'
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
]
|
||||
'
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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 ] $*"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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."
|
||||
544
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/modules/05_network.sh
Executable file
544
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/modules/05_network.sh
Executable 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 "$@"
|
||||
550
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/modules/10_hardware.sh
Executable file
550
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/modules/10_hardware.sh
Executable 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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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 "$@"
|
||||
@ -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."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user