diff --git a/backends/fedora/backend.sh b/backends/fedora/backend.sh new file mode 100644 index 0000000..93ea909 --- /dev/null +++ b/backends/fedora/backend.sh @@ -0,0 +1,133 @@ +#!/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="fedora" + +BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KICKSTART_TEMPLATE="${BACKEND_DIR}/kickstart.tpl" + +RUNTIME_BLUEPRINT_FILE="/run/tuxflotte/runtime/runtime_blueprint.json" +SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.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() { + for cmd in jq envsubst; do + command -v "${cmd}" >/dev/null 2>&1 || + { backend_fatal "Benötigtes Werkzeug fehlt: ${cmd}"; return 1; } + done + + [[ -r "${KICKSTART_TEMPLATE}" ]] || + { backend_fatal "Kickstart-Template nicht gefunden: ${KICKSTART_TEMPLATE}"; 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 == "string") + and (.secure_boot_required | type == "boolean") + ' "${RUNTIME_BLUEPRINT_FILE}" >/dev/null || + { backend_fatal "Installationszeitliche Vorgaben fehlen oder sind ungültig."; return 1; } + + backend_log "Runtime Blueprint ist gültig für Backend '${BACKEND_KEY}'." +} + +backend_generate_config() { + local hostname + local disk_encryption + local partitioning + local secure_boot_required + local partitioning_command + local 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; } + + disk_encryption="$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" + partitioning="$(jq --raw-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) + if [[ "${disk_encryption}" == "true" ]]; then + partitioning_command="autopart --encrypted" + else + partitioning_command="autopart" + fi + ;; + *) + backend_fatal "Nicht unterstützte Partitionierungsvorgabe: ${partitioning}" + return 1 + ;; + esac + + if [[ "${secure_boot_required}" == "true" ]]; then + backend_log "Hinweis: secure_boot_required=true wird derzeit nicht in der Kickstart-Konfiguration durchgesetzt (Phase 1)." + fi + + blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")" + + TUXFLOTTE_HOSTNAME="${hostname}" \ + TUXFLOTTE_PARTITIONING_COMMAND="${partitioning_command}" \ + TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \ + envsubst '${TUXFLOTTE_HOSTNAME} ${TUXFLOTTE_PARTITIONING_COMMAND} ${TUXFLOTTE_BLUEPRINTS_JSON}' \ + <"${KICKSTART_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() { + backend_log "Phase 1: Start des nativen Installers ist noch nicht aktiv." + backend_log "Erzeugte Konfiguration liegt bereit unter: ${CONFIG_FILE}" +} + +backend_postinstall() { + backend_log "Phase 1: Vorbereitung des Provisioning-Agent erfolgt bereits im %post-Abschnitt der Kickstart-Konfiguration." +} diff --git a/backends/fedora/kickstart.tpl b/backends/fedora/kickstart.tpl new file mode 100644 index 0000000..f10f3cc --- /dev/null +++ b/backends/fedora/kickstart.tpl @@ -0,0 +1,55 @@ +#version=DEVEL + +text +reboot + +lang de_DE.UTF-8 +keyboard de +timezone Europe/Berlin --utc + +# Lab-Bootstrap-Zugangsdaten. Ersetzt ein noch fehlendes Secret-Reference-Modell +# (siehe 09-data-model-v1.md) und darf nicht als Produktionsmechanismus gelten. +rootpw --plaintext test123 +user --name=tuxflotte --groups=wheel --password=test123 + +network --bootproto=dhcp --activate --hostname=${TUXFLOTTE_HOSTNAME} + +zerombr +clearpart --all --initlabel +${TUXFLOTTE_PARTITIONING_COMMAND} + +firewall --enabled +selinux --enforcing + +bootloader --location=mbr + +%packages +@core +vim +curl +git +ansible-core +%end + +%post +cat > /etc/motd <<'EOF' +Provisioned by tuxflotte + +https://tuxflotte.de +EOF + +localectl set-locale LANG=de_DE.UTF-8 +localectl set-keymap de +localectl set-x11-keymap de + +mkdir -p /etc/tuxflotte + +cat > /etc/tuxflotte/runtime_blueprint.json <<'RUNTIME_BLUEPRINT_EOF' +${TUXFLOTTE_BLUEPRINTS_JSON} +RUNTIME_BLUEPRINT_EOF + +# Der Provisioning Agent existiert noch nicht als Build-Artefakt. +# Anwendung der obigen Blueprints per Ansible-Pull erfolgt erst nach dessen Implementierung. +echo "tuxflotte: Runtime Blueprint unter /etc/tuxflotte/runtime_blueprint.json hinterlegt." >> /var/log/tuxflotte-postinstall.log +echo "tuxflotte: Provisioning-Agent-Installation ist noch nicht implementiert (Phase 1)." >> /var/log/tuxflotte-postinstall.log +%end diff --git a/scripts/installer.sh b/scripts/installer.sh index 06d1995..e4ed7d7 100755 --- a/scripts/installer.sh +++ b/scripts/installer.sh @@ -76,6 +76,8 @@ case "${TUXFLOTTE_INSTALLATION_CONFIRMED:-}" in ;; 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" diff --git a/scripts/modules/30_runtime_blueprint.sh b/scripts/modules/30_runtime_blueprint.sh new file mode 100644 index 0000000..c206eae --- /dev/null +++ b/scripts/modules/30_runtime_blueprint.sh @@ -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 "$@" diff --git a/scripts/modules/40_backend.sh b/scripts/modules/40_backend.sh new file mode 100644 index 0000000..9bb7821 --- /dev/null +++ b/scripts/modules/40_backend.sh @@ -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 "$@"