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>
545 lines
13 KiB
Bash
Executable File
545 lines
13 KiB
Bash
Executable File
#!/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 "$@"
|