Compare commits

...

2 Commits

Author SHA1 Message Date
086917deec feat: resolve Runtime Blueprint and add Fedora backend for kickstart generation
30_runtime_blueprint.sh calls POST /templates/{id}/resolve with the
device_id from the server handshake and the template_id from the
Bereitstellungsvorlage selection, storing the resulting Runtime
Blueprint under /run/tuxflotte/runtime/.

40_backend.sh dispatches to backends/${backend_id}/backend.sh based on
the resolved backend_id and drives the backend_init/validate/
generate_config/launch/postinstall lifecycle from 06-backend-api.md.

backends/fedora/backend.sh implements that lifecycle for Fedora:
backend_generate_config() renders kickstart.tpl via envsubst using the
Runtime Blueprint's installation_directives and the device hostname,
embedding the resolved Merkmal blueprints as JSON for the (not yet
implemented) Provisioning Agent to apply later. Replaces the old
git-clone-based %post bootstrap. backend_launch()/backend_postinstall()
are Phase 1 stubs pending the live-ISO boot integration (see
platform-docs ADR-0003).
2026-07-18 10:49:50 +02:00
3a7549adf1 fix: align profile selection and installation confirm with Bereitstellungsvorlage model
20_profile_selection.sh and 25_installation_confirm.sh still spoke the
old flat profile model (.profiles[], .profile.distribution,
.profile.installer.type) that the provisioning server no longer
returns - it now returns .templates[] with workspace/backend objects
(see provisioning-server's Merkmal/Blueprint/Bereitstellungsvorlage
migration). Both modules now read/write
/run/tuxflotte/assignment/template.json against the current
Bereitstellungsvorlage schema, matching 10-interactive-provisioning-flow.md.
2026-07-18 10:49:42 +02:00
7 changed files with 444 additions and 56 deletions

133
backends/fedora/backend.sh Normal file
View File

@ -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."
}

View File

@ -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

View File

@ -76,6 +76,8 @@ case "${TUXFLOTTE_INSTALLATION_CONFIRMED:-}" in
;; ;;
esac 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/20_storage.sh" "dry-run-safe"
run_module "$SCRIPT_DIR/modules/99_finish.sh" "always" run_module "$SCRIPT_DIR/modules/99_finish.sh" "always"

View File

@ -6,7 +6,7 @@ readonly SCRIPT_NAME
readonly SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json" readonly SERVER_RESPONSE_FILE="/run/tuxflotte/server/response.json"
readonly RUNTIME_DIR="/run/tuxflotte/assignment" readonly RUNTIME_DIR="/run/tuxflotte/assignment"
readonly PROFILE_FILE="${RUNTIME_DIR}/profile.json" readonly TEMPLATE_FILE="${RUNTIME_DIR}/template.json"
log() { log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" printf '[%s] %s\n' "${SCRIPT_NAME}" "$*"
@ -25,7 +25,7 @@ require_root() {
prepare_runtime() { prepare_runtime() {
install -d -m 0700 "${RUNTIME_DIR}" install -d -m 0700 "${RUNTIME_DIR}"
rm -f -- "${PROFILE_FILE}" rm -f -- "${TEMPLATE_FILE}"
} }
validate_server_response() { validate_server_response() {
@ -37,47 +37,53 @@ validate_server_response() {
jq --exit-status ' jq --exit-status '
.success == true .success == true
and (.profiles | type == "array") and (.templates | type == "array")
and (.profiles | length > 0) and (.templates | length > 0)
' "${SERVER_RESPONSE_FILE}" >/dev/null || ' "${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält keine auswählbaren Profile." fatal "Serverantwort enthält keine auswählbaren Bereitstellungsvorlagen."
} }
show_profiles() { show_templates() {
local index=1 local index=1
local profile local template
log "Verfügbare Provisioning-Profile:" log "Verfügbare Bereitstellungsvorlagen:"
while IFS= read -r profile; do while IFS= read -r template; do
printf '\n' printf '\n'
printf ' %d) %s\n' \ printf ' %d) %s' \
"${index}" \ "${index}" \
"$(jq -r '.label' <<<"${profile}")" "$(jq -r '.label' <<<"${template}")"
printf ' Distribution: %s %s\n' \ if [[ "$(jq -r '.is_default' <<<"${template}")" == "true" ]]; then
"$(jq -r '.distribution' <<<"${profile}")" \ printf ' (Standard)'
"$(jq -r '.version' <<<"${profile}")" fi
printf ' %s\n' \ printf '\n'
"$(jq -r '.description // ""' <<<"${profile}")"
printf ' Workspace: %s\n' \
"$(jq -r '.workspace.name' <<<"${template}")"
printf ' Backend: %s %s\n' \
"$(jq -r '.backend.name' <<<"${template}")" \
"$(jq -r '.backend.version' <<<"${template}")"
((index += 1)) ((index += 1))
done < <(jq -c '.profiles[]' "${SERVER_RESPONSE_FILE}") done < <(jq -c '.templates[]' "${SERVER_RESPONSE_FILE}")
} }
select_profile() { select_template() {
local profile_count local template_count
local selection local selection
profile_count="$(jq '.profiles | length' "${SERVER_RESPONSE_FILE}")" template_count="$(jq '.templates | length' "${SERVER_RESPONSE_FILE}")"
while true; do while true; do
printf '\n' printf '\n'
read -r -p "Profil auswählen [1-${profile_count}]: " selection read -r -p "Bereitstellungsvorlage auswählen [1-${template_count}]: " selection
if [[ "${selection}" =~ ^[0-9]+$ ]] && if [[ "${selection}" =~ ^[0-9]+$ ]] &&
((selection >= 1 && selection <= profile_count)); then ((selection >= 1 && selection <= template_count)); then
break break
fi fi
@ -88,30 +94,32 @@ select_profile() {
--argjson index "$((selection - 1))" \ --argjson index "$((selection - 1))" \
'{ '{
schema_version: 1, schema_version: 1,
profile: .profiles[$index] template: .templates[$index]
}' \ }' \
"${SERVER_RESPONSE_FILE}" >"${PROFILE_FILE}" "${SERVER_RESPONSE_FILE}" >"${TEMPLATE_FILE}"
chmod 0600 "${PROFILE_FILE}" chmod 0600 "${TEMPLATE_FILE}"
jq --exit-status ' jq --exit-status '
.schema_version == 1 .schema_version == 1
and (.profile | type == "object") and (.template | type == "object")
and (.profile.id | type == "string") and (.template.id | type == "string")
and (.profile.id | length > 0) and (.template.id | length > 0)
' "${PROFILE_FILE}" >/dev/null || and (.template.workspace | type == "object")
fatal "Ausgewähltes Profil konnte nicht gültig gespeichert werden." and (.template.backend | type == "object")
' "${TEMPLATE_FILE}" >/dev/null ||
fatal "Ausgewählte Bereitstellungsvorlage konnte nicht gültig gespeichert werden."
log "Profil $(jq -r '.profile.id' "${PROFILE_FILE}") wurde ausgewählt." log "Bereitstellungsvorlage $(jq -r '.template.id' "${TEMPLATE_FILE}") wurde ausgewählt."
log "Profilauswahl wurde unter ${PROFILE_FILE} gespeichert." log "Auswahl wurde unter ${TEMPLATE_FILE} gespeichert."
} }
main() { main() {
require_root require_root
prepare_runtime prepare_runtime
validate_server_response validate_server_response
show_profiles show_templates
select_profile select_template
} }
main "$@" main "$@"

View File

@ -4,7 +4,7 @@ set -Eeuo pipefail
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME readonly SCRIPT_NAME
readonly PROFILE_FILE="/run/tuxflotte/assignment/profile.json" readonly TEMPLATE_FILE="/run/tuxflotte/assignment/template.json"
readonly RUNTIME_DIR="/run/tuxflotte/installation" readonly RUNTIME_DIR="/run/tuxflotte/installation"
readonly STATE_FILE="${RUNTIME_DIR}/state.env" readonly STATE_FILE="${RUNTIME_DIR}/state.env"
@ -34,38 +34,39 @@ prepare_runtime() {
rm -f -- "${STATE_FILE}" rm -f -- "${STATE_FILE}"
} }
validate_profile() { validate_template() {
[[ -r "${PROFILE_FILE}" ]] || [[ -r "${TEMPLATE_FILE}" ]] ||
fatal "Profilauswahl nicht gefunden: ${PROFILE_FILE}" fatal "Bereitstellungsvorlage nicht gefunden: ${TEMPLATE_FILE}"
jq --exit-status ' jq --exit-status '
.schema_version == 1 .schema_version == 1
and (.profile | type == "object") and (.template | type == "object")
and (.profile.id | type == "string") and (.template.id | type == "string")
and (.profile.label | type == "string") and (.template.label | type == "string")
and (.profile.distribution | type == "string") and (.template.workspace.name | type == "string")
and (.profile.version | type == "string") and (.template.backend.name | type == "string")
' "${PROFILE_FILE}" >/dev/null || and (.template.backend.version | type == "string")
fatal "Profilauswahl enthält kein gültiges Profil." ' "${TEMPLATE_FILE}" >/dev/null ||
fatal "Bereitstellungsvorlage enthält keine gültige Auswahl."
} }
show_installation_plan() { show_installation_plan() {
local label local label
local distribution local workspace
local version local backend_name
local installer_type local backend_version
label="$(jq -r '.profile.label' "${PROFILE_FILE}")" label="$(jq -r '.template.label' "${TEMPLATE_FILE}")"
distribution="$(jq -r '.profile.distribution' "${PROFILE_FILE}")" workspace="$(jq -r '.template.workspace.name' "${TEMPLATE_FILE}")"
version="$(jq -r '.profile.version' "${PROFILE_FILE}")" backend_name="$(jq -r '.template.backend.name' "${TEMPLATE_FILE}")"
installer_type="$(jq -r '.profile.installer.type // "unbekannt"' "${PROFILE_FILE}")" backend_version="$(jq -r '.template.backend.version' "${TEMPLATE_FILE}")"
printf '\n' printf '\n'
printf 'Geplanter Installationsvorgang\n' printf 'Geplanter Installationsvorgang\n'
printf '==============================\n\n' printf '==============================\n\n'
printf 'Profil: %s\n' "${label}" printf 'Bereitstellungsvorlage: %s\n' "${label}"
printf 'Distribution: %s %s\n' "${distribution}" "${version}" printf 'Workspace: %s\n' "${workspace}"
printf 'Installer: %s\n' "${installer_type}" printf 'Backend: %s %s\n' "${backend_name}" "${backend_version}"
printf '\n' printf '\n'
printf 'Die eigentliche Installation kann lokale Datenträger verändern.\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 'Bis zu dieser Bestätigung wurden keine destruktiven Installationsaktionen gestartet.\n'
@ -104,7 +105,7 @@ confirm_installation() {
main() { main() {
require_root require_root
prepare_runtime prepare_runtime
validate_profile validate_template
show_installation_plan show_installation_plan
confirm_installation confirm_installation
} }

View File

@ -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 "$@"

View File

@ -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 "$@"