#!/bin/bash #=============================================================================== # build-felhom-iso.sh — R-21 slice A: turn the official PVE ISO into a Felhom auto-install ISO. # # Renders answer.toml (from answer.toml.tmpl + a profile), mints a fresh THROWAWAY root hash, # gates the answer through validate-answer by PARSING ITS OUTPUT (never $? — validate-answer returns # exit 0 even on failure, spike S1 trap), renders the first-boot stub (injecting the bootstrap # script/unit/env), and runs prepare-iso --fetch-from iso --on-first-boot. Emits the ISO + sha256 + # a build manifest. # # SECRET-BEARING: if the bootstrap-env carries a retrieval passphrase (it must, for an unattended # install — see README "secret-bearing"), the produced ISO embeds it. Supervised/single-use only; # never distributed; delete after the run. The build log says so loudly. # # Runs on DooPlex; delegates validate-answer + prepare-iso to the felhom-iso-assistant container. #=============================================================================== set -euo pipefail ISO_VERSION="1.16.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION). IMAGE="${FELHOM_ISO_ASSISTANT_IMAGE:-felhom-iso-assistant:trixie}" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # --- logging (host-install idiom) ----------------------------------------------------------------- if [[ -t 1 ]]; then RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; BLUE=$'\033[0;34m'; CYAN=$'\033[0;36m'; NC=$'\033[0m' else RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; NC=""; fi log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; } log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; } die() { log_error "$1"; exit 1; } PVE_ISO=""; ISO_SHA256=""; PROFILE=""; BOOTSTRAP_ENV=""; OUT_DIR="${HOME}/felhom-iso/out"; PVE_VERSION=""; DRY_RUN=false usage() { cat </dev/null || die "docker not found (needed for the assistant container)" docker image inspect "$IMAGE" >/dev/null 2>&1 || die "assistant image '$IMAGE' not found — build it: docker build -f $HERE/Dockerfile.assistant -t $IMAGE $HERE" # --- verify source ISO ---------------------------------------------------------------------------- log_step "verifying source ISO sha256" actual_sha=$(sha256sum "$PVE_ISO" | awk '{print $1}') [[ "$actual_sha" == "$ISO_SHA256" ]] || die "ISO sha256 MISMATCH: expected $ISO_SHA256, got $actual_sha" log_success "source ISO sha256 OK ($actual_sha)" if [[ -z "$PVE_VERSION" ]]; then PVE_VERSION=$(basename "$PVE_ISO" | sed -E 's/^proxmox-ve_(.+)\.iso$/\1/') [[ "$PVE_VERSION" != "$(basename "$PVE_ISO")" ]] || die "cannot parse PVE version from '$(basename "$PVE_ISO")' — pass --pve-version" fi PROFILE_NAME="$(basename "$PROFILE")"; PROFILE_NAME="${PROFILE_NAME%.profile}" # --- load + validate profile ---------------------------------------------------------------------- log_step "loading profile: $PROFILE" FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY="" # shellcheck disable=SC1090 source "$PROFILE" [[ -n "$FELHOM_FQDN" ]] || die "profile missing FELHOM_FQDN" [[ -n "$FELHOM_DISK_SETUP" ]] || die "profile missing FELHOM_DISK_SETUP" # Optional emergency/validation SSH key baked into the installed root account. ROOT_SSH_LINE="" [[ -n "$FELHOM_ROOT_SSH_KEY" ]] && ROOT_SSH_LINE="root-ssh-keys = [\"${FELHOM_ROOT_SSH_KEY}\"]" # --- validate bootstrap-env (secret-bearing detection) -------------------------------------------- log_step "checking bootstrap-env (secret-bearing detection)" ( set +e FELHOM_CUSTOMER_ID=""; FELHOM_MODE=""; FELHOM_RETRIEVAL_PASSPHRASE="" # shellcheck disable=SC1090 source "$BOOTSTRAP_ENV" [[ -n "$FELHOM_CUSTOMER_ID" ]] || { echo "MISSING FELHOM_CUSTOMER_ID"; exit 3; } [[ -n "$FELHOM_MODE" ]] || { echo "MISSING FELHOM_MODE"; exit 3; } [[ -n "$FELHOM_RETRIEVAL_PASSPHRASE" ]] || { echo "MISSING FELHOM_RETRIEVAL_PASSPHRASE"; exit 3; } ) || die "bootstrap-env invalid ($BOOTSTRAP_ENV) — must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE" SECRET_BEARING="yes" # a valid bootstrap-env always carries the retrieval passphrase log_warn "this ISO will be SECRET-BEARING (embeds the customer retrieval passphrase) — supervised/single-use only" # --- workspace ------------------------------------------------------------------------------------ WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")" cleanup() { rm -rf "$WORK"; } trap cleanup EXIT mkdir -p "$OUT_DIR" "$WORK/tmp" ISO_DIR="$(cd "$(dirname "$PVE_ISO")" && pwd)"; ISO_BASE="$(basename "$PVE_ISO")" # --- mint fresh THROWAWAY root hash --------------------------------------------------------------- log_step "minting fresh throwaway root password hash" ROOT_PLAIN="felhom-throwaway-$(head -c12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9')" ROOT_HASH="$(openssl passwd -6 "$ROOT_PLAIN")" unset ROOT_PLAIN [[ -n "$ROOT_HASH" ]] || die "failed to mint root hash" log_info "throwaway root hash written to the answer file (value stored out-of-band, not logged)" # --- render answer.toml (pure bash param-expansion; no metachar hazards) --------------------------- log_step "rendering answer.toml" ANSWER="$WORK/answer.toml" : > "$ANSWER" while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == "__DISK_SETUP__" ]]; then printf '%s\n' "$FELHOM_DISK_SETUP" >> "$ANSWER" elif [[ "$line" == "__ROOT_SSH_KEYS__" ]]; then [[ -n "$ROOT_SSH_LINE" ]] && printf '%s\n' "$ROOT_SSH_LINE" >> "$ANSWER" # blank -> omit line else line="${line//__FQDN__/$FELHOM_FQDN}" line="${line//__ROOT_HASH__/$ROOT_HASH}" printf '%s\n' "$line" >> "$ANSWER" fi done < "$HERE/answer.toml.tmpl" # --- validate-answer OUTPUT-PARSE gate (never $? — spike S1) -------------------------------------- gate_validate_answer() { local out out=$(docker run --rm -v "$WORK":/work "$IMAGE" \ proxmox-auto-install-assistant validate-answer /work/answer.toml 2>&1) || true echo "----- validate-answer output -----"; echo "$out"; echo "----------------------------------" # LOAD-BEARING: validate-answer exits 0 even on failure; decide on the MESSAGE TEXT, not $?. if echo "$out" | grep -q "parsed successfully" && ! echo "$out" | grep -qi "Found issues"; then return 0 fi return 1 } log_step "validating rendered answer (output-parse gate)" if $DRY_RUN; then log_dry "docker run … validate-answer /work/answer.toml (output-parse gate)" else gate_validate_answer || die "answer validation FAILED — NO ISO produced (fix the answer/profile)" log_success "answer validated" fi # --- render the first-boot stub (inject bootstrap script/unit/env as base64) ---------------------- log_step "rendering first-boot stub" STUB="$WORK/stub-first-boot.sh" sh_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.sh")" unit_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.service")" env_b64="$(base64 -w0 < "$BOOTSTRAP_ENV")" awk -v sh="$sh_b64" -v unit="$unit_b64" -v env="$env_b64" ' { gsub(/@@BOOTSTRAP_SH_B64@@/, sh); gsub(/@@BOOTSTRAP_UNIT_B64@@/, unit); gsub(/@@BOOTSTRAP_ENV_B64@@/, env); print } ' "$HERE/stub-first-boot.sh" > "$STUB" chmod 0755 "$STUB" grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers — injection failed" # --- prepare-iso ---------------------------------------------------------------------------------- OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}.iso" log_step "building ISO: $(basename "$OUT_ISO")" if $DRY_RUN; then log_dry "docker run … prepare-iso /iso/$ISO_BASE --fetch-from iso --answer-file /work/answer.toml --on-first-boot /work/stub-first-boot.sh --output /work/out.iso" log_info "DRY-RUN: no ISO produced" exit 0 fi docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \ proxmox-auto-install-assistant prepare-iso "/iso/$ISO_BASE" \ --fetch-from iso --answer-file /work/answer.toml \ --on-first-boot /work/stub-first-boot.sh \ --tmp /work/tmp --output /work/out.iso [[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output" cp "$WORK/out.iso" "$OUT_ISO" # --- sha256 + manifest ---------------------------------------------------------------------------- OUT_SHA="$(sha256sum "$OUT_ISO" | awk '{print $1}')" OUT_SIZE="$(stat -c '%s' "$OUT_ISO")" ASSISTANT_VER="$(docker run --rm "$IMAGE" proxmox-auto-install-assistant --version 2>&1 | head -1)" echo "$OUT_SHA $(basename "$OUT_ISO")" > "$OUT_ISO.sha256" cat > "$OUT_ISO.manifest.txt" </dev/null || echo 'https://felhom.eu/scripts/felhom-host-install.sh (default)') secret-bearing : ${SECRET_BEARING} (embeds the customer retrieval passphrase — supervised/single-use, delete after the run) output : $(basename "$OUT_ISO") output-sha256 : ${OUT_SHA} output-size-bytes : ${OUT_SIZE} EOF log_success "ISO built: $OUT_ISO" log_info "sha256 : $OUT_SHA" log_info "size : $OUT_SIZE bytes" log_info "manifest : $OUT_ISO.manifest.txt" log_warn "SECRET-BEARING ISO (embeds the retrieval passphrase). Supervised/single-use; never distribute; delete after the run."