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/*.contents
|
||||||
boot-medium/*.files
|
boot-medium/*.files
|
||||||
boot-medium/*.packages
|
boot-medium/*.packages
|
||||||
|
boot-medium/*.zsync
|
||||||
|
boot-medium/binary.modified_timestamps
|
||||||
|
boot-medium/chroot.packages.install
|
||||||
|
boot-medium/chroot.packages.live
|
||||||
|
|
||||||
# Editor/system files
|
# Editor/system files
|
||||||
*~
|
*~
|
||||||
|
|||||||
@ -64,27 +64,27 @@ _mint_image_detect_target_disk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend_init() {
|
backend_init() {
|
||||||
local live_packages_needed=()
|
# Historisch (bis zum Umstieg auf das eigenstaendige, per live-build
|
||||||
|
# gebaute Boot-Medium) wurden diese Werkzeuge hier noch zur Laufzeit per
|
||||||
|
# apt-get nachinstalliert, weil das damalige Boot-Medium (eine gepatchte
|
||||||
|
# Linux-Mint-Live-ISO) sie nicht immer mitbrachte. Das eigenstaendige
|
||||||
|
# Boot-Medium bringt sie bereits im Paketsatz mit (siehe
|
||||||
|
# boot-medium/config/package-lists/tuxflotte.list.chroot) - hier bleibt
|
||||||
|
# nur noch eine reine Assertion, damit ein kuenftiger Paketlisten-Fehler
|
||||||
|
# fruh und klar auffaellt.
|
||||||
|
local missing=()
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || live_packages_needed+=(jq)
|
command -v jq >/dev/null 2>&1 || missing+=(jq)
|
||||||
command -v envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base)
|
command -v envsubst >/dev/null 2>&1 || missing+=(gettext-base)
|
||||||
command -v parted >/dev/null 2>&1 || live_packages_needed+=(parted)
|
command -v parted >/dev/null 2>&1 || missing+=(parted)
|
||||||
command -v mkfs.vfat >/dev/null 2>&1 || live_packages_needed+=(dosfstools)
|
command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools)
|
||||||
command -v mkfs.ext4 >/dev/null 2>&1 || live_packages_needed+=(e2fsprogs)
|
command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs)
|
||||||
command -v zstd >/dev/null 2>&1 || live_packages_needed+=(zstd)
|
command -v mkfs.btrfs >/dev/null 2>&1 || missing+=(btrfs-progs)
|
||||||
command -v curl >/dev/null 2>&1 || live_packages_needed+=(curl)
|
command -v zstd >/dev/null 2>&1 || missing+=(zstd)
|
||||||
|
command -v curl >/dev/null 2>&1 || missing+=(curl)
|
||||||
|
|
||||||
if [[ "${#live_packages_needed[@]}" -gt 0 ]]; then
|
[[ "${#missing[@]}" -eq 0 ]] ||
|
||||||
backend_log "Werkzeuge fehlen auf dem Live-Medium, installiere nach: ${live_packages_needed[*]}"
|
{ backend_fatal "Werkzeuge fehlen auf dem Boot-Medium (Paketliste pruefen): ${missing[*]}"; return 1; }
|
||||||
|
|
||||||
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true
|
|
||||||
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true
|
|
||||||
|
|
||||||
apt-get update -qq ||
|
|
||||||
{ backend_fatal "apt-get update fehlgeschlagen."; return 1; }
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${live_packages_needed[@]}" ||
|
|
||||||
{ backend_fatal "Installation fehlender Werkzeuge fehlgeschlagen."; return 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
|
[[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
|
||||||
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
|
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
|
||||||
|
|||||||
@ -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"
|
LB_PARENT_DISTRIBUTION_BINARY="trixie"
|
||||||
|
|
||||||
# Select parent distribution for debian-installer to use
|
# Select parent distribution for debian-installer to use
|
||||||
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION=""
|
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
|
||||||
|
|
||||||
# Select archive areas to use
|
# Select archive areas to use
|
||||||
LB_ARCHIVE_AREAS="main contrib non-free non-free-firmware"
|
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
|
# Tuxflotte Installer
|
||||||
# Phase 0 – Preflight
|
# Phase 0 – Preflight
|
||||||
#
|
#
|
||||||
# Stellt sicher, dass Werkzeuge vorhanden sind, die spaetere Module (ab
|
# Prueft, dass Werkzeuge vorhanden sind, die spaetere Module (ab
|
||||||
# 10_hardware.sh) brauchen, aber auf dem Live-Medium selbst (anders als im
|
# 10_hardware.sh) brauchen. jq ist der einzige hier betroffene Fall:
|
||||||
# Zielsystem, siehe pkgsel/include in preseed.tpl) nicht vorinstalliert sind.
|
# 10_hardware.sh, 12_enrollment_auth.sh, 15_server_handshake.sh,
|
||||||
|
# 17_device_status.sh, 20_profile_selection.sh, 25_installation_confirm.sh
|
||||||
|
# und 30_runtime_blueprint.sh nutzen es alle.
|
||||||
#
|
#
|
||||||
# jq ist der einzige hier betroffene Fall: 10_hardware.sh, 12_enrollment_auth.sh,
|
# Historisch (bis zum Umstieg auf das eigenstaendige, per live-build gebaute
|
||||||
# 15_server_handshake.sh, 17_device_status.sh, 20_profile_selection.sh,
|
# Boot-Medium) wurde jq hier noch zur Laufzeit per apt-get nachinstalliert,
|
||||||
# 25_installation_confirm.sh und 30_runtime_blueprint.sh nutzen es alle, das
|
# weil das damalige Boot-Medium (eine gepatchte Linux-Mint-Live-ISO) es nicht
|
||||||
# erste davon (10_hardware.sh) bereits deutlich vor 40_backend.sh, wo
|
# mitbrachte - real entdeckt: beim automatisierten Start ueber
|
||||||
# backend_init() denselben Nachinstallations-Mechanismus fuer den Kexec-Pfad
|
|
||||||
# schon kennt (siehe backends/mint/backend.sh) - hier zu spaet fuer die
|
|
||||||
# frueheren Module. Real entdeckt: beim automatisierten Start ueber
|
|
||||||
# start-kiosk.sh (kein Terminal, keine sichtbare Fehlermeldung) blieb der
|
# start-kiosk.sh (kein Terminal, keine sichtbare Fehlermeldung) blieb der
|
||||||
# Installer bereits in 10_hardware.sh mit "Benoetigtes Programm nicht
|
# Installer in 10_hardware.sh mit "Benoetigtes Programm nicht gefunden: jq"
|
||||||
# gefunden: jq" haengen, sichtbar nur in ~/.xsession-errors - manuelle Testlaeufe
|
# haengen, sichtbar nur in ~/.xsession-errors. Das eigenstaendige Boot-Medium
|
||||||
# in dieser Session sind daran nie gescheitert, weil jq dabei stets vorab von
|
# bringt jq bereits im Paketsatz mit (siehe boot-medium/config/package-lists/
|
||||||
# Hand nachinstalliert wurde, bevor installer.sh gestartet wurde.
|
# tuxflotte.list.chroot) - diese Pruefung bleibt trotzdem als reine Assertion
|
||||||
|
# bestehen, damit ein kuenftiger Paketlisten-Fehler hier fruh und klar auffaellt,
|
||||||
|
# statt erst kryptisch in 10_hardware.sh.
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
readonly SCRIPT_NAME="${0##*/}"
|
readonly SCRIPT_NAME="${0##*/}"
|
||||||
@ -27,14 +28,10 @@ log() {
|
|||||||
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
|
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
fatal() {
|
||||||
log "jq fehlt auf dem Live-Medium, installiere nach."
|
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true
|
command -v jq >/dev/null 2>&1 ||
|
||||||
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true
|
fatal "jq fehlt auf dem Boot-Medium - Paketliste (tuxflotte.list.chroot) pruefen."
|
||||||
|
|
||||||
apt-get update -qq ||
|
|
||||||
{ log "FEHLER: apt-get update fehlgeschlagen."; exit 1; }
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y jq ||
|
|
||||||
{ log "FEHLER: Installation von jq fehlgeschlagen."; exit 1; }
|
|
||||||
fi
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user