Compare commits

..

No commits in common. "086917deece353c29d34a35b15f3a54be18d7717" and "f7f8e93c3021dc3b58571f0d5a132d1c5eb47e93" have entirely different histories.

7 changed files with 56 additions and 444 deletions

View File

@ -1,133 +0,0 @@
#!/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

@ -1,55 +0,0 @@
#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,8 +76,6 @@ 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 TEMPLATE_FILE="${RUNTIME_DIR}/template.json" readonly PROFILE_FILE="${RUNTIME_DIR}/profile.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 -- "${TEMPLATE_FILE}" rm -f -- "${PROFILE_FILE}"
} }
validate_server_response() { validate_server_response() {
@ -37,53 +37,47 @@ validate_server_response() {
jq --exit-status ' jq --exit-status '
.success == true .success == true
and (.templates | type == "array") and (.profiles | type == "array")
and (.templates | length > 0) and (.profiles | length > 0)
' "${SERVER_RESPONSE_FILE}" >/dev/null || ' "${SERVER_RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält keine auswählbaren Bereitstellungsvorlagen." fatal "Serverantwort enthält keine auswählbaren Profile."
} }
show_templates() { show_profiles() {
local index=1 local index=1
local template local profile
log "Verfügbare Bereitstellungsvorlagen:" log "Verfügbare Provisioning-Profile:"
while IFS= read -r template; do while IFS= read -r profile; do
printf '\n' printf '\n'
printf ' %d) %s' \ printf ' %d) %s\n' \
"${index}" \ "${index}" \
"$(jq -r '.label' <<<"${template}")" "$(jq -r '.label' <<<"${profile}")"
if [[ "$(jq -r '.is_default' <<<"${template}")" == "true" ]]; then printf ' Distribution: %s %s\n' \
printf ' (Standard)' "$(jq -r '.distribution' <<<"${profile}")" \
fi "$(jq -r '.version' <<<"${profile}")"
printf '\n' printf ' %s\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 '.templates[]' "${SERVER_RESPONSE_FILE}") done < <(jq -c '.profiles[]' "${SERVER_RESPONSE_FILE}")
} }
select_template() { select_profile() {
local template_count local profile_count
local selection local selection
template_count="$(jq '.templates | length' "${SERVER_RESPONSE_FILE}")" profile_count="$(jq '.profiles | length' "${SERVER_RESPONSE_FILE}")"
while true; do while true; do
printf '\n' printf '\n'
read -r -p "Bereitstellungsvorlage auswählen [1-${template_count}]: " selection read -r -p "Profil auswählen [1-${profile_count}]: " selection
if [[ "${selection}" =~ ^[0-9]+$ ]] && if [[ "${selection}" =~ ^[0-9]+$ ]] &&
((selection >= 1 && selection <= template_count)); then ((selection >= 1 && selection <= profile_count)); then
break break
fi fi
@ -94,32 +88,30 @@ select_template() {
--argjson index "$((selection - 1))" \ --argjson index "$((selection - 1))" \
'{ '{
schema_version: 1, schema_version: 1,
template: .templates[$index] profile: .profiles[$index]
}' \ }' \
"${SERVER_RESPONSE_FILE}" >"${TEMPLATE_FILE}" "${SERVER_RESPONSE_FILE}" >"${PROFILE_FILE}"
chmod 0600 "${TEMPLATE_FILE}" chmod 0600 "${PROFILE_FILE}"
jq --exit-status ' jq --exit-status '
.schema_version == 1 .schema_version == 1
and (.template | type == "object") and (.profile | type == "object")
and (.template.id | type == "string") and (.profile.id | type == "string")
and (.template.id | length > 0) and (.profile.id | length > 0)
and (.template.workspace | type == "object") ' "${PROFILE_FILE}" >/dev/null ||
and (.template.backend | type == "object") fatal "Ausgewähltes Profil konnte nicht gültig gespeichert werden."
' "${TEMPLATE_FILE}" >/dev/null ||
fatal "Ausgewählte Bereitstellungsvorlage konnte nicht gültig gespeichert werden."
log "Bereitstellungsvorlage $(jq -r '.template.id' "${TEMPLATE_FILE}") wurde ausgewählt." log "Profil $(jq -r '.profile.id' "${PROFILE_FILE}") wurde ausgewählt."
log "Auswahl wurde unter ${TEMPLATE_FILE} gespeichert." log "Profilauswahl wurde unter ${PROFILE_FILE} gespeichert."
} }
main() { main() {
require_root require_root
prepare_runtime prepare_runtime
validate_server_response validate_server_response
show_templates show_profiles
select_template select_profile
} }
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 TEMPLATE_FILE="/run/tuxflotte/assignment/template.json" readonly PROFILE_FILE="/run/tuxflotte/assignment/profile.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,39 +34,38 @@ prepare_runtime() {
rm -f -- "${STATE_FILE}" rm -f -- "${STATE_FILE}"
} }
validate_template() { validate_profile() {
[[ -r "${TEMPLATE_FILE}" ]] || [[ -r "${PROFILE_FILE}" ]] ||
fatal "Bereitstellungsvorlage nicht gefunden: ${TEMPLATE_FILE}" fatal "Profilauswahl nicht gefunden: ${PROFILE_FILE}"
jq --exit-status ' jq --exit-status '
.schema_version == 1 .schema_version == 1
and (.template | type == "object") and (.profile | type == "object")
and (.template.id | type == "string") and (.profile.id | type == "string")
and (.template.label | type == "string") and (.profile.label | type == "string")
and (.template.workspace.name | type == "string") and (.profile.distribution | type == "string")
and (.template.backend.name | type == "string") and (.profile.version | type == "string")
and (.template.backend.version | type == "string") ' "${PROFILE_FILE}" >/dev/null ||
' "${TEMPLATE_FILE}" >/dev/null || fatal "Profilauswahl enthält kein gültiges Profil."
fatal "Bereitstellungsvorlage enthält keine gültige Auswahl."
} }
show_installation_plan() { show_installation_plan() {
local label local label
local workspace local distribution
local backend_name local version
local backend_version local installer_type
label="$(jq -r '.template.label' "${TEMPLATE_FILE}")" label="$(jq -r '.profile.label' "${PROFILE_FILE}")"
workspace="$(jq -r '.template.workspace.name' "${TEMPLATE_FILE}")" distribution="$(jq -r '.profile.distribution' "${PROFILE_FILE}")"
backend_name="$(jq -r '.template.backend.name' "${TEMPLATE_FILE}")" version="$(jq -r '.profile.version' "${PROFILE_FILE}")"
backend_version="$(jq -r '.template.backend.version' "${TEMPLATE_FILE}")" installer_type="$(jq -r '.profile.installer.type // "unbekannt"' "${PROFILE_FILE}")"
printf '\n' printf '\n'
printf 'Geplanter Installationsvorgang\n' printf 'Geplanter Installationsvorgang\n'
printf '==============================\n\n' printf '==============================\n\n'
printf 'Bereitstellungsvorlage: %s\n' "${label}" printf 'Profil: %s\n' "${label}"
printf 'Workspace: %s\n' "${workspace}" printf 'Distribution: %s %s\n' "${distribution}" "${version}"
printf 'Backend: %s %s\n' "${backend_name}" "${backend_version}" printf 'Installer: %s\n' "${installer_type}"
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'
@ -105,7 +104,7 @@ confirm_installation() {
main() { main() {
require_root require_root
prepare_runtime prepare_runtime
validate_template validate_profile
show_installation_plan show_installation_plan
confirm_installation confirm_installation
} }

View File

@ -1,119 +0,0 @@
#!/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

@ -1,70 +0,0 @@
#!/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 "$@"