Thomas Stallinger 108444ecf7 feat: Auto-Modus + prozentuale Partitionierung für Mint (Phase 2)
Auto-Modus (TUXFLOTTE_AUTO_MODE=true, über config/installer.conf als
exportierte Env-Var vor 05_network.sh gesetzt) überspringt alle drei
interaktiven Gates, die bei genauerem Hinsehen existierten (nicht nur
das eine ursprünglich im Plan genannte):
- 05_network.sh: nicht-interaktiver WLAN-Pfad über TUXFLOTTE_WIFI_SSID/
  TUXFLOTTE_WIFI_PSK, analog zum bestehenden TUXFLOTTE_ACTIVATION_CODE-
  Muster in 12_enrollment_auth.sh (unverändert, unterstützte das schon).
- 17_device_status.sh: "Provisionierung fortsetzen?"-Prompt übersprungen.
- 20_profile_selection.sh: "Vorlage auswählen?"-Prompt übersprungen,
  wählt automatisch die als is_default markierte Vorlage (echter
  Server-Hinweis via Enrollment Session erst mit Phase 3 möglich).
- 25_installation_confirm.sh: eigentliches Commit-Gate übersprungen.

Alle drei Auto-Modus-Zweige lokal verifiziert (Skripte direkt mit
TUXFLOTTE_AUTO_MODE=true und präparierten Eingabedateien ausgeführt,
kein Hängenbleiben an read -p, korrekte state.env/template.json-Ausgabe).

Partitionierung: installation_directives.partitioning von einfachem
String auf strukturiertes Objekt umgestellt ({"scheme": "single"|
"custom", "root_filesystem", "extra_partitions": [{"mountpoint",
"filesystem", "percent"}]}) - Vertrag, an den sich provisioning-server
in Phase 3 halten muss. backend_generate_config() baut daraus ein
partman-auto/expert_recipe (ersetzt die bisherige choose_recipe-
Fallunterscheidung mit nur "default"/"atomic"), Prozentangaben werden
anhand der realen Zieldatenträgergröße (lsblk/blockdev, erst live auf
dem Zielgerät bekannt) in feste MB-Größen umgerechnet.

Auf UEFI-Systemen wird zusätzlich eine EFI-System-Partition ins Recipe
aufgenommen (sonst verweigert/warnt der Installer, "No EFI System
Partition was found") - exakte Stanza-Syntax nicht aus der Erinnerung
geraten, sondern aus /usr/lib/partman/recipes-amd64-efi/30atomic auf
dem echten Live-Medium ausgelesen ($reusemethod{ } war der fehlende
Teil in einem ersten, geparsten aber nicht erkannten Versuch).

backend_init() installiert jetzt auch jq/envsubst(gettext-base)/cpio
bei Bedarf nach (vorbestehende Lücke neben dem schon in Phase 1
behobenen kexec-tools).

Real per QEMU verifiziert: eigene Ein-Datenträger-Erkennung musste
gehärtet werden (nbd/zram-Geräte mit Größe 0 wurden fälschlich vor dem
echten Datenträger gewählt - auf einer sauberen VM allein wäre das nicht
aufgefallen). Custom-Recipe mit ext4-Root + ext4-/home (20%) +
btrfs-/var (10%) auf 40GB-Testplatte: vollständige unbeaufsichtigte
Installation inkl. Paketinstallation durchlaufen lassen, danach von der
Festplatte (nicht der Live-CD) gebootet - Login-Bildschirm mit korrektem
Hostname erscheint, System bootet einwandfrei per UEFI/ESP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 16:12:56 +02:00

545 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"