Compare commits
7 Commits
bc9cefa1b8
...
d179f9c2f5
| Author | SHA1 | Date | |
|---|---|---|---|
| d179f9c2f5 | |||
| b9ccae6e85 | |||
| 27d6fa0f47 | |||
| 95447f7486 | |||
| bc13169418 | |||
| 2190d6f403 | |||
| ff4f48432e |
18
.gitignore
vendored
18
.gitignore
vendored
@ -7,6 +7,24 @@ work/
|
|||||||
output/
|
output/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# live-build (boot-medium/) own working/cache/output directories - only
|
||||||
|
# boot-medium/config/ and boot-medium/auto/ are actual source, everything else
|
||||||
|
# is regenerated by `lb build`
|
||||||
|
boot-medium/.build/
|
||||||
|
boot-medium/cache/
|
||||||
|
boot-medium/chroot/
|
||||||
|
boot-medium/binary*/
|
||||||
|
boot-medium/*.iso
|
||||||
|
boot-medium/*.img
|
||||||
|
boot-medium/*.contents
|
||||||
|
boot-medium/*.files
|
||||||
|
boot-medium/*.packages
|
||||||
|
boot-medium/*.zsync
|
||||||
|
boot-medium/binary.modified_timestamps
|
||||||
|
boot-medium/config/includes.chroot/etc/tuxflotte-dev-build
|
||||||
|
boot-medium/chroot.packages.install
|
||||||
|
boot-medium/chroot.packages.live
|
||||||
|
|
||||||
# Editor/system files
|
# Editor/system files
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
21
README.md
21
README.md
@ -14,3 +14,24 @@ Ziele:
|
|||||||
- lokaler Festplattenstart
|
- lokaler Festplattenstart
|
||||||
|
|
||||||
Die eigentliche Provisionierung erfolgt über den Tuxflotte Provisioning Server.
|
Die eigentliche Provisionierung erfolgt über den Tuxflotte Provisioning Server.
|
||||||
|
|
||||||
|
## Boot-Medium (Mint)
|
||||||
|
|
||||||
|
Das Mint-Boot-Medium ist ein eigenständiges, per Debian `live-build`
|
||||||
|
gebautes Image (`boot-medium/`, siehe ADR-0025) - kein gepatchter
|
||||||
|
Distributions-Installer mehr. Es enthält nur Tuxflottes eigenen,
|
||||||
|
headless-tauglichen Code (Module `scripts/modules/00`-`40`,
|
||||||
|
`backends/mint-image/`) und bootet direkt in einen systemd-Dienst, der
|
||||||
|
`installer.sh` startet.
|
||||||
|
|
||||||
|
- `scripts/build_boot_medium.sh [--dev]` baut das Basis-Image
|
||||||
|
(`boot-medium/live-image-amd64.hybrid.iso`). `--dev` aktiviert einen
|
||||||
|
SSH-Debug-Zugang (`tuxflotte`/`test123`); ohne `--dev` bleibt SSH
|
||||||
|
deaktiviert (Produktiv-Default).
|
||||||
|
- `scripts/build_customer_iso.sh <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]`
|
||||||
|
personalisiert ein bereits gebautes Basis-Image für eine konkrete
|
||||||
|
Kundenorganisation (Aktivierungscode + WLAN-Zugangsdaten).
|
||||||
|
|
||||||
|
Fedora nutzt weiterhin das ältere Kickstart/Anaconda-Muster
|
||||||
|
(`scripts/build.sh <source.iso> fedora`) - noch nicht auf dasselbe
|
||||||
|
Boot-Medium-Muster umgestellt.
|
||||||
|
|||||||
@ -28,7 +28,6 @@ HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
|
|||||||
|
|
||||||
RUNTIME_DIR="/run/tuxflotte/backend"
|
RUNTIME_DIR="/run/tuxflotte/backend"
|
||||||
CONFIG_FILE="${RUNTIME_DIR}/config.json"
|
CONFIG_FILE="${RUNTIME_DIR}/config.json"
|
||||||
GOLDEN_IMAGE_FILE="${RUNTIME_DIR}/golden-image.tar.zst"
|
|
||||||
|
|
||||||
# Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch()
|
# Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch()
|
||||||
# und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe
|
# und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe
|
||||||
@ -64,27 +63,27 @@ _mint_image_detect_target_disk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend_init() {
|
backend_init() {
|
||||||
local live_packages_needed=()
|
# Historisch (bis zum Umstieg auf das eigenstaendige, per live-build
|
||||||
|
# gebaute Boot-Medium) wurden diese Werkzeuge hier noch zur Laufzeit per
|
||||||
|
# apt-get nachinstalliert, weil das damalige Boot-Medium (eine gepatchte
|
||||||
|
# Linux-Mint-Live-ISO) sie nicht immer mitbrachte. Das eigenstaendige
|
||||||
|
# Boot-Medium bringt sie bereits im Paketsatz mit (siehe
|
||||||
|
# boot-medium/config/package-lists/tuxflotte.list.chroot) - hier bleibt
|
||||||
|
# nur noch eine reine Assertion, damit ein kuenftiger Paketlisten-Fehler
|
||||||
|
# fruh und klar auffaellt.
|
||||||
|
local missing=()
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || live_packages_needed+=(jq)
|
command -v jq >/dev/null 2>&1 || missing+=(jq)
|
||||||
command -v envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base)
|
command -v envsubst >/dev/null 2>&1 || missing+=(gettext-base)
|
||||||
command -v parted >/dev/null 2>&1 || live_packages_needed+=(parted)
|
command -v parted >/dev/null 2>&1 || missing+=(parted)
|
||||||
command -v mkfs.vfat >/dev/null 2>&1 || live_packages_needed+=(dosfstools)
|
command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools)
|
||||||
command -v mkfs.ext4 >/dev/null 2>&1 || live_packages_needed+=(e2fsprogs)
|
command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs)
|
||||||
command -v zstd >/dev/null 2>&1 || live_packages_needed+=(zstd)
|
command -v mkfs.btrfs >/dev/null 2>&1 || missing+=(btrfs-progs)
|
||||||
command -v curl >/dev/null 2>&1 || live_packages_needed+=(curl)
|
command -v zstd >/dev/null 2>&1 || missing+=(zstd)
|
||||||
|
command -v curl >/dev/null 2>&1 || missing+=(curl)
|
||||||
|
|
||||||
if [[ "${#live_packages_needed[@]}" -gt 0 ]]; then
|
[[ "${#missing[@]}" -eq 0 ]] ||
|
||||||
backend_log "Werkzeuge fehlen auf dem Live-Medium, installiere nach: ${live_packages_needed[*]}"
|
{ backend_fatal "Werkzeuge fehlen auf dem Boot-Medium (Paketliste pruefen): ${missing[*]}"; return 1; }
|
||||||
|
|
||||||
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true
|
|
||||||
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true
|
|
||||||
|
|
||||||
apt-get update -qq ||
|
|
||||||
{ backend_fatal "apt-get update fehlgeschlagen."; return 1; }
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${live_packages_needed[@]}" ||
|
|
||||||
{ backend_fatal "Installation fehlender Werkzeuge fehlgeschlagen."; return 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
|
[[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
|
||||||
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
|
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
|
||||||
@ -100,7 +99,7 @@ backend_init() {
|
|||||||
--group=root \
|
--group=root \
|
||||||
"${RUNTIME_DIR}"
|
"${RUNTIME_DIR}"
|
||||||
|
|
||||||
rm -f -- "${CONFIG_FILE}" "${GOLDEN_IMAGE_FILE}"
|
rm -f -- "${CONFIG_FILE}"
|
||||||
|
|
||||||
backend_log "Initialisiert."
|
backend_log "Initialisiert."
|
||||||
}
|
}
|
||||||
@ -203,11 +202,6 @@ backend_launch() {
|
|||||||
|
|
||||||
root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")"
|
root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")"
|
||||||
|
|
||||||
backend_log "Lade Golden Image von ${GOLDEN_IMAGE_URL}"
|
|
||||||
curl --silent --show-error --fail --location \
|
|
||||||
--output "${GOLDEN_IMAGE_FILE}" "${GOLDEN_IMAGE_URL}" ||
|
|
||||||
{ backend_fatal "Golden Image konnte nicht geladen werden: ${GOLDEN_IMAGE_URL}"; return 1; }
|
|
||||||
|
|
||||||
backend_log "Partitioniere ${disk}"
|
backend_log "Partitioniere ${disk}"
|
||||||
read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" ||
|
read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" ||
|
||||||
return 1
|
return 1
|
||||||
@ -218,9 +212,15 @@ backend_launch() {
|
|||||||
backend_log "Mounte unter ${TARGET_DIR}"
|
backend_log "Mounte unter ${TARGET_DIR}"
|
||||||
image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1
|
image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1
|
||||||
|
|
||||||
backend_log "Entpacke Golden Image"
|
# Live gefunden (31.08.2026, erster echter End-to-End-Lauf mit dem vollen
|
||||||
image_deploy_extract_image "${GOLDEN_IMAGE_FILE}" "${TARGET_DIR}" || return 1
|
# Referenz-VM-Archiv): "erst nach /run/tuxflotte/... herunterladen, dann
|
||||||
rm -f "${GOLDEN_IMAGE_FILE}"
|
# entpacken" scheiterte an /run (RAM-Tmpfs, viel kleiner als das
|
||||||
|
# 2,3-GB-Archiv - "curl: (23) Failure writing output to destination").
|
||||||
|
# Direktes Streamen in die Extraktion braucht nur ein paar MB Puffer,
|
||||||
|
# unabhaengig von der Archivgroesse - deshalb erst ab hier (nach
|
||||||
|
# Partitionieren/Formatieren/Mounten), kein Zwischenspeichern mehr.
|
||||||
|
backend_log "Lade und entpacke Golden Image von ${GOLDEN_IMAGE_URL}"
|
||||||
|
image_deploy_extract_image_from_url "${GOLDEN_IMAGE_URL}" "${TARGET_DIR}" || return 1
|
||||||
|
|
||||||
backend_log "Schreibe fstab"
|
backend_log "Schreibe fstab"
|
||||||
image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1
|
image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1
|
||||||
@ -270,6 +270,31 @@ backend_postinstall() {
|
|||||||
printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh"
|
printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh"
|
||||||
chmod 0700 "${TARGET_DIR}/tmp/postinstall.sh"
|
chmod 0700 "${TARGET_DIR}/tmp/postinstall.sh"
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026, erster vollstaendig durchgelaufener
|
||||||
|
# End-to-End-Test): postinstall.sh braucht jq (+curl), aber die echte
|
||||||
|
# Mint-Referenz-VM bringt das nicht zwingend mit (anders als das
|
||||||
|
# Boot-Medium selbst, das jq ja schon vorinstalliert hat - das hilft
|
||||||
|
# dem ausgerollten Zielsystem hier nichts, das ist ein komplett
|
||||||
|
# eigener chroot). python3 wird nicht von postinstall.sh selbst
|
||||||
|
# gebraucht, aber vom heruntergeladenen agent.py nach dem naechsten
|
||||||
|
# Boot - hier gleich mit absichern, um nicht noch einen ganzen
|
||||||
|
# Referenz-VM-Neupack-Zyklus wegen eines einzelnen fehlenden Pakets zu
|
||||||
|
# brauchen. package_golden_image.sh leert ausserdem /var/lib/apt/lists
|
||||||
|
# als Teil der Bereinigung - "apt-get update" ist deshalb hier noetig,
|
||||||
|
# bevor "apt-get install" ueberhaupt Pakete finden kann. Netzwerk ist
|
||||||
|
# im chroot verfuegbar (resolv.conf wurde schon in
|
||||||
|
# image_deploy_bind_mounts kopiert, dieselben Bind-Mounts sind noch
|
||||||
|
# aktiv).
|
||||||
|
chroot "${TARGET_DIR}" bash -c '
|
||||||
|
missing=()
|
||||||
|
command -v jq >/dev/null 2>&1 || missing+=(jq)
|
||||||
|
command -v curl >/dev/null 2>&1 || missing+=(curl)
|
||||||
|
command -v python3 >/dev/null 2>&1 || missing+=(python3)
|
||||||
|
[[ "${#missing[@]}" -eq 0 ]] && exit 0
|
||||||
|
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}"
|
||||||
|
' ||
|
||||||
|
{ backend_fatal "jq/curl/python3 konnten im Zielsystem nicht sichergestellt werden."; return 1; }
|
||||||
|
|
||||||
backend_log "Führe Postinstall-Skript im chroot aus."
|
backend_log "Führe Postinstall-Skript im chroot aus."
|
||||||
chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh ||
|
chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh ||
|
||||||
{ backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; }
|
{ backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; }
|
||||||
|
|||||||
@ -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
|
||||||
@ -1,454 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -Eeuo pipefail
|
|
||||||
|
|
||||||
# Dieses Skript wird von einem Orchestrator-Modul (z.B. 40_backend.sh) per
|
|
||||||
# `source` in dessen Shell geladen. Variablen bleiben deshalb bewusst nicht
|
|
||||||
# readonly, um Namenskollisionen mit dem ladenden Modul zu vermeiden.
|
|
||||||
BACKEND_KEY="mint"
|
|
||||||
|
|
||||||
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PRESEED_TEMPLATE="${BACKEND_DIR}/preseed.tpl"
|
|
||||||
POSTINSTALL_SCRIPT="${BACKEND_DIR}/postinstall.sh"
|
|
||||||
|
|
||||||
RUNTIME_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json"
|
|
||||||
SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
|
|
||||||
# Von 10_hardware.sh im selben Live-Boot geschrieben (siehe dort) - Quelle
|
|
||||||
# fuer den Fingerprint, der jetzt auch auf dem installierten Geraet selbst
|
|
||||||
# hinterlegt wird (siehe backend_generate_config()/postinstall.sh).
|
|
||||||
HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
|
|
||||||
|
|
||||||
RUNTIME_DIR="/run/tuxflotte/backend"
|
|
||||||
CONFIG_FILE="${RUNTIME_DIR}/config"
|
|
||||||
|
|
||||||
backend_log() {
|
|
||||||
printf '[backend:%s] %s\n' "${BACKEND_KEY}" "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_fatal() {
|
|
||||||
printf '[backend:%s] FEHLER: %s\n' "${BACKEND_KEY}" "$*" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_init() {
|
|
||||||
# jq/envsubst(gettext-base)/cpio/kexec-tools sind auf dem Live-Medium
|
|
||||||
# selbst (anders als im Zielsystem, siehe pkgsel/include in preseed.tpl)
|
|
||||||
# nicht vorinstalliert - real gegen das Live-Abbild verifiziert
|
|
||||||
# (Phase-1-Spike, zunächst nur für kexec-tools behoben, hier auf alle
|
|
||||||
# vier Live-only-Werkzeuge ausgeweitet). base64 kommt aus coreutils und
|
|
||||||
# ist auf jedem Debian-Derivat immer vorhanden, deshalb ohne Nachinstal-
|
|
||||||
# lationspfad.
|
|
||||||
local live_packages_needed=()
|
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || live_packages_needed+=(jq)
|
|
||||||
command -v envsubst >/dev/null 2>&1 || live_packages_needed+=(gettext-base)
|
|
||||||
command -v cpio >/dev/null 2>&1 || live_packages_needed+=(cpio)
|
|
||||||
command -v kexec >/dev/null 2>&1 || live_packages_needed+=(kexec-tools)
|
|
||||||
|
|
||||||
if [[ "${#live_packages_needed[@]}" -gt 0 ]]; then
|
|
||||||
backend_log "Werkzeuge fehlen auf dem Live-Medium, installiere nach: ${live_packages_needed[*]}"
|
|
||||||
|
|
||||||
sed -i '/^deb cdrom/d' /etc/apt/sources.list 2>/dev/null || true
|
|
||||||
rm -f /etc/apt/sources.list.d/*cdrom* 2>/dev/null || true
|
|
||||||
|
|
||||||
apt-get update -qq ||
|
|
||||||
{ backend_fatal "apt-get update fehlgeschlagen."; return 1; }
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${live_packages_needed[@]}" ||
|
|
||||||
{ backend_fatal "Installation fehlender Werkzeuge fehlgeschlagen."; return 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v base64 >/dev/null 2>&1 ||
|
|
||||||
{ backend_fatal "Benötigtes Werkzeug fehlt: base64"; return 1; }
|
|
||||||
|
|
||||||
[[ -r "${PRESEED_TEMPLATE}" ]] ||
|
|
||||||
{ backend_fatal "Preseed-Template nicht gefunden: ${PRESEED_TEMPLATE}"; return 1; }
|
|
||||||
|
|
||||||
[[ -r "${POSTINSTALL_SCRIPT}" ]] ||
|
|
||||||
{ backend_fatal "Postinstall-Skript nicht gefunden: ${POSTINSTALL_SCRIPT}"; return 1; }
|
|
||||||
|
|
||||||
install -d \
|
|
||||||
--mode=0700 \
|
|
||||||
--owner=root \
|
|
||||||
--group=root \
|
|
||||||
"${RUNTIME_DIR}"
|
|
||||||
|
|
||||||
rm -f -- "${CONFIG_FILE}"
|
|
||||||
|
|
||||||
backend_log "Initialisiert."
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_validate() {
|
|
||||||
[[ -r "${RUNTIME_BLUEPRINT_FILE}" ]] ||
|
|
||||||
{ backend_fatal "Runtime Blueprint nicht gefunden: ${RUNTIME_BLUEPRINT_FILE}"; return 1; }
|
|
||||||
|
|
||||||
jq --exit-status \
|
|
||||||
--arg backend_key "${BACKEND_KEY}" \
|
|
||||||
'.runtime_blueprint.backend_id == $backend_key' \
|
|
||||||
"${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
|
|
||||||
{ backend_fatal "Runtime Blueprint ist nicht für Backend '${BACKEND_KEY}' aufgelöst."; return 1; }
|
|
||||||
|
|
||||||
jq --exit-status '
|
|
||||||
.runtime_blueprint.installation_directives
|
|
||||||
| (.disk_encryption | type == "boolean")
|
|
||||||
and (.partitioning | type == "object")
|
|
||||||
and (.secure_boot_required | type == "boolean")
|
|
||||||
' "${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
|
|
||||||
{ backend_fatal "Installationszeitliche Vorgaben fehlen oder sind ungültig."; return 1; }
|
|
||||||
|
|
||||||
# disk_encryption wird für Mint (noch) nicht unterstützt - kein getesteter
|
|
||||||
# LUKS-Preseed-Mechanismus (anders als Fedoras "autopart --encrypted").
|
|
||||||
if [[ "$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" == "true" ]]; then
|
|
||||||
backend_fatal "disk_encryption=true wird vom Mint-Backend derzeit nicht unterstützt."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
backend_log "Runtime Blueprint ist gültig für Backend '${BACKEND_KEY}'."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Baut einen einzelnen partman-auto/expert_recipe-Partitionsblock als
|
|
||||||
# EINZEILIGEN String (Felder durch Leerzeichen statt Zeilenumbrueche
|
|
||||||
# getrennt) - Debconf-Preseed-Werte mit eingebetteten Zeilenumbruechen
|
|
||||||
# brechen leicht lautlos (siehe base64-Kommentar bei success_command weiter
|
|
||||||
# unten fuer denselben Fallstrick an anderer Stelle), partmans Parser selbst
|
|
||||||
# ist bei Leerzeichen als Trenner tolerant.
|
|
||||||
_tuxflotte_partman_stanza() {
|
|
||||||
local size_mb="$1"
|
|
||||||
local filesystem="$2"
|
|
||||||
local mountpoint="$3"
|
|
||||||
local extra_flags="${4:-}"
|
|
||||||
|
|
||||||
printf '%s %s %s %s %s method{ format } format{ } use_filesystem{ } filesystem{ %s } mountpoint{ %s } . ' \
|
|
||||||
"${size_mb}" "${size_mb}" "${size_mb}" "${filesystem}" "${extra_flags}" "${filesystem}" "${mountpoint}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ermittelt den ersten echten Datentraeger des Zielgeraets fuer die
|
|
||||||
# Groessenberechnung bei prozentualer Partitionierung (Fedoras autopart macht
|
|
||||||
# implizit dieselbe Ein-Datentraeger-Annahme, siehe fedora/backend.sh).
|
|
||||||
# Groessenfilter (>0) und Namensausschluss noetig - reale Systeme koennen
|
|
||||||
# nbd-/zram-/loop-Geraete mit type=="disk" aber ohne echte Speicherkapazitaet
|
|
||||||
# auflisten, die sonst faelschlich vor dem echten Zieldatentraeger gewaehlt
|
|
||||||
# wuerden (real beim Testen entdeckt).
|
|
||||||
_tuxflotte_detect_target_disk() {
|
|
||||||
lsblk --nodeps --noheadings --bytes --output NAME,TYPE,SIZE --paths |
|
|
||||||
awk '$2 == "disk" && $3 > 0 && $1 !~ /(nbd|zram|loop)[0-9]*$/ { print $1; exit }'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Baut den kompletten partman-auto/expert_recipe-Rezeptkoerper aus dem
|
|
||||||
# installation_directives.partitioning-Objekt. Prozentangaben werden anhand
|
|
||||||
# der realen Zieldatentraegergroesse (erst hier, live auf dem Zielgeraet,
|
|
||||||
# bekannt - nicht beim ISO-Bau) in feste MB-Groessen umgerechnet. Bewusst
|
|
||||||
# feste Groessen (min=priority=max) statt partmans eigener proportionaler
|
|
||||||
# Prioritaets-Verteilung - deterministischer und leichter zu verifizieren.
|
|
||||||
_tuxflotte_render_partman_recipe() {
|
|
||||||
local partitioning_json="$1"
|
|
||||||
local root_filesystem="$2"
|
|
||||||
local scheme
|
|
||||||
local disk_device
|
|
||||||
local disk_size_mb
|
|
||||||
local extra_count
|
|
||||||
local extra_percent_sum=0
|
|
||||||
local root_mb
|
|
||||||
local recipe_body=""
|
|
||||||
local i
|
|
||||||
local mountpoint
|
|
||||||
local filesystem
|
|
||||||
local percent
|
|
||||||
local size_mb
|
|
||||||
local esp_mb=0
|
|
||||||
local bios_grub_mb=0
|
|
||||||
local root_extra_flags
|
|
||||||
|
|
||||||
scheme="$(jq --raw-output '.scheme // "single"' <<<"${partitioning_json}")"
|
|
||||||
|
|
||||||
disk_device="$(_tuxflotte_detect_target_disk)"
|
|
||||||
[[ -n "${disk_device}" ]] ||
|
|
||||||
{ backend_fatal "Zieldatenträger konnte nicht ermittelt werden."; return 1; }
|
|
||||||
disk_size_mb="$(( $(blockdev --getsize64 "${disk_device}") / 1024 / 1024 ))"
|
|
||||||
|
|
||||||
# Die eingebauten partman-Recipes ("atomic" etc.) legen auf UEFI-Systemen
|
|
||||||
# automatisch eine EFI-System-Partition an - ein eigenes expert_recipe
|
|
||||||
# muss das selbst tun, sonst warnt/verweigert der Installer (real beim
|
|
||||||
# Testen entdeckt: "No EFI System Partition was found"). 512 MB vorab
|
|
||||||
# reserviert, vor der Prozentaufteilung der restlichen Platte.
|
|
||||||
if [[ -d /sys/firmware/efi ]]; then
|
|
||||||
# Exakte Stanza-Form aus der eingebauten "atomic"-Recipe uebernommen
|
|
||||||
# (/usr/lib/partman/recipes-amd64-efi/30atomic auf dem Live-Medium
|
|
||||||
# ausgelesen) - eine erste eigene Vermutung ohne $reusemethod{ } und
|
|
||||||
# mit $bootable{ } wurde von partman zwar anstandslos geparst, aber
|
|
||||||
# nicht als gueltige EFI-System-Partition erkannt ("No EFI System
|
|
||||||
# Partition was found", real beim Testen entdeckt).
|
|
||||||
esp_mb=512
|
|
||||||
disk_size_mb="$(( disk_size_mb - esp_mb ))"
|
|
||||||
recipe_body="${esp_mb} ${esp_mb} ${esp_mb} fat32 \$reusemethod{ } \$primary{ } method{ efi } format{ } . "
|
|
||||||
# Root NICHT zusaetzlich $bootable{ } markieren wie im BIOS-Zweig
|
|
||||||
# unten (real beim Testen entdeckt, 29.08.2026): auf UEFI traegt
|
|
||||||
# bereits die ESP-Stanza oben die eigentliche Boot-Kennzeichnung
|
|
||||||
# (method{ efi }) - das im Original-Rezept
|
|
||||||
# /usr/lib/partman/recipes-amd64-efi/30atomic (Quelle der
|
|
||||||
# ESP-Stanza) uebernommene Wurzel-Partitionsschema markiert die
|
|
||||||
# Root-Partition dort ebenfalls NICHT als bootable. Ein
|
|
||||||
# zusaetzliches $bootable{ } auf der Root-Partition scheint partman
|
|
||||||
# in einen Zustand zu bringen, den es nach dem Commit staendig neu
|
|
||||||
# bewerten will: der Installer klickte "Jetzt installieren"
|
|
||||||
# nachweislich korrekt (Debug-Log erreichte sogar
|
|
||||||
# "grub-installer/bootdev seen"), sprang aber danach immer wieder
|
|
||||||
# zurueck auf dieselbe Partitionierungsseite (ubi-partman.py:
|
|
||||||
# rebuild_cache(), ausgeloest durch die wiederholt gestellte
|
|
||||||
# Debconf-Frage "ubiquity/partman-rebuild-cache" aus
|
|
||||||
# /lib/partman/update.d/99signal_ubiquity). Noch nicht abschliessend
|
|
||||||
# verifiziert, ob dies die alleinige Ursache ist - siehe
|
|
||||||
# ADR-0023-Nachtrag.
|
|
||||||
root_extra_flags='$primary{ }'
|
|
||||||
else
|
|
||||||
# Analoges Pendant fuer reinen BIOS-Betrieb: partman legt den
|
|
||||||
# Datentraeger auch ohne EFI offenbar als GPT an (real beim Testen
|
|
||||||
# bestaetigt: freier Speicher vor Partition 1 und nach der letzten
|
|
||||||
# Partition ist die GPT-Kopfdaten-Signatur, kein MSDOS-Layout). GPT +
|
|
||||||
# BIOS-Boot braucht eine kleine unformatierte Boot-Partition fuer den
|
|
||||||
# GRUB-Core, sonst schlaegt /usr/lib/partman/check.d/08biosgrub fehl
|
|
||||||
# und partman-partitioning/no_bootable_biosgrub sorgt fuer eine
|
|
||||||
# Endlosschleife zurueck ins choose_partition-Menue statt
|
|
||||||
# abzuschliessen (real beim Testen entdeckt und via 08biosgrub-
|
|
||||||
# Quelltext auf dem Live-Medium verifiziert - "true" bei dieser Frage
|
|
||||||
# bedeutet dort "Problem besteht weiterhin", nicht "trotzdem
|
|
||||||
# fortfahren", anders als bei den meisten uebrigen Boolean-Fragen in
|
|
||||||
# diesem Rezept). "$iflabel{ gpt }" macht die Stanza auf einem
|
|
||||||
# MSDOS-Datentraeger automatisch wirkungslos.
|
|
||||||
bios_grub_mb=1
|
|
||||||
disk_size_mb="$(( disk_size_mb - bios_grub_mb ))"
|
|
||||||
recipe_body="${bios_grub_mb} ${bios_grub_mb} ${bios_grub_mb} free \$iflabel{ gpt } \$reusemethod{ } method{ biosgrub } . "
|
|
||||||
# Im BIOS-Zweig (anders als EFI oben) bleibt $bootable{ } auf der
|
|
||||||
# Root-Partition noetig - hier gibt es keine ESP, die diese Rolle
|
|
||||||
# uebernimmt.
|
|
||||||
root_extra_flags='$primary{ } $bootable{ }'
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${scheme}" in
|
|
||||||
single)
|
|
||||||
root_mb="$(( disk_size_mb - 1024 ))"
|
|
||||||
[[ "${root_mb}" -ge 2048 ]] ||
|
|
||||||
{ backend_fatal "Zieldatenträger ist zu klein (${disk_size_mb} MB)."; return 1; }
|
|
||||||
|
|
||||||
recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${root_mb}" "${root_filesystem}" "/" "${root_extra_flags}")"
|
|
||||||
;;
|
|
||||||
custom)
|
|
||||||
extra_count="$(jq '.extra_partitions | length' <<<"${partitioning_json}")"
|
|
||||||
[[ "${extra_count}" -gt 0 ]] ||
|
|
||||||
{ backend_fatal "scheme=custom ohne extra_partitions angegeben."; return 1; }
|
|
||||||
|
|
||||||
for ((i = 0; i < extra_count; i++)); do
|
|
||||||
mountpoint="$(jq --raw-output ".extra_partitions[${i}].mountpoint" <<<"${partitioning_json}")"
|
|
||||||
filesystem="$(jq --raw-output ".extra_partitions[${i}].filesystem" <<<"${partitioning_json}")"
|
|
||||||
percent="$(jq --raw-output ".extra_partitions[${i}].percent" <<<"${partitioning_json}")"
|
|
||||||
|
|
||||||
case "${mountpoint}" in
|
|
||||||
/home|/var) ;;
|
|
||||||
*) backend_fatal "Nicht unterstützter Einhängepunkt: ${mountpoint}"; return 1 ;;
|
|
||||||
esac
|
|
||||||
case "${filesystem}" in
|
|
||||||
ext4|btrfs) ;;
|
|
||||||
*) backend_fatal "Nicht unterstütztes Dateisystem: ${filesystem}"; return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
extra_percent_sum="$(( extra_percent_sum + percent ))"
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ "${extra_percent_sum}" -gt 0 && "${extra_percent_sum}" -lt 90 ]] ||
|
|
||||||
{ backend_fatal "Summe der Partitions-Prozentangaben ist ungültig: ${extra_percent_sum}"; return 1; }
|
|
||||||
|
|
||||||
root_mb="$(( disk_size_mb * (100 - extra_percent_sum) / 100 - 1024 ))"
|
|
||||||
[[ "${root_mb}" -ge 2048 ]] ||
|
|
||||||
{ backend_fatal "Root-Partition wäre bei dieser Aufteilung zu klein."; return 1; }
|
|
||||||
|
|
||||||
recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${root_mb}" "${root_filesystem}" "/" "${root_extra_flags}")"
|
|
||||||
|
|
||||||
for ((i = 0; i < extra_count; i++)); do
|
|
||||||
mountpoint="$(jq --raw-output ".extra_partitions[${i}].mountpoint" <<<"${partitioning_json}")"
|
|
||||||
filesystem="$(jq --raw-output ".extra_partitions[${i}].filesystem" <<<"${partitioning_json}")"
|
|
||||||
percent="$(jq --raw-output ".extra_partitions[${i}].percent" <<<"${partitioning_json}")"
|
|
||||||
size_mb="$(( disk_size_mb * percent / 100 ))"
|
|
||||||
|
|
||||||
recipe_body="${recipe_body}$(_tuxflotte_partman_stanza "${size_mb}" "${filesystem}" "${mountpoint}")"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
backend_fatal "Nicht unterstütztes Partitionierungsschema: ${scheme}"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
printf 'tuxflotte :: %s' "${recipe_body}"
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_generate_config() {
|
|
||||||
local hostname
|
|
||||||
local device_id
|
|
||||||
local partitioning_json
|
|
||||||
local root_filesystem
|
|
||||||
local secure_boot_required
|
|
||||||
local partman_recipe
|
|
||||||
local blueprints_json
|
|
||||||
local postinstall_rendered
|
|
||||||
local postinstall_b64
|
|
||||||
|
|
||||||
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
|
|
||||||
{ backend_fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"; return 1; }
|
|
||||||
|
|
||||||
hostname="$(jq --raw-output '.device.hostname // empty' "${SERVER_RESPONSE_FILE}")"
|
|
||||||
[[ -n "${hostname}" ]] ||
|
|
||||||
{ backend_fatal "Kein Hostname in der Serverantwort gefunden."; return 1; }
|
|
||||||
|
|
||||||
device_id="$(jq --raw-output '.device.id // empty' "${SERVER_RESPONSE_FILE}")"
|
|
||||||
[[ -n "${device_id}" ]] ||
|
|
||||||
{ backend_fatal "Keine Geräte-ID in der Serverantwort gefunden."; return 1; }
|
|
||||||
|
|
||||||
# Fuer die beidseitige Identifikation (Geraeteliste <-> Geraet selbst,
|
|
||||||
# siehe postinstall.sh) - aus der bereits waehrend des Live-Boots
|
|
||||||
# berechneten hardware.json, nicht aus der Serverantwort (die kennt nur
|
|
||||||
# die zugewiesene device_id, nicht den urspruenglichen Hardware-Hash).
|
|
||||||
local device_fingerprint
|
|
||||||
[[ -r "${HARDWARE_FILE}" ]] ||
|
|
||||||
{ backend_fatal "Hardware-Erfassung nicht gefunden: ${HARDWARE_FILE}"; return 1; }
|
|
||||||
device_fingerprint="$(jq --raw-output '.identity.device_fingerprint // empty' "${HARDWARE_FILE}")"
|
|
||||||
[[ -n "${device_fingerprint}" ]] ||
|
|
||||||
{ backend_fatal "Kein device_fingerprint in ${HARDWARE_FILE} gefunden."; return 1; }
|
|
||||||
|
|
||||||
partitioning_json="$(jq --compact-output '.runtime_blueprint.installation_directives.partitioning' "${RUNTIME_BLUEPRINT_FILE}")"
|
|
||||||
secure_boot_required="$(jq --raw-output '.runtime_blueprint.installation_directives.secure_boot_required' "${RUNTIME_BLUEPRINT_FILE}")"
|
|
||||||
|
|
||||||
root_filesystem="$(jq --raw-output '.root_filesystem // "ext4"' <<<"${partitioning_json}")"
|
|
||||||
case "${root_filesystem}" in
|
|
||||||
ext4|btrfs) ;;
|
|
||||||
*) backend_fatal "Nicht unterstütztes Root-Dateisystem: ${root_filesystem}"; return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
partman_recipe="$(_tuxflotte_render_partman_recipe "${partitioning_json}" "${root_filesystem}")" ||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if [[ "${secure_boot_required}" == "true" ]]; then
|
|
||||||
backend_log "Hinweis: secure_boot_required=true wird derzeit nicht in der Preseed-Konfiguration durchgesetzt (Phase 1)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")"
|
|
||||||
|
|
||||||
# Erste Stufe: postinstall.sh-Platzhalter auflösen.
|
|
||||||
postinstall_rendered="$(
|
|
||||||
TUXFLOTTE_DEVICE_ID="${device_id}" \
|
|
||||||
TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \
|
|
||||||
TUXFLOTTE_DEVICE_FINGERPRINT="${device_fingerprint}" \
|
|
||||||
envsubst '${TUXFLOTTE_DEVICE_ID} ${TUXFLOTTE_BLUEPRINTS_JSON} ${TUXFLOTTE_DEVICE_FINGERPRINT}' \
|
|
||||||
<"${POSTINSTALL_SCRIPT}"
|
|
||||||
)"
|
|
||||||
|
|
||||||
if grep -q '\${TUXFLOTTE_' <<<"${postinstall_rendered}"; then
|
|
||||||
backend_fatal "postinstall.sh enthält nach envsubst nicht aufgelöste Platzhalter."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# base64-Kodierung: mehrzeilige/zitierte Preseed-Werte brechen unter
|
|
||||||
# Debconf lautlos (real erprobt, siehe backends/mint/wlan-test.seed) -
|
|
||||||
# als einzeiliger Base64-Blob besteht der success_command-Wert nur noch
|
|
||||||
# aus unkritischen Zeichen.
|
|
||||||
postinstall_b64="$(printf '%s' "${postinstall_rendered}" | base64 -w0)"
|
|
||||||
|
|
||||||
# Zweite Stufe: preseed.tpl mit allen Werten inkl. des fertigen Base64-Blobs auflösen.
|
|
||||||
TUXFLOTTE_HOSTNAME="${hostname}" \
|
|
||||||
TUXFLOTTE_PARTMAN_RECIPE="${partman_recipe}" \
|
|
||||||
TUXFLOTTE_POSTINSTALL_B64="${postinstall_b64}" \
|
|
||||||
envsubst '${TUXFLOTTE_HOSTNAME} ${TUXFLOTTE_PARTMAN_RECIPE} ${TUXFLOTTE_POSTINSTALL_B64}' \
|
|
||||||
<"${PRESEED_TEMPLATE}" >"${CONFIG_FILE}"
|
|
||||||
|
|
||||||
chmod 0600 "${CONFIG_FILE}"
|
|
||||||
|
|
||||||
[[ -s "${CONFIG_FILE}" ]] ||
|
|
||||||
{ backend_fatal "Erzeugte Konfigurationsdatei ist leer: ${CONFIG_FILE}"; return 1; }
|
|
||||||
|
|
||||||
if grep -q '\${TUXFLOTTE_' "${CONFIG_FILE}"; then
|
|
||||||
backend_fatal "Erzeugte Konfigurationsdatei enthält nicht aufgelöste Platzhalter."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
backend_log "Konfiguration erzeugt: ${CONFIG_FILE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_launch() {
|
|
||||||
local cdrom_vmlinuz="/cdrom/casper/vmlinuz"
|
|
||||||
local cdrom_initrd="/cdrom/casper/initrd.lz"
|
|
||||||
local extra_initrd_dir="${RUNTIME_DIR}/initrd-extra"
|
|
||||||
local extra_cpio="${RUNTIME_DIR}/extra.cpio.gz"
|
|
||||||
local custom_initrd="${RUNTIME_DIR}/initrd-custom.lz"
|
|
||||||
|
|
||||||
[[ -r "${cdrom_vmlinuz}" && -r "${cdrom_initrd}" ]] ||
|
|
||||||
{ backend_fatal "Casper-Kernel/-Initrd nicht gefunden unter /cdrom/casper."; return 1; }
|
|
||||||
|
|
||||||
# Das personalisierte Preseed (CONFIG_FILE, erst live auf diesem Gerät
|
|
||||||
# erzeugt - Hostname/Geräte-ID/Partitionierung sind erst hier bekannt,
|
|
||||||
# nicht schon beim ISO-Bau) kann nicht per file=/cdrom/... übergeben
|
|
||||||
# werden (read-only Medium, Inhalt seit ISO-Bau fixiert) und auch nicht
|
|
||||||
# per url= von einem selbst gestarteten lokalen Server - der komplette
|
|
||||||
# Prozess- und Netzwerkzustand dieser Sitzung geht beim Kexec-Sprung
|
|
||||||
# verloren, ein soeben gestarteter HTTP-Server koennte die neue
|
|
||||||
# Boot-Umgebung also nicht mehr bedienen. Stattdessen wird das Preseed in
|
|
||||||
# eine zusaetzliche Initrd-Schicht eingebettet: der Kernel unterstuetzt
|
|
||||||
# aneinandergehaengte cpio-Archive als initramfs (spaetere Archive
|
|
||||||
# ergaenzen fruehere), das uebersteht den Kexec-Uebergang unveraendert.
|
|
||||||
# Real gegen QEMU verifiziert (Phase-1-Spike, beide Ansaetze getestet).
|
|
||||||
rm -rf "${extra_initrd_dir}"
|
|
||||||
install -d --mode=0700 --owner=root --group=root "${extra_initrd_dir}"
|
|
||||||
cp "${CONFIG_FILE}" "${extra_initrd_dir}/preseed.cfg"
|
|
||||||
|
|
||||||
(cd "${extra_initrd_dir}" && find . | cpio -o -H newc 2>/dev/null | gzip) \
|
|
||||||
>"${extra_cpio}" ||
|
|
||||||
{ backend_fatal "Preseed-Initrd-Schicht konnte nicht gebaut werden."; return 1; }
|
|
||||||
|
|
||||||
cat "${cdrom_initrd}" "${extra_cpio}" >"${custom_initrd}" ||
|
|
||||||
{ backend_fatal "Initrd konnte nicht zusammengesetzt werden."; return 1; }
|
|
||||||
|
|
||||||
backend_log "Lade Kexec-Ziel fuer automatisierten Ubiquity-Start."
|
|
||||||
|
|
||||||
# Ubiquitys eigenes "noninteractive"-Frontend (ubiquity/frontend/
|
|
||||||
# noninteractive.py, ueber das gleichnamige Boot-Keyword ausgewaehlt)
|
|
||||||
# arbeitet zwar rein ueber Debconf ohne je ein Fenster zu zeichnen, aber
|
|
||||||
# dessen Seite fuer die gefuehrte Partitionierung (ubi-partman.py) haengt
|
|
||||||
# sich bei einem vollstaendig vorbefuellten Rezept in einer echten
|
|
||||||
# Endlosschleife auf (staendiges Neuaufbauen des choose_partition-Menues,
|
|
||||||
# "partman/confirm" wird nie erreicht - real ueber >800 Wiederholungen
|
|
||||||
# ohne Fortschritt bestaetigt, siehe ADR-0023-Nachtrag). Ursache: die
|
|
||||||
# PageNoninteractive-Klasse liefert fuer etliche vom GTK-Codepfad
|
|
||||||
# benoetigte Rueckfragen (get_autopartition_choice(), get_crypto_keys())
|
|
||||||
# nur "pass"/None statt echter Werte.
|
|
||||||
#
|
|
||||||
# Deshalb bewusst zurueck auf "automatic-ubiquity" (echte GTK-Oberflaeche,
|
|
||||||
# PageGtk-Klasse - der von Ubiquity selbst getestete, produktiv genutzte
|
|
||||||
# Codepfad, auch fuer die Partitionierung). Der GTK-Assistent fuellt jede
|
|
||||||
# Seite aus dem Preseed vor, wartet aber weiterhin auf einen "Weiter"-Klick
|
|
||||||
# pro Seite (real verifiziert, siehe ADR-0023-Nachtrag) - dafuer laeuft
|
|
||||||
# zusaetzlich autoclicker.sh (live-updates/opt/tuxflotte/scripts/), per
|
|
||||||
# systemd-Service und ausgeloest durch das eigene Boot-Keyword
|
|
||||||
# "tuxflotte-autoclick" (harmlos bei jedem anderen Boot ohne dieses
|
|
||||||
# Keyword). "debug-ubiquity" (setzt debug="-d") macht
|
|
||||||
# /var/log/installer/debug ausfuehrlicher, hilfreich bei weiterer
|
|
||||||
# Fehlersuche.
|
|
||||||
# username=/hostname=mint bewusst ergaenzt (real beim Testen entdeckt,
|
|
||||||
# 29.08.2026): ohne diese beiden Parameter faellt casper auf dem
|
|
||||||
# kexec-Boot auf einen anderen Live-Account-Zustand zurueck als beim
|
|
||||||
# ersten Boot (der die grub.cfg-Vorlage explizit mit "username=mint
|
|
||||||
# hostname=mint" startet) - konkret verlangt der Konsolenlogin auf
|
|
||||||
# diesem zweiten Boot ein echtes Passwort statt des sonst leeren
|
|
||||||
# Live-Session-Passworts. Fuer den eigentlichen Auto-Install-Ablauf
|
|
||||||
# (GTK-Assistent unter ubiquity-dm, kein Konsolenlogin noetig)
|
|
||||||
# folgenlos, aber inkonsistent gegenueber dem ersten Boot und erschwert
|
|
||||||
# die Fehlersuche via Konsole unnoetig - deshalb hier angeglichen.
|
|
||||||
kexec -l "${cdrom_vmlinuz}" \
|
|
||||||
--initrd="${custom_initrd}" \
|
|
||||||
--append="boot=casper automatic-ubiquity tuxflotte-autoclick debug-ubiquity noprompt file=/preseed.cfg debian-installer/language=de keyboard-configuration/layoutcode=de username=mint hostname=mint quiet splash ---" ||
|
|
||||||
{ backend_fatal "kexec -l fehlgeschlagen."; return 1; }
|
|
||||||
|
|
||||||
backend_log "Starte unbeaufsichtigte Installation (kexec -e). Kein Ruecksprung erwartet - ab hier laeuft die eigentliche Installation im neuen Kernel weiter."
|
|
||||||
|
|
||||||
kexec -e
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_postinstall() {
|
|
||||||
backend_log "Provisioning-Agent-Einrichtung erfolgt im ubiquity/success_command der Preseed-Konfiguration (Agent-Abruf, Bootstrap-Registrierung, systemd-Aktivierung)."
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
{
|
|
||||||
echo "tuxflotte: early_command lief, $(date -u)"
|
|
||||||
echo "--- ip link ---"
|
|
||||||
ip link 2>&1
|
|
||||||
echo "--- nmcli device status (vorher) ---"
|
|
||||||
nmcli device status 2>&1
|
|
||||||
echo "--- WLAN verbinden (mit Wiederholung, falls Treiber noch nicht bereit) ---"
|
|
||||||
for i in 1 2 3 4 5 6; do
|
|
||||||
nmcli device wifi rescan 2>&1
|
|
||||||
sleep 2
|
|
||||||
if nmcli device wifi connect stallinux password a987654321 2>&1; then
|
|
||||||
echo "WLAN verbunden nach Versuch $i"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "Versuch $i fehlgeschlagen, warte..."
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo "--- nmcli device status (nachher) ---"
|
|
||||||
nmcli device status 2>&1
|
|
||||||
echo "--- curl anode /health ---"
|
|
||||||
curl -m 10 -sS https://anode.tuxflotte.de/health 2>&1
|
|
||||||
echo
|
|
||||||
echo "--- debconf-set-selections Test (simuliert dynamisch von anode geholten Wert) ---"
|
|
||||||
echo "d-i netcfg/get_hostname string tuxflotte-early-test" | debconf-set-selections
|
|
||||||
echo "debconf-set-selections exit: $?"
|
|
||||||
echo "--- Verifikation per debconf-communicate ---"
|
|
||||||
echo "GET netcfg/get_hostname" | debconf-communicate 2>&1
|
|
||||||
} >> /var/log/tuxflotte-early-command.log 2>&1
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
### Tuxflotte Auto-Install Preseed fuer Linux Mint (Ubiquity/Debian-Installer).
|
|
||||||
### Liegt direkt auf dem Medium (file=/cdrom/preseed/tuxflotte.seed), keine
|
|
||||||
### Netz-Zustellung noetig -- analog zu Fedoras inst.ks=cdrom:/ks.cfg.
|
|
||||||
###
|
|
||||||
### d-i preseed/early_command wird bewusst NICHT verwendet -- unter Ubiquity
|
|
||||||
### bestaetigt wirkungslos (Ubiquity nutzt eigene Python-Plugins statt der
|
|
||||||
### klassischen Debian-Installer-Komponenten, an die early_command haengt).
|
|
||||||
|
|
||||||
d-i debian-installer/locale string de_DE.UTF-8
|
|
||||||
d-i keyboard-configuration/xkb-keymap select de
|
|
||||||
d-i keyboard-configuration/layoutcode string de
|
|
||||||
|
|
||||||
d-i netcfg/get_hostname string ${TUXFLOTTE_HOSTNAME}
|
|
||||||
d-i netcfg/get_domain string unassigned-domain
|
|
||||||
|
|
||||||
# Lab-Bootstrap-Zugangsdaten. Ersetzt ein noch fehlendes Secret-Reference-Modell
|
|
||||||
# (siehe 09-data-model-v1.md) und darf nicht als Produktionsmechanismus gelten.
|
|
||||||
d-i passwd/user-fullname string Tuxflotte
|
|
||||||
d-i passwd/username string tuxflotte
|
|
||||||
d-i passwd/user-password password test123
|
|
||||||
d-i passwd/user-password-again password test123
|
|
||||||
d-i user-setup/allow-password-weak boolean true
|
|
||||||
|
|
||||||
d-i clock-setup/utc boolean true
|
|
||||||
d-i time/zone string Europe/Berlin
|
|
||||||
d-i clock-setup/ntp boolean true
|
|
||||||
|
|
||||||
d-i partman-auto/method string regular
|
|
||||||
d-i partman-auto/expert_recipe string ${TUXFLOTTE_PARTMAN_RECIPE}
|
|
||||||
d-i partman-auto/choose_recipe select tuxflotte
|
|
||||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
|
||||||
d-i partman-partitioning/confirm_new_label boolean true
|
|
||||||
# "partman/choose_partition select finish" wurde bewusst entfernt: das ist
|
|
||||||
# die Frage des MANUELLEN/erweiterten Partitionierers ("Menu" -> "Finish
|
|
||||||
# partitioning"), keine des gefuehrten/automatischen Ablaufs. Direktes
|
|
||||||
# Preseeden ohne echten Seitenaufbau (weder im GTK- noch im
|
|
||||||
# noninteractive-Frontend) fuehrt in ubi-partman.py real reproduzierbar zu
|
|
||||||
# einer Endlosschleife im internen "building_cache"-Zustandsautomaten der
|
|
||||||
# choose_partition-Verarbeitung (ueber 100.000 Debconf-Zeilen in wenigen
|
|
||||||
# Sekunden ohne echten Fortschritt, sowohl mit automatic-ubiquity/GTK als
|
|
||||||
# auch mit dem verworfenen noninteractive-Frontend - siehe ADR-0023-Nachtrag).
|
|
||||||
# Ohne diese Zeile uebernehmen partman-auto/method + expert_recipe +
|
|
||||||
# choose_recipe (oben) die gefuehrte Partitionierung auf dem dafuer
|
|
||||||
# vorgesehenen Weg.
|
|
||||||
d-i partman/confirm boolean true
|
|
||||||
d-i partman/confirm_nochanges boolean true
|
|
||||||
d-i partman/confirm_nooverwrite boolean true
|
|
||||||
d-i partman/unmount_active boolean true
|
|
||||||
d-i partman/automount boolean true
|
|
||||||
d-i partman/filter_mounted boolean true
|
|
||||||
d-i partman-partitioning/confirm_resize boolean true
|
|
||||||
d-i partman-ext3/lazy_itable_init boolean true
|
|
||||||
d-i partman/boot_not_first_partition boolean true
|
|
||||||
d-i partman-basicfilesystems/boot_not_first_partition boolean true
|
|
||||||
d-i partman-basicfilesystems/boot_not_ext2 boolean true
|
|
||||||
d-i partman-ext3/boot_not_bootable boolean true
|
|
||||||
d-i partman-ext3/boot_not_ext2_or_ext3 boolean true
|
|
||||||
d-i partman-basicfilesystems/no_mount_point boolean true
|
|
||||||
d-i partman-basicfilesystems/no_swap boolean true
|
|
||||||
d-i partman-basicfilesystems/check_failed boolean true
|
|
||||||
d-i partman-basicfilesystems/swap_check_failed boolean true
|
|
||||||
d-i partman-ext3/bad_alignment boolean true
|
|
||||||
# Dieselbe Polaritaets-Falle wie bei no_bootable_biosgrub weiter unten (siehe
|
|
||||||
# dortiger Kommentar) - "true" hiesse "Problem besteht wirklich, abbrechen".
|
|
||||||
# Fuer UEFI-Zielgeraete legt _tuxflotte_render_partman_recipe() bereits eine
|
|
||||||
# echte ESP an, dieser Fallback sollte also nie greifen.
|
|
||||||
d-i partman-partitioning/no_bootable_efi boolean false
|
|
||||||
# Anders als die meisten uebrigen Boolean-Fragen hier bedeutet "true" bei
|
|
||||||
# no_bootable_biosgrub NICHT "trotzdem fortfahren", sondern "das Problem
|
|
||||||
# besteht wirklich" -> der pruefende Skript (check.d/08biosgrub) bricht dann
|
|
||||||
# mit exit 1 ab, was zur Endlosschleife zurueck ins choose_partition-Menue
|
|
||||||
# fuehrt (real entdeckt und via Quelltext auf dem Live-Medium verifiziert,
|
|
||||||
# siehe ADR-0023-Nachtrag). Der eigentliche Fix ist eine echte BIOS-Boot-
|
|
||||||
# Partition im Rezept (siehe _tuxflotte_render_partman_recipe() in
|
|
||||||
# backend.sh) - "false" hier bleibt nur als defensiver Fallback, falls die
|
|
||||||
# Partition aus irgendeinem Grund nicht als "biosgrub" erkannt wird.
|
|
||||||
d-i partman-partitioning/no_bootable_biosgrub boolean false
|
|
||||||
d-i partman-partitioning/bootable_logical boolean true
|
|
||||||
d-i partman-partitioning/unknown_label boolean true
|
|
||||||
d-i partman-partitioning/unsupported_label boolean true
|
|
||||||
d-i partman-basicmethods/method_only boolean true
|
|
||||||
d-i grub-installer/only_debian boolean true
|
|
||||||
d-i grub-installer/with_other_os boolean true
|
|
||||||
d-i grub-installer/make_active boolean true
|
|
||||||
d-i grub-installer/grub2_instead_of_grub_legacy boolean true
|
|
||||||
d-i grub-installer/grub_not_mature_on_this_platform boolean true
|
|
||||||
d-i grub-installer/multipath boolean true
|
|
||||||
d-i grub-installer/sataraid boolean true
|
|
||||||
# Wichtig: false, nicht true -- "skip" heisst hier woertlich "GRUB-Installation
|
|
||||||
# ueberspringen". true wuerde die Bootloader-Installation aktiv verhindern.
|
|
||||||
d-i grub-installer/skip boolean false
|
|
||||||
d-i partman-auto-lvm/no_boot boolean true
|
|
||||||
d-i partman-target/mount_failed boolean true
|
|
||||||
# partman-efi/no_efi wird ENTGEGEN der urspruenglichen Annahme sehr wohl
|
|
||||||
# gefragt, auch im reinen BIOS/SeaBIOS-Betrieb ohne NVRAM (real via
|
|
||||||
# debug-ubiquity-Log verifiziert, siehe ADR-0023-Nachtrag) -- Ubiquity prueft
|
|
||||||
# offenbar unabhaengig vom aktuellen Boot-Modus, ob eine EFI-System-Partition
|
|
||||||
# existiert. "false" heisst hier "trotzdem fortfahren" (die Alternative
|
|
||||||
# "true" wuerde zurueck ins Partitionierungsmenue springen und den Ablauf
|
|
||||||
# blockieren -- fuer ein bewusstes BIOS/MBR-Setup ohne EFI-Partition ist
|
|
||||||
# false die richtige Antwort).
|
|
||||||
d-i partman-efi/no_efi boolean false
|
|
||||||
# Bewusst weiterhin NICHT preseeded: grub-installer/force-efi-extra-removable
|
|
||||||
# (EFI-spezifisch, im BIOS-Betrieb ohne Wirkung) sowie partman-crypto/*,
|
|
||||||
# partman-lvm/*, partman-jfs/* (Recipe nutzt weder Crypto noch LVM noch JFS,
|
|
||||||
# koennen also nie auftreten; einige dieser Fragen sind bei "true" destruktiv
|
|
||||||
# (z.B. crypto_warn_erase), daher hier absichtlich nicht blind auf true
|
|
||||||
# gesetzt).
|
|
||||||
|
|
||||||
ubiquity ubiquity/summary note
|
|
||||||
ubiquity ubiquity/reboot boolean true
|
|
||||||
ubiquity ubiquity/use_nonfree boolean true
|
|
||||||
# Ohne diese Zeile stuerzt Ubiquity im automatic-ubiquity-GTK-Modus real
|
|
||||||
# reproduzierbar ab (TypeError: Argument 1 does not allow None as a value,
|
|
||||||
# in ubi-prepare.py enable_download_updates() -> label_download_updates.
|
|
||||||
# set_label(), ausgeloest durch einen globalen Online-Status-Callback, der
|
|
||||||
# unabhaengig vom Automatik-Modus feuert, obwohl die zugehoerige Seite dort
|
|
||||||
# nie aufgebaut wird - die Widget-Referenz bleibt None). Die "Waehrend der
|
|
||||||
# Installation aktualisieren"-Option wird dadurch bewusst deaktiviert;
|
|
||||||
# funktional kein Verlust, da der Provisioning Agent das System nach der
|
|
||||||
# Ersteinrichtung ohnehin selbst aktuell haelt.
|
|
||||||
ubiquity ubiquity/download_updates boolean false
|
|
||||||
|
|
||||||
# Pendant zu Fedoras kickstart.tpl %packages (ansible-core, git) - der
|
|
||||||
# Provisioning Agent braucht ansible-pull, das wiederum git zum Klonen des
|
|
||||||
# Ansible-Repos. Ohne diese Zeile fehlen beide auf einer frischen
|
|
||||||
# Mint-Installation, der Agent-Dienst laeuft dann in einer
|
|
||||||
# Restart-Fehlerschleife ("ansible-pull nicht gefunden") - real gegen eine
|
|
||||||
# frische Testinstallation gefunden und verifiziert (2026-08-04).
|
|
||||||
d-i pkgsel/include string ansible-core git
|
|
||||||
|
|
||||||
# success_command laeuft in-target (gechrootet ins Zielsystem) nach der
|
|
||||||
# Paketinstallation, vor dem Reboot - das Pendant zu Kickstarts %post. Der
|
|
||||||
# Payload ist base64-kodiert (backend_generate_config() in backend.sh baut
|
|
||||||
# ihn aus postinstall.sh): mehrzeilige/zitierte Preseed-Werte brechen unter
|
|
||||||
# Debconf lautlos, Base64 umgeht das (real erprobt, siehe wlan-test.seed).
|
|
||||||
ubiquity ubiquity/success_command string in-target bash -c 'echo ${TUXFLOTTE_POSTINSTALL_B64} | base64 -d | bash'
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
loadfont unicode
|
|
||||||
|
|
||||||
set color_normal=white/black
|
|
||||||
set color_highlight=black/light-gray
|
|
||||||
|
|
||||||
set timeout=30
|
|
||||||
|
|
||||||
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit" --class linuxmint {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit (compatibility mode)" {
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} noapic noacpi nosplash irqpoll nomodeset --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "OEM install (for manufacturers)" {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz oem-config/enable=true only-ubiquity boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "Tuxflotte Auto-Install (Linux Mint 22.3 Cinnamon)" --class linuxmint {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
grub_platform
|
|
||||||
if [ "$grub_platform" = "efi" ]; then
|
|
||||||
menuentry 'Von lokaler Festplatte booten (Standard)' {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
set default="Von lokaler Festplatte booten (Standard)"
|
|
||||||
menuentry 'UEFI Firmware Settings' {
|
|
||||||
fwsetup
|
|
||||||
}
|
|
||||||
menuentry 'Memory test' {
|
|
||||||
linux /boot/memtest.efi
|
|
||||||
}
|
|
||||||
else
|
|
||||||
set default="0"
|
|
||||||
fi
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
timeout 100
|
|
||||||
|
|
||||||
menu background splash.png
|
|
||||||
menu title Welcome to Linux Mint 22.3 64-bit
|
|
||||||
|
|
||||||
menu color screen 37;40 #80ffffff #00000000 std
|
|
||||||
MENU COLOR border 30;44 #40ffffff #a0000000 std
|
|
||||||
MENU COLOR title 1;36;44 #ffffffff #a0000000 std
|
|
||||||
MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all
|
|
||||||
MENU COLOR unsel 37;44 #50ffffff #a0000000 std
|
|
||||||
MENU COLOR help 37;40 #c0ffffff #a0000000 std
|
|
||||||
MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std
|
|
||||||
MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std
|
|
||||||
MENU COLOR msg07 37;40 #90ffffff #a0000000 std
|
|
||||||
MENU COLOR tabmsg 31;40 #ffDEDEDE #00000000 std
|
|
||||||
MENU WIDTH 78
|
|
||||||
MENU MARGIN 15
|
|
||||||
MENU ROWS 6
|
|
||||||
MENU VSHIFT 10
|
|
||||||
MENU TABMSGROW 12
|
|
||||||
MENU CMDLINEROW 12
|
|
||||||
MENU HELPMSGROW 16
|
|
||||||
MENU HELPMSGENDROW 29
|
|
||||||
|
|
||||||
label tuxflotte
|
|
||||||
menu label Tuxflotte Auto-Install
|
|
||||||
kernel /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
|
|
||||||
|
|
||||||
label live
|
|
||||||
menu label Start Linux Mint
|
|
||||||
kernel /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
|
|
||||||
|
|
||||||
label compat
|
|
||||||
menu label Start Linux Mint in compatibility mode
|
|
||||||
linux /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint noapic noacpi nosplash irqpoll nomodeset --
|
|
||||||
|
|
||||||
label oem
|
|
||||||
menu label OEM install (for manufacturers)
|
|
||||||
linux /casper/vmlinuz
|
|
||||||
append oem-config/enable=true only-ubiquity boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
|
|
||||||
|
|
||||||
label hdt
|
|
||||||
menu label Hardware Detection
|
|
||||||
kernel hdt.c32
|
|
||||||
|
|
||||||
label local
|
|
||||||
menu label Boot from local drive
|
|
||||||
menu default
|
|
||||||
COM32 chain.c32
|
|
||||||
APPEND hd0
|
|
||||||
|
|
||||||
label memtest
|
|
||||||
menu label Memory test
|
|
||||||
linux /boot/memtest.bin
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
{
|
|
||||||
echo "tuxflotte: success_command lief, $(date -u)"
|
|
||||||
echo "--- nmcli device status (vorher) ---"
|
|
||||||
nmcli device status 2>&1
|
|
||||||
echo "--- WLAN verbinden (live, fuer den anode-Test waehrend der Installation) ---"
|
|
||||||
nmcli device wifi rescan 2>&1
|
|
||||||
sleep 3
|
|
||||||
nmcli device wifi connect stallinux password a987654321 2>&1
|
|
||||||
sleep 3
|
|
||||||
echo "--- nmcli device status (nachher) ---"
|
|
||||||
nmcli device status 2>&1
|
|
||||||
echo "--- curl anode /health ---"
|
|
||||||
curl -m 10 -sS https://anode.tuxflotte.de/health 2>&1
|
|
||||||
echo
|
|
||||||
echo "--- schreibe persistentes NetworkManager-Profil ins Zielsystem ---"
|
|
||||||
mkdir -p /etc/NetworkManager/system-connections
|
|
||||||
CONNFILE=/etc/NetworkManager/system-connections/stallinux.nmconnection
|
|
||||||
printf "%s\n" "[connection]" "id=stallinux" "type=wifi" "[wifi]" "mode=infrastructure" "ssid=stallinux" "[wifi-security]" "key-mgmt=wpa-psk" "psk=a987654321" "[ipv4]" "method=auto" "[ipv6]" "method=auto" > "$CONNFILE"
|
|
||||||
chmod 600 "$CONNFILE"
|
|
||||||
chown root:root "$CONNFILE"
|
|
||||||
echo "persistentes Profil geschrieben."
|
|
||||||
} >> /var/log/tuxflotte-postinstall.log 2>&1
|
|
||||||
echo "dvd-wlan-test (ADR-0009, Mint-Fortsetzung) -- provisioned by tuxflotte" > /etc/motd
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
### Test-Preseed fuer ADR-0009 Fortsetzung mit Mint/Ubuntu (31.07.2026-01.08.2026).
|
|
||||||
### Liegt direkt auf dem Medium (file=/cdrom/preseed/tuxflotte.seed), keine
|
|
||||||
### Netz-Zustellung noetig -- analog zu Fedoras inst.ks=cdrom:/ks.cfg.
|
|
||||||
|
|
||||||
# Test 01.08.2026: laeuft VOR allen anderen Fragen (Partitionierung etc.),
|
|
||||||
# nicht gechrootet -- Ziel ist zu pruefen, ob so frueh schon WLAN herstellbar
|
|
||||||
# ist und ob debconf-set-selections tatsaechlich spaetere Fragen beeinflusst.
|
|
||||||
# Base64 von Anfang an (Lehre aus success_command Versuch 3 gestern).
|
|
||||||
d-i preseed/early_command string bash -c 'echo IyEvYmluL2Jhc2gKewogIGVjaG8gInR1eGZsb3R0ZTogZWFybHlfY29tbWFuZCBsaWVmLCAkKGRhdGUgLXUpIgogIGVjaG8gIi0tLSBpcCBsaW5rIC0tLSIKICBpcCBsaW5rIDI+JjEKICBlY2hvICItLS0gbm1jbGkgZGV2aWNlIHN0YXR1cyAodm9yaGVyKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHN0YXR1cyAyPiYxCiAgZWNobyAiLS0tIFdMQU4gdmVyYmluZGVuIChtaXQgV2llZGVyaG9sdW5nLCBmYWxscyBUcmVpYmVyIG5vY2ggbmljaHQgYmVyZWl0KSAtLS0iCiAgZm9yIGkgaW4gMSAyIDMgNCA1IDY7IGRvCiAgICBubWNsaSBkZXZpY2Ugd2lmaSByZXNjYW4gMj4mMQogICAgc2xlZXAgMgogICAgaWYgbm1jbGkgZGV2aWNlIHdpZmkgY29ubmVjdCBzdGFsbGludXggcGFzc3dvcmQgYTk4NzY1NDMyMSAyPiYxOyB0aGVuCiAgICAgIGVjaG8gIldMQU4gdmVyYnVuZGVuIG5hY2ggVmVyc3VjaCAkaSIKICAgICAgYnJlYWsKICAgIGZpCiAgICBlY2hvICJWZXJzdWNoICRpIGZlaGxnZXNjaGxhZ2VuLCB3YXJ0ZS4uLiIKICAgIHNsZWVwIDUKICBkb25lCiAgZWNobyAiLS0tIG5tY2xpIGRldmljZSBzdGF0dXMgKG5hY2hoZXIpIC0tLSIKICBubWNsaSBkZXZpY2Ugc3RhdHVzIDI+JjEKICBlY2hvICItLS0gY3VybCBhbm9kZSAvaGVhbHRoIC0tLSIKICBjdXJsIC1tIDEwIC1zUyBodHRwczovL2Fub2RlLnR1eGZsb3R0ZS5kZS9oZWFsdGggMj4mMQogIGVjaG8KICBlY2hvICItLS0gZGViY29uZi1zZXQtc2VsZWN0aW9ucyBUZXN0IChzaW11bGllcnQgZHluYW1pc2NoIHZvbiBhbm9kZSBnZWhvbHRlbiBXZXJ0KSAtLS0iCiAgZWNobyAiZC1pIG5ldGNmZy9nZXRfaG9zdG5hbWUgc3RyaW5nIHR1eGZsb3R0ZS1lYXJseS10ZXN0IiB8IGRlYmNvbmYtc2V0LXNlbGVjdGlvbnMKICBlY2hvICJkZWJjb25mLXNldC1zZWxlY3Rpb25zIGV4aXQ6ICQ/IgogIGVjaG8gIi0tLSBWZXJpZmlrYXRpb24gcGVyIGRlYmNvbmYtY29tbXVuaWNhdGUgLS0tIgogIGVjaG8gIkdFVCBuZXRjZmcvZ2V0X2hvc3RuYW1lIiB8IGRlYmNvbmYtY29tbXVuaWNhdGUgMj4mMQp9ID4+IC92YXIvbG9nL3R1eGZsb3R0ZS1lYXJseS1jb21tYW5kLmxvZyAyPiYxCg== | base64 -d | bash'
|
|
||||||
|
|
||||||
d-i debian-installer/locale string de_DE.UTF-8
|
|
||||||
d-i keyboard-configuration/xkb-keymap select de
|
|
||||||
d-i keyboard-configuration/layoutcode string de
|
|
||||||
|
|
||||||
d-i netcfg/get_hostname string tuxflotte-mint-test
|
|
||||||
d-i netcfg/get_domain string unassigned-domain
|
|
||||||
|
|
||||||
d-i passwd/user-fullname string Tuxflotte
|
|
||||||
d-i passwd/username string tuxflotte
|
|
||||||
d-i passwd/user-password password test123
|
|
||||||
d-i passwd/user-password-again password test123
|
|
||||||
d-i user-setup/allow-password-weak boolean true
|
|
||||||
|
|
||||||
d-i clock-setup/utc boolean true
|
|
||||||
d-i time/zone string Europe/Berlin
|
|
||||||
d-i clock-setup/ntp boolean true
|
|
||||||
|
|
||||||
d-i partman-auto/method string regular
|
|
||||||
d-i partman-auto/choose_recipe select atomic
|
|
||||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
|
||||||
d-i partman/choose_partition select finish
|
|
||||||
d-i partman/confirm boolean true
|
|
||||||
d-i partman/confirm_nooverwrite boolean true
|
|
||||||
|
|
||||||
ubiquity ubiquity/summary note
|
|
||||||
|
|
||||||
# Versuch 4: kein automatischer Reboot mehr, damit wir vor dem Neustart aus
|
|
||||||
# der Live-Sitzung heraus direkt unter /target/... nachsehen koennen, ob
|
|
||||||
# alles geschrieben wurde -- auf Vorschlag des Nutzers.
|
|
||||||
ubiquity ubiquity/reboot boolean false
|
|
||||||
|
|
||||||
# Versuch 1: wlp9s0 (Intel AX210) erkannt, aber nicht verbunden (keine
|
|
||||||
# Zugangsdaten). Versuch 2: nmcli verbindet + anode per curl erreichbar
|
|
||||||
# WAEHREND der Installation -- funktioniert. Verbindung ueberlebte den
|
|
||||||
# Reboot nicht (nmcli sprach vermutlich mit der Live-NetworkManager-Instanz,
|
|
||||||
# nicht mit einer, die ins Zielsystem schreibt). Versuch 3: direktes
|
|
||||||
# Schreiben der .nmconnection-Datei als eigener Preseed-Wert -- Datei kam
|
|
||||||
# nie an, kein Log vorhanden, vermutlich Debconf-Problem mit einem so
|
|
||||||
# langen, mehrzeiligen Preseed-Wert (lokaler Bash-Syntaxtest war fehlerfrei,
|
|
||||||
# also kein Bash-Problem). Versuch 4 (dieser): dasselbe Skript Base64-
|
|
||||||
# kodiert -- der Preseed-Wert besteht dadurch nur noch aus unkritischen
|
|
||||||
# Zeichen, keine Anfuehrungszeichen/Klammern mehr, die Debconf Probleme
|
|
||||||
# machen koennten.
|
|
||||||
ubiquity ubiquity/use_nonfree boolean true
|
|
||||||
|
|
||||||
ubiquity ubiquity/success_command string in-target bash -c 'echo IyEvYmluL2Jhc2gKewogIGVjaG8gInR1eGZsb3R0ZTogc3VjY2Vzc19jb21tYW5kIGxpZWYsICQoZGF0ZSAtdSkiCiAgZWNobyAiLS0tIG5tY2xpIGRldmljZSBzdGF0dXMgKHZvcmhlcikgLS0tIgogIG5tY2xpIGRldmljZSBzdGF0dXMgMj4mMQogIGVjaG8gIi0tLSBXTEFOIHZlcmJpbmRlbiAobGl2ZSwgZnVlciBkZW4gYW5vZGUtVGVzdCB3YWVocmVuZCBkZXIgSW5zdGFsbGF0aW9uKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHdpZmkgcmVzY2FuIDI+JjEKICBzbGVlcCAzCiAgbm1jbGkgZGV2aWNlIHdpZmkgY29ubmVjdCBzdGFsbGludXggcGFzc3dvcmQgYTk4NzY1NDMyMSAyPiYxCiAgc2xlZXAgMwogIGVjaG8gIi0tLSBubWNsaSBkZXZpY2Ugc3RhdHVzIChuYWNoaGVyKSAtLS0iCiAgbm1jbGkgZGV2aWNlIHN0YXR1cyAyPiYxCiAgZWNobyAiLS0tIGN1cmwgYW5vZGUgL2hlYWx0aCAtLS0iCiAgY3VybCAtbSAxMCAtc1MgaHR0cHM6Ly9hbm9kZS50dXhmbG90dGUuZGUvaGVhbHRoIDI+JjEKICBlY2hvCiAgZWNobyAiLS0tIHNjaHJlaWJlIHBlcnNpc3RlbnRlcyBOZXR3b3JrTWFuYWdlci1Qcm9maWwgaW5zIFppZWxzeXN0ZW0gLS0tIgogIG1rZGlyIC1wIC9ldGMvTmV0d29ya01hbmFnZXIvc3lzdGVtLWNvbm5lY3Rpb25zCiAgQ09OTkZJTEU9L2V0Yy9OZXR3b3JrTWFuYWdlci9zeXN0ZW0tY29ubmVjdGlvbnMvc3RhbGxpbnV4Lm5tY29ubmVjdGlvbgogIHByaW50ZiAiJXNcbiIgIltjb25uZWN0aW9uXSIgImlkPXN0YWxsaW51eCIgInR5cGU9d2lmaSIgIlt3aWZpXSIgIm1vZGU9aW5mcmFzdHJ1Y3R1cmUiICJzc2lkPXN0YWxsaW51eCIgIlt3aWZpLXNlY3VyaXR5XSIgImtleS1tZ210PXdwYS1wc2siICJwc2s9YTk4NzY1NDMyMSIgIltpcHY0XSIgIm1ldGhvZD1hdXRvIiAiW2lwdjZdIiAibWV0aG9kPWF1dG8iID4gIiRDT05ORklMRSIKICBjaG1vZCA2MDAgIiRDT05ORklMRSIKICBjaG93biByb290OnJvb3QgIiRDT05ORklMRSIKICBlY2hvICJwZXJzaXN0ZW50ZXMgUHJvZmlsIGdlc2NocmllYmVuLiIKfSA+PiAvdmFyL2xvZy90dXhmbG90dGUtcG9zdGluc3RhbGwubG9nIDI+JjEKZWNobyAiZHZkLXdsYW4tdGVzdCAoQURSLTAwMDksIE1pbnQtRm9ydHNldHp1bmcpIC0tIHByb3Zpc2lvbmVkIGJ5IHR1eGZsb3R0ZSIgPiAvZXRjL21vdGQK | base64 -d | bash'
|
|
||||||
119
boot-medium/config/binary
Normal file
119
boot-medium/config/binary
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# config/binary - options for live-build(7), binary stage
|
||||||
|
|
||||||
|
# Set image type
|
||||||
|
LB_IMAGE_TYPE="iso-hybrid"
|
||||||
|
|
||||||
|
# Set image filesystem
|
||||||
|
LB_BINARY_FILESYSTEM="fat32"
|
||||||
|
|
||||||
|
# Set apt/aptitude generic indices
|
||||||
|
LB_APT_INDICES="true"
|
||||||
|
|
||||||
|
# Set boot parameters
|
||||||
|
LB_BOOTAPPEND_LIVE="boot=live components quiet splash vconsole.keymap=de"
|
||||||
|
|
||||||
|
# Set boot parameters
|
||||||
|
LB_BOOTAPPEND_INSTALL=""
|
||||||
|
|
||||||
|
# Set boot parameters
|
||||||
|
LB_BOOTAPPEND_LIVE_FAILSAFE="boot=live components memtest noapic noapm nodma nomce nolapic nosmp nosplash vga=788"
|
||||||
|
|
||||||
|
# Set BIOS bootloader
|
||||||
|
LB_BOOTLOADER_BIOS="syslinux"
|
||||||
|
|
||||||
|
# Set EFI bootloader
|
||||||
|
LB_BOOTLOADER_EFI="grub-efi"
|
||||||
|
|
||||||
|
# Set bootloaders
|
||||||
|
LB_BOOTLOADERS="grub-efi syslinux"
|
||||||
|
|
||||||
|
# Set checksums
|
||||||
|
LB_CHECKSUMS="sha256"
|
||||||
|
|
||||||
|
# Set compression
|
||||||
|
LB_COMPRESSION="none"
|
||||||
|
|
||||||
|
# Support dm-verity on rootfs
|
||||||
|
LB_DM_VERITY=""
|
||||||
|
|
||||||
|
# Support FEC on dm-verity rootfs
|
||||||
|
LB_DM_VERITY_FEC_ROOTS=""
|
||||||
|
|
||||||
|
# Set sign script for roothash for dm-verity rootfs
|
||||||
|
LB_DM_VERITY_SIGN=""
|
||||||
|
|
||||||
|
# Set zsync
|
||||||
|
LB_ZSYNC="true"
|
||||||
|
|
||||||
|
# Control if we build binary images chrooted
|
||||||
|
# NEVER, *EVER*, *E*V*E*R* SET THIS OPTION to false.
|
||||||
|
LB_BUILD_WITH_CHROOT="true"
|
||||||
|
|
||||||
|
# Set debian-installer
|
||||||
|
LB_DEBIAN_INSTALLER="none"
|
||||||
|
|
||||||
|
# Set debian-installer suite
|
||||||
|
LB_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
|
||||||
|
|
||||||
|
# Set debian-installer preseed filename/url
|
||||||
|
LB_DEBIAN_INSTALLER_PRESEEDFILE=""
|
||||||
|
|
||||||
|
# Toggle use of GUI debian-installer
|
||||||
|
LB_DEBIAN_INSTALLER_GUI="true"
|
||||||
|
|
||||||
|
# Set hdd label
|
||||||
|
LB_HDD_LABEL="DEBIAN_LIVE"
|
||||||
|
|
||||||
|
# Set hdd filesystem size
|
||||||
|
LB_HDD_SIZE="auto"
|
||||||
|
|
||||||
|
# Set start of partition for the hdd target for BIOSes that expect a specific boot partition start (e.g. "63s"). If empty, use optimal layout.
|
||||||
|
LB_HDD_PARTITION_START=""
|
||||||
|
|
||||||
|
# Set iso author
|
||||||
|
LB_ISO_APPLICATION="Debian Live"
|
||||||
|
|
||||||
|
# Set iso preparer
|
||||||
|
LB_ISO_PREPARER="live-build @LB_VERSION@; https://salsa.debian.org/live-team/live-build"
|
||||||
|
|
||||||
|
# Set iso publisher
|
||||||
|
LB_ISO_PUBLISHER="Debian Live project; https://wiki.debian.org/DebianLive; debian-live@lists.debian.org"
|
||||||
|
|
||||||
|
# Set iso volume (max 32 chars)
|
||||||
|
LB_ISO_VOLUME="Debian trixie @ISOVOLUME_TS@"
|
||||||
|
|
||||||
|
# Set jffs2 eraseblock size
|
||||||
|
LB_JFFS2_ERASEBLOCK=""
|
||||||
|
|
||||||
|
# Set memtest
|
||||||
|
LB_MEMTEST="none"
|
||||||
|
|
||||||
|
# Set loadlin
|
||||||
|
LB_LOADLIN="false"
|
||||||
|
|
||||||
|
# Set win32-loader
|
||||||
|
LB_WIN32_LOADER="false"
|
||||||
|
|
||||||
|
# Set net tarball
|
||||||
|
LB_NET_TARBALL="true"
|
||||||
|
|
||||||
|
# Set onie
|
||||||
|
LB_ONIE="false"
|
||||||
|
|
||||||
|
# Set onie additional kernel cmdline options
|
||||||
|
LB_ONIE_KERNEL_CMDLINE=""
|
||||||
|
|
||||||
|
# Set inclusion of firmware packages in debian-installer
|
||||||
|
LB_FIRMWARE_BINARY="true"
|
||||||
|
|
||||||
|
# Set inclusion of firmware packages in the live image
|
||||||
|
LB_FIRMWARE_CHROOT="true"
|
||||||
|
|
||||||
|
# Set swap file path
|
||||||
|
LB_SWAP_FILE_PATH=""
|
||||||
|
|
||||||
|
# Set swap file size
|
||||||
|
LB_SWAP_FILE_SIZE="512"
|
||||||
|
|
||||||
|
# Enable/disable UEFI secure boot support
|
||||||
|
LB_UEFI_SECURE_BOOT="auto"
|
||||||
76
boot-medium/config/bootstrap
Normal file
76
boot-medium/config/bootstrap
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# config/bootstrap - options for live-build(7), bootstrap stage
|
||||||
|
|
||||||
|
# Select architecture to use
|
||||||
|
LB_ARCHITECTURE="amd64"
|
||||||
|
|
||||||
|
# Select distribution to use
|
||||||
|
LB_DISTRIBUTION="trixie"
|
||||||
|
|
||||||
|
# Select parent distribution to use
|
||||||
|
LB_PARENT_DISTRIBUTION=""
|
||||||
|
|
||||||
|
# Select distribution to use in the chroot
|
||||||
|
LB_DISTRIBUTION_CHROOT="trixie"
|
||||||
|
|
||||||
|
# Select parent distribution to use in the chroot
|
||||||
|
LB_PARENT_DISTRIBUTION_CHROOT="trixie"
|
||||||
|
|
||||||
|
# Select distribution to use in the final image
|
||||||
|
LB_DISTRIBUTION_BINARY="trixie"
|
||||||
|
|
||||||
|
# Select parent distribution to use in the final image
|
||||||
|
LB_PARENT_DISTRIBUTION_BINARY="trixie"
|
||||||
|
|
||||||
|
# Select parent distribution for debian-installer to use
|
||||||
|
LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION="trixie"
|
||||||
|
|
||||||
|
# Select archive areas to use
|
||||||
|
LB_ARCHIVE_AREAS="main contrib non-free non-free-firmware"
|
||||||
|
|
||||||
|
# Select parent archive areas to use
|
||||||
|
LB_PARENT_ARCHIVE_AREAS="main contrib non-free non-free-firmware"
|
||||||
|
|
||||||
|
# Set parent mirror to bootstrap from
|
||||||
|
LB_PARENT_MIRROR_BOOTSTRAP="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set parent mirror to fetch packages from
|
||||||
|
LB_PARENT_MIRROR_CHROOT="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set security parent mirror to fetch packages from
|
||||||
|
LB_PARENT_MIRROR_CHROOT_SECURITY="http://security.debian.org/"
|
||||||
|
|
||||||
|
# Set parent mirror which ends up in the image
|
||||||
|
LB_PARENT_MIRROR_BINARY="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set security parent mirror which ends up in the image
|
||||||
|
LB_PARENT_MIRROR_BINARY_SECURITY="http://security.debian.org/"
|
||||||
|
|
||||||
|
# Set debian-installer parent mirror
|
||||||
|
LB_PARENT_MIRROR_DEBIAN_INSTALLER="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set mirror to bootstrap from
|
||||||
|
LB_MIRROR_BOOTSTRAP="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set mirror to fetch packages from
|
||||||
|
LB_MIRROR_CHROOT="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set security mirror to fetch packages from
|
||||||
|
LB_MIRROR_CHROOT_SECURITY="http://security.debian.org/"
|
||||||
|
|
||||||
|
# Set mirror which ends up in the image
|
||||||
|
LB_MIRROR_BINARY="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set security mirror which ends up in the image
|
||||||
|
LB_MIRROR_BINARY_SECURITY="http://security.debian.org/"
|
||||||
|
|
||||||
|
# Set debian-installer mirror
|
||||||
|
LB_MIRROR_DEBIAN_INSTALLER="http://deb.debian.org/debian/"
|
||||||
|
|
||||||
|
# Set architectures to use foreign bootstrap
|
||||||
|
LB_BOOTSTRAP_QEMU_ARCHITECTURE=""
|
||||||
|
|
||||||
|
# Set packages to exclude during foreign bootstrap
|
||||||
|
LB_BOOTSTRAP_QEMU_EXCLUDE=""
|
||||||
|
|
||||||
|
# Set static qemu binary for foreign bootstrap
|
||||||
|
LB_BOOTSTRAP_QEMU_STATIC=""
|
||||||
34
boot-medium/config/chroot
Normal file
34
boot-medium/config/chroot
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# config/chroot - options for live-build(7), chroot stage
|
||||||
|
|
||||||
|
# Set chroot filesystem
|
||||||
|
LB_CHROOT_FILESYSTEM="squashfs"
|
||||||
|
|
||||||
|
# Set chroot squashfs compression level
|
||||||
|
LB_CHROOT_SQUASHFS_COMPRESSION_LEVEL=""
|
||||||
|
|
||||||
|
# Set chroot squashfs compression type
|
||||||
|
LB_CHROOT_SQUASHFS_COMPRESSION_TYPE=""
|
||||||
|
|
||||||
|
# Set union filesystem
|
||||||
|
LB_UNION_FILESYSTEM="overlay"
|
||||||
|
|
||||||
|
# Set interactive build
|
||||||
|
LB_INTERACTIVE="false"
|
||||||
|
|
||||||
|
# Set keyring packages
|
||||||
|
LB_KEYRING_PACKAGES="debian-archive-keyring"
|
||||||
|
|
||||||
|
# Set kernel flavour to use (with arch)
|
||||||
|
LB_LINUX_FLAVOURS_WITH_ARCH="amd64"
|
||||||
|
|
||||||
|
# Set kernel packages to use
|
||||||
|
LB_LINUX_PACKAGES="linux-image"
|
||||||
|
|
||||||
|
# Enable security updates
|
||||||
|
LB_SECURITY="true"
|
||||||
|
|
||||||
|
# Enable updates updates
|
||||||
|
LB_UPDATES="true"
|
||||||
|
|
||||||
|
# Enable backports updates
|
||||||
|
LB_BACKPORTS="false"
|
||||||
102
boot-medium/config/common
Normal file
102
boot-medium/config/common
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# config/common - common options for live-build(7)
|
||||||
|
|
||||||
|
# Version of live-build used to build config (config format version)
|
||||||
|
LB_CONFIGURATION_VERSION="20230502"
|
||||||
|
|
||||||
|
# Set package manager
|
||||||
|
LB_APT="apt"
|
||||||
|
|
||||||
|
# Set proxy for HTTP connections
|
||||||
|
LB_APT_HTTP_PROXY=""
|
||||||
|
|
||||||
|
# Set apt/aptitude pipeline depth
|
||||||
|
LB_APT_PIPELINE=""
|
||||||
|
|
||||||
|
# Set apt/aptitude recommends
|
||||||
|
LB_APT_RECOMMENDS="true"
|
||||||
|
|
||||||
|
# Set apt/aptitude security
|
||||||
|
LB_APT_SECURE="true"
|
||||||
|
|
||||||
|
# Set apt/aptitude source entries in sources.list
|
||||||
|
LB_APT_SOURCE_ARCHIVES="true"
|
||||||
|
|
||||||
|
# Control cache
|
||||||
|
LB_CACHE="true"
|
||||||
|
|
||||||
|
# Control if downloaded package indices should be cached
|
||||||
|
LB_CACHE_INDICES="false"
|
||||||
|
|
||||||
|
# Control if downloaded packages files should be cached
|
||||||
|
LB_CACHE_PACKAGES="true"
|
||||||
|
|
||||||
|
# Control if completed stages should be cached
|
||||||
|
LB_CACHE_STAGES="bootstrap"
|
||||||
|
|
||||||
|
# Set debconf(1) frontend to use
|
||||||
|
LB_DEBCONF_FRONTEND="noninteractive"
|
||||||
|
|
||||||
|
# Set debconf(1) priority to use
|
||||||
|
LB_DEBCONF_PRIORITY="critical"
|
||||||
|
|
||||||
|
# Set initramfs hook
|
||||||
|
LB_INITRAMFS="live-boot"
|
||||||
|
|
||||||
|
# Set initramfs compression
|
||||||
|
LB_INITRAMFS_COMPRESSION="gzip"
|
||||||
|
|
||||||
|
# Set init system
|
||||||
|
LB_INITSYSTEM="systemd"
|
||||||
|
|
||||||
|
# Set distribution mode
|
||||||
|
LB_MODE="debian"
|
||||||
|
|
||||||
|
# Set system type
|
||||||
|
LB_SYSTEM="live"
|
||||||
|
|
||||||
|
# Set base name of the image
|
||||||
|
LB_IMAGE_NAME="live-image"
|
||||||
|
|
||||||
|
# Set options to use with apt
|
||||||
|
APT_OPTIONS="--yes -o Acquire::Retries=5"
|
||||||
|
|
||||||
|
# Set options to use with aptitude
|
||||||
|
APTITUDE_OPTIONS="--assume-yes -o Acquire::Retries=5"
|
||||||
|
|
||||||
|
# Set options to use with debootstrap
|
||||||
|
DEBOOTSTRAP_OPTIONS=""
|
||||||
|
|
||||||
|
# Set script to use with debootstrap
|
||||||
|
DEBOOTSTRAP_SCRIPT=""
|
||||||
|
|
||||||
|
# Set options to use with gzip
|
||||||
|
GZIP_OPTIONS="-6 --rsyncable"
|
||||||
|
|
||||||
|
# Enable UTC timestamps
|
||||||
|
LB_UTC_TIME="false"
|
||||||
|
|
||||||
|
# live-build options
|
||||||
|
|
||||||
|
# Enable breakpoints
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_BREAKPOINTS="false"
|
||||||
|
|
||||||
|
# Enable debug
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_DEBUG="false"
|
||||||
|
|
||||||
|
# Enable color
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_COLOR="auto"
|
||||||
|
|
||||||
|
# Enable force
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_FORCE="false"
|
||||||
|
|
||||||
|
# Enable quiet
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_QUIET="false"
|
||||||
|
|
||||||
|
# Enable verbose
|
||||||
|
# If set here, overrides the command line option
|
||||||
|
#_VERBOSE="false"
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/live/0010-disable-kexec-tools.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/live/0050-disable-sysvinit-tmpfs.hook.chroot
|
||||||
24
boot-medium/config/hooks/live/0900-tuxflotte-ssh-mode.hook.chroot
Executable file
24
boot-medium/config/hooks/live/0900-tuxflotte-ssh-mode.hook.chroot
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Steuert SSH-Zugang ueber einen Build-Parameter (scripts/build_boot_medium.sh
|
||||||
|
# --dev), nicht ueber einen fest gebackenen Zustand. Der Marker
|
||||||
|
# /etc/tuxflotte-dev-build wird von build_boot_medium.sh vor dem lb-build-Lauf
|
||||||
|
# gesetzt/entfernt - siehe dort.
|
||||||
|
|
||||||
|
if [ -e /etc/tuxflotte-dev-build ]; then
|
||||||
|
echo "Dev-Build: SSH-Debug-Zugang wird aktiviert (tuxflotte/test123)."
|
||||||
|
id tuxflotte >/dev/null 2>&1 || useradd -m -s /bin/bash tuxflotte
|
||||||
|
echo "tuxflotte:test123" | chpasswd
|
||||||
|
usermod -aG sudo tuxflotte
|
||||||
|
# Debians trixie-sshd_config-Default hat PasswordAuthentication live
|
||||||
|
# entdeckt bereits deaktiviert (nur publickey angeboten) - fuer den
|
||||||
|
# Testzugang per Passwort explizit wieder aktivieren.
|
||||||
|
mkdir -p /etc/ssh/sshd_config.d
|
||||||
|
echo "PasswordAuthentication yes" > /etc/ssh/sshd_config.d/90-tuxflotte-dev.conf
|
||||||
|
rm -f /etc/tuxflotte-dev-build
|
||||||
|
else
|
||||||
|
echo "Produktiv-Build: SSH bleibt deaktiviert, keine gebackenen Zugangsdaten."
|
||||||
|
rm -f /etc/systemd/system/multi-user.target.wants/ssh.service
|
||||||
|
ln -sf /dev/null /etc/systemd/system/ssh.service
|
||||||
|
fi
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/1000-create-mtab-symlink.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/1010-enable-cryptsetup.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/1020-create-locales-files.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/5000-update-apt-file-cache.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/5010-update-apt-xapian-index.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/5020-update-glx-alternative.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/5030-update-plocate-database.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/5040-update-nvidia-alternative.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8000-remove-adjtime-configuration.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8010-remove-backup-files.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8020-remove-dbus-machine-id.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8030-truncate-log-files.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8040-remove-mdadm-configuration.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8050-remove-openssh-server-host-keys.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8060-remove-systemd-machine-id.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8070-remove-temporary-files.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8080-reproducible-glibc.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8090-remove-ssl-cert-snakeoil.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8100-remove-udev-persistent-cd-rules.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/8110-remove-udev-persistent-net-rules.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/9000-remove-gnome-icon-cache.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/9010-remove-python-pyc.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
/usr/share/live/build/hooks/normal/9020-remove-man-cache.hook.chroot
|
||||||
@ -0,0 +1 @@
|
|||||||
|
../tuxflotte-installer.service
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tuxflotte Installer Auto-Start
|
||||||
|
Conflicts=getty@tty1.service
|
||||||
|
After=getty@tty1.service NetworkManager.service
|
||||||
|
Wants=NetworkManager.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart=/opt/tuxflotte/scripts/boot-autostart.sh
|
||||||
|
StandardInput=tty-force
|
||||||
|
StandardOutput=journal+console
|
||||||
|
StandardError=journal+console
|
||||||
|
TTYPath=/dev/tty1
|
||||||
|
TTYReset=yes
|
||||||
|
TTYVHangup=yes
|
||||||
|
# Live gefunden (31.08.2026): ein ExecStopPost, der getty@tty1.service beim
|
||||||
|
# Dienstende neu startet, ist unzuverlaessig (Race mit dem eigenen
|
||||||
|
# TTY-Teardown). Geloest stattdessen in boot-autostart.sh selbst: die
|
||||||
|
# Konsole faellt nach installer.sh deterministisch in eine interaktive
|
||||||
|
# Shell, kein getty-Neustart noetig.
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,311 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Dieses Skript wird von einem Orchestrator-Modul (40_backend.sh) per
|
||||||
|
# `source` in dessen Shell geladen. Variablen bleiben deshalb bewusst nicht
|
||||||
|
# readonly, um Namenskollisionen mit dem ladenden Modul zu vermeiden.
|
||||||
|
#
|
||||||
|
# Golden-Image-Deployment-Backend (siehe ADR-0024) - ersetzt die
|
||||||
|
# Ubiquity-Automatisierung von backends/mint/ durch das curtin/FAI-Muster:
|
||||||
|
# Zieldatentraeger direkt partitionieren, ein fertiges Root-Filesystem-
|
||||||
|
# Image entpacken, per chroot nacharbeiten. Kein GUI-Installer, kein
|
||||||
|
# Preseed/Kickstart mehr - die eigentliche Mechanik steckt in
|
||||||
|
# scripts/lib/image_deploy.sh (Phase 1, isoliert live verifiziert).
|
||||||
|
#
|
||||||
|
# backends/mint/ bleibt unveraendert als Referenz bestehen - dieses
|
||||||
|
# Backend ist ein bewusst NEUER backend_id ("mint-image"), nichts wird
|
||||||
|
# live umgeschaltet.
|
||||||
|
BACKEND_KEY="mint-image"
|
||||||
|
|
||||||
|
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "${BACKEND_DIR}/../.." && pwd)"
|
||||||
|
POSTINSTALL_SCRIPT="${BACKEND_DIR}/postinstall.sh"
|
||||||
|
IMAGE_DEPLOY_LIB="${REPO_DIR}/scripts/lib/image_deploy.sh"
|
||||||
|
|
||||||
|
RUNTIME_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json"
|
||||||
|
SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
|
||||||
|
HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
|
||||||
|
|
||||||
|
RUNTIME_DIR="/run/tuxflotte/backend"
|
||||||
|
CONFIG_FILE="${RUNTIME_DIR}/config.json"
|
||||||
|
|
||||||
|
# Ziel-Mountpunkt fuer die Deployment-Mechanik - global, da backend_launch()
|
||||||
|
# und backend_postinstall() (separate Funktionsaufrufe, aber dieselbe
|
||||||
|
# Shell/derselbe Prozess, siehe 40_backend.sh) sich denselben Baum teilen.
|
||||||
|
TARGET_DIR="/target"
|
||||||
|
declare -a MOUNT_STACK=()
|
||||||
|
|
||||||
|
# Aus einer manuell in Proxmox installierten Referenz-VM gezogen (nicht
|
||||||
|
# debootstrap - siehe ADR-0024-Nachtrag "Referenz-VM statt debootstrap",
|
||||||
|
# 31.08.2026), bereinigt via scripts/package_golden_image.sh, gehostet
|
||||||
|
# ueber die unauthentifizierte /golden-images/-Route in
|
||||||
|
# provisioning-server (analog ks.cfg) - live verifiziert per Public-HTTPS-
|
||||||
|
# Download (200, byte-exakte Groesse) am 31.08.2026.
|
||||||
|
GOLDEN_IMAGE_URL="https://anode.tuxflotte.de/golden-images/linux-mint-22.3-cinnamon.tar.zst"
|
||||||
|
|
||||||
|
backend_log() {
|
||||||
|
printf '[backend:%s] %s\n' "${BACKEND_KEY}" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_fatal() {
|
||||||
|
printf '[backend:%s] FEHLER: %s\n' "${BACKEND_KEY}" "$*" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Analog zu _tuxflotte_detect_target_disk() in backends/mint/backend.sh -
|
||||||
|
# bewusst hier dupliziert statt geteilt, um dieses Backend unabhaengig vom
|
||||||
|
# Mint-Referenzbackend zu halten (siehe Modul-Kommentar oben). Ein Umzug in
|
||||||
|
# eine gemeinsame lib waere ein sinnvolles spaeteres Aufraeumen, sobald
|
||||||
|
# mehr als zwei Backends dieselbe Logik brauchen.
|
||||||
|
_mint_image_detect_target_disk() {
|
||||||
|
lsblk --nodeps --noheadings --bytes --output NAME,TYPE,SIZE --paths |
|
||||||
|
awk '$2 == "disk" && $3 > 0 && $1 !~ /(nbd|zram|loop)[0-9]*$/ { print $1; exit }'
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_init() {
|
||||||
|
# Historisch (bis zum Umstieg auf das eigenstaendige, per live-build
|
||||||
|
# gebaute Boot-Medium) wurden diese Werkzeuge hier noch zur Laufzeit per
|
||||||
|
# apt-get nachinstalliert, weil das damalige Boot-Medium (eine gepatchte
|
||||||
|
# Linux-Mint-Live-ISO) sie nicht immer mitbrachte. Das eigenstaendige
|
||||||
|
# Boot-Medium bringt sie bereits im Paketsatz mit (siehe
|
||||||
|
# boot-medium/config/package-lists/tuxflotte.list.chroot) - hier bleibt
|
||||||
|
# nur noch eine reine Assertion, damit ein kuenftiger Paketlisten-Fehler
|
||||||
|
# fruh und klar auffaellt.
|
||||||
|
local missing=()
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || missing+=(jq)
|
||||||
|
command -v envsubst >/dev/null 2>&1 || missing+=(gettext-base)
|
||||||
|
command -v parted >/dev/null 2>&1 || missing+=(parted)
|
||||||
|
command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools)
|
||||||
|
command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs)
|
||||||
|
command -v mkfs.btrfs >/dev/null 2>&1 || missing+=(btrfs-progs)
|
||||||
|
command -v zstd >/dev/null 2>&1 || missing+=(zstd)
|
||||||
|
command -v curl >/dev/null 2>&1 || missing+=(curl)
|
||||||
|
|
||||||
|
[[ "${#missing[@]}" -eq 0 ]] ||
|
||||||
|
{ backend_fatal "Werkzeuge fehlen auf dem Boot-Medium (Paketliste pruefen): ${missing[*]}"; return 1; }
|
||||||
|
|
||||||
|
[[ -r "${IMAGE_DEPLOY_LIB}" ]] ||
|
||||||
|
{ backend_fatal "Deployment-Bibliothek nicht gefunden: ${IMAGE_DEPLOY_LIB}"; return 1; }
|
||||||
|
# shellcheck source=../../scripts/lib/image_deploy.sh
|
||||||
|
source "${IMAGE_DEPLOY_LIB}"
|
||||||
|
|
||||||
|
[[ -r "${POSTINSTALL_SCRIPT}" ]] ||
|
||||||
|
{ backend_fatal "Postinstall-Skript nicht gefunden: ${POSTINSTALL_SCRIPT}"; return 1; }
|
||||||
|
|
||||||
|
install -d \
|
||||||
|
--mode=0700 \
|
||||||
|
--owner=root \
|
||||||
|
--group=root \
|
||||||
|
"${RUNTIME_DIR}"
|
||||||
|
|
||||||
|
rm -f -- "${CONFIG_FILE}"
|
||||||
|
|
||||||
|
backend_log "Initialisiert."
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_validate() {
|
||||||
|
[[ -r "${RUNTIME_BLUEPRINT_FILE}" ]] ||
|
||||||
|
{ backend_fatal "Runtime Blueprint nicht gefunden: ${RUNTIME_BLUEPRINT_FILE}"; return 1; }
|
||||||
|
|
||||||
|
jq --exit-status \
|
||||||
|
--arg backend_key "${BACKEND_KEY}" \
|
||||||
|
'.runtime_blueprint.backend_id == $backend_key' \
|
||||||
|
"${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
|
||||||
|
{ backend_fatal "Runtime Blueprint ist nicht für Backend '${BACKEND_KEY}' aufgelöst."; return 1; }
|
||||||
|
|
||||||
|
jq --exit-status '
|
||||||
|
.runtime_blueprint.installation_directives
|
||||||
|
| (.disk_encryption | type == "boolean")
|
||||||
|
and (.partitioning | type == "object")
|
||||||
|
and (.secure_boot_required | type == "boolean")
|
||||||
|
' "${RUNTIME_BLUEPRINT_FILE}" >/dev/null ||
|
||||||
|
{ backend_fatal "Installationszeitliche Vorgaben fehlen oder sind ungültig."; return 1; }
|
||||||
|
|
||||||
|
# Phase 2 deckt bewusst nur das einfache Schema ab (ESP/biosgrub +
|
||||||
|
# eine Root-Partition, siehe image_deploy_partition()) - "custom" mit
|
||||||
|
# extra_partitions (/home, /var) ist noch nicht auf die neue
|
||||||
|
# parted-basierte Mechanik uebertragen. Klarer Fehler statt stiller
|
||||||
|
# Fehlinterpretation.
|
||||||
|
local scheme
|
||||||
|
scheme="$(jq --raw-output '.runtime_blueprint.installation_directives.partitioning.scheme // "single"' "${RUNTIME_BLUEPRINT_FILE}")"
|
||||||
|
[[ "${scheme}" == "single" ]] ||
|
||||||
|
{ backend_fatal "Partitionierungsschema '${scheme}' wird von diesem Backend noch nicht unterstützt (nur 'single')."; return 1; }
|
||||||
|
|
||||||
|
if [[ "$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" == "true" ]]; then
|
||||||
|
backend_fatal "disk_encryption=true wird von diesem Backend derzeit nicht unterstützt."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
backend_log "Runtime Blueprint ist gültig für Backend '${BACKEND_KEY}'."
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_generate_config() {
|
||||||
|
local hostname device_id device_fingerprint
|
||||||
|
local root_filesystem partitioning_json blueprints_json
|
||||||
|
|
||||||
|
[[ -r "${SERVER_RESPONSE_FILE}" ]] ||
|
||||||
|
{ backend_fatal "Serverantwort nicht gefunden: ${SERVER_RESPONSE_FILE}"; return 1; }
|
||||||
|
|
||||||
|
hostname="$(jq --raw-output '.device.hostname // empty' "${SERVER_RESPONSE_FILE}")"
|
||||||
|
[[ -n "${hostname}" ]] ||
|
||||||
|
{ backend_fatal "Kein Hostname in der Serverantwort gefunden."; return 1; }
|
||||||
|
|
||||||
|
device_id="$(jq --raw-output '.device.id // empty' "${SERVER_RESPONSE_FILE}")"
|
||||||
|
[[ -n "${device_id}" ]] ||
|
||||||
|
{ backend_fatal "Keine Geräte-ID in der Serverantwort gefunden."; return 1; }
|
||||||
|
|
||||||
|
[[ -r "${HARDWARE_FILE}" ]] ||
|
||||||
|
{ backend_fatal "Hardware-Erfassung nicht gefunden: ${HARDWARE_FILE}"; return 1; }
|
||||||
|
device_fingerprint="$(jq --raw-output '.identity.device_fingerprint // empty' "${HARDWARE_FILE}")"
|
||||||
|
[[ -n "${device_fingerprint}" ]] ||
|
||||||
|
{ backend_fatal "Kein device_fingerprint in ${HARDWARE_FILE} gefunden."; return 1; }
|
||||||
|
|
||||||
|
partitioning_json="$(jq --compact-output '.runtime_blueprint.installation_directives.partitioning' "${RUNTIME_BLUEPRINT_FILE}")"
|
||||||
|
root_filesystem="$(jq --raw-output '.root_filesystem // "ext4"' <<<"${partitioning_json}")"
|
||||||
|
case "${root_filesystem}" in
|
||||||
|
ext4|btrfs) ;;
|
||||||
|
*) backend_fatal "Nicht unterstütztes Root-Dateisystem: ${root_filesystem}"; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")"
|
||||||
|
|
||||||
|
jq --null-input \
|
||||||
|
--arg hostname "${hostname}" \
|
||||||
|
--arg device_id "${device_id}" \
|
||||||
|
--arg device_fingerprint "${device_fingerprint}" \
|
||||||
|
--arg root_filesystem "${root_filesystem}" \
|
||||||
|
--argjson blueprints "${blueprints_json}" \
|
||||||
|
'{
|
||||||
|
hostname: $hostname,
|
||||||
|
device_id: $device_id,
|
||||||
|
device_fingerprint: $device_fingerprint,
|
||||||
|
root_filesystem: $root_filesystem,
|
||||||
|
blueprints: $blueprints
|
||||||
|
}' > "${CONFIG_FILE}" ||
|
||||||
|
{ backend_fatal "Konfigurationsdatei konnte nicht erzeugt werden."; return 1; }
|
||||||
|
|
||||||
|
chmod 0600 "${CONFIG_FILE}"
|
||||||
|
|
||||||
|
backend_log "Konfiguration erzeugt: ${CONFIG_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_launch() {
|
||||||
|
local disk is_efi root_fs boot_part root_part
|
||||||
|
|
||||||
|
disk="$(_mint_image_detect_target_disk)"
|
||||||
|
[[ -n "${disk}" ]] ||
|
||||||
|
{ backend_fatal "Zieldatenträger konnte nicht ermittelt werden."; return 1; }
|
||||||
|
|
||||||
|
[[ -d /sys/firmware/efi ]] && is_efi="true" || is_efi="false"
|
||||||
|
backend_log "Zieldatenträger: ${disk} (Firmware: $([ "${is_efi}" = true ] && echo UEFI || echo BIOS))"
|
||||||
|
|
||||||
|
root_fs="$(jq --raw-output '.root_filesystem' "${CONFIG_FILE}")"
|
||||||
|
|
||||||
|
backend_log "Partitioniere ${disk}"
|
||||||
|
read -r boot_part root_part <<<"$(image_deploy_partition "${disk}" "${is_efi}")" ||
|
||||||
|
return 1
|
||||||
|
|
||||||
|
backend_log "Formatiere Partitionen"
|
||||||
|
image_deploy_format "${boot_part}" "${root_part}" "${root_fs}" || return 1
|
||||||
|
|
||||||
|
backend_log "Mounte unter ${TARGET_DIR}"
|
||||||
|
image_deploy_mount "${TARGET_DIR}" "${boot_part}" "${root_part}" || return 1
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026, erster echter End-to-End-Lauf mit dem vollen
|
||||||
|
# Referenz-VM-Archiv): "erst nach /run/tuxflotte/... herunterladen, dann
|
||||||
|
# entpacken" scheiterte an /run (RAM-Tmpfs, viel kleiner als das
|
||||||
|
# 2,3-GB-Archiv - "curl: (23) Failure writing output to destination").
|
||||||
|
# Direktes Streamen in die Extraktion braucht nur ein paar MB Puffer,
|
||||||
|
# unabhaengig von der Archivgroesse - deshalb erst ab hier (nach
|
||||||
|
# Partitionieren/Formatieren/Mounten), kein Zwischenspeichern mehr.
|
||||||
|
backend_log "Lade und entpacke Golden Image von ${GOLDEN_IMAGE_URL}"
|
||||||
|
image_deploy_extract_image_from_url "${GOLDEN_IMAGE_URL}" "${TARGET_DIR}" || return 1
|
||||||
|
|
||||||
|
backend_log "Schreibe fstab"
|
||||||
|
image_deploy_write_fstab "${TARGET_DIR}" "${boot_part}" "${root_part}" "${root_fs}" || return 1
|
||||||
|
|
||||||
|
backend_log "Binde /dev, /proc, /sys ein"
|
||||||
|
image_deploy_bind_mounts "${TARGET_DIR}" MOUNT_STACK || return 1
|
||||||
|
|
||||||
|
backend_log "chroot-Fixup (machine-id, SSH-Hostkeys, initramfs)"
|
||||||
|
image_deploy_chroot_fixup "${TARGET_DIR}" || return 1
|
||||||
|
|
||||||
|
backend_log "Installiere Bootloader"
|
||||||
|
image_deploy_install_bootloader "${TARGET_DIR}" "${disk}" "${is_efi}" || return 1
|
||||||
|
|
||||||
|
local hostname
|
||||||
|
hostname="$(jq --raw-output '.hostname' "${CONFIG_FILE}")"
|
||||||
|
backend_log "Setze Hostname (${hostname})"
|
||||||
|
image_deploy_set_hostname "${TARGET_DIR}" "${hostname}" || return 1
|
||||||
|
|
||||||
|
backend_log "Deployment abgeschlossen."
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_postinstall() {
|
||||||
|
local device_id device_fingerprint blueprints_json
|
||||||
|
local postinstall_rendered
|
||||||
|
|
||||||
|
device_id="$(jq --raw-output '.device_id' "${CONFIG_FILE}")"
|
||||||
|
device_fingerprint="$(jq --raw-output '.device_fingerprint' "${CONFIG_FILE}")"
|
||||||
|
blueprints_json="$(jq --compact-output '.blueprints' "${CONFIG_FILE}")"
|
||||||
|
|
||||||
|
# Dasselbe Template wie backends/mint/postinstall.sh (per Symlink
|
||||||
|
# geteilt, siehe Verzeichnis) - rein distributionsunabhaengiges
|
||||||
|
# Bash-Skript (curl/jq gegen anode), hier per chroot statt per
|
||||||
|
# ubiquity/success_command ausgefuehrt.
|
||||||
|
postinstall_rendered="$(
|
||||||
|
TUXFLOTTE_DEVICE_ID="${device_id}" \
|
||||||
|
TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \
|
||||||
|
TUXFLOTTE_DEVICE_FINGERPRINT="${device_fingerprint}" \
|
||||||
|
envsubst '${TUXFLOTTE_DEVICE_ID} ${TUXFLOTTE_BLUEPRINTS_JSON} ${TUXFLOTTE_DEVICE_FINGERPRINT}' \
|
||||||
|
<"${POSTINSTALL_SCRIPT}"
|
||||||
|
)"
|
||||||
|
|
||||||
|
if grep -q '\${TUXFLOTTE_' <<<"${postinstall_rendered}"; then
|
||||||
|
backend_fatal "postinstall.sh enthält nach envsubst nicht aufgelöste Platzhalter."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "${postinstall_rendered}" > "${TARGET_DIR}/tmp/postinstall.sh"
|
||||||
|
chmod 0700 "${TARGET_DIR}/tmp/postinstall.sh"
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026, erster vollstaendig durchgelaufener
|
||||||
|
# End-to-End-Test): postinstall.sh braucht jq (+curl), aber die echte
|
||||||
|
# Mint-Referenz-VM bringt das nicht zwingend mit (anders als das
|
||||||
|
# Boot-Medium selbst, das jq ja schon vorinstalliert hat - das hilft
|
||||||
|
# dem ausgerollten Zielsystem hier nichts, das ist ein komplett
|
||||||
|
# eigener chroot). python3 wird nicht von postinstall.sh selbst
|
||||||
|
# gebraucht, aber vom heruntergeladenen agent.py nach dem naechsten
|
||||||
|
# Boot - hier gleich mit absichern, um nicht noch einen ganzen
|
||||||
|
# Referenz-VM-Neupack-Zyklus wegen eines einzelnen fehlenden Pakets zu
|
||||||
|
# brauchen. package_golden_image.sh leert ausserdem /var/lib/apt/lists
|
||||||
|
# als Teil der Bereinigung - "apt-get update" ist deshalb hier noetig,
|
||||||
|
# bevor "apt-get install" ueberhaupt Pakete finden kann. Netzwerk ist
|
||||||
|
# im chroot verfuegbar (resolv.conf wurde schon in
|
||||||
|
# image_deploy_bind_mounts kopiert, dieselben Bind-Mounts sind noch
|
||||||
|
# aktiv).
|
||||||
|
chroot "${TARGET_DIR}" bash -c '
|
||||||
|
missing=()
|
||||||
|
command -v jq >/dev/null 2>&1 || missing+=(jq)
|
||||||
|
command -v curl >/dev/null 2>&1 || missing+=(curl)
|
||||||
|
command -v python3 >/dev/null 2>&1 || missing+=(python3)
|
||||||
|
[[ "${#missing[@]}" -eq 0 ]] && exit 0
|
||||||
|
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}"
|
||||||
|
' ||
|
||||||
|
{ backend_fatal "jq/curl/python3 konnten im Zielsystem nicht sichergestellt werden."; return 1; }
|
||||||
|
|
||||||
|
backend_log "Führe Postinstall-Skript im chroot aus."
|
||||||
|
chroot "${TARGET_DIR}" /bin/bash /tmp/postinstall.sh ||
|
||||||
|
{ backend_fatal "Postinstall-Skript ist im chroot fehlgeschlagen."; return 1; }
|
||||||
|
|
||||||
|
rm -f "${TARGET_DIR}/tmp/postinstall.sh"
|
||||||
|
|
||||||
|
backend_log "Hänge Ziel-Dateisystem aus."
|
||||||
|
image_deploy_unbind_mounts MOUNT_STACK
|
||||||
|
umount --recursive "${TARGET_DIR}" ||
|
||||||
|
{ backend_fatal "${TARGET_DIR} konnte nicht ausgehängt werden."; return 1; }
|
||||||
|
|
||||||
|
backend_log "Starte neu - kein Rücksprung erwartet, ab hier läuft das frisch installierte System."
|
||||||
|
reboot
|
||||||
|
}
|
||||||
36
boot-medium/config/includes.chroot/opt/tuxflotte/banner.txt
Normal file
36
boot-medium/config/includes.chroot/opt/tuxflotte/banner.txt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
_______ _ ___ ________ _ ____ _______ _______ ______
|
||||||
|
|__ __| | | \ \ / / ____| | / __ \__ __|__ __| ____|
|
||||||
|
| | | | | |\ V /| |__ | | | | | | | | | | | |__
|
||||||
|
| | | | | | > < | __| | | | | | | | | | | | __|
|
||||||
|
| | | |__| |/ . \| | | |___| |__| | | | | | | |____
|
||||||
|
|_| \____//_/ \_\_| |______\____/ |_| |_| |______|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
#### ###
|
||||||
|
## ##
|
||||||
|
## ### ##
|
||||||
|
## ### ########
|
||||||
|
## # ######
|
||||||
|
## ###
|
||||||
|
# ##
|
||||||
|
# #
|
||||||
|
# ##########
|
||||||
|
########### # ###
|
||||||
|
### ### # ###
|
||||||
|
## ### ## #
|
||||||
|
# ## ## ###########
|
||||||
|
######### ## # ### ##
|
||||||
|
#### ## ## ## ##
|
||||||
|
## ### ####
|
||||||
|
# ### #########
|
||||||
|
############### ##
|
||||||
|
### ##
|
||||||
|
## ##############
|
||||||
|
# ####
|
||||||
|
## ##
|
||||||
|
## ###
|
||||||
|
# #
|
||||||
|
# ##
|
||||||
|
## #
|
||||||
45
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/boot-autostart.sh
Executable file
45
boot-medium/config/includes.chroot/opt/tuxflotte/scripts/boot-autostart.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Wird von tuxflotte-installer.service beim Boot direkt auf tty1 gestartet -
|
||||||
|
# unabhaengig davon, ob config/installer.conf einen Auto-Modus-
|
||||||
|
# Aktivierungscode mitbringt (personalisierte Kunden-ISO, siehe
|
||||||
|
# scripts/build_customer_iso.sh) oder nicht (generisches Test-/
|
||||||
|
# Entwicklungsabbild): installer.sh fragt in letzterem Fall selbst
|
||||||
|
# interaktiv nach (12_enrollment_auth.sh, 05_network.sh-WLAN-Auswahl), auf
|
||||||
|
# genau dieser Konsole - der Service leitet dafuer TTYPath=/dev/tty1 durch.
|
||||||
|
#
|
||||||
|
# Schreibt zusaetzlich zur journal-Ausgabe (StandardOutput=journal+console
|
||||||
|
# im systemd-Unit) in eine Logdatei - per SSH/Konsole nachtraeglich
|
||||||
|
# einsehbar, ohne journalctl zu brauchen.
|
||||||
|
LOG_FILE="/var/log/tuxflotte-installer.log"
|
||||||
|
BANNER_FILE="/opt/tuxflotte/banner.txt"
|
||||||
|
|
||||||
|
# Begruessungsbanner (Tuxflotte-Schriftzug + Flotten-Grafik, 80 Spalten) -
|
||||||
|
# reine Optik, ins Logfile mitschreiben schadet nicht, aber nicht fatal
|
||||||
|
# wenn die Datei mal fehlt (set -e: "if" statt "&&", sonst wuerde ein
|
||||||
|
# fehlendes Banner das ganze Skript beenden).
|
||||||
|
if [[ -r "${BANNER_FILE}" ]]; then
|
||||||
|
{ cat "${BANNER_FILE}"; echo; } | tee -a "${LOG_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
/opt/tuxflotte/scripts/installer.sh 2>&1 | tee -a "${LOG_FILE}"
|
||||||
|
STATUS="${PIPESTATUS[0]}"
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026): ein ExecStopPost, der beim Dienstende
|
||||||
|
# getty@tty1.service neu startet, ist unzuverlaessig (Race mit dem eigenen
|
||||||
|
# TTY-Teardown dieses Dienstes - der Job wurde angestossen, blieb aber
|
||||||
|
# "inactive/dead"). Stattdessen: diese Konsole faellt nach installer.sh
|
||||||
|
# (egal ob Erfolg, kontrollierter Abbruch an einem Commit-Gate, oder
|
||||||
|
# Fehler) deterministisch in eine interaktive Root-Shell - kein Race,
|
||||||
|
# garantiert nutzbare Konsole fuer Fehlersuche/manuellen Neustart. Im
|
||||||
|
# Erfolgsfall mit tatsaechlichem Deployment loest 40_backend.sh ohnehin
|
||||||
|
# selbst einen Reboot aus, wodurch diese Shell nie sichtbar wird.
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "Tuxflotte-Installer beendet (Exit-Code: ${STATUS})."
|
||||||
|
echo "Diese Konsole steht fuer Fehlersuche/manuellen Neustart zur Verfuegung."
|
||||||
|
echo
|
||||||
|
} | tee -a "${LOG_FILE}"
|
||||||
|
|
||||||
|
exec /bin/bash --login
|
||||||
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,346 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Kernmechanik des Golden-Image-Deployments (siehe ADR-0024 und
|
||||||
|
# platform-docs-Plan "Golden-Image-Deployment statt Ubiquity-
|
||||||
|
# Automatisierung"). Wird von source eingebunden - nicht eigenstaendig
|
||||||
|
# ausfuehrbar. Ersetzt den bisherigen Ansatz "nativen Distributions-
|
||||||
|
# Installer automatisieren" (Ubiquity/Anaconda) durch das curtin/FAI-
|
||||||
|
# Muster: Zieldatentraeger direkt partitionieren, ein fertiges
|
||||||
|
# Root-Filesystem-Image entpacken, per chroot nacharbeiten
|
||||||
|
# (fstab/initramfs/machine-id/SSH-Hostkeys/Bootloader).
|
||||||
|
#
|
||||||
|
# Bewusst als eigenstaendige Funktionsbibliothek (wie lib/initrd.sh,
|
||||||
|
# lib/checks.sh) statt einer einzigen "mach alles"-Funktion - sowohl der
|
||||||
|
# isolierte Phase-1-Testtreiber als auch das spaetere
|
||||||
|
# backends/mint-image/backend.sh rufen dieselben Bausteine in derselben
|
||||||
|
# Reihenfolge auf.
|
||||||
|
|
||||||
|
image_deploy_log() {
|
||||||
|
printf '[image_deploy] %s\n' "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_fatal() {
|
||||||
|
printf '[image_deploy] FEHLER: %s\n' "$*" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nvme/mmcblk-Geraete brauchen ein "p" vor der Partitionsnummer
|
||||||
|
# (/dev/nvme0n1p1), sd/vd-Geraete nicht (/dev/sda1) - Heuristik: Geraete-
|
||||||
|
# name endet auf eine Ziffer -> braucht "p".
|
||||||
|
_image_deploy_part_suffix() {
|
||||||
|
local disk="$1"
|
||||||
|
if [[ "${disk}" =~ [0-9]$ ]]; then
|
||||||
|
echo "p"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_partition_path() {
|
||||||
|
local disk="$1"
|
||||||
|
local num="$2"
|
||||||
|
echo "${disk}$(_image_deploy_part_suffix "${disk}")${num}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Partitioniert den Zieldatentraeger komplett neu (GPT, wie schon in
|
||||||
|
# _tuxflotte_render_partman_recipe() begruendet - auch ohne EFI legt GPT
|
||||||
|
# an, siehe dortiger Kommentar zu bios_grub). Gibt "BOOT_PART ROOT_PART"
|
||||||
|
# auf stdout aus - BOOT_PART ist bei BIOS leer (die biosgrub-Partition
|
||||||
|
# braucht kein Dateisystem/keinen Mountpunkt).
|
||||||
|
image_deploy_partition() {
|
||||||
|
local disk="$1"
|
||||||
|
local is_efi="$2"
|
||||||
|
|
||||||
|
parted --script "${disk}" mklabel gpt ||
|
||||||
|
{ image_deploy_fatal "mklabel gpt fehlgeschlagen auf ${disk}"; return 1; }
|
||||||
|
|
||||||
|
if [[ "${is_efi}" == "true" ]]; then
|
||||||
|
parted --script "${disk}" mkpart ESP fat32 1MiB 513MiB &&
|
||||||
|
parted --script "${disk}" set 1 esp on &&
|
||||||
|
parted --script "${disk}" mkpart root ext4 513MiB 100% ||
|
||||||
|
{ image_deploy_fatal "Partitionierung (UEFI) fehlgeschlagen auf ${disk}"; return 1; }
|
||||||
|
else
|
||||||
|
parted --script "${disk}" mkpart biosgrub 1MiB 2MiB &&
|
||||||
|
parted --script "${disk}" set 1 bios_grub on &&
|
||||||
|
parted --script "${disk}" mkpart root ext4 2MiB 100% ||
|
||||||
|
{ image_deploy_fatal "Partitionierung (BIOS) fehlgeschlagen auf ${disk}"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
partprobe "${disk}" 2>/dev/null || true
|
||||||
|
udevadm settle
|
||||||
|
|
||||||
|
local part1 part2
|
||||||
|
part1="$(image_deploy_partition_path "${disk}" 1)"
|
||||||
|
part2="$(image_deploy_partition_path "${disk}" 2)"
|
||||||
|
|
||||||
|
[[ -b "${part1}" && -b "${part2}" ]] ||
|
||||||
|
{ image_deploy_fatal "Partitionen nach partprobe nicht gefunden (${part1}, ${part2})"; return 1; }
|
||||||
|
|
||||||
|
if [[ "${is_efi}" == "true" ]]; then
|
||||||
|
echo "${part1} ${part2}"
|
||||||
|
else
|
||||||
|
echo " ${part2}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_format() {
|
||||||
|
local boot_part="$1"
|
||||||
|
local root_part="$2"
|
||||||
|
local root_fs="$3"
|
||||||
|
|
||||||
|
if [[ -n "${boot_part}" ]]; then
|
||||||
|
mkfs.vfat -F32 -n ESP "${boot_part}" ||
|
||||||
|
{ image_deploy_fatal "mkfs.vfat auf ${boot_part} fehlgeschlagen"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${root_fs}" in
|
||||||
|
ext4) mkfs.ext4 -F -L root "${root_part}" ;;
|
||||||
|
btrfs) mkfs.btrfs -f -L root "${root_part}" ;;
|
||||||
|
*) image_deploy_fatal "Nicht unterstuetztes Root-Dateisystem: ${root_fs}"; return 1 ;;
|
||||||
|
esac || { image_deploy_fatal "mkfs auf ${root_part} fehlgeschlagen"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_mount() {
|
||||||
|
local target="$1"
|
||||||
|
local boot_part="$2"
|
||||||
|
local root_part="$3"
|
||||||
|
|
||||||
|
install -d "${target}"
|
||||||
|
mount "${root_part}" "${target}" ||
|
||||||
|
{ image_deploy_fatal "Root-Partition ${root_part} konnte nicht auf ${target} gemountet werden"; return 1; }
|
||||||
|
|
||||||
|
if [[ -n "${boot_part}" ]]; then
|
||||||
|
install -d "${target}/boot/efi"
|
||||||
|
mount "${boot_part}" "${target}/boot/efi" ||
|
||||||
|
{ image_deploy_fatal "ESP ${boot_part} konnte nicht gemountet werden"; return 1; }
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# package_golden_image.sh schliesst /proc, /sys, /dev, /run, /tmp, /var/tmp
|
||||||
|
# bewusst aus dem Archiv aus (Laufzeit-Zustand der Referenz-VM, nicht Teil
|
||||||
|
# eines portablen Images). Nach dem Entpacken fehlen diese Verzeichnisse
|
||||||
|
# deshalb komplett - live gefunden (31.08.2026, erster echter Lauf mit dem
|
||||||
|
# echten Referenz-VM-Archiv): erst "mount --bind /dev" ("mount point does
|
||||||
|
# not exist"), dann - nach Behebung dessen - "mktemp: failed to create
|
||||||
|
# directory via template '/var/tmp/mkinitramfs_XXXXXX'" bei
|
||||||
|
# update-initramfs. Statt jedes Fehlen einzeln zu jagen: alle sechs
|
||||||
|
# ausgeschlossenen Verzeichnisse direkt nach dem Entpacken einmalig mit
|
||||||
|
# sinnvollen Rechten wiederherstellen. /dev, /proc, /sys werden gleich
|
||||||
|
# danach ohnehin ueberbindmounted (image_deploy_bind_mounts) - schadet
|
||||||
|
# aber nicht, sie schon hier als leere Verzeichnisse anzulegen.
|
||||||
|
image_deploy_restore_excluded_dirs() {
|
||||||
|
local target="$1"
|
||||||
|
|
||||||
|
mkdir -p "${target}"/{proc,sys,dev,run,tmp,var/tmp}
|
||||||
|
chmod 1777 "${target}/tmp" "${target}/var/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Entpackt das per build_golden_image.sh gebaute Golden Image direkt in
|
||||||
|
# den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt
|
||||||
|
# sowohl .tar.zst als auch (falls spaeter gewechselt) andere
|
||||||
|
# zstd-komprimierte Varianten ab.
|
||||||
|
image_deploy_extract_image() {
|
||||||
|
local image_path="$1"
|
||||||
|
local target="$2"
|
||||||
|
|
||||||
|
[[ -r "${image_path}" ]] ||
|
||||||
|
{ image_deploy_fatal "Golden Image nicht lesbar: ${image_path}"; return 1; }
|
||||||
|
|
||||||
|
tar --zstd -xpf "${image_path}" -C "${target}" ||
|
||||||
|
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; }
|
||||||
|
|
||||||
|
image_deploy_restore_excluded_dirs "${target}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Streamt das Golden Image direkt vom Server in die Extraktion, ohne es
|
||||||
|
# jemals als Ganzes zwischenzuspeichern - live gefunden (31.08.2026,
|
||||||
|
# erster echter End-to-End-Lauf mit dem vollen 2,3-GB-Referenz-VM-Archiv
|
||||||
|
# ueber das neue Boot-Medium): das bisherige "erst nach /run/tuxflotte/...
|
||||||
|
# herunterladen, dann entpacken" scheiterte an /run (RAM-Tmpfs, oft nur
|
||||||
|
# ein paar hundert MB gross, unabhaengig von der Geraete-RAM-Groesse) -
|
||||||
|
# "curl: (23) Failure writing output to destination". Direktes
|
||||||
|
# Zusammenrohren braucht dagegen nur ein paar MB Puffer, unabhaengig von
|
||||||
|
# der Archivgroesse. PIPESTATUS wird explizit geprueft statt sich auf
|
||||||
|
# "pipefail"-Semantik (rechteste fehlgeschlagene Stufe gewinnt) zu
|
||||||
|
# verlassen, damit die Fehlermeldung eindeutig zwischen Download- und
|
||||||
|
# Entpack-Fehler unterscheidet.
|
||||||
|
image_deploy_extract_image_from_url() {
|
||||||
|
local url="$1"
|
||||||
|
local target="$2"
|
||||||
|
local pipe_status
|
||||||
|
|
||||||
|
# "|| true" verhindert, dass "set -e" hier sofort abbricht - der
|
||||||
|
# eigentliche Fehlerfall wird gleich anhand von PIPESTATUS gezielt
|
||||||
|
# ausgewertet, statt die Pipeline roh durchschlagen zu lassen.
|
||||||
|
curl --silent --show-error --fail --location "${url}" |
|
||||||
|
tar --zstd -xpf - -C "${target}" || true
|
||||||
|
pipe_status=("${PIPESTATUS[@]}")
|
||||||
|
|
||||||
|
[[ "${pipe_status[0]}" -eq 0 ]] ||
|
||||||
|
{ image_deploy_fatal "Golden Image konnte nicht geladen werden: ${url}"; return 1; }
|
||||||
|
[[ "${pipe_status[1]}" -eq 0 ]] ||
|
||||||
|
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; }
|
||||||
|
|
||||||
|
image_deploy_restore_excluded_dirs "${target}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein
|
||||||
|
# genfstab auf Debian/Mint verfuegbar (das ist ein Arch-Linux-Werkzeug),
|
||||||
|
# blkid reicht fuer unseren einfachen Fall (Root + optional ESP) locker.
|
||||||
|
image_deploy_write_fstab() {
|
||||||
|
local target="$1"
|
||||||
|
local boot_part="$2"
|
||||||
|
local root_part="$3"
|
||||||
|
local root_fs="$4"
|
||||||
|
|
||||||
|
local root_uuid
|
||||||
|
root_uuid="$(blkid -s UUID -o value "${root_part}")"
|
||||||
|
[[ -n "${root_uuid}" ]] ||
|
||||||
|
{ image_deploy_fatal "Keine UUID fuer ${root_part} gefunden"; return 1; }
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Von image_deploy.sh generiert - siehe ADR-0024."
|
||||||
|
echo "UUID=${root_uuid} / ${root_fs} defaults 0 1"
|
||||||
|
if [[ -n "${boot_part}" ]]; then
|
||||||
|
local boot_uuid
|
||||||
|
boot_uuid="$(blkid -s UUID -o value "${boot_part}")"
|
||||||
|
[[ -n "${boot_uuid}" ]] ||
|
||||||
|
{ image_deploy_fatal "Keine UUID fuer ${boot_part} gefunden"; return 1; }
|
||||||
|
echo "UUID=${boot_uuid} /boot/efi vfat umask=0077 0 1"
|
||||||
|
fi
|
||||||
|
} > "${target}/etc/fstab"
|
||||||
|
}
|
||||||
|
|
||||||
|
# /dev, /proc, /sys, /dev/pts in den Zielbaum einbinden (fuer chroot-
|
||||||
|
# Operationen, die Geraeteknoten/Kernel-Interfaces brauchen - initramfs,
|
||||||
|
# grub-install) sowie eine funktionierende DNS-Aufloesung fuers
|
||||||
|
# apt-get-basierte Bootloader-Nachinstallieren. MOUNTED_STACK wird vom
|
||||||
|
# aufrufenden Skript vorgehalten (siehe image_deploy_unbind_mounts()).
|
||||||
|
image_deploy_bind_mounts() {
|
||||||
|
local target="$1"
|
||||||
|
local -n stack_ref="$2"
|
||||||
|
|
||||||
|
# Golden Image schliesst /dev, /proc, /sys bewusst aus
|
||||||
|
# (package_golden_image.sh --exclude=dev/sys/proc) - die Zielverzeichnisse
|
||||||
|
# existieren nach dem Entpacken deshalb noch nicht und muessen vor dem
|
||||||
|
# Bind-Mount erst angelegt werden. Live gefunden (31.08.2026, erster
|
||||||
|
# echter Lauf mit der echten Mint-Referenz-VM): ohne das schlaegt
|
||||||
|
# "mount --bind" mit "mount point does not exist" fehl - und zwar
|
||||||
|
# SILENT, weil das bisherige "cmd && stack_ref+=(...)"-Muster einen
|
||||||
|
# Fehlschlag von "cmd" unter "set -e" nicht als Statement-Fehler wertet
|
||||||
|
# (wohlbekannte Bash-Ausnahme fuer &&/||-Ketten) - der fehlende
|
||||||
|
# /dev-Bind-Mount fiel dann erst beim naechsten chroot-Aufruf auf
|
||||||
|
# ("ssh-keygen -A: Couldn't open /dev/null"), weit weg von der
|
||||||
|
# eigentlichen Ursache. Jetzt: Zielverzeichnisse anlegen, jeden
|
||||||
|
# Mount-Fehler explizit und einzeln pruefen statt auf &&-Verkettung
|
||||||
|
# zu vertrauen.
|
||||||
|
mkdir -p "${target}/dev" "${target}/proc" "${target}/sys" "${target}/dev/pts"
|
||||||
|
|
||||||
|
mount --bind /dev "${target}/dev" ||
|
||||||
|
{ image_deploy_fatal "/dev konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/dev")
|
||||||
|
|
||||||
|
mount -t proc proc "${target}/proc" ||
|
||||||
|
{ image_deploy_fatal "/proc konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/proc")
|
||||||
|
|
||||||
|
mount -t sysfs sysfs "${target}/sys" ||
|
||||||
|
{ image_deploy_fatal "/sys konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/sys")
|
||||||
|
|
||||||
|
mount -t devpts devpts "${target}/dev/pts" ||
|
||||||
|
{ image_deploy_fatal "/dev/pts konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/dev/pts")
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026, erster echter Lauf mit der echten
|
||||||
|
# Mint-Referenz-VM statt eines debootstrap-Test-Images): dort ist
|
||||||
|
# /etc/resolv.conf ein von NetworkManager verwalteter Symlink (z.B. auf
|
||||||
|
# /run/NetworkManager/resolv.conf) - im gerade erst entpackten,
|
||||||
|
# ungebooteten Zielbaum zeigt der ins Leere, "cp" verweigert dann das
|
||||||
|
# Schreiben "through a dangling symlink". Ziel deshalb vorher entfernen
|
||||||
|
# (Symlink oder Datei, beides abgedeckt), erst dann kopieren.
|
||||||
|
rm -f "${target}/etc/resolv.conf"
|
||||||
|
cp /etc/resolv.conf "${target}/etc/resolv.conf" ||
|
||||||
|
{ image_deploy_fatal "resolv.conf konnte nicht nach ${target} kopiert werden"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_unbind_mounts() {
|
||||||
|
local -n stack_ref="$1"
|
||||||
|
local i mnt
|
||||||
|
|
||||||
|
for ((i = ${#stack_ref[@]} - 1; i >= 0; i--)); do
|
||||||
|
mnt="${stack_ref[i]}"
|
||||||
|
umount --recursive "${mnt}" 2>/dev/null || umount --lazy "${mnt}" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
stack_ref=()
|
||||||
|
}
|
||||||
|
|
||||||
|
# machine-id + SSH-Hostkeys werden im Golden Image bewusst NICHT
|
||||||
|
# mitgeliefert (siehe build_golden_image.sh) - hier, pro tatsaechlich
|
||||||
|
# ausgerolltem Geraet, frisch erzeugt. update-initramfs regeneriert das
|
||||||
|
# initrd fuer die tatsaechliche Zielhardware (Golden Image wurde auf
|
||||||
|
# einem anderen System gebaut).
|
||||||
|
image_deploy_chroot_fixup() {
|
||||||
|
local target="$1"
|
||||||
|
|
||||||
|
chroot "${target}" systemd-machine-id-setup ||
|
||||||
|
{ image_deploy_fatal "machine-id konnte nicht erzeugt werden"; return 1; }
|
||||||
|
chroot "${target}" ssh-keygen -A ||
|
||||||
|
{ image_deploy_fatal "SSH-Hostkeys konnten nicht erzeugt werden"; return 1; }
|
||||||
|
chroot "${target}" update-initramfs -u -k all ||
|
||||||
|
{ image_deploy_fatal "initramfs-Regenerierung fehlgeschlagen"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grub wird bewusst NICHT ins Golden Image eingebaut (spart Platz, das
|
||||||
|
# Image kennt beim Bauen das Zielfirmware-Schema noch nicht) - stattdessen
|
||||||
|
# hier zur Deployment-Zeit nachinstalliert, wo bereits Netzwerk verfuegbar
|
||||||
|
# ist (Stufe-1-Umgebung hat sich schon fuer die Aktivierung verbunden).
|
||||||
|
image_deploy_install_bootloader() {
|
||||||
|
local target="$1"
|
||||||
|
local disk="$2"
|
||||||
|
local is_efi="$3"
|
||||||
|
|
||||||
|
if [[ "${is_efi}" == "true" ]]; then
|
||||||
|
chroot "${target}" /bin/bash -c '
|
||||||
|
set -e
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y grub-efi-amd64
|
||||||
|
' || { image_deploy_fatal "grub-efi-amd64-Installation fehlgeschlagen"; return 1; }
|
||||||
|
chroot "${target}" grub-install --target=x86_64-efi \
|
||||||
|
--efi-directory=/boot/efi --bootloader-id=tuxflotte --recheck ||
|
||||||
|
{ image_deploy_fatal "grub-install (UEFI) fehlgeschlagen"; return 1; }
|
||||||
|
# Zusaetzlich auf den Standard-Fallback-Pfad (EFI/BOOT/BOOTX64.EFI)
|
||||||
|
# installieren - real beim Testen entdeckt: grub-install kann in
|
||||||
|
# verschachtelten/eingeschraenkten Umgebungen keinen NVRAM-
|
||||||
|
# Booteintrag setzen ("EFI variables are not supported on this
|
||||||
|
# system"), ohne --removable bleibt dann NUR der benannte
|
||||||
|
# /EFI/tuxflotte/-Pfad uebrig, den die Firmware ohne NVRAM-Eintrag
|
||||||
|
# nie findet ("No bootable option or device was found"). Der
|
||||||
|
# Fallback-Pfad wird von JEDER UEFI-Firmware ohne NVRAM-Eintrag
|
||||||
|
# automatisch versucht - robuster fuer heterogene Zielhardware
|
||||||
|
# generell, nicht nur fuer dieses Testszenario.
|
||||||
|
chroot "${target}" grub-install --target=x86_64-efi \
|
||||||
|
--efi-directory=/boot/efi --removable --recheck ||
|
||||||
|
{ image_deploy_fatal "grub-install (UEFI, removable-Fallback) fehlgeschlagen"; return 1; }
|
||||||
|
else
|
||||||
|
chroot "${target}" /bin/bash -c '
|
||||||
|
set -e
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y grub-pc
|
||||||
|
' || { image_deploy_fatal "grub-pc-Installation fehlgeschlagen"; return 1; }
|
||||||
|
chroot "${target}" grub-install --target=i386-pc --recheck "${disk}" ||
|
||||||
|
{ image_deploy_fatal "grub-install (BIOS) fehlgeschlagen"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
chroot "${target}" update-grub ||
|
||||||
|
{ image_deploy_fatal "update-grub fehlgeschlagen"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
image_deploy_set_hostname() {
|
||||||
|
local target="$1"
|
||||||
|
local hostname="$2"
|
||||||
|
|
||||||
|
echo "${hostname}" > "${target}/etc/hostname"
|
||||||
|
printf '127.0.1.1\t%s\n' "${hostname}" >> "${target}/etc/hosts"
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo "[INFO ] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo "[WARN ] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo "[ERROR] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo "[ OK ] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Klar abgesetzte Fortschrittsanzeige fuer die tty-Konsole des Boot-Mediums
|
||||||
|
# (kein Desktop, kein Kiosk-Browser - die Terminal-Ausgabe selbst ist die
|
||||||
|
# einzige UI, die eine Person vor dem Geraet sieht). Gilt fuer Dev- UND
|
||||||
|
# Produktiv-Build gleichermassen, nicht nur zum Debuggen - siehe Plan
|
||||||
|
# "Klare Fortschrittsanzeige auf der Konsole". Format/Stil bewusst an die
|
||||||
|
# bestehenden Banner in 17_device_status.sh angelehnt (Titel + "="-Linie
|
||||||
|
# gleicher Laenge, keine Sonderzeichen, die auf einer rohen VT-Konsole
|
||||||
|
# fehlen koennten).
|
||||||
|
declare -A TUXFLOTTE_STEP_LABELS=(
|
||||||
|
[00_preflight.sh]="Vorbereitung"
|
||||||
|
[05_network.sh]="Netzwerkverbindung wird hergestellt"
|
||||||
|
[10_hardware.sh]="Hardware wird erkannt"
|
||||||
|
[12_enrollment_auth.sh]="Aktivierungscode wird geprüft"
|
||||||
|
[15_server_handshake.sh]="Verbindung zu anode wird hergestellt"
|
||||||
|
[17_device_status.sh]="Gerätestatus wird abgerufen"
|
||||||
|
[20_profile_selection.sh]="Bereitstellungsvorlage wird ausgewählt"
|
||||||
|
[25_installation_confirm.sh]="Installationsbestätigung"
|
||||||
|
[30_runtime_blueprint.sh]="Installationsplan wird aufgelöst"
|
||||||
|
[40_backend.sh]="Golden Image wird geladen und installiert"
|
||||||
|
[20_storage.sh]="Speicher-Zusatzprüfung"
|
||||||
|
[99_finish.sh]="Abschluss"
|
||||||
|
)
|
||||||
|
readonly -a TUXFLOTTE_STEP_ORDER=(
|
||||||
|
00_preflight.sh 05_network.sh 10_hardware.sh 12_enrollment_auth.sh
|
||||||
|
15_server_handshake.sh 17_device_status.sh 20_profile_selection.sh
|
||||||
|
25_installation_confirm.sh 30_runtime_blueprint.sh 40_backend.sh
|
||||||
|
20_storage.sh 99_finish.sh
|
||||||
|
)
|
||||||
|
TUXFLOTTE_STEP_COUNT=0
|
||||||
|
|
||||||
|
log_step() {
|
||||||
|
local module_name="$1"
|
||||||
|
local label="${TUXFLOTTE_STEP_LABELS[$module_name]:-$module_name}"
|
||||||
|
local total="${#TUXFLOTTE_STEP_ORDER[@]}"
|
||||||
|
local bold="" reset=""
|
||||||
|
|
||||||
|
TUXFLOTTE_STEP_COUNT=$((TUXFLOTTE_STEP_COUNT + 1))
|
||||||
|
|
||||||
|
# Klartext-Fallback ohne Terminal (z.B. wenn stdout in eine Log-Datei
|
||||||
|
# umgeleitet wird) - kein tput-Aufruf, ncurses-bin ist bewusst nicht Teil
|
||||||
|
# des schlanken Paketsatzes, rohe ANSI-Codes reichen und sind ueberall
|
||||||
|
# ohne Zusatzpaket verfuegbar.
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
bold=$'\033[1m'
|
||||||
|
reset=$'\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
local title
|
||||||
|
title="[${TUXFLOTTE_STEP_COUNT}/${total}] ${label}"
|
||||||
|
|
||||||
|
printf '\n%s%s%s\n' "${bold}" "${title}" "${reset}"
|
||||||
|
printf '%s\n\n' "$(printf '=%.0s' $(seq 1 ${#title}))"
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
local config_file="$1"
|
||||||
|
|
||||||
|
if [[ -f "$config_file" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$config_file"
|
||||||
|
log_info "Konfiguration geladen: $config_file"
|
||||||
|
else
|
||||||
|
log_warn "Keine Konfiguration gefunden: $config_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_module() {
|
||||||
|
local module="$1"
|
||||||
|
local mode="${2:-normal}"
|
||||||
|
|
||||||
|
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
|
||||||
|
|
||||||
|
log_step "$(basename "$module")"
|
||||||
|
log_info "Starte Modul: $(basename "$module")"
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then
|
||||||
|
log_warn "Dry-Run: Modul übersprungen: $module"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
"$module"
|
||||||
|
|
||||||
|
log_success "Modul abgeschlossen: $(basename "$module")"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
[[ "$EUID" -eq 0 ]] || error_exit "Installer muss als root ausgeführt werden."
|
||||||
|
}
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
@ -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 "$@"
|
||||||
4
boot-medium/config/package-lists/live.list.chroot
Normal file
4
boot-medium/config/package-lists/live.list.chroot
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
live-boot
|
||||||
|
live-config
|
||||||
|
live-config-systemd
|
||||||
|
systemd-sysv
|
||||||
33
boot-medium/config/package-lists/tuxflotte.list.chroot
Normal file
33
boot-medium/config/package-lists/tuxflotte.list.chroot
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Tuxflotte Boot-Medium Paketliste (Phase 0 des unabhängigen Boot-Mediums)
|
||||||
|
# Basis: scripts/build_golden_image.sh (Zeilen 151-169), erweitert um Werkzeuge, die
|
||||||
|
# bisher erst zur Laufzeit per apt-get nachinstalliert wurden (00_preflight.sh,
|
||||||
|
# backend_init() in backends/mint-image/backend.sh), sowie btrfs-progs auf Nutzerwunsch.
|
||||||
|
|
||||||
|
linux-image-amd64
|
||||||
|
firmware-linux
|
||||||
|
firmware-realtek
|
||||||
|
firmware-iwlwifi
|
||||||
|
firmware-atheros
|
||||||
|
firmware-brcm80211
|
||||||
|
firmware-misc-nonfree
|
||||||
|
wireless-regdb
|
||||||
|
wpasupplicant
|
||||||
|
iw
|
||||||
|
rfkill
|
||||||
|
network-manager
|
||||||
|
openssh-server
|
||||||
|
sudo
|
||||||
|
locales
|
||||||
|
curl
|
||||||
|
ca-certificates
|
||||||
|
parted
|
||||||
|
|
||||||
|
# Deployment-Mechanik (scripts/lib/image_deploy.sh, backends/mint-image/backend.sh) -
|
||||||
|
# vorinstalliert statt Laufzeit-apt-get, damit 00_preflight.sh keine Netzwerk-vor-Preflight-
|
||||||
|
# Annahme mehr braucht
|
||||||
|
jq
|
||||||
|
gettext-base
|
||||||
|
dosfstools
|
||||||
|
e2fsprogs
|
||||||
|
zstd
|
||||||
|
btrfs-progs
|
||||||
7
boot-medium/config/source
Normal file
7
boot-medium/config/source
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# config/source - options for live-build(7), source stage
|
||||||
|
|
||||||
|
# Set source option
|
||||||
|
LB_SOURCE="false"
|
||||||
|
|
||||||
|
# Set image type
|
||||||
|
LB_SOURCE_IMAGES="tar"
|
||||||
@ -1,44 +0,0 @@
|
|||||||
loadfont unicode
|
|
||||||
|
|
||||||
set color_normal=white/black
|
|
||||||
set color_highlight=black/light-gray
|
|
||||||
|
|
||||||
set timeout=30
|
|
||||||
|
|
||||||
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit" --class linuxmint {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "Start Linux Mint 22.3 Cinnamon 64-bit (compatibility mode)" {
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} noapic noacpi nosplash irqpoll nomodeset --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "OEM install (for manufacturers)" {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz oem-config/enable=true only-ubiquity boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
menuentry "Tuxflotte Auto-Install (Linux Mint 22.3 Cinnamon)" --class linuxmint {
|
|
||||||
set gfxpayload=keep
|
|
||||||
linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt priority=critical debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
|
|
||||||
initrd /casper/initrd.lz
|
|
||||||
}
|
|
||||||
grub_platform
|
|
||||||
if [ "$grub_platform" = "efi" ]; then
|
|
||||||
menuentry 'Von lokaler Festplatte booten (Standard)' {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
set default="Von lokaler Festplatte booten (Standard)"
|
|
||||||
menuentry 'UEFI Firmware Settings' {
|
|
||||||
fwsetup
|
|
||||||
}
|
|
||||||
menuentry 'Memory test' {
|
|
||||||
linux /boot/memtest.efi
|
|
||||||
}
|
|
||||||
else
|
|
||||||
menuentry 'Von lokaler Festplatte booten (Standard)' {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
set default="Von lokaler Festplatte booten (Standard)"
|
|
||||||
fi
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
timeout 100
|
|
||||||
|
|
||||||
menu background splash.png
|
|
||||||
menu title Welcome to Linux Mint 22.3 64-bit
|
|
||||||
|
|
||||||
menu color screen 37;40 #80ffffff #00000000 std
|
|
||||||
MENU COLOR border 30;44 #40ffffff #a0000000 std
|
|
||||||
MENU COLOR title 1;36;44 #ffffffff #a0000000 std
|
|
||||||
MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all
|
|
||||||
MENU COLOR unsel 37;44 #50ffffff #a0000000 std
|
|
||||||
MENU COLOR help 37;40 #c0ffffff #a0000000 std
|
|
||||||
MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std
|
|
||||||
MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std
|
|
||||||
MENU COLOR msg07 37;40 #90ffffff #a0000000 std
|
|
||||||
MENU COLOR tabmsg 31;40 #ffDEDEDE #00000000 std
|
|
||||||
MENU WIDTH 78
|
|
||||||
MENU MARGIN 15
|
|
||||||
MENU ROWS 6
|
|
||||||
MENU VSHIFT 10
|
|
||||||
MENU TABMSGROW 12
|
|
||||||
MENU CMDLINEROW 12
|
|
||||||
MENU HELPMSGROW 16
|
|
||||||
MENU HELPMSGENDROW 29
|
|
||||||
|
|
||||||
label tuxflotte
|
|
||||||
menu label Tuxflotte Auto-Install
|
|
||||||
kernel /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt priority=critical debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash --
|
|
||||||
|
|
||||||
label live
|
|
||||||
menu label Start Linux Mint
|
|
||||||
kernel /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
|
|
||||||
|
|
||||||
label compat
|
|
||||||
menu label Start Linux Mint in compatibility mode
|
|
||||||
linux /casper/vmlinuz
|
|
||||||
append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint noapic noacpi nosplash irqpoll nomodeset --
|
|
||||||
|
|
||||||
label oem
|
|
||||||
menu label OEM install (for manufacturers)
|
|
||||||
linux /casper/vmlinuz
|
|
||||||
append oem-config/enable=true only-ubiquity boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash --
|
|
||||||
|
|
||||||
label hdt
|
|
||||||
menu label Hardware Detection
|
|
||||||
kernel hdt.c32
|
|
||||||
|
|
||||||
label local
|
|
||||||
menu label Boot from local drive
|
|
||||||
menu default
|
|
||||||
COM32 chain.c32
|
|
||||||
APPEND hd0
|
|
||||||
|
|
||||||
label memtest
|
|
||||||
menu label Memory test
|
|
||||||
linux /boot/memtest.bin
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
#! /bin/sh
|
|
||||||
|
|
||||||
PREREQ=""
|
|
||||||
|
|
||||||
prereqs()
|
|
||||||
{
|
|
||||||
echo "$PREREQ"
|
|
||||||
}
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
# get pre-requisites
|
|
||||||
prereqs)
|
|
||||||
prereqs
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Ueberschreibt das originale 99casperboot (macht nur `touch
|
|
||||||
# /run/.casper-boot`) statt einen zusaetzlichen, neuen Hook-Skriptnamen
|
|
||||||
# einzufuehren: initramfs-tools generiert beim ISO-Bau eine ORDER-Datei
|
|
||||||
# (scripts/casper-bottom/ORDER) mit der fest verdrahteten Aufruf-Reihenfolge
|
|
||||||
# aller casper-bottom-Skripte - real durch Extraktion des echten initrd.lz
|
|
||||||
# mit unmkinitramfs bestaetigt. Ein per Initrd-Cpio-Layer NEU hinzugefuegtes
|
|
||||||
# Skript taucht in dieser ORDER-Datei nicht auf und wird nie aufgerufen,
|
|
||||||
# selbst wenn die Datei im initramfs physisch vorhanden ist (erste Version
|
|
||||||
# dieses Fixes ist daran gescheitert). Das Ueberschreiben eines bereits in
|
|
||||||
# ORDER referenzierten Pfads funktioniert dagegen zuverlaessig (spaetere
|
|
||||||
# cpio-Schichten ueberschreiben Dateiinhalte am selben Pfad, siehe
|
|
||||||
# scripts/lib/initrd.sh - derselbe Mechanismus, der schon fuer das
|
|
||||||
# Kexec-Preseed in Phase 1 verifiziert wurde). 99casperboot ist laut ORDER
|
|
||||||
# das letzte casper-bottom-Skript ueberhaupt - garantiert nach 05mountpoints,
|
|
||||||
# das /cdrom nach /root/cdrom umhaengt und damit die Voraussetzung fuer den
|
|
||||||
# folgenden Kopiervorgang schafft.
|
|
||||||
if [ -d /root/cdrom/updates ]; then
|
|
||||||
cp -a /root/cdrom/updates/. /root/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ubiquity stuerzt im automatic-ubiquity-GTK-Modus real reproduzierbar ab
|
|
||||||
# (TypeError: Argument 1 does not allow None as a value), sobald
|
|
||||||
# enable_download_updates(False) den Netzwerkstatus als "nicht verbunden"
|
|
||||||
# meldet: ubi-prepare.py uebergibt das Ergebnis von
|
|
||||||
# self.controller.get_string('ubiquity/text/label_download_updates_na')
|
|
||||||
# ungeprueft an GtkLabel.set_label() - fehlt fuer diese Vorlage/Sprache ein
|
|
||||||
# Uebersetzungsstring, liefert get_string() None statt eines leeren Strings,
|
|
||||||
# und GTK akzeptiert kein None als Label-Text (real ueber QEMU-Testlauf
|
|
||||||
# gefunden, siehe ADR-0023-Nachtrag). Gezielter Sed-Patch statt vollstaendigem
|
|
||||||
# Datei-Ersatz, um nicht die komplette Drittanbieter-Datei mitpflegen zu
|
|
||||||
# muessen - betrifft nur die eine Zeile, "or ''" faengt jeden None-Rueckgabewert
|
|
||||||
# ab, unabhaengig von der genauen Ursache der fehlenden Uebersetzung.
|
|
||||||
ubi_prepare="/root/usr/lib/ubiquity/plugins/ubi-prepare.py"
|
|
||||||
if [ -f "$ubi_prepare" ]; then
|
|
||||||
sed -i \
|
|
||||||
"s/self\.controller\.get_string(template))/self.controller.get_string(template) or '')/" \
|
|
||||||
"$ubi_prepare"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# AT-SPI-Bruecke fuer den Autoklicker (atspi_click.py) muss VOR dem
|
|
||||||
# GTK-Start von Ubiquity aktiv sein - siehe live-updates/usr/share/ubiquity/
|
|
||||||
# tuxflotte-atspi-env.sh (dorthin per casper-bottom-Kopie oben bereits nach
|
|
||||||
# /root/usr/share/ubiquity/ ausgeliefert). Einzeilige Einbindung direkt nach
|
|
||||||
# dem fruehen Existenz-Check von ubiquity-dm - alles Weitere, inkl.
|
|
||||||
# Paketinstallation, uebernimmt die eingebundene Datei selbst.
|
|
||||||
start_ubiquity_dm="/root/usr/share/ubiquity/start-ubiquity-dm"
|
|
||||||
if [ -f "$start_ubiquity_dm" ]; then
|
|
||||||
sed -i \
|
|
||||||
'/^test -x \/usr\/bin\/ubiquity-dm || exit 0$/a\
|
|
||||||
. /usr/share/ubiquity/tuxflotte-atspi-env.sh' \
|
|
||||||
"$start_ubiquity_dm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
touch /run/.casper-boot
|
|
||||||
@ -1 +0,0 @@
|
|||||||
mint ALL=(ALL) NOPASSWD: ALL
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../tuxflotte-autoclicker.service
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Tuxflotte Autoklicker fuer Ubiquitys GTK-Installationsassistenten
|
|
||||||
# Nur bei automatisierter Installation relevant (siehe autoclicker.sh -
|
|
||||||
# prueft selbst auf das Boot-Keyword "tuxflotte-autoclick" und beendet sich
|
|
||||||
# sofort, wenn es fehlt). Bewusst kein "After=graphical.target" o.ae., da
|
|
||||||
# ubiquity-dm keine normale Display-Manager-Sitzung startet, deren
|
|
||||||
# Bereitschaft systemd auf ueblichem Weg erkennen wuerde - das Skript selbst
|
|
||||||
# wartet auf den X-Socket.
|
|
||||||
After=multi-user.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/opt/tuxflotte/scripts/autoclicker.sh
|
|
||||||
Restart=no
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=SELinux Troubleshooter Notifier
|
|
||||||
Hidden=true
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=Tuxflotte Provisioning
|
|
||||||
Exec=/opt/tuxflotte/kiosk/start-kiosk.sh
|
|
||||||
X-GNOME-Autostart-enabled=true
|
|
||||||
NoDisplay=true
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Tuxflotte Provisioning</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #3465a4;
|
|
||||||
font-family: sans-serif;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
max-width: 60%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img src="logo.svg" alt="Tuxflotte">
|
|
||||||
<p>Kiosk-Oberfläche in Entwicklung.</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 77 KiB |
@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# German keyboard layout for the graphical session (vconsole.keymap=de on the
|
|
||||||
# kernel cmdline only covers the text console, not X11/Wayland input). Cinnamon
|
|
||||||
# ships its own parallel org.cinnamon.desktop.input-sources schema, separate
|
|
||||||
# from GNOME's org.gnome.desktop.input-sources — writing org.gnome alone had
|
|
||||||
# no effect (confirmed on real hardware), so both are set here.
|
|
||||||
dconf write /org/cinnamon/desktop/input-sources/sources "[('xkb', 'de')]"
|
|
||||||
dconf write /org/gnome/desktop/input-sources/sources "[('xkb', 'de')]"
|
|
||||||
|
|
||||||
# Suppresses Epiphany's "set as default browser?" first-run prompt (schema
|
|
||||||
# default is true, see /usr/share/glib-2.0/schemas/org.gnome.epiphany.gschema.xml).
|
|
||||||
dconf write /org/gnome/epiphany/ask-for-default false
|
|
||||||
|
|
||||||
# Kiosk cleanup: no desktop icons (Nemo). The panel itself is kept (removing
|
|
||||||
# the last panel via panels-enabled triggers Cinnamon's own "you don't have
|
|
||||||
# any panels, open panel settings?" confirmation dialog — hardcoded in
|
|
||||||
# js/ui/panel.js, no way to suppress it, so panels-enabled is deliberately
|
|
||||||
# left untouched). Instead, only the two useful applets are kept: a window
|
|
||||||
# list (to restore Epiphany if a user minimizes it) and power (shutdown/
|
|
||||||
# reboot) — everything else (menu, systray, clock, workspace switcher etc.)
|
|
||||||
# is removed. Deliberately the classic window-list@cinnamon.org, not
|
|
||||||
# grouped-window-list@cinnamon.org: the grouped variant ships default
|
|
||||||
# "pinned apps" launchers (firefox.desktop, org.gnome.Terminal.desktop,
|
|
||||||
# nemo.desktop — confirmed via its settings-schema.json) that showed up in
|
|
||||||
# the panel even with unrelated applets removed. Plain window-list has no
|
|
||||||
# pinned-apps concept at all, only ever shows actually open windows.
|
|
||||||
dconf write /org/nemo/desktop/show-desktop-icons false
|
|
||||||
dconf write /org/cinnamon/enabled-applets "['panel1:left:0:window-list@cinnamon.org:1', 'panel1:right:0:power@cinnamon.org:2']"
|
|
||||||
|
|
||||||
mkdir -p "${HOME}/.local/share/tuxflotte-kiosk"
|
|
||||||
|
|
||||||
# Auto-Modus (personalisierte Kunden-ISO, siehe scripts/build_customer_iso.sh)
|
|
||||||
# startet den Installer sofort unbeaufsichtigt statt der Kiosk-Startseite -
|
|
||||||
# config/installer.conf wird nur bei personalisierten Abbildern mitgeliefert,
|
|
||||||
# das Standard-/Testabbild bleibt unverändert interaktiv.
|
|
||||||
if grep -qE '^\s*(export\s+)?TUXFLOTTE_AUTO_MODE=true\s*$' /opt/tuxflotte/config/installer.conf 2>/dev/null; then
|
|
||||||
exec sudo /opt/tuxflotte/scripts/installer.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec epiphany --profile="${HOME}/.local/share/tuxflotte-kiosk" file:///opt/tuxflotte/kiosk/index.html
|
|
||||||
@ -1,308 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Klickt den primaeren "Weiter"-Button der aktuell sichtbaren Ubiquity-Seite
|
|
||||||
per AT-SPI (Linux-Barrierefreiheits-API) - wird von autoclicker.sh wiederholt
|
|
||||||
aufgerufen (ein Prozessstart pro Klickversuch, absichtlich einfach statt
|
|
||||||
dauerhaft im Hintergrund am Accessibility-Bus zu lauschen).
|
|
||||||
|
|
||||||
Warum AT-SPI statt Bildschirmkoordinaten: der urspruengliche xdotool-Ansatz
|
|
||||||
scheiterte daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher
|
|
||||||
Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - eine "groesstes
|
|
||||||
sichtbares Fenster"-Heuristik traf wiederholt das falsche Fenster (siehe
|
|
||||||
ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber seine
|
|
||||||
Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung
|
|
||||||
selbst - unabhaengig von Fenstergeometrie/-dekoration.
|
|
||||||
|
|
||||||
WICHTIG (real beim Testen entdeckt, 29.08.2026): trotz fest gesetztem
|
|
||||||
"debian-installer/language=de" sind NICHT alle Assistentenseiten auf
|
|
||||||
Deutsch beschriftet - die Mint-eigene "Multimedia-Codecs"-Seite zeigt reale
|
|
||||||
englische Buttons ("Quit"/"Back"/"Continue"), vermutlich dieselbe Ursache
|
|
||||||
wie der bereits gepatchte ubi-prepare.py-None-Bug (fehlender/unvollstaendiger
|
|
||||||
.mo-Uebersetzungskatalog fuer dieses Plugin auf dem Live-Medium). Eine reine
|
|
||||||
Werteliste bekannter deutscher Beschriftungen (fruehere Version dieses
|
|
||||||
Skripts) haette hier fuer immer haengen bleiben. Die Logik wurde deshalb auf
|
|
||||||
eine sprachunabhaengige Ausschluss-Strategie umgestellt: bekannte
|
|
||||||
rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden aus der
|
|
||||||
Kandidatenliste entfernt, der bzw. die verbleibenden Kandidaten werden
|
|
||||||
geklickt - siehe select_target() fuer die genaue Regel samt
|
|
||||||
Sicherheitsverhalten bei Mehrdeutigkeit.
|
|
||||||
|
|
||||||
Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed
|
|
||||||
grundsaetzlich auf de_DE gesetzt (siehe backend.sh), s.o. aber nicht
|
|
||||||
verlaesslich fuer jede einzelne Seite.
|
|
||||||
|
|
||||||
NACHTRAG (real beim Testen entdeckt, 29.08.2026): auf der manuellen
|
|
||||||
Partitionierungsseite ("Etwas Anderes") lief der Klick auf "Jetzt
|
|
||||||
installieren" nachweislich zuverlaessig (AT-SPI meldete jedes Mal Erfolg),
|
|
||||||
aber die Seite kam nie voran - stattdessen loeste jeder Klick erneut die
|
|
||||||
Debconf-Frage "ubiquity/partman-rebuild-cache" aus
|
|
||||||
(/lib/partman/update.d/99signal_ubiquity), was ubi-partman.py zurueck auf
|
|
||||||
dieselbe Seite wirft (siehe ADR-0023-Nachtrag). Ein gezielter Fix am
|
|
||||||
Partitionsrezept (kein zusaetzliches $bootable{ } auf der Root-Partition
|
|
||||||
unter UEFI) aenderte NICHTS - das deutet auf einen zeitlichen statt
|
|
||||||
strukturellen Fehler hin: die Seite zeigt waehrend dieser Phase ein
|
|
||||||
Status-Panel "Konfiguration der Installation wird ueberprueft ..." - klickt
|
|
||||||
der Autoklicker WAEHREND dieser Hintergrundpruefung noch laeuft, koennte
|
|
||||||
er sie unterbrechen/neu anstossen, statt ihr Zeit zum Abschliessen zu
|
|
||||||
geben. Dieses Skript wartet deshalb jetzt aktiv, solange dieser Text
|
|
||||||
sichtbar ist (siehe VALIDATION_TEXT_MARKERS/find_showing_text()), und
|
|
||||||
klickt erst, wenn er verschwunden ist.
|
|
||||||
|
|
||||||
Exit-Codes: 0 = ein Button wurde gefunden und geklickt,
|
|
||||||
1 = kein eindeutiger Button gefunden (z.B. Seite im Ladezustand, oder echte
|
|
||||||
Mehrdeutigkeit - dann bewusst NICHT geklickt, siehe select_target()),
|
|
||||||
2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv),
|
|
||||||
3 = Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar - bewusst
|
|
||||||
nicht geklickt, siehe Nachtrag oben.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pyatspi
|
|
||||||
except ImportError:
|
|
||||||
print("pyatspi nicht verfuegbar", file=sys.stderr)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
# Zweisprachig, da nicht jede Seite verlaesslich deutsch beschriftet ist
|
|
||||||
# (siehe Modul-Docstring). Diese Namen werden aus der Kandidatenliste
|
|
||||||
# AUSGESCHLOSSEN, nie geklickt - unabhaengig davon, ob sie der einzige
|
|
||||||
# sichtbare Button waeren.
|
|
||||||
EXCLUDE_LABELS = {
|
|
||||||
"zurück",
|
|
||||||
"zurueck",
|
|
||||||
"back",
|
|
||||||
"abbrechen",
|
|
||||||
"abbruch",
|
|
||||||
"cancel",
|
|
||||||
"beenden",
|
|
||||||
"quit",
|
|
||||||
"exit",
|
|
||||||
"verwerfen",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bevorzugt geklickt, falls mehrere Kandidaten nach dem Ausschluss uebrig
|
|
||||||
# bleiben (siehe select_target()). Kein Ausschlusskriterium fuer
|
|
||||||
# Kandidaten, die HIER nicht drin stehen - die duerfen trotzdem als
|
|
||||||
# alleiniger Kandidat geklickt werden (Sole-Survivor-Regel), damit auch
|
|
||||||
# unbekannte/nicht uebersetzte Beschriftungen wie das reale "Continue"
|
|
||||||
# oben nicht zum Stillstand fuehren.
|
|
||||||
PREFER_LABELS = {
|
|
||||||
"weiter",
|
|
||||||
"continue",
|
|
||||||
"next",
|
|
||||||
"jetzt installieren",
|
|
||||||
"install now",
|
|
||||||
"fortfahren",
|
|
||||||
"proceed",
|
|
||||||
"installieren",
|
|
||||||
"install",
|
|
||||||
"neu starten",
|
|
||||||
"restart now",
|
|
||||||
"schließen",
|
|
||||||
"schliessen",
|
|
||||||
"close",
|
|
||||||
"finish",
|
|
||||||
"fertigstellen",
|
|
||||||
"done",
|
|
||||||
}
|
|
||||||
|
|
||||||
MAX_DEPTH = 40
|
|
||||||
|
|
||||||
# Zweisprachig vorsichtshalber (siehe Multimedia-Codecs-Erfahrung oben),
|
|
||||||
# auch wenn dieser konkrete Text bisher immer deutsch beobachtet wurde.
|
|
||||||
VALIDATION_TEXT_MARKERS = (
|
|
||||||
"wird überprüft",
|
|
||||||
"wird ueberprueft",
|
|
||||||
"being checked",
|
|
||||||
"checking the install",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _node_text(node):
|
|
||||||
"""Sammelt allen aus einem Accessible auslesbaren Text: .name,
|
|
||||||
.description UND (falls unterstuetzt) das separate Text-Interface.
|
|
||||||
WICHTIG (real beim Testen entdeckt, 29.08.2026): ein einfaches GtkLabel
|
|
||||||
exponiert seinen sichtbaren Text ueblicherweise NICHT ueber .name/
|
|
||||||
.description (die bleiben dort oft leer) sondern ausschliesslich ueber
|
|
||||||
das AT-SPI-Text-Interface (queryText().getText(0, -1)) - ohne diesen
|
|
||||||
dritten Zugriffsweg fand find_showing_text() den sichtbar angezeigten
|
|
||||||
Status-Text nie, obwohl er laut Screenshot die ganze Zeit auf dem Schirm
|
|
||||||
stand."""
|
|
||||||
parts = []
|
|
||||||
try:
|
|
||||||
if node.name:
|
|
||||||
parts.append(node.name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
if node.description:
|
|
||||||
parts.append(node.description)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
text_iface = node.queryText()
|
|
||||||
content = text_iface.getText(0, -1)
|
|
||||||
if content:
|
|
||||||
parts.append(content)
|
|
||||||
except NotImplementedError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return " ".join(parts).strip().lower()
|
|
||||||
|
|
||||||
|
|
||||||
def find_showing_text(node, needle, depth=0):
|
|
||||||
"""Sucht ein sichtbares Accessible, dessen Text (siehe _node_text()) den
|
|
||||||
Suchtext enthaelt - unabhaengig von seiner Rolle (Label/Expander/...),
|
|
||||||
da Ubiquitys Status-Panel-Text nicht zuverlaessig unter derselben Rolle
|
|
||||||
exponiert sein muss. Bei Unsicherheit ueber den Sichtbarkeitsstatus
|
|
||||||
bewusst konservativ (lieber einen Zyklus zu viel warten als einen
|
|
||||||
Klick mitten in die Pruefung setzen)."""
|
|
||||||
if node is None or depth > MAX_DEPTH:
|
|
||||||
return False
|
|
||||||
if needle in _node_text(node):
|
|
||||||
try:
|
|
||||||
state = node.getState()
|
|
||||||
return state.contains(pyatspi.STATE_SHOWING)
|
|
||||||
except Exception:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
count = node.childCount
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
for i in range(count):
|
|
||||||
try:
|
|
||||||
child = node.getChildAtIndex(i)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if find_showing_text(child, needle, depth + 1):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def collect_buttons(node, depth=0, acc=None):
|
|
||||||
"""Alle sichtbaren+aktivierten Push-Buttons im Baum einsammeln (Rolle,
|
|
||||||
Zustand). Absichtlich ALLE Kandidaten sammeln statt beim ersten Treffer
|
|
||||||
abzubrechen - select_target() braucht die vollstaendige Liste, um
|
|
||||||
Mehrdeutigkeit erkennen zu koennen."""
|
|
||||||
if acc is None:
|
|
||||||
acc = []
|
|
||||||
if node is None or depth > MAX_DEPTH:
|
|
||||||
return acc
|
|
||||||
try:
|
|
||||||
role = node.getRole()
|
|
||||||
except Exception:
|
|
||||||
return acc
|
|
||||||
|
|
||||||
if role == pyatspi.ROLE_PUSH_BUTTON:
|
|
||||||
try:
|
|
||||||
state = node.getState()
|
|
||||||
if state.contains(pyatspi.STATE_SENSITIVE) and state.contains(
|
|
||||||
pyatspi.STATE_SHOWING
|
|
||||||
):
|
|
||||||
acc.append(node)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return acc
|
|
||||||
|
|
||||||
try:
|
|
||||||
count = node.childCount
|
|
||||||
except Exception:
|
|
||||||
return acc
|
|
||||||
for i in range(count):
|
|
||||||
try:
|
|
||||||
child = node.getChildAtIndex(i)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
collect_buttons(child, depth + 1, acc)
|
|
||||||
return acc
|
|
||||||
|
|
||||||
|
|
||||||
def select_target(buttons):
|
|
||||||
"""Waehlt aus allen sichtbaren+aktivierten Buttons genau einen aus, der
|
|
||||||
geklickt werden darf - oder None, wenn das nicht sicher moeglich ist.
|
|
||||||
|
|
||||||
Regel (bewusst sicherheitsorientiert, siehe Modul-Docstring):
|
|
||||||
1. Bekannte rueckwaerts-/abbrechende Beschriftungen (EXCLUDE_LABELS)
|
|
||||||
werden nie geklickt, unabhaengig vom Rest.
|
|
||||||
2. Von den verbleibenden Kandidaten wird bevorzugt, wer eine bekannte
|
|
||||||
vorwaerts-/abschliessende Beschriftung traegt (PREFER_LABELS).
|
|
||||||
3. Bleibt nach 1./2. KEIN bekannter Treffer, aber genau EIN Kandidat
|
|
||||||
uebrig, wird dieser geklickt (Sole-Survivor-Regel - deckt unbekannte/
|
|
||||||
nicht uebersetzte Beschriftungen wie das real beobachtete "Continue"
|
|
||||||
ab, siehe Modul-Docstring).
|
|
||||||
4. Bleiben mehrere unbekannte Kandidaten gleichzeitig uebrig, wird NICHT
|
|
||||||
geklickt (echte Mehrdeutigkeit - ein Fehlklick waere hier riskanter
|
|
||||||
als ein uebersprungener Zyklus, die Schleife in autoclicker.sh
|
|
||||||
versucht es 2 Sekunden spaeter erneut)."""
|
|
||||||
candidates = []
|
|
||||||
for btn in buttons:
|
|
||||||
name = (btn.name or "").strip().lower()
|
|
||||||
if name in EXCLUDE_LABELS:
|
|
||||||
continue
|
|
||||||
candidates.append(btn)
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
return None
|
|
||||||
|
|
||||||
preferred = [b for b in candidates if (b.name or "").strip().lower() in PREFER_LABELS]
|
|
||||||
if preferred:
|
|
||||||
return preferred[0]
|
|
||||||
|
|
||||||
if len(candidates) == 1:
|
|
||||||
return candidates[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
desktop = pyatspi.Registry.getDesktop(0)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
apps = []
|
|
||||||
for i in range(desktop.childCount):
|
|
||||||
try:
|
|
||||||
app = desktop.getChildAtIndex(i)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if app is not None:
|
|
||||||
apps.append(app)
|
|
||||||
|
|
||||||
for marker in VALIDATION_TEXT_MARKERS:
|
|
||||||
for app in apps:
|
|
||||||
if find_showing_text(app, marker):
|
|
||||||
print(
|
|
||||||
f"Hintergrundpruefung noch sichtbar ('{marker}'), warte statt zu klicken.",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 3
|
|
||||||
|
|
||||||
all_buttons = []
|
|
||||||
for app in apps:
|
|
||||||
collect_buttons(app, 0, all_buttons)
|
|
||||||
|
|
||||||
target = select_target(all_buttons)
|
|
||||||
if target is None:
|
|
||||||
names = [repr((b.name or "").strip()) for b in all_buttons]
|
|
||||||
print(
|
|
||||||
"kein eindeutiger Button gefunden, sichtbare Buttons: "
|
|
||||||
+ (", ".join(names) if names else "(keine)"),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
action = target.queryAction()
|
|
||||||
action.doAction(0)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"Klick auf '{target.name}' fehlgeschlagen: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(f"geklickt: {target.name}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
### Klickt automatisiert durch Ubiquitys GTK-Installationsassistenten
|
|
||||||
### (automatic-ubiquity) - noetig, weil Ubiquitys eigenes "noninteractive"-
|
|
||||||
### Frontend die gefuehrte Partitionierung nicht zuverlaessig automatisieren
|
|
||||||
### kann (endlose choose_partition-Schleife in ubi-partman.py, real
|
|
||||||
### bestaetigt, siehe ADR-0023-Nachtrag). Der GTK-Assistent fuellt jede Seite
|
|
||||||
### bereits vollstaendig aus dem Preseed vor (siehe preseed.tpl) - es fehlt
|
|
||||||
### nur der "Weiter"/"Installieren"-Klick pro Seite.
|
|
||||||
###
|
|
||||||
### Klickt ueber AT-SPI (Barrierefreiheits-API), nicht mehr ueber
|
|
||||||
### Bildschirmkoordinaten/Enter-Taste (siehe atspi_click.py fuer die
|
|
||||||
### eigentliche Klicklogik). Der fruehere xdotool-Ansatz scheiterte real
|
|
||||||
### daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher
|
|
||||||
### Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - weder eine
|
|
||||||
### "groesstes sichtbares Fenster"-Heuristik noch die Enter-Taste (aktiviert
|
|
||||||
### auf der Partitionierungs-Uebersichtsseite einen Ausklapp-Pfeil statt des
|
|
||||||
### "Jetzt installieren"-Buttons) waren zuverlaessig genug (siehe
|
|
||||||
### ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber
|
|
||||||
### seine Rolle+seinen Namen im Accessibility-Baum der Anwendung selbst -
|
|
||||||
### unabhaengig von Fenstergeometrie/-dekoration.
|
|
||||||
###
|
|
||||||
### Die AT-SPI-Bruecke selbst (GTK_MODULES=gail:atk-bridge, Session-/A11y-
|
|
||||||
### Bus) wird NICHT hier, sondern bereits VOR dem GTK-Start von Ubiquity in
|
|
||||||
### tuxflotte-atspi-env.sh aktiviert (eingebunden von start-ubiquity-dm,
|
|
||||||
### siehe Sed-Patch in initrd-hooks/casper-bottom/99casperboot) - eine erst
|
|
||||||
### nachtraeglich aktivierte Bruecke wird von einer bereits laufenden
|
|
||||||
### GTK-Anwendung nicht mehr nachgeladen.
|
|
||||||
###
|
|
||||||
### Wird per systemd-Service (siehe ...service im selben Ausliefer-Layer) bei
|
|
||||||
### JEDEM Boot gestartet, bricht aber sofort ab, wenn das eigene Boot-Keyword
|
|
||||||
### "tuxflotte-autoclick" nicht in /proc/cmdline steht - siehe backend_launch()
|
|
||||||
### in backends/mint/backend.sh, wo dieses Keyword gesetzt wird. Auf einem
|
|
||||||
### normalen (nicht automatisierten) Boot also wirkungslos.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
LOG=/var/log/tuxflotte-autoclicker.log
|
|
||||||
log() {
|
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >>"${LOG}"
|
|
||||||
}
|
|
||||||
|
|
||||||
grep -qw tuxflotte-autoclick /proc/cmdline || exit 0
|
|
||||||
|
|
||||||
log "tuxflotte-autoclick erkannt, starte Autoklicker."
|
|
||||||
export DISPLAY=:0
|
|
||||||
|
|
||||||
is_ubiquity_running() {
|
|
||||||
pgrep -f '/usr/bin/ubiquity' >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ubiquity-dm startet Ubiquity frueh im Boot zu einem nicht exakt
|
|
||||||
# vorhersagbaren Zeitpunkt - auf den Prozess warten statt auf eine feste
|
|
||||||
# Wartezeit zu vertrauen. AT-SPI braucht anders als der fruehere
|
|
||||||
# xdotool-Ansatz keinen eigenen X-Socket-Check - der Zugriff laeuft ueber
|
|
||||||
# den Accessibility-Bus, nicht direkt ueber X11.
|
|
||||||
ubiquity_ready=0
|
|
||||||
for _ in $(seq 1 120); do
|
|
||||||
if is_ubiquity_running; then
|
|
||||||
ubiquity_ready=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
if [ "${ubiquity_ready}" -ne 1 ]; then
|
|
||||||
log "ubiquity-Prozess nach 120s nicht erschienen, breche ab."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "ubiquity-Prozess erkannt."
|
|
||||||
|
|
||||||
if ! python3 -c 'import pyatspi' >/dev/null 2>&1; then
|
|
||||||
log "pyatspi fehlt auf dem Live-Medium, installiere nach."
|
|
||||||
if ! apt-get install -y python3-pyatspi >>"${LOG}" 2>&1; then
|
|
||||||
log "pyatspi-Installation fehlgeschlagen, breche ab."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
ATSPI_CLICK=/opt/tuxflotte/scripts/atspi_click.py
|
|
||||||
|
|
||||||
log "Starte Klick-Schleife."
|
|
||||||
# Harte Obergrenze als Sicherheitsnetz (60 Minuten) - falls die Installation
|
|
||||||
# haengen bleibt, soll dieser Dienst nicht unbegrenzt weiterlaufen.
|
|
||||||
end=$((SECONDS + 3600))
|
|
||||||
clicks=0
|
|
||||||
misses=0
|
|
||||||
while [ "${SECONDS}" -lt "${end}" ]; do
|
|
||||||
if ! is_ubiquity_running; then
|
|
||||||
log "ubiquity-Prozess nicht mehr aktiv (vermutlich fertig oder abgestuerzt), beende Schleife nach ${clicks} Klick(s)."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
out="$(python3 "${ATSPI_CLICK}" 2>&1)"
|
|
||||||
rc=$?
|
|
||||||
if [ "${rc}" -eq 0 ]; then
|
|
||||||
clicks=$((clicks + 1))
|
|
||||||
misses=0
|
|
||||||
log "Klick ${clicks}: ${out}"
|
|
||||||
elif [ "${rc}" -eq 2 ]; then
|
|
||||||
log "AT-SPI-Bus nicht erreichbar, breche ab: ${out}"
|
|
||||||
break
|
|
||||||
elif [ "${rc}" -eq 3 ]; then
|
|
||||||
# Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar (siehe
|
|
||||||
# atspi_click.py-Nachtrag) - bewusst NICHT als Fehlversuch zaehlen,
|
|
||||||
# nur abwarten. Real beim Testen entdeckt: ein Klick auf "Jetzt
|
|
||||||
# installieren" WAEHREND dieser Pruefung fuehrte wiederholt in die
|
|
||||||
# partman-rebuild-cache-Schleife statt zum eigentlichen Commit.
|
|
||||||
log "wartet auf Hintergrundpruefung: ${out}"
|
|
||||||
else
|
|
||||||
misses=$((misses + 1))
|
|
||||||
log "kein Treffer (${misses}): ${out}"
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
log "Autoklicker beendet (${clicks} Klick(s) insgesamt)."
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
### Wird per Ein-Zeilen-Sed-Patch (siehe initrd-hooks/casper-bottom/99casperboot)
|
|
||||||
### GANZ AM ANFANG von start-ubiquity-dm eingebunden ("."), also BEVOR
|
|
||||||
### ubiquity-dm den X-Server startet und Ubiquity selbst exec't.
|
|
||||||
###
|
|
||||||
### Zweck: die AT-SPI-Barrierefreiheits-Bruecke (atk-bridge) fuer Ubiquitys
|
|
||||||
### GTK-Prozess aktivieren, damit atspi_click.py (siehe
|
|
||||||
### /opt/tuxflotte/scripts/) Buttons ueber ihre Rolle/ihren Namen im
|
|
||||||
### Accessibility-Baum anklicken kann statt ueber Bildschirmkoordinaten.
|
|
||||||
### Der xdotool-Koordinaten-Ansatz ist an ueberlappenden X11-Fenstern durch
|
|
||||||
### die Metacity-Fensterdekoration gescheitert (mehrere Fenster aehnlicher
|
|
||||||
### Groesse, "groesstes Fenster"-Heuristik traf wiederholt das falsche -
|
|
||||||
### siehe ADR-0023-Nachtrag).
|
|
||||||
###
|
|
||||||
### WICHTIG: das muss VOR dem GTK-Init von Ubiquity passieren - eine erst
|
|
||||||
### nachtraeglich verfuegbare Bruecke wird von einer bereits laufenden
|
|
||||||
### GTK-Anwendung nicht mehr nachgeladen (GTK_MODULES wird nur beim Start
|
|
||||||
### ausgewertet). Deshalb blockierend (kein Hintergrund-"&" fuer die
|
|
||||||
### Paketinstallation) und deshalb im Skript, das ubiquity-dm selbst
|
|
||||||
### aufruft, statt im spaeter laufenden Autoklicker-Service.
|
|
||||||
###
|
|
||||||
### Absichtlich EIN gemeinsamer apt-get-Aufruf fuer alle drei Pakete (statt
|
|
||||||
### getrennter Aufrufe je Skript) - der Autoklicker installiert xdotool
|
|
||||||
### zeitlich nah, ein zweiter paralleler apt-get/dpkg-Aufruf haette real
|
|
||||||
### beobachtete Lock-Konflikte riskiert (debconf: "config.dat is locked by
|
|
||||||
### another process").
|
|
||||||
|
|
||||||
for pkg in at-spi2-core dbus-x11 python3-pyatspi; do
|
|
||||||
dpkg -s "$pkg" >/dev/null 2>&1 && continue
|
|
||||||
if [ "${_tuxflotte_atspi_apt_updated:-}" != "1" ]; then
|
|
||||||
apt-get update -qq
|
|
||||||
_tuxflotte_atspi_apt_updated=1
|
|
||||||
fi
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg"
|
|
||||||
done
|
|
||||||
|
|
||||||
export GTK_MODULES="gail:atk-bridge"
|
|
||||||
export NO_AT_BRIDGE=0
|
|
||||||
|
|
||||||
# Eigene Session-Bus-Instanz, falls keine geerbt wurde - die minimale
|
|
||||||
# ubiquity-dm-X-Sitzung startet anders als eine normale Desktop-Sitzung
|
|
||||||
# vermutlich keinen eigenen Session-Bus.
|
|
||||||
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then
|
|
||||||
eval "$(dbus-launch --sh-syntax)"
|
|
||||||
export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v at-spi-bus-launcher >/dev/null 2>&1; then
|
|
||||||
at-spi-bus-launcher --launch-immediately >/var/log/tuxflotte-atspi-bus.log 2>&1 &
|
|
||||||
# Kurz abwarten, bis der a11y-Bus unter org.a11y.Bus registriert ist,
|
|
||||||
# bevor ubiquity-dm/Ubiquity (naechste Zeile in start-ubiquity-dm)
|
|
||||||
# GTK initialisiert.
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
45
scripts/boot-autostart.sh
Executable file
45
scripts/boot-autostart.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Wird von tuxflotte-installer.service beim Boot direkt auf tty1 gestartet -
|
||||||
|
# unabhaengig davon, ob config/installer.conf einen Auto-Modus-
|
||||||
|
# Aktivierungscode mitbringt (personalisierte Kunden-ISO, siehe
|
||||||
|
# scripts/build_customer_iso.sh) oder nicht (generisches Test-/
|
||||||
|
# Entwicklungsabbild): installer.sh fragt in letzterem Fall selbst
|
||||||
|
# interaktiv nach (12_enrollment_auth.sh, 05_network.sh-WLAN-Auswahl), auf
|
||||||
|
# genau dieser Konsole - der Service leitet dafuer TTYPath=/dev/tty1 durch.
|
||||||
|
#
|
||||||
|
# Schreibt zusaetzlich zur journal-Ausgabe (StandardOutput=journal+console
|
||||||
|
# im systemd-Unit) in eine Logdatei - per SSH/Konsole nachtraeglich
|
||||||
|
# einsehbar, ohne journalctl zu brauchen.
|
||||||
|
LOG_FILE="/var/log/tuxflotte-installer.log"
|
||||||
|
BANNER_FILE="/opt/tuxflotte/banner.txt"
|
||||||
|
|
||||||
|
# Begruessungsbanner (Tuxflotte-Schriftzug + Flotten-Grafik, 80 Spalten) -
|
||||||
|
# reine Optik, ins Logfile mitschreiben schadet nicht, aber nicht fatal
|
||||||
|
# wenn die Datei mal fehlt (set -e: "if" statt "&&", sonst wuerde ein
|
||||||
|
# fehlendes Banner das ganze Skript beenden).
|
||||||
|
if [[ -r "${BANNER_FILE}" ]]; then
|
||||||
|
{ cat "${BANNER_FILE}"; echo; } | tee -a "${LOG_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
/opt/tuxflotte/scripts/installer.sh 2>&1 | tee -a "${LOG_FILE}"
|
||||||
|
STATUS="${PIPESTATUS[0]}"
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026): ein ExecStopPost, der beim Dienstende
|
||||||
|
# getty@tty1.service neu startet, ist unzuverlaessig (Race mit dem eigenen
|
||||||
|
# TTY-Teardown dieses Dienstes - der Job wurde angestossen, blieb aber
|
||||||
|
# "inactive/dead"). Stattdessen: diese Konsole faellt nach installer.sh
|
||||||
|
# (egal ob Erfolg, kontrollierter Abbruch an einem Commit-Gate, oder
|
||||||
|
# Fehler) deterministisch in eine interaktive Root-Shell - kein Race,
|
||||||
|
# garantiert nutzbare Konsole fuer Fehlersuche/manuellen Neustart. Im
|
||||||
|
# Erfolgsfall mit tatsaechlichem Deployment loest 40_backend.sh ohnehin
|
||||||
|
# selbst einen Reboot aus, wodurch diese Shell nie sichtbar wird.
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "Tuxflotte-Installer beendet (Exit-Code: ${STATUS})."
|
||||||
|
echo "Diese Konsole steht fuer Fehlersuche/manuellen Neustart zur Verfuegung."
|
||||||
|
echo
|
||||||
|
} | tee -a "${LOG_FILE}"
|
||||||
|
|
||||||
|
exec /bin/bash --login
|
||||||
141
scripts/build.sh
141
scripts/build.sh
@ -1,6 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Nur noch fuer das Fedora-Backend (Kickstart/Anaconda) - das Mint-Backend
|
||||||
|
# ist auf das eigenstaendige, per live-build gebaute Boot-Medium umgezogen
|
||||||
|
# (siehe scripts/build_boot_medium.sh, boot-medium/). Der bisherige
|
||||||
|
# Mint-Zweig hier (Casper-Patchen einer echten Mint-Live-ISO + Ubiquity-
|
||||||
|
# Preseed) ist damit Geschichte - siehe ADR-0025 fuer die Begruendung.
|
||||||
|
|
||||||
SOURCE_ISO="${1:-}"
|
SOURCE_ISO="${1:-}"
|
||||||
BACKEND="${2:-}"
|
BACKEND="${2:-}"
|
||||||
|
|
||||||
@ -10,18 +16,8 @@ BUILD_DIR="$BASE_DIR/build"
|
|||||||
WORK_DIR="$BUILD_DIR/work"
|
WORK_DIR="$BUILD_DIR/work"
|
||||||
OUTPUT_DIR="$BUILD_DIR/output"
|
OUTPUT_DIR="$BUILD_DIR/output"
|
||||||
|
|
||||||
# shellcheck source=lib/initrd.sh
|
|
||||||
source "$REPO_DIR/scripts/lib/initrd.sh"
|
|
||||||
|
|
||||||
# Test-Device fuer den in bake_test_preseed() eingebrannten, nicht
|
|
||||||
# personalisierten Test-Preseed (Mint) - dasselbe "Default Lab"-Testgeraet,
|
|
||||||
# das schon fuer die Fedora-Verifikation genutzt wurde. Echte Personalisierung
|
|
||||||
# pro Kunde ist nicht Teil dieses Build-Skripts (siehe ADR-0011-Kontext).
|
|
||||||
TEST_DEVICE_ID="a0238a0b-d2b5-4516-a6ce-da7170041d11"
|
|
||||||
TEST_HOSTNAME="tuxflotte-mint-test"
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 /path/to/source.iso <fedora|mint>"
|
echo "Usage: $0 /path/to/source.iso fedora"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_input() {
|
check_input() {
|
||||||
@ -35,13 +31,10 @@ check_input() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$BACKEND" in
|
if [[ "$BACKEND" != "fedora" ]]; then
|
||||||
fedora|mint) ;;
|
echo "Error: unbekanntes Backend '$BACKEND' (erwartet: fedora)"
|
||||||
*)
|
|
||||||
echo "Error: unbekanntes Backend '$BACKEND' (erwartet: fedora oder mint)"
|
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
fi
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_dependencies() {
|
check_dependencies() {
|
||||||
@ -51,15 +44,6 @@ check_dependencies() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$BACKEND" == "mint" ]]; then
|
|
||||||
for cmd in jq envsubst base64 cpio; do
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
echo "Error: missing dependency: $cmd"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_dirs() {
|
prepare_dirs() {
|
||||||
@ -87,135 +71,39 @@ patch_grub() {
|
|||||||
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
|
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
|
||||||
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
|
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
|
||||||
# Ueberschreiben mit "Keine Berechtigung" fehl.
|
# Ueberschreiben mit "Keine Berechtigung" fehl.
|
||||||
if [[ "$BACKEND" == "fedora" ]]; then
|
|
||||||
cp -f "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \
|
cp -f "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \
|
||||||
"$WORK_DIR/EFI/BOOT/grub.cfg"
|
"$WORK_DIR/EFI/BOOT/grub.cfg"
|
||||||
|
|
||||||
cp -f "$REPO_DIR/grub/boot-grub2-grub.cfg" \
|
cp -f "$REPO_DIR/grub/boot-grub2-grub.cfg" \
|
||||||
"$WORK_DIR/boot/grub2/grub.cfg"
|
"$WORK_DIR/boot/grub2/grub.cfg"
|
||||||
else
|
|
||||||
# Mint hat, anders als Fedora, keine zweite ESP/FAT-Kopie des GRUB-Menüs,
|
|
||||||
# die separat gepatcht werden müsste (real gegen die vorhandene
|
|
||||||
# tuxflotte-mint-test.iso verifiziert - x86_64-efi/grub.cfg ist nur ein
|
|
||||||
# Loader-Stub, der per `source /boot/grub/grub.cfg` zurückverweist).
|
|
||||||
cp -f "$REPO_DIR/grub/mint-boot-grub.cfg" \
|
|
||||||
"$WORK_DIR/boot/grub/grub.cfg"
|
|
||||||
|
|
||||||
cp -f "$REPO_DIR/grub/mint-isolinux-live.cfg" \
|
|
||||||
"$WORK_DIR/isolinux/live.cfg"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_workdir() {
|
verify_workdir() {
|
||||||
echo "Verifying workdir..."
|
echo "Verifying workdir..."
|
||||||
|
|
||||||
if [[ "$BACKEND" == "fedora" ]]; then
|
|
||||||
grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg"
|
grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg"
|
||||||
grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg"
|
grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg"
|
||||||
else
|
|
||||||
grep -q "Tuxflotte" "$WORK_DIR/boot/grub/grub.cfg"
|
|
||||||
grep -q "Tuxflotte" "$WORK_DIR/isolinux/live.cfg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "GRUB verification passed."
|
echo "GRUB verification passed."
|
||||||
}
|
}
|
||||||
|
|
||||||
bake_test_preseed() {
|
|
||||||
[[ "$BACKEND" == "mint" ]] || return 0
|
|
||||||
|
|
||||||
echo "Rendering test preseed (nicht personalisiert, siehe TEST_DEVICE_ID)..."
|
|
||||||
|
|
||||||
local preseed_dir="$WORK_DIR/preseed"
|
|
||||||
local runtime_blueprint_file="$WORK_DIR/runtime_blueprint.json"
|
|
||||||
local server_response_file="$WORK_DIR/response.json"
|
|
||||||
|
|
||||||
mkdir -p "$preseed_dir"
|
|
||||||
|
|
||||||
cat > "$runtime_blueprint_file" <<EOF
|
|
||||||
{
|
|
||||||
"runtime_blueprint": {
|
|
||||||
"backend_id": "mint",
|
|
||||||
"blueprints": [],
|
|
||||||
"installation_directives": {
|
|
||||||
"disk_encryption": false,
|
|
||||||
"partitioning": {"scheme": "single", "root_filesystem": "ext4"},
|
|
||||||
"secure_boot_required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$server_response_file" <<EOF
|
|
||||||
{
|
|
||||||
"device": {"hostname": "${TEST_HOSTNAME}", "id": "${TEST_DEVICE_ID}"}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# backend.sh direkt wiederverwenden statt die Templating-Logik hier zu
|
|
||||||
# duplizieren - Variablen sind bewusst nicht readonly (siehe backend.sh),
|
|
||||||
# backend_init() wird übersprungen (braucht root für --owner/--group,
|
|
||||||
# hier nicht nötig, mkdir reicht für den Build-Kontext).
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source "$REPO_DIR/backends/mint/backend.sh"
|
|
||||||
|
|
||||||
RUNTIME_BLUEPRINT_FILE="$runtime_blueprint_file"
|
|
||||||
SERVER_RESPONSE_FILE="$server_response_file"
|
|
||||||
RUNTIME_DIR="$preseed_dir"
|
|
||||||
CONFIG_FILE="$preseed_dir/tuxflotte.seed"
|
|
||||||
|
|
||||||
backend_validate
|
|
||||||
backend_generate_config
|
|
||||||
|
|
||||||
echo "Test-Preseed erzeugt: $CONFIG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_updates() {
|
prepare_updates() {
|
||||||
echo "Assembling live-updates payload..."
|
echo "Assembling updates payload..."
|
||||||
|
|
||||||
local updates_dir="$WORK_DIR/updates"
|
local updates_dir="$WORK_DIR/updates"
|
||||||
|
|
||||||
rm -rf "$updates_dir"
|
rm -rf "$updates_dir"
|
||||||
mkdir -p "$updates_dir/opt/tuxflotte"
|
mkdir -p "$updates_dir/opt/tuxflotte"
|
||||||
|
|
||||||
cp -a "$REPO_DIR/live-updates/." "$updates_dir/"
|
|
||||||
cp -a "$REPO_DIR/scripts" "$updates_dir/opt/tuxflotte/"
|
cp -a "$REPO_DIR/scripts" "$updates_dir/opt/tuxflotte/"
|
||||||
cp -a "$REPO_DIR/backends" "$updates_dir/opt/tuxflotte/"
|
cp -a "$REPO_DIR/backends" "$updates_dir/opt/tuxflotte/"
|
||||||
cp -a "$REPO_DIR/config" "$updates_dir/opt/tuxflotte/"
|
cp -a "$REPO_DIR/config" "$updates_dir/opt/tuxflotte/"
|
||||||
|
|
||||||
chmod 0440 "$updates_dir/etc/sudoers.d/90-tuxflotte"
|
|
||||||
}
|
|
||||||
|
|
||||||
patch_initrd() {
|
|
||||||
[[ "$BACKEND" == "mint" ]] || return 0
|
|
||||||
|
|
||||||
echo "Injecting casper-bottom updates-hook into initrd..."
|
|
||||||
|
|
||||||
inject_casper_bottom_hooks "$REPO_DIR" \
|
|
||||||
"$WORK_DIR/casper/initrd.lz" \
|
|
||||||
"$WORK_DIR/casper/initrd-tuxflotte.lz"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create_iso() {
|
create_iso() {
|
||||||
echo "Creating Tuxflotte ISO..."
|
echo "Creating Tuxflotte ISO..."
|
||||||
|
|
||||||
local output_iso
|
local output_iso="$OUTPUT_DIR/tuxflotte-provisioning-0.2.iso"
|
||||||
local -a grub_map_args
|
|
||||||
|
|
||||||
if [[ "$BACKEND" == "fedora" ]]; then
|
|
||||||
output_iso="$OUTPUT_DIR/tuxflotte-provisioning-0.2.iso"
|
|
||||||
grub_map_args=(
|
|
||||||
-map "$REPO_DIR/grub/EFI-BOOT-grub.cfg" /EFI/BOOT/grub.cfg
|
|
||||||
-map "$REPO_DIR/grub/boot-grub2-grub.cfg" /boot/grub2/grub.cfg
|
|
||||||
)
|
|
||||||
else
|
|
||||||
output_iso="$OUTPUT_DIR/tuxflotte-mint-provisioning-0.1.iso"
|
|
||||||
grub_map_args=(
|
|
||||||
-map "$REPO_DIR/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg
|
|
||||||
-map "$REPO_DIR/grub/mint-isolinux-live.cfg" /isolinux/live.cfg
|
|
||||||
-map "$WORK_DIR/preseed/tuxflotte.seed" /preseed/tuxflotte.seed
|
|
||||||
-map "$WORK_DIR/casper/initrd-tuxflotte.lz" /casper/initrd.lz
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$output_iso"
|
rm -f "$output_iso"
|
||||||
|
|
||||||
@ -224,7 +112,8 @@ create_iso() {
|
|||||||
-outdev "$output_iso" \
|
-outdev "$output_iso" \
|
||||||
-compliance no_emul_toc \
|
-compliance no_emul_toc \
|
||||||
-volid TUXFLOTTE \
|
-volid TUXFLOTTE \
|
||||||
"${grub_map_args[@]}" \
|
-map "$REPO_DIR/grub/EFI-BOOT-grub.cfg" /EFI/BOOT/grub.cfg \
|
||||||
|
-map "$REPO_DIR/grub/boot-grub2-grub.cfg" /boot/grub2/grub.cfg \
|
||||||
-map "$WORK_DIR/updates" /updates \
|
-map "$WORK_DIR/updates" /updates \
|
||||||
-chown_r 0 /updates -- \
|
-chown_r 0 /updates -- \
|
||||||
-chgrp_r 0 /updates -- \
|
-chgrp_r 0 /updates -- \
|
||||||
@ -240,9 +129,7 @@ main() {
|
|||||||
extract_iso
|
extract_iso
|
||||||
patch_grub
|
patch_grub
|
||||||
verify_workdir
|
verify_workdir
|
||||||
bake_test_preseed
|
|
||||||
prepare_updates
|
prepare_updates
|
||||||
patch_initrd
|
|
||||||
create_iso
|
create_iso
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
79
scripts/build_boot_medium.sh
Executable file
79
scripts/build_boot_medium.sh
Executable file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Baut das eigenstaendige Tuxflotte-Boot-Medium (Debian trixie, per
|
||||||
|
# Debian-live-build) - siehe Plan ~/.claude/plans/inherited-floating-treasure.md
|
||||||
|
# und Memory project_independent_boot_medium.md.
|
||||||
|
#
|
||||||
|
# Ersetzt langfristig scripts/build.sh/build_customer_iso.sh (Casper-Patchen
|
||||||
|
# einer echten Linux-Mint-Live-ISO): dieses Boot-Medium enthaelt nur
|
||||||
|
# Tuxflottes eigenen, headless-tauglichen Code (Module 00-40,
|
||||||
|
# backends/mint-image/) und beruehrt niemals Ubiquity/Casper/eine
|
||||||
|
# Distributions-Live-Session - macht die Bug-Klasse, die zu dieser
|
||||||
|
# Architektur gefuehrt hat (siehe Memory-Datei), strukturell unmoeglich.
|
||||||
|
#
|
||||||
|
# --dev aktiviert einen SSH-Debug-Zugang (Testzugang tuxflotte/test123,
|
||||||
|
# analog bestehender Test-Fixtures wie WLAN-Test-Org) - ohne --dev bleibt
|
||||||
|
# SSH deaktiviert, keine gebackenen Zugangsdaten (Produktiv-Default,
|
||||||
|
# konsistent damit, dass auch das Golden Image selbst keine Zugangsdaten
|
||||||
|
# enthaelt). Siehe boot-medium/config/hooks/live/0900-tuxflotte-ssh-mode.hook.chroot.
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [--dev]" >&2
|
||||||
|
echo " --dev SSH-Debug-Zugang aktivieren (tuxflotte/test123)" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
DEV_BUILD=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dev) DEV_BUILD=true ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Error: Unbekannter Parameter: $arg" >&2; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo "Error: muss als root laufen (live-build braucht root fuer chroot/mount)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in lb; do
|
||||||
|
command -v "${cmd}" >/dev/null 2>&1 ||
|
||||||
|
{ echo "Error: fehlendes Werkzeug: ${cmd} (apt-get install live-build)" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BOOT_MEDIUM_DIR="${REPO_DIR}/boot-medium"
|
||||||
|
DEV_MARKER="${BOOT_MEDIUM_DIR}/config/includes.chroot/etc/tuxflotte-dev-build"
|
||||||
|
|
||||||
|
[[ -d "${BOOT_MEDIUM_DIR}/config" ]] ||
|
||||||
|
{ echo "Error: ${BOOT_MEDIUM_DIR}/config fehlt - lb config wurde noch nie ausgefuehrt." >&2; exit 1; }
|
||||||
|
|
||||||
|
cd "${BOOT_MEDIUM_DIR}"
|
||||||
|
|
||||||
|
if [[ "${DEV_BUILD}" == true ]]; then
|
||||||
|
echo "==> Dev-Build: SSH-Debug-Zugang wird aktiviert (tuxflotte/test123)."
|
||||||
|
mkdir -p "$(dirname "${DEV_MARKER}")"
|
||||||
|
touch "${DEV_MARKER}"
|
||||||
|
else
|
||||||
|
echo "==> Produktiv-Build: SSH bleibt deaktiviert."
|
||||||
|
rm -f "${DEV_MARKER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> lb clean"
|
||||||
|
lb clean
|
||||||
|
|
||||||
|
echo "==> lb config"
|
||||||
|
lb config \
|
||||||
|
--distribution trixie \
|
||||||
|
--architectures amd64 \
|
||||||
|
--archive-areas "main contrib non-free non-free-firmware" \
|
||||||
|
--binary-images iso-hybrid \
|
||||||
|
--bootloaders "grub-efi,syslinux" \
|
||||||
|
--debian-installer none \
|
||||||
|
--bootappend-live "boot=live components quiet splash vconsole.keymap=de"
|
||||||
|
|
||||||
|
echo "==> lb build"
|
||||||
|
lb build
|
||||||
|
|
||||||
|
echo "==> Boot-Medium gebaut: ${BOOT_MEDIUM_DIR}/live-image-amd64.hybrid.iso"
|
||||||
@ -1,16 +1,34 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Baut eine personalisierte Kunden-ISO fuer den Self-Service-Flow (Phase 4).
|
# Personalisiert das eigenstaendige Boot-Medium (scripts/build_boot_medium.sh)
|
||||||
# Anders als build.sh (das fuer Testzwecke einen statischen Preseed mit
|
# fuer eine bestimmte Kundenorganisation - befuellt config/installer.conf mit
|
||||||
# Fake-Geraete-ID einbrennt, siehe bake_test_preseed()) wird hier nur
|
# Aktivierungscode + WLAN-Zugangsdaten. Hostname/Geraete-ID/Partitionierung
|
||||||
# config/installer.conf mit den echten, organisationsspezifischen Werten
|
# werden weiterhin erst live auf dem Zielgeraet erzeugt (siehe
|
||||||
# befuellt - Hostname/Geraete-ID/Partitionierung werden weiterhin erst live
|
# backend_generate_config() in backends/mint-image/backend.sh).
|
||||||
# auf dem Zielgeraet erzeugt (siehe backend_generate_config() in
|
#
|
||||||
# backends/mint/backend.sh). Hier geht es nur um WLAN-Zugangsdaten +
|
# Anders als das alte, Casper-basierte Verfahren (Xorriso-Extraktion der
|
||||||
# Enrollment-Session-Code als Boot-Konfiguration, die start-kiosk.sh dazu
|
# ganzen ISO + Initrd-Hook-Injection, siehe scripts/lib/initrd.sh) kann hier
|
||||||
# bringt, den Installer beim Hochfahren automatisch zu starten statt der
|
# NICHT einfach eine einzelne Datei direkt auf der ISO9660-Ebene ersetzt
|
||||||
# interaktiven Kiosk-Startseite.
|
# werden: /opt/tuxflotte/ liegt innerhalb von /live/filesystem.squashfs
|
||||||
|
# (read-only, von live-boot gemountet). Ein direktes
|
||||||
|
# `xorriso -update datei /opt/tuxflotte/config/installer.conf` traf live
|
||||||
|
# getestet ins Leere - das gebootete System sah weiterhin die leere
|
||||||
|
# Platzhalter-Konfiguration aus dem Basis-Image, weil dieser Pfad ausserhalb
|
||||||
|
# des Squashfs liegt und live-boot ihn nie liest (31.08.2026, live in der
|
||||||
|
# enterprise-QEMU-VM gefunden: Aktivierungscode-Prompt erschien trotz
|
||||||
|
# TUXFLOTTE_AUTO_MODE=true in der vermeintlich aktualisierten ISO).
|
||||||
|
#
|
||||||
|
# Stattdessen: das Squashfs wird entpackt, installer.conf darin ersetzt, neu
|
||||||
|
# gepackt (mksquashfs -no-progress braucht dafuer nur reine Kompressionszeit,
|
||||||
|
# ~1 Minute - kein debootstrap/apt, kein Netzwerk) und als Ganzes per
|
||||||
|
# `xorriso -update` ins Ziel-ISO eingespielt (Boot-Sektoren/GRUB-Branding
|
||||||
|
# bleiben unangetastet, stecken schon im Basis-Image aus
|
||||||
|
# scripts/build_boot_medium.sh). mksquashfs kann ein bestehendes Squashfs
|
||||||
|
# zwar inkrementell erweitern, aber NICHT bereits vorhandene Dateien
|
||||||
|
# ueberschreiben (live getestet: identischer Dateiinhalt trotz
|
||||||
|
# augenscheinlich erfolgreichem Lauf) - deshalb der volle
|
||||||
|
# unsquashfs/mksquashfs-Zyklus statt eines inkrementellen Appends.
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]" >&2
|
echo "Usage: $0 <source.iso> <output.iso> <activation_code> [wifi_ssid] [wifi_psk] [volid]" >&2
|
||||||
@ -28,20 +46,6 @@ WIFI_PSK="${5:-}"
|
|||||||
# Aufrufe ohne sechsten Parameter (z.B. manuelle Tests).
|
# Aufrufe ohne sechsten Parameter (z.B. manuelle Tests).
|
||||||
VOLID="${6:-TUXFLOTTE}"
|
VOLID="${6:-TUXFLOTTE}"
|
||||||
|
|
||||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
WORK_DIR="$(mktemp -d)"
|
|
||||||
|
|
||||||
# shellcheck source=lib/initrd.sh
|
|
||||||
source "$REPO_DIR/scripts/lib/initrd.sh"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
# xorriso extrahiert Verzeichnisse teils mit den Original-ISO-Rechten
|
|
||||||
# (z.B. 555, kein Write-Bit) - ohne chmod schlaegt rm -rf darauf fehl.
|
|
||||||
chmod -R u+w "${WORK_DIR}" 2>/dev/null || true
|
|
||||||
rm -rf "${WORK_DIR}"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
if [[ -z "${SOURCE_ISO}" || -z "${OUTPUT_ISO}" || -z "${ACTIVATION_CODE}" ]]; then
|
if [[ -z "${SOURCE_ISO}" || -z "${OUTPUT_ISO}" || -z "${ACTIVATION_CODE}" ]]; then
|
||||||
usage
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
@ -50,51 +54,47 @@ fi
|
|||||||
[[ -f "${SOURCE_ISO}" ]] ||
|
[[ -f "${SOURCE_ISO}" ]] ||
|
||||||
{ echo "Error: Quell-ISO nicht gefunden: ${SOURCE_ISO}" >&2; exit 1; }
|
{ echo "Error: Quell-ISO nicht gefunden: ${SOURCE_ISO}" >&2; exit 1; }
|
||||||
|
|
||||||
for cmd in xorriso cpio; do
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo "Error: muss als root laufen (unsquashfs/mksquashfs brauchen root, um" >&2
|
||||||
|
echo " Datei-Eigentuemer im Squashfs korrekt zu erhalten - ohne root" >&2
|
||||||
|
echo " gehen z.B. root:shadow-Zuordnungen verloren, live gefunden)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in xorriso unsquashfs mksquashfs; do
|
||||||
command -v "${cmd}" >/dev/null 2>&1 ||
|
command -v "${cmd}" >/dev/null 2>&1 ||
|
||||||
{ echo "Error: fehlendes Werkzeug: ${cmd}" >&2; exit 1; }
|
{ echo "Error: fehlendes Werkzeug: ${cmd}" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Extrahiere Quell-ISO..."
|
WORK_DIR="$(mktemp -d)"
|
||||||
xorriso -indev "${SOURCE_ISO}" -osirrox on -extract / "${WORK_DIR}" >/dev/null
|
|
||||||
# xorriso uebernimmt die Original-ISO-Rechte (haeufig 555 auf
|
|
||||||
# Verzeichnissen, 444 auf Dateien) - ohne Write-Bit koennen weder
|
|
||||||
# spaetere Schritte noch der Cleanup-Trap Dateien darin ersetzen/entfernen.
|
|
||||||
chmod -R u+w "${WORK_DIR}"
|
|
||||||
|
|
||||||
echo "Installiere Tuxflotte-GRUB-Konfiguration..."
|
cleanup() {
|
||||||
# -f: xorriso extrahiert ISO-Dateien mit deren Original-Rechten, die auf
|
# unsquashfs uebernimmt Original-Rechte (teils ohne Write-Bit) - ohne
|
||||||
# Live-Medien haeufig read-only (444) sind - ohne -f schlaegt cp beim
|
# chmod schlaegt rm -rf darauf fehl.
|
||||||
# Ueberschreiben mit "Keine Berechtigung" fehl.
|
chmod -R u+w "${WORK_DIR}" 2>/dev/null || true
|
||||||
cp -f "${REPO_DIR}/grub/mint-boot-grub.cfg" "${WORK_DIR}/boot/grub/grub.cfg"
|
rm -rf "${WORK_DIR}"
|
||||||
cp -f "${REPO_DIR}/grub/mint-isolinux-live.cfg" "${WORK_DIR}/isolinux/live.cfg"
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
grep -q "Tuxflotte" "${WORK_DIR}/boot/grub/grub.cfg"
|
echo "Extrahiere Squashfs aus Quell-ISO..."
|
||||||
grep -q "Tuxflotte" "${WORK_DIR}/isolinux/live.cfg"
|
xorriso -indev "${SOURCE_ISO}" -osirrox on -extract /live/filesystem.squashfs \
|
||||||
|
"${WORK_DIR}/orig.squashfs" >/dev/null
|
||||||
|
|
||||||
echo "Baue personalisierte Boot-Konfiguration..."
|
echo "Entpacke Squashfs..."
|
||||||
updates_dir="${WORK_DIR}/updates"
|
unsquashfs -d "${WORK_DIR}/root" "${WORK_DIR}/orig.squashfs" >/dev/null
|
||||||
rm -rf "${updates_dir}"
|
|
||||||
mkdir -p "${updates_dir}"
|
|
||||||
cp -a "${REPO_DIR}/live-updates/." "${updates_dir}/"
|
|
||||||
cp -a "${REPO_DIR}/scripts" "${updates_dir}/opt/tuxflotte/"
|
|
||||||
cp -a "${REPO_DIR}/backends" "${updates_dir}/opt/tuxflotte/"
|
|
||||||
mkdir -p "${updates_dir}/opt/tuxflotte/config"
|
|
||||||
|
|
||||||
|
echo "Schreibe personalisierte installer.conf..."
|
||||||
|
mkdir -p "${WORK_DIR}/root/opt/tuxflotte/config"
|
||||||
{
|
{
|
||||||
echo "export TUXFLOTTE_AUTO_MODE=true"
|
echo "export TUXFLOTTE_AUTO_MODE=true"
|
||||||
printf 'export TUXFLOTTE_ACTIVATION_CODE=%q\n' "${ACTIVATION_CODE}"
|
printf 'export TUXFLOTTE_ACTIVATION_CODE=%q\n' "${ACTIVATION_CODE}"
|
||||||
[[ -n "${WIFI_SSID}" ]] && printf 'export TUXFLOTTE_WIFI_SSID=%q\n' "${WIFI_SSID}"
|
[[ -n "${WIFI_SSID}" ]] && printf 'export TUXFLOTTE_WIFI_SSID=%q\n' "${WIFI_SSID}"
|
||||||
[[ -n "${WIFI_PSK}" ]] && printf 'export TUXFLOTTE_WIFI_PSK=%q\n' "${WIFI_PSK}"
|
[[ -n "${WIFI_PSK}" ]] && printf 'export TUXFLOTTE_WIFI_PSK=%q\n' "${WIFI_PSK}"
|
||||||
} >"${updates_dir}/opt/tuxflotte/config/installer.conf"
|
} >"${WORK_DIR}/root/opt/tuxflotte/config/installer.conf"
|
||||||
|
|
||||||
chmod 0440 "${updates_dir}/etc/sudoers.d/90-tuxflotte"
|
echo "Packe Squashfs neu (nur Kompression, kein apt/debootstrap)..."
|
||||||
|
mksquashfs "${WORK_DIR}/root" "${WORK_DIR}/new.squashfs" -comp xz -no-progress >/dev/null
|
||||||
echo "Injiziere casper-bottom Updates-Hook in die Initrd..."
|
chmod 444 "${WORK_DIR}/new.squashfs"
|
||||||
custom_initrd="${WORK_DIR}/casper/initrd-tuxflotte.lz"
|
|
||||||
inject_casper_bottom_hooks "${REPO_DIR}" \
|
|
||||||
"${WORK_DIR}/casper/initrd.lz" \
|
|
||||||
"${custom_initrd}"
|
|
||||||
|
|
||||||
echo "Erzeuge Kunden-ISO..."
|
echo "Erzeuge Kunden-ISO..."
|
||||||
rm -f "${OUTPUT_ISO}"
|
rm -f "${OUTPUT_ISO}"
|
||||||
@ -102,14 +102,8 @@ rm -f "${OUTPUT_ISO}"
|
|||||||
xorriso \
|
xorriso \
|
||||||
-indev "${SOURCE_ISO}" \
|
-indev "${SOURCE_ISO}" \
|
||||||
-outdev "${OUTPUT_ISO}" \
|
-outdev "${OUTPUT_ISO}" \
|
||||||
-compliance no_emul_toc \
|
|
||||||
-volid "${VOLID}" \
|
-volid "${VOLID}" \
|
||||||
-map "${REPO_DIR}/grub/mint-boot-grub.cfg" /boot/grub/grub.cfg \
|
-update "${WORK_DIR}/new.squashfs" /live/filesystem.squashfs \
|
||||||
-map "${REPO_DIR}/grub/mint-isolinux-live.cfg" /isolinux/live.cfg \
|
|
||||||
-map "${custom_initrd}" /casper/initrd.lz \
|
|
||||||
-map "${updates_dir}" /updates \
|
|
||||||
-chown_r 0 /updates -- \
|
|
||||||
-chgrp_r 0 /updates -- \
|
|
||||||
-boot_image any replay
|
-boot_image any replay
|
||||||
|
|
||||||
echo "Kunden-ISO erstellt: ${OUTPUT_ISO}"
|
echo "Kunden-ISO erstellt: ${OUTPUT_ISO}"
|
||||||
|
|||||||
@ -115,6 +115,25 @@ image_deploy_mount() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# package_golden_image.sh schliesst /proc, /sys, /dev, /run, /tmp, /var/tmp
|
||||||
|
# bewusst aus dem Archiv aus (Laufzeit-Zustand der Referenz-VM, nicht Teil
|
||||||
|
# eines portablen Images). Nach dem Entpacken fehlen diese Verzeichnisse
|
||||||
|
# deshalb komplett - live gefunden (31.08.2026, erster echter Lauf mit dem
|
||||||
|
# echten Referenz-VM-Archiv): erst "mount --bind /dev" ("mount point does
|
||||||
|
# not exist"), dann - nach Behebung dessen - "mktemp: failed to create
|
||||||
|
# directory via template '/var/tmp/mkinitramfs_XXXXXX'" bei
|
||||||
|
# update-initramfs. Statt jedes Fehlen einzeln zu jagen: alle sechs
|
||||||
|
# ausgeschlossenen Verzeichnisse direkt nach dem Entpacken einmalig mit
|
||||||
|
# sinnvollen Rechten wiederherstellen. /dev, /proc, /sys werden gleich
|
||||||
|
# danach ohnehin ueberbindmounted (image_deploy_bind_mounts) - schadet
|
||||||
|
# aber nicht, sie schon hier als leere Verzeichnisse anzulegen.
|
||||||
|
image_deploy_restore_excluded_dirs() {
|
||||||
|
local target="$1"
|
||||||
|
|
||||||
|
mkdir -p "${target}"/{proc,sys,dev,run,tmp,var/tmp}
|
||||||
|
chmod 1777 "${target}/tmp" "${target}/var/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
# Entpackt das per build_golden_image.sh gebaute Golden Image direkt in
|
# Entpackt das per build_golden_image.sh gebaute Golden Image direkt in
|
||||||
# den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt
|
# den gemounteten Zielbaum. -p erhaelt Rechte/Eigentuemer, --zstd deckt
|
||||||
# sowohl .tar.zst als auch (falls spaeter gewechselt) andere
|
# sowohl .tar.zst als auch (falls spaeter gewechselt) andere
|
||||||
@ -128,6 +147,40 @@ image_deploy_extract_image() {
|
|||||||
|
|
||||||
tar --zstd -xpf "${image_path}" -C "${target}" ||
|
tar --zstd -xpf "${image_path}" -C "${target}" ||
|
||||||
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; }
|
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; }
|
||||||
|
|
||||||
|
image_deploy_restore_excluded_dirs "${target}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Streamt das Golden Image direkt vom Server in die Extraktion, ohne es
|
||||||
|
# jemals als Ganzes zwischenzuspeichern - live gefunden (31.08.2026,
|
||||||
|
# erster echter End-to-End-Lauf mit dem vollen 2,3-GB-Referenz-VM-Archiv
|
||||||
|
# ueber das neue Boot-Medium): das bisherige "erst nach /run/tuxflotte/...
|
||||||
|
# herunterladen, dann entpacken" scheiterte an /run (RAM-Tmpfs, oft nur
|
||||||
|
# ein paar hundert MB gross, unabhaengig von der Geraete-RAM-Groesse) -
|
||||||
|
# "curl: (23) Failure writing output to destination". Direktes
|
||||||
|
# Zusammenrohren braucht dagegen nur ein paar MB Puffer, unabhaengig von
|
||||||
|
# der Archivgroesse. PIPESTATUS wird explizit geprueft statt sich auf
|
||||||
|
# "pipefail"-Semantik (rechteste fehlgeschlagene Stufe gewinnt) zu
|
||||||
|
# verlassen, damit die Fehlermeldung eindeutig zwischen Download- und
|
||||||
|
# Entpack-Fehler unterscheidet.
|
||||||
|
image_deploy_extract_image_from_url() {
|
||||||
|
local url="$1"
|
||||||
|
local target="$2"
|
||||||
|
local pipe_status
|
||||||
|
|
||||||
|
# "|| true" verhindert, dass "set -e" hier sofort abbricht - der
|
||||||
|
# eigentliche Fehlerfall wird gleich anhand von PIPESTATUS gezielt
|
||||||
|
# ausgewertet, statt die Pipeline roh durchschlagen zu lassen.
|
||||||
|
curl --silent --show-error --fail --location "${url}" |
|
||||||
|
tar --zstd -xpf - -C "${target}" || true
|
||||||
|
pipe_status=("${PIPESTATUS[@]}")
|
||||||
|
|
||||||
|
[[ "${pipe_status[0]}" -eq 0 ]] ||
|
||||||
|
{ image_deploy_fatal "Golden Image konnte nicht geladen werden: ${url}"; return 1; }
|
||||||
|
[[ "${pipe_status[1]}" -eq 0 ]] ||
|
||||||
|
{ image_deploy_fatal "Golden Image konnte nicht nach ${target} entpackt werden"; return 1; }
|
||||||
|
|
||||||
|
image_deploy_restore_excluded_dirs "${target}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein
|
# /etc/fstab aus den tatsaechlichen Partitions-UUIDs neu erzeugen - kein
|
||||||
@ -166,11 +219,48 @@ image_deploy_bind_mounts() {
|
|||||||
local target="$1"
|
local target="$1"
|
||||||
local -n stack_ref="$2"
|
local -n stack_ref="$2"
|
||||||
|
|
||||||
mount --bind /dev "${target}/dev" && stack_ref+=("${target}/dev")
|
# Golden Image schliesst /dev, /proc, /sys bewusst aus
|
||||||
mount -t proc proc "${target}/proc" && stack_ref+=("${target}/proc")
|
# (package_golden_image.sh --exclude=dev/sys/proc) - die Zielverzeichnisse
|
||||||
mount -t sysfs sysfs "${target}/sys" && stack_ref+=("${target}/sys")
|
# existieren nach dem Entpacken deshalb noch nicht und muessen vor dem
|
||||||
mount -t devpts devpts "${target}/dev/pts" && stack_ref+=("${target}/dev/pts")
|
# Bind-Mount erst angelegt werden. Live gefunden (31.08.2026, erster
|
||||||
cp /etc/resolv.conf "${target}/etc/resolv.conf"
|
# echter Lauf mit der echten Mint-Referenz-VM): ohne das schlaegt
|
||||||
|
# "mount --bind" mit "mount point does not exist" fehl - und zwar
|
||||||
|
# SILENT, weil das bisherige "cmd && stack_ref+=(...)"-Muster einen
|
||||||
|
# Fehlschlag von "cmd" unter "set -e" nicht als Statement-Fehler wertet
|
||||||
|
# (wohlbekannte Bash-Ausnahme fuer &&/||-Ketten) - der fehlende
|
||||||
|
# /dev-Bind-Mount fiel dann erst beim naechsten chroot-Aufruf auf
|
||||||
|
# ("ssh-keygen -A: Couldn't open /dev/null"), weit weg von der
|
||||||
|
# eigentlichen Ursache. Jetzt: Zielverzeichnisse anlegen, jeden
|
||||||
|
# Mount-Fehler explizit und einzeln pruefen statt auf &&-Verkettung
|
||||||
|
# zu vertrauen.
|
||||||
|
mkdir -p "${target}/dev" "${target}/proc" "${target}/sys" "${target}/dev/pts"
|
||||||
|
|
||||||
|
mount --bind /dev "${target}/dev" ||
|
||||||
|
{ image_deploy_fatal "/dev konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/dev")
|
||||||
|
|
||||||
|
mount -t proc proc "${target}/proc" ||
|
||||||
|
{ image_deploy_fatal "/proc konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/proc")
|
||||||
|
|
||||||
|
mount -t sysfs sysfs "${target}/sys" ||
|
||||||
|
{ image_deploy_fatal "/sys konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/sys")
|
||||||
|
|
||||||
|
mount -t devpts devpts "${target}/dev/pts" ||
|
||||||
|
{ image_deploy_fatal "/dev/pts konnte nicht eingebunden werden"; return 1; }
|
||||||
|
stack_ref+=("${target}/dev/pts")
|
||||||
|
|
||||||
|
# Live gefunden (31.08.2026, erster echter Lauf mit der echten
|
||||||
|
# Mint-Referenz-VM statt eines debootstrap-Test-Images): dort ist
|
||||||
|
# /etc/resolv.conf ein von NetworkManager verwalteter Symlink (z.B. auf
|
||||||
|
# /run/NetworkManager/resolv.conf) - im gerade erst entpackten,
|
||||||
|
# ungebooteten Zielbaum zeigt der ins Leere, "cp" verweigert dann das
|
||||||
|
# Schreiben "through a dangling symlink". Ziel deshalb vorher entfernen
|
||||||
|
# (Symlink oder Datei, beides abgedeckt), erst dann kopieren.
|
||||||
|
rm -f "${target}/etc/resolv.conf"
|
||||||
|
cp /etc/resolv.conf "${target}/etc/resolv.conf" ||
|
||||||
|
{ image_deploy_fatal "resolv.conf konnte nicht nach ${target} kopiert werden"; return 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
image_deploy_unbind_mounts() {
|
image_deploy_unbind_mounts() {
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Wird von build.sh und build_customer_iso.sh eingebunden (source), nicht
|
|
||||||
# eigenstaendig ausgefuehrt.
|
|
||||||
|
|
||||||
# Haengt initrd-hooks/casper-bottom/* als zusaetzliche cpio-Schicht an ein
|
|
||||||
# bestehendes Casper-Initrd an - derselbe Mechanismus, den
|
|
||||||
# backend_launch() (backends/mint/backend.sh) bereits fuer das
|
|
||||||
# Kexec-Preseed nutzt und der per QEMU verifiziert ist: der Kernel haengt
|
|
||||||
# spaeter aneinandergereihte cpio-Archive additiv an ein bestehendes
|
|
||||||
# initramfs an. Noetig, weil Casper keinen automatischen /updates-Merge
|
|
||||||
# kennt (real per QEMU-Boot widerlegt) - der Hook selbst kopiert
|
|
||||||
# /cdrom/updates beim Boot auf das Root-Dateisystem, siehe
|
|
||||||
# initrd-hooks/casper-bottom/21tuxflotte_updates.
|
|
||||||
inject_casper_bottom_hooks() {
|
|
||||||
local repo_dir="$1"
|
|
||||||
local src_initrd="$2"
|
|
||||||
local dest_initrd="$3"
|
|
||||||
|
|
||||||
local hook_dir="${repo_dir}/initrd-hooks"
|
|
||||||
local layer_dir
|
|
||||||
layer_dir="$(mktemp -d)"
|
|
||||||
# Ausserhalb von layer_dir, damit die Archiv-Datei sich beim Bauen nicht
|
|
||||||
# selbst als leeren Eintrag einfaengt (find laeuft im selben Moment, in
|
|
||||||
# dem die Shell-Redirection sie bereits anlegt).
|
|
||||||
local cpio_out
|
|
||||||
cpio_out="$(mktemp)"
|
|
||||||
|
|
||||||
cp -a "${hook_dir}/casper-bottom" "${layer_dir}/scripts-casper-bottom-tmp"
|
|
||||||
mkdir -p "${layer_dir}/scripts"
|
|
||||||
mv "${layer_dir}/scripts-casper-bottom-tmp" "${layer_dir}/scripts/casper-bottom"
|
|
||||||
chmod 0755 "${layer_dir}"/scripts/casper-bottom/*
|
|
||||||
|
|
||||||
(cd "${layer_dir}" && find . -mindepth 1 | cpio -o -H newc 2>/dev/null | gzip) \
|
|
||||||
>"${cpio_out}" ||
|
|
||||||
{ echo "Error: Casper-Bottom-Hook-Schicht konnte nicht gebaut werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
|
|
||||||
|
|
||||||
cat "${src_initrd}" "${cpio_out}" >"${dest_initrd}" ||
|
|
||||||
{ echo "Error: Initrd konnte nicht mit Hook-Schicht zusammengesetzt werden." >&2; rm -rf "${layer_dir}"; rm -f "${cpio_out}"; return 1; }
|
|
||||||
|
|
||||||
rm -rf "${layer_dir}"
|
|
||||||
rm -f "${cpio_out}"
|
|
||||||
}
|
|
||||||
@ -15,3 +15,57 @@ log_error() {
|
|||||||
log_success() {
|
log_success() {
|
||||||
echo "[ OK ] $*"
|
echo "[ OK ] $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Klar abgesetzte Fortschrittsanzeige fuer die tty-Konsole des Boot-Mediums
|
||||||
|
# (kein Desktop, kein Kiosk-Browser - die Terminal-Ausgabe selbst ist die
|
||||||
|
# einzige UI, die eine Person vor dem Geraet sieht). Gilt fuer Dev- UND
|
||||||
|
# Produktiv-Build gleichermassen, nicht nur zum Debuggen - siehe Plan
|
||||||
|
# "Klare Fortschrittsanzeige auf der Konsole". Format/Stil bewusst an die
|
||||||
|
# bestehenden Banner in 17_device_status.sh angelehnt (Titel + "="-Linie
|
||||||
|
# gleicher Laenge, keine Sonderzeichen, die auf einer rohen VT-Konsole
|
||||||
|
# fehlen koennten).
|
||||||
|
declare -A TUXFLOTTE_STEP_LABELS=(
|
||||||
|
[00_preflight.sh]="Vorbereitung"
|
||||||
|
[05_network.sh]="Netzwerkverbindung wird hergestellt"
|
||||||
|
[10_hardware.sh]="Hardware wird erkannt"
|
||||||
|
[12_enrollment_auth.sh]="Aktivierungscode wird geprüft"
|
||||||
|
[15_server_handshake.sh]="Verbindung zu anode wird hergestellt"
|
||||||
|
[17_device_status.sh]="Gerätestatus wird abgerufen"
|
||||||
|
[20_profile_selection.sh]="Bereitstellungsvorlage wird ausgewählt"
|
||||||
|
[25_installation_confirm.sh]="Installationsbestätigung"
|
||||||
|
[30_runtime_blueprint.sh]="Installationsplan wird aufgelöst"
|
||||||
|
[40_backend.sh]="Golden Image wird geladen und installiert"
|
||||||
|
[20_storage.sh]="Speicher-Zusatzprüfung"
|
||||||
|
[99_finish.sh]="Abschluss"
|
||||||
|
)
|
||||||
|
readonly -a TUXFLOTTE_STEP_ORDER=(
|
||||||
|
00_preflight.sh 05_network.sh 10_hardware.sh 12_enrollment_auth.sh
|
||||||
|
15_server_handshake.sh 17_device_status.sh 20_profile_selection.sh
|
||||||
|
25_installation_confirm.sh 30_runtime_blueprint.sh 40_backend.sh
|
||||||
|
20_storage.sh 99_finish.sh
|
||||||
|
)
|
||||||
|
TUXFLOTTE_STEP_COUNT=0
|
||||||
|
|
||||||
|
log_step() {
|
||||||
|
local module_name="$1"
|
||||||
|
local label="${TUXFLOTTE_STEP_LABELS[$module_name]:-$module_name}"
|
||||||
|
local total="${#TUXFLOTTE_STEP_ORDER[@]}"
|
||||||
|
local bold="" reset=""
|
||||||
|
|
||||||
|
TUXFLOTTE_STEP_COUNT=$((TUXFLOTTE_STEP_COUNT + 1))
|
||||||
|
|
||||||
|
# Klartext-Fallback ohne Terminal (z.B. wenn stdout in eine Log-Datei
|
||||||
|
# umgeleitet wird) - kein tput-Aufruf, ncurses-bin ist bewusst nicht Teil
|
||||||
|
# des schlanken Paketsatzes, rohe ANSI-Codes reichen und sind ueberall
|
||||||
|
# ohne Zusatzpaket verfuegbar.
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
bold=$'\033[1m'
|
||||||
|
reset=$'\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
local title
|
||||||
|
title="[${TUXFLOTTE_STEP_COUNT}/${total}] ${label}"
|
||||||
|
|
||||||
|
printf '\n%s%s%s\n' "${bold}" "${title}" "${reset}"
|
||||||
|
printf '%s\n\n' "$(printf '=%.0s' $(seq 1 ${#title}))"
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ run_module() {
|
|||||||
|
|
||||||
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
|
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
|
||||||
|
|
||||||
|
log_step "$(basename "$module")"
|
||||||
log_info "Starte Modul: $(basename "$module")"
|
log_info "Starte Modul: $(basename "$module")"
|
||||||
|
|
||||||
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then
|
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -73,11 +73,26 @@ echo "==> Packe nach ${OUTPUT_FILE}"
|
|||||||
# nicht absolute (/proc/...) - ein --exclude mit fuehrendem Slash matcht
|
# nicht absolute (/proc/...) - ein --exclude mit fuehrendem Slash matcht
|
||||||
# dagegen NICHT (live am 31.08.2026 entdeckt: das lief zunaechst
|
# dagegen NICHT (live am 31.08.2026 entdeckt: das lief zunaechst
|
||||||
# unbemerkt gegen die echte Referenz-VM und haette u.a. /proc/kcore mit
|
# unbemerkt gegen die echte Referenz-VM und haette u.a. /proc/kcore mit
|
||||||
# eingepackt). Ausschluesse deshalb ohne fuehrenden Slash angeben.
|
# eingepackt).
|
||||||
|
#
|
||||||
|
# ZWEITER FUND (31.08.2026, spaeter am selben Tag): ein --exclude OHNE
|
||||||
|
# fuehrenden Slash UND ohne "./" matcht bei GNU tar gegen den Basisnamen
|
||||||
|
# JEDER Datei/jedes Verzeichnisses im ganzen Baum, nicht nur den
|
||||||
|
# Top-Level-Eintrag - "--exclude=dev" hat dadurch auch
|
||||||
|
# kernel/drivers/net/can/dev/ (ein voellig unverwandter, aber zufaellig
|
||||||
|
# gleichnamiger Kernelmodul-Ordner) mit ausgeschlossen und damit das
|
||||||
|
# gesamte CAN-Bus-Treibermodul aus dem Archiv gerissen - live erst beim
|
||||||
|
# tatsaechlichen Deployment aufgefallen (update-initramfs scheiterte an
|
||||||
|
# fehlenden can-dev.ko.zst-Abhaengigkeiten). Ein Praefix "./" verankert
|
||||||
|
# das Muster auf den exakten Top-Level-Pfad (ein "/" im Muster laesst tar
|
||||||
|
# gegen den vollen Namen matchen statt nur den Basisnamen) - lokal mit
|
||||||
|
# einem Wegwerf-Testbaum verifiziert: ./dev/./proc/./run schliessen nur
|
||||||
|
# die Top-Level-Eintraege aus, kernel/drivers/net/can/dev/ und
|
||||||
|
# nested/proc/... bleiben erhalten.
|
||||||
tar --numeric-owner --xattrs \
|
tar --numeric-owner --xattrs \
|
||||||
--exclude=proc --exclude=sys --exclude=dev \
|
--exclude=./proc --exclude=./sys --exclude=./dev \
|
||||||
--exclude=run --exclude=tmp --exclude=var/tmp \
|
--exclude=./run --exclude=./tmp --exclude=./var/tmp \
|
||||||
--exclude="${OUTPUT_FILE#/}" \
|
--exclude="./${OUTPUT_FILE#/}" \
|
||||||
-C / -cf - . | zstd -f -T0 -19 -o "${OUTPUT_FILE}"
|
-C / -cf - . | zstd -f -T0 -19 -o "${OUTPUT_FILE}"
|
||||||
|
|
||||||
echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))"
|
echo "==> Fertig: ${OUTPUT_FILE} ($(du -h "${OUTPUT_FILE}" | cut -f1))"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user