From f1cb04bd24eb64d3d7072667976e85c66fb3392e Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Tue, 14 Jul 2026 12:41:01 +0200 Subject: [PATCH] feat: add interactive provisioning flow and commit point --- scripts/installer.sh | 25 +++++ scripts/lib/utils.sh | 2 +- scripts/modules/20_profile_selection.sh | 117 +++++++++++++++++++++ scripts/modules/25_installation_confirm.sh | 112 ++++++++++++++++++++ 4 files changed, 255 insertions(+), 1 deletion(-) create mode 100755 scripts/modules/20_profile_selection.sh create mode 100755 scripts/modules/25_installation_confirm.sh diff --git a/scripts/installer.sh b/scripts/installer.sh index fa34996..0477195 100755 --- a/scripts/installer.sh +++ b/scripts/installer.sh @@ -29,6 +29,31 @@ 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/15_server_handshake.sh" "always" +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/20_storage.sh" "dry-run-safe" run_module "$SCRIPT_DIR/modules/99_finish.sh" "always" diff --git a/scripts/lib/utils.sh b/scripts/lib/utils.sh index e24ee95..d2ff045 100644 --- a/scripts/lib/utils.sh +++ b/scripts/lib/utils.sh @@ -26,7 +26,7 @@ run_module() { fi # shellcheck disable=SC1090 - source "$module" + "$module" log_success "Modul abgeschlossen: $(basename "$module")" } diff --git a/scripts/modules/20_profile_selection.sh b/scripts/modules/20_profile_selection.sh new file mode 100755 index 0000000..94e0d9a --- /dev/null +++ b/scripts/modules/20_profile_selection.sh @@ -0,0 +1,117 @@ +#!/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 PROFILE_FILE="${RUNTIME_DIR}/profile.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 -- "${PROFILE_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 (.profiles | type == "array") + and (.profiles | length > 0) + ' "${SERVER_RESPONSE_FILE}" >/dev/null || + fatal "Serverantwort enthält keine auswählbaren Profile." +} + +show_profiles() { + local index=1 + local profile + + log "Verfügbare Provisioning-Profile:" + + while IFS= read -r profile; do + printf '\n' + printf ' %d) %s\n' \ + "${index}" \ + "$(jq -r '.label' <<<"${profile}")" + + printf ' Distribution: %s %s\n' \ + "$(jq -r '.distribution' <<<"${profile}")" \ + "$(jq -r '.version' <<<"${profile}")" + + printf ' %s\n' \ + "$(jq -r '.description // ""' <<<"${profile}")" + + ((index += 1)) + done < <(jq -c '.profiles[]' "${SERVER_RESPONSE_FILE}") +} + +select_profile() { + local profile_count + local selection + + profile_count="$(jq '.profiles | length' "${SERVER_RESPONSE_FILE}")" + + while true; do + printf '\n' + read -r -p "Profil auswählen [1-${profile_count}]: " selection + + if [[ "${selection}" =~ ^[0-9]+$ ]] && + ((selection >= 1 && selection <= profile_count)); then + break + fi + + log "Ungültige Auswahl." + done + + jq \ + --argjson index "$((selection - 1))" \ + '{ + schema_version: 1, + profile: .profiles[$index] + }' \ + "${SERVER_RESPONSE_FILE}" >"${PROFILE_FILE}" + + chmod 0600 "${PROFILE_FILE}" + + jq --exit-status ' + .schema_version == 1 + and (.profile | type == "object") + and (.profile.id | type == "string") + and (.profile.id | length > 0) + ' "${PROFILE_FILE}" >/dev/null || + fatal "Ausgewähltes Profil konnte nicht gültig gespeichert werden." + + log "Profil $(jq -r '.profile.id' "${PROFILE_FILE}") wurde ausgewählt." + log "Profilauswahl wurde unter ${PROFILE_FILE} gespeichert." +} + +main() { + require_root + prepare_runtime + validate_server_response + show_profiles + select_profile +} + +main "$@" diff --git a/scripts/modules/25_installation_confirm.sh b/scripts/modules/25_installation_confirm.sh new file mode 100755 index 0000000..952dd75 --- /dev/null +++ b/scripts/modules/25_installation_confirm.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" +readonly SCRIPT_NAME + +readonly PROFILE_FILE="/run/tuxflotte/assignment/profile.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_profile() { + [[ -r "${PROFILE_FILE}" ]] || + fatal "Profilauswahl nicht gefunden: ${PROFILE_FILE}" + + jq --exit-status ' + .schema_version == 1 + and (.profile | type == "object") + and (.profile.id | type == "string") + and (.profile.label | type == "string") + and (.profile.distribution | type == "string") + and (.profile.version | type == "string") + ' "${PROFILE_FILE}" >/dev/null || + fatal "Profilauswahl enthält kein gültiges Profil." +} + +show_installation_plan() { + local label + local distribution + local version + local installer_type + + label="$(jq -r '.profile.label' "${PROFILE_FILE}")" + distribution="$(jq -r '.profile.distribution' "${PROFILE_FILE}")" + version="$(jq -r '.profile.version' "${PROFILE_FILE}")" + installer_type="$(jq -r '.profile.installer.type // "unbekannt"' "${PROFILE_FILE}")" + + printf '\n' + printf 'Geplanter Installationsvorgang\n' + printf '==============================\n\n' + printf 'Profil: %s\n' "${label}" + printf 'Distribution: %s %s\n' "${distribution}" "${version}" + printf 'Installer: %s\n' "${installer_type}" + 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 + + 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_profile + show_installation_plan + confirm_installation +} + +main "$@"