1fa3250aa3
scripts/iso/: a DooPlex pipeline (build-felhom-iso.sh + Dockerfile.assistant) that turns the official PVE ISO into a Felhom auto-install ISO whose first-boot stub installs a retry-forever felhom-bootstrap unit which unattended-fetches felhom-host-install.sh from the public felhom.eu/scripts channel and runs it until the host is enrolled + a guest provisioned. host-install is UNMODIFIED (invoked only). - build gates the answer on validate-answer OUTPUT text, never $? (spike S1 exit-0 trap) - stub is from-iso, fully-up, exactly-once; retry unit owns all network work (S8a) - retry-vs-resume encoded once: plain first, --resume when install state exists (v1.11.3) - secret-bearing (embeds the retrieval passphrase): supervised/single-use; env shredded on success Validated on VM 310: build gate + red-proof, disk-filter fail-safe, chain + retry, resume-decision, exactly-once, no-net retry+recovery. Terminal host-install rc-0 success operator-gated (drill customer needs the password-gated create-UI). scripts v1.16.0; ROADMAP R-21 -> in-progress. Detail in REPORT.md.
219 lines
11 KiB
Bash
219 lines
11 KiB
Bash
#!/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 <<EOF
|
|
Usage: build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
|
|
|
|
Required:
|
|
--pve-iso PATH pre-downloaded official PVE ISO (not fetched here)
|
|
--iso-sha256 SHA expected sha256 of --pve-iso (verified before build; abort on mismatch)
|
|
--profile FILE build profile (fqdn + [disk-setup]); see profiles/ and README
|
|
--bootstrap-env FILE the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval passphrase).
|
|
Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
|
|
Options:
|
|
--out DIR output directory (default: \$HOME/felhom-iso/out)
|
|
--pve-version VER override PVE version tag (default: parsed from the ISO filename)
|
|
--dry-run print the steps without producing an ISO
|
|
-h, --help this help
|
|
EOF
|
|
}
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--pve-iso) PVE_ISO="$2"; shift 2 ;;
|
|
--iso-sha256) ISO_SHA256="$2"; shift 2 ;;
|
|
--profile) PROFILE="$2"; shift 2 ;;
|
|
--bootstrap-env) BOOTSTRAP_ENV="$2"; shift 2 ;;
|
|
--out) OUT_DIR="$2"; shift 2 ;;
|
|
--pve-version) PVE_VERSION="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "unknown argument: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
|
|
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
|
|
[[ -n "$PROFILE" ]] || die "--profile is required"
|
|
[[ -n "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env is required"
|
|
[[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO"
|
|
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
|
|
[[ -f "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env not found: $BOOTSTRAP_ENV"
|
|
|
|
command -v docker >/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" <<EOF
|
|
Felhom bare-metal ISO build manifest (R-21 slice A)
|
|
built : $(date -Is)
|
|
iso-version-tag : v${ISO_VERSION}
|
|
pve-version : ${PVE_VERSION}
|
|
source-iso : ${ISO_BASE}
|
|
source-iso-sha256 : ${ISO_SHA256}
|
|
assistant-version : ${ASSISTANT_VER}
|
|
profile : ${PROFILE_NAME}
|
|
fqdn : ${FELHOM_FQDN}
|
|
host-install-url : $(grep -oE 'FELHOM_INSTALL_URL=[^ ]*' "$BOOTSTRAP_ENV" 2>/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."
|