diff --git a/backends/mint/backend.sh b/backends/mint/backend.sh new file mode 100644 index 0000000..b730d9e --- /dev/null +++ b/backends/mint/backend.sh @@ -0,0 +1,165 @@ +#!/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="mint" + +BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PRESEED_TEMPLATE="${BACKEND_DIR}/preseed.tpl" +POSTINSTALL_SCRIPT="${BACKEND_DIR}/postinstall.sh" + +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 base64; do + command -v "${cmd}" >/dev/null 2>&1 || + { backend_fatal "Benötigtes Werkzeug fehlt: ${cmd}"; return 1; } + done + + [[ -r "${PRESEED_TEMPLATE}" ]] || + { backend_fatal "Preseed-Template nicht gefunden: ${PRESEED_TEMPLATE}"; return 1; } + + [[ -r "${POSTINSTALL_SCRIPT}" ]] || + { backend_fatal "Postinstall-Skript nicht gefunden: ${POSTINSTALL_SCRIPT}"; 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; } + + # disk_encryption wird für Mint (noch) nicht unterstützt - kein getesteter + # LUKS-Preseed-Mechanismus (anders als Fedoras "autopart --encrypted"). + if [[ "$(jq --raw-output '.runtime_blueprint.installation_directives.disk_encryption' "${RUNTIME_BLUEPRINT_FILE}")" == "true" ]]; then + backend_fatal "disk_encryption=true wird vom Mint-Backend derzeit nicht unterstützt." + return 1 + fi + + backend_log "Runtime Blueprint ist gültig für Backend '${BACKEND_KEY}'." +} + +backend_generate_config() { + local hostname + local device_id + local partitioning + local secure_boot_required + local partman_recipe + local blueprints_json + local postinstall_rendered + local postinstall_b64 + + [[ -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; } + + device_id="$(jq --raw-output '.device.id // empty' "${SERVER_RESPONSE_FILE}")" + [[ -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}")" + 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 + ;; + esac + + if [[ "${secure_boot_required}" == "true" ]]; then + backend_log "Hinweis: secure_boot_required=true wird derzeit nicht in der Preseed-Konfiguration durchgesetzt (Phase 1)." + fi + + blueprints_json="$(jq --compact-output '.runtime_blueprint.blueprints' "${RUNTIME_BLUEPRINT_FILE}")" + + # Erste Stufe: postinstall.sh-Platzhalter auflösen. + postinstall_rendered="$( + TUXFLOTTE_DEVICE_ID="${device_id}" \ + TUXFLOTTE_BLUEPRINTS_JSON="${blueprints_json}" \ + envsubst '${TUXFLOTTE_DEVICE_ID} ${TUXFLOTTE_BLUEPRINTS_JSON}' \ + <"${POSTINSTALL_SCRIPT}" + )" + + if grep -q '\${TUXFLOTTE_' <<<"${postinstall_rendered}"; then + backend_fatal "postinstall.sh enthält nach envsubst nicht aufgelöste Platzhalter." + return 1 + fi + + # base64-Kodierung: mehrzeilige/zitierte Preseed-Werte brechen unter + # Debconf lautlos (real erprobt, siehe backends/mint/wlan-test.seed) - + # als einzeiliger Base64-Blob besteht der success_command-Wert nur noch + # aus unkritischen Zeichen. + postinstall_b64="$(printf '%s' "${postinstall_rendered}" | base64 -w0)" + + # Zweite Stufe: preseed.tpl mit allen Werten inkl. des fertigen Base64-Blobs auflösen. + TUXFLOTTE_HOSTNAME="${hostname}" \ + TUXFLOTTE_PARTMAN_RECIPE="${partman_recipe}" \ + TUXFLOTTE_POSTINSTALL_B64="${postinstall_b64}" \ + envsubst '${TUXFLOTTE_HOSTNAME} ${TUXFLOTTE_PARTMAN_RECIPE} ${TUXFLOTTE_POSTINSTALL_B64}' \ + <"${PRESEED_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 "Provisioning-Agent-Einrichtung erfolgt im ubiquity/success_command der Preseed-Konfiguration (Agent-Abruf, Bootstrap-Registrierung, systemd-Aktivierung)." +} diff --git a/backends/mint/postinstall.sh b/backends/mint/postinstall.sh new file mode 100644 index 0000000..2e03ced --- /dev/null +++ b/backends/mint/postinstall.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Lesbare Referenzfassung des Agent-Bootstraps, den backend_generate_config() +# in backend.sh zur Laufzeit envsubst-auflöst und anschließend base64-kodiert +# in preseed.tpls ubiquity/success_command einsetzt (siehe backend.sh). Diese +# Datei selbst wird nie direkt ausgeführt - sie existiert, damit der Code +# lesbar bleibt statt nur als Base64-Blob im Preseed zu existieren. +# +# Inhaltlich das Bash-Pendant zu backends/fedora/kickstart.tpl %post: gleiche +# curl/jq-Aufrufe, nur eingebettet über ubiquity/success_command (in-target, +# chrooted) statt Kickstart %post. +tuxflotte_agent_fatal() { + echo "tuxflotte: Provisioning-Agent-Einrichtung fehlgeschlagen: $*" >> /var/log/tuxflotte-postinstall.log + exit 1 +} + +ANODE_URL="https://anode.tuxflotte.de" +AGENT_REPO_RAW="https://git.tuxflotte.de/admin/provisioning-agent/raw/branch/main" + +install -d -m 0700 /etc/tuxflotte || + tuxflotte_agent_fatal "Verzeichnis /etc/tuxflotte konnte nicht angelegt werden." + +cat > /etc/tuxflotte/runtime_blueprint.json <<'RUNTIME_BLUEPRINT_EOF' +${TUXFLOTTE_BLUEPRINTS_JSON} +RUNTIME_BLUEPRINT_EOF + +install -d /opt/tuxflotte/agent || + tuxflotte_agent_fatal "Verzeichnis /opt/tuxflotte/agent konnte nicht angelegt werden." + +curl --silent --show-error --fail --location \ + --output /opt/tuxflotte/agent/agent.py \ + "${AGENT_REPO_RAW}/agent.py" || + tuxflotte_agent_fatal "agent.py konnte nicht von ${AGENT_REPO_RAW} geladen werden." + +curl --silent --show-error --fail --location \ + --output /etc/systemd/system/tuxflotte-agent.service \ + "${AGENT_REPO_RAW}/tuxflotte-agent.service" || + tuxflotte_agent_fatal "tuxflotte-agent.service konnte nicht von ${AGENT_REPO_RAW} geladen werden." + +AGENT_BOOTSTRAP_RESPONSE="$( + curl --silent --show-error --fail --location \ + --header 'Content-Type: application/json' \ + --data-binary "{\"device_id\": \"${TUXFLOTTE_DEVICE_ID}\"}" \ + "${ANODE_URL}/api/v1/agent/bootstrap" +)" || + tuxflotte_agent_fatal "Bootstrap-Aufruf gegen ${ANODE_URL} ist fehlgeschlagen." + +jq --exit-status '.success == true' <<<"${AGENT_BOOTSTRAP_RESPONSE}" >/dev/null || + tuxflotte_agent_fatal "Server hat den Bootstrap abgelehnt: ${AGENT_BOOTSTRAP_RESPONSE}" + +jq --null-input \ + --arg device_id "${TUXFLOTTE_DEVICE_ID}" \ + --argjson response "${AGENT_BOOTSTRAP_RESPONSE}" \ + '{device_id: $device_id, agent_secret: $response.agent_secret}' \ + > /etc/tuxflotte/agent.credentials || + tuxflotte_agent_fatal "Credentials-Datei konnte nicht erzeugt werden." +chmod 0600 /etc/tuxflotte/agent.credentials + +systemctl enable tuxflotte-agent.service || + tuxflotte_agent_fatal "systemd-Dienst tuxflotte-agent konnte nicht aktiviert werden." + +echo "tuxflotte: Runtime Blueprint unter /etc/tuxflotte/runtime_blueprint.json hinterlegt." >> /var/log/tuxflotte-postinstall.log +echo "tuxflotte: Provisioning-Agent installiert, registriert und für den ersten Boot aktiviert." >> /var/log/tuxflotte-postinstall.log diff --git a/backends/mint/preseed.tpl b/backends/mint/preseed.tpl new file mode 100644 index 0000000..fb6e4c5 --- /dev/null +++ b/backends/mint/preseed.tpl @@ -0,0 +1,52 @@ +### Tuxflotte Auto-Install Preseed fuer Linux Mint (Ubiquity/Debian-Installer). +### Liegt direkt auf dem Medium (file=/cdrom/preseed/tuxflotte.seed), keine +### Netz-Zustellung noetig -- analog zu Fedoras inst.ks=cdrom:/ks.cfg. +### +### d-i preseed/early_command wird bewusst NICHT verwendet -- unter Ubiquity +### bestaetigt wirkungslos (Ubiquity nutzt eigene Python-Plugins statt der +### klassischen Debian-Installer-Komponenten, an die early_command haengt). + +d-i debian-installer/locale string de_DE.UTF-8 +d-i keyboard-configuration/xkb-keymap select de +d-i keyboard-configuration/layoutcode string de + +d-i netcfg/get_hostname string ${TUXFLOTTE_HOSTNAME} +d-i netcfg/get_domain string unassigned-domain + +# Lab-Bootstrap-Zugangsdaten. Ersetzt ein noch fehlendes Secret-Reference-Modell +# (siehe 09-data-model-v1.md) und darf nicht als Produktionsmechanismus gelten. +d-i passwd/user-fullname string Tuxflotte +d-i passwd/username string tuxflotte +d-i passwd/user-password password test123 +d-i passwd/user-password-again password test123 +d-i user-setup/allow-password-weak boolean true + +d-i clock-setup/utc boolean true +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-partitioning/confirm_write_new_label boolean true +d-i partman/choose_partition select finish +d-i partman/confirm boolean true +d-i partman/confirm_nooverwrite boolean true + +ubiquity ubiquity/summary note +ubiquity ubiquity/reboot boolean true +ubiquity ubiquity/use_nonfree boolean true + +# Pendant zu Fedoras kickstart.tpl %packages (ansible-core, git) - der +# Provisioning Agent braucht ansible-pull, das wiederum git zum Klonen des +# Ansible-Repos. Ohne diese Zeile fehlen beide auf einer frischen +# Mint-Installation, der Agent-Dienst laeuft dann in einer +# Restart-Fehlerschleife ("ansible-pull nicht gefunden") - real gegen eine +# frische Testinstallation gefunden und verifiziert (2026-08-04). +d-i pkgsel/include string ansible-core git + +# success_command laeuft in-target (gechrootet ins Zielsystem) nach der +# Paketinstallation, vor dem Reboot - das Pendant zu Kickstarts %post. Der +# Payload ist base64-kodiert (backend_generate_config() in backend.sh baut +# ihn aus postinstall.sh): mehrzeilige/zitierte Preseed-Werte brechen unter +# Debconf lautlos, Base64 umgeht das (real erprobt, siehe wlan-test.seed). +ubiquity ubiquity/success_command string in-target bash -c 'echo ${TUXFLOTTE_POSTINSTALL_B64} | base64 -d | bash' diff --git a/grub/mint-boot-grub.cfg b/grub/mint-boot-grub.cfg new file mode 100644 index 0000000..acf9115 --- /dev/null +++ b/grub/mint-boot-grub.cfg @@ -0,0 +1,41 @@ +loadfont unicode + +set color_normal=white/black +set color_highlight=black/light-gray + +set timeout=30 + +menuentry "Start Linux Mint 22.3 Cinnamon 64-bit" --class linuxmint { + set gfxpayload=keep + linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash -- + initrd /casper/initrd.lz +} +menuentry "Start Linux Mint 22.3 Cinnamon 64-bit (compatibility mode)" { + linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} noapic noacpi nosplash irqpoll nomodeset -- + initrd /casper/initrd.lz +} +menuentry "OEM install (for manufacturers)" { + set gfxpayload=keep + linux /casper/vmlinuz oem-config/enable=true only-ubiquity boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} quiet splash -- + initrd /casper/initrd.lz +} +menuentry "Tuxflotte Auto-Install (Linux Mint 22.3 Cinnamon)" --class linuxmint { + set gfxpayload=keep + linux /casper/vmlinuz boot=casper uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint iso-scan/filename=${iso_path} file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash -- + initrd /casper/initrd.lz +} +grub_platform +if [ "$grub_platform" = "efi" ]; then +menuentry 'Von lokaler Festplatte booten (Standard)' { + exit 1 +} +set default="Von lokaler Festplatte booten (Standard)" +menuentry 'UEFI Firmware Settings' { + fwsetup +} +menuentry 'Memory test' { + linux /boot/memtest.efi +} +else +set default="0" +fi diff --git a/grub/mint-isolinux-live.cfg b/grub/mint-isolinux-live.cfg new file mode 100644 index 0000000..93fd5dc --- /dev/null +++ b/grub/mint-isolinux-live.cfg @@ -0,0 +1,57 @@ +timeout 100 + +menu background splash.png +menu title Welcome to Linux Mint 22.3 64-bit + +menu color screen 37;40 #80ffffff #00000000 std +MENU COLOR border 30;44 #40ffffff #a0000000 std +MENU COLOR title 1;36;44 #ffffffff #a0000000 std +MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all +MENU COLOR unsel 37;44 #50ffffff #a0000000 std +MENU COLOR help 37;40 #c0ffffff #a0000000 std +MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std +MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std +MENU COLOR msg07 37;40 #90ffffff #a0000000 std +MENU COLOR tabmsg 31;40 #ffDEDEDE #00000000 std +MENU WIDTH 78 +MENU MARGIN 15 +MENU ROWS 6 +MENU VSHIFT 10 +MENU TABMSGROW 12 +MENU CMDLINEROW 12 +MENU HELPMSGROW 16 +MENU HELPMSGENDROW 29 + +label tuxflotte + menu label Tuxflotte Auto-Install + kernel /casper/vmlinuz + append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint file=/cdrom/preseed/tuxflotte.seed automatic-ubiquity noprompt debian-installer/language=de keyboard-configuration/layoutcode=de quiet splash -- + +label live + menu label Start Linux Mint + kernel /casper/vmlinuz + append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash -- + +label compat + menu label Start Linux Mint in compatibility mode + linux /casper/vmlinuz + append boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint noapic noacpi nosplash irqpoll nomodeset -- + +label oem + menu label OEM install (for manufacturers) + linux /casper/vmlinuz + append oem-config/enable=true only-ubiquity boot=casper initrd=/casper/initrd.lz uuid=6e72f523-dc09-4880-8910-93ffa64401c5 username=mint hostname=mint quiet splash -- + +label hdt + menu label Hardware Detection + kernel hdt.c32 + +label local + menu label Boot from local drive + menu default + COM32 chain.c32 + APPEND hd0 + +label memtest + menu label Memory test + linux /boot/memtest.bin diff --git a/scripts/build.sh b/scripts/build.sh index 1cf9ed0..caa608a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -2,6 +2,7 @@ set -euo pipefail SOURCE_ISO="${1:-}" +BACKEND="${2:-}" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BASE_DIR="$(cd "$REPO_DIR/.." && pwd)" @@ -9,12 +10,19 @@ BUILD_DIR="$BASE_DIR/build" WORK_DIR="$BUILD_DIR/work" OUTPUT_DIR="$BUILD_DIR/output" +# Test-Device fuer den in bake_test_preseed() eingebrannten, nicht +# personalisierten Test-Preseed (Mint) - dasselbe "Default Lab"-Testgeraet, +# das schon fuer die Fedora-Verifikation genutzt wurde. Echte Personalisierung +# pro Kunde ist nicht Teil dieses Build-Skripts (siehe ADR-0011-Kontext). +TEST_DEVICE_ID="a0238a0b-d2b5-4516-a6ce-da7170041d11" +TEST_HOSTNAME="tuxflotte-mint-test" + usage() { - echo "Usage: $0 /path/to/source.iso" + echo "Usage: $0 /path/to/source.iso " } check_input() { - if [[ -z "$SOURCE_ISO" ]]; then + if [[ -z "$SOURCE_ISO" || -z "$BACKEND" ]]; then usage exit 1 fi @@ -23,6 +31,14 @@ check_input() { echo "Error: ISO not found: $SOURCE_ISO" exit 1 fi + + case "$BACKEND" in + fedora|mint) ;; + *) + echo "Error: unbekanntes Backend '$BACKEND' (erwartet: fedora oder mint)" + exit 1 + ;; + esac } check_dependencies() { @@ -32,6 +48,15 @@ check_dependencies() { exit 1 fi done + + if [[ "$BACKEND" == "mint" ]]; then + for cmd in jq envsubst base64; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: missing dependency: $cmd" + exit 1 + fi + done + fi } prepare_dirs() { @@ -48,22 +73,88 @@ extract_iso() { patch_grub() { echo "Installing Tuxflotte GRUB configuration..." - cp "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \ - "$WORK_DIR/EFI/BOOT/grub.cfg" + if [[ "$BACKEND" == "fedora" ]]; then + cp "$REPO_DIR/grub/EFI-BOOT-grub.cfg" \ + "$WORK_DIR/EFI/BOOT/grub.cfg" - cp "$REPO_DIR/grub/boot-grub2-grub.cfg" \ - "$WORK_DIR/boot/grub2/grub.cfg" + cp "$REPO_DIR/grub/boot-grub2-grub.cfg" \ + "$WORK_DIR/boot/grub2/grub.cfg" + else + # Mint hat, anders als Fedora, keine zweite ESP/FAT-Kopie des GRUB-Menüs, + # die separat gepatcht werden müsste (real gegen die vorhandene + # tuxflotte-mint-test.iso verifiziert - x86_64-efi/grub.cfg ist nur ein + # Loader-Stub, der per `source /boot/grub/grub.cfg` zurückverweist). + cp "$REPO_DIR/grub/mint-boot-grub.cfg" \ + "$WORK_DIR/boot/grub/grub.cfg" + + cp "$REPO_DIR/grub/mint-isolinux-live.cfg" \ + "$WORK_DIR/isolinux/live.cfg" + fi } verify_workdir() { echo "Verifying workdir..." - grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg" - grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg" + if [[ "$BACKEND" == "fedora" ]]; then + grep -q "Tuxflotte" "$WORK_DIR/EFI/BOOT/grub.cfg" + grep -q "Tuxflotte" "$WORK_DIR/boot/grub2/grub.cfg" + else + grep -q "Tuxflotte" "$WORK_DIR/boot/grub/grub.cfg" + grep -q "Tuxflotte" "$WORK_DIR/isolinux/live.cfg" + fi echo "GRUB verification passed." } +bake_test_preseed() { + [[ "$BACKEND" == "mint" ]] || return 0 + + echo "Rendering test preseed (nicht personalisiert, siehe TEST_DEVICE_ID)..." + + local preseed_dir="$WORK_DIR/preseed" + local runtime_blueprint_file="$WORK_DIR/runtime_blueprint.json" + local server_response_file="$WORK_DIR/response.json" + + mkdir -p "$preseed_dir" + + cat > "$runtime_blueprint_file" < "$server_response_file" <