diff --git a/backends/mint/backend.sh b/backends/mint/backend.sh index 2451970..ad26976 100644 --- a/backends/mint/backend.sh +++ b/backends/mint/backend.sh @@ -26,27 +26,35 @@ backend_fatal() { } backend_init() { - for cmd in jq envsubst base64; do - command -v "${cmd}" >/dev/null 2>&1 || - { backend_fatal "Benötigtes Werkzeug fehlt: ${cmd}"; return 1; } - done + # 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=() - # kexec-tools ist 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). Bis das Paket fest in die - # ISO gebacken wird (Folgeschritt), hier als Live-Nachinstallation. - if ! command -v kexec >/dev/null 2>&1; then - backend_log "kexec-tools fehlt auf dem Live-Medium, installiere nach." + 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 (kexec-tools)."; return 1; } - DEBIAN_FRONTEND=noninteractive apt-get install -y kexec-tools || - { backend_fatal "Installation von kexec-tools fehlgeschlagen."; return 1; } + { 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; } @@ -77,7 +85,7 @@ backend_validate() { jq --exit-status ' .runtime_blueprint.installation_directives | (.disk_encryption | type == "boolean") - and (.partitioning | type == "string") + 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; } @@ -92,10 +100,144 @@ backend_validate() { 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 + + 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{ } . " + 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}" "/" '$primary{ } $bootable{ }')" + ;; + 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}" "/" '$primary{ } $bootable{ }')" + + 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 + local partitioning_json + local root_filesystem local secure_boot_required local partman_recipe local blueprints_json @@ -113,19 +255,18 @@ backend_generate_config() { [[ -n "${device_id}" ]] || { backend_fatal "Keine Geräte-ID in der Serverantwort gefunden."; return 1; } - partitioning="$(jq --raw-output '.runtime_blueprint.installation_directives.partitioning' "${RUNTIME_BLUEPRINT_FILE}")" + 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}")" - case "${partitioning}" in - default) - partman_recipe="atomic" - ;; - *) - backend_fatal "Nicht unterstützte Partitionierungsvorgabe: ${partitioning}" - return 1 - ;; + 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 diff --git a/backends/mint/preseed.tpl b/backends/mint/preseed.tpl index fb6e4c5..49d3cf7 100644 --- a/backends/mint/preseed.tpl +++ b/backends/mint/preseed.tpl @@ -26,7 +26,8 @@ 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 ${TUXFLOTTE_PARTMAN_RECIPE} +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/choose_partition select finish d-i partman/confirm boolean true diff --git a/scripts/build.sh b/scripts/build.sh index caa608a..6a52756 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -124,7 +124,7 @@ bake_test_preseed() { "blueprints": [], "installation_directives": { "disk_encryption": false, - "partitioning": "default", + "partitioning": {"scheme": "single", "root_filesystem": "ext4"}, "secure_boot_required": false } } diff --git a/scripts/modules/05_network.sh b/scripts/modules/05_network.sh index 4b3acd9..59ef7e1 100755 --- a/scripts/modules/05_network.sh +++ b/scripts/modules/05_network.sh @@ -377,6 +377,48 @@ connect_wifi() { 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 @@ -484,8 +526,13 @@ main() { log "Ethernet ist nicht verfügbar. WLAN-Initialisierung wird gestartet." - connect_wifi "${wifi_device}" || - fatal "Es konnte keine Verbindung zum Tuxflotte-Server hergestellt werden." + 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 diff --git a/scripts/modules/17_device_status.sh b/scripts/modules/17_device_status.sh index 7cf42f9..80fe5f1 100755 --- a/scripts/modules/17_device_status.sh +++ b/scripts/modules/17_device_status.sh @@ -101,6 +101,12 @@ show_device_status() { 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 diff --git a/scripts/modules/20_profile_selection.sh b/scripts/modules/20_profile_selection.sh index 546e153..20b3dc2 100755 --- a/scripts/modules/20_profile_selection.sh +++ b/scripts/modules/20_profile_selection.sh @@ -72,7 +72,33 @@ show_templates() { done < <(jq -c '.templates[]' "${SERVER_RESPONSE_FILE}") } -select_template() { +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 @@ -97,6 +123,14 @@ select_template() { 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}" diff --git a/scripts/modules/25_installation_confirm.sh b/scripts/modules/25_installation_confirm.sh index 22e8a18..e5d70aa 100755 --- a/scripts/modules/25_installation_confirm.sh +++ b/scripts/modules/25_installation_confirm.sh @@ -87,6 +87,12 @@ store_confirmation_state() { 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