70d034a3f3
The 1.26.1 manifest — the file a tester reads to know what they have, and which is published alongside the ISO — carried four statements that were false for a release build: boot-menu 'single entry Felhom telepítés, default, 5s' -> it has TWO, timeout 15 menu-entries '1 (... timeout 5s)' -> 2 menu-removed 'Graphical, Terminal UI, ...' -> those are exactly what it SHIPS kernel-line '... proxmox-start-auto-installer' -> the release menu deliberately has none secret-bearing 'no (embeds the customer retrieval passphrase...)' -> self-contradictory All four came from branding/pairing notes that predate --release and were emitted unconditionally. A public artifact whose own manifest misdescribes it is the false-claim class this arc exists to correct, so it is fixed before publication rather than after.
521 lines
33 KiB
Bash
Executable File
521 lines
33 KiB
Bash
Executable File
#!/bin/bash
|
|
#===============================================================================
|
|
# build-felhom-iso.sh — R-21 slice A+B+C: turn the official PVE ISO into a Felhom auto-install ISO.
|
|
#
|
|
# SLICE C — --pairing builds the GENERIC, SECRET-FREE universal ISO: no customer-id / passphrase is
|
|
# baked in. The box registers itself at the hub as an UNCLAIMED APPLIANCE, the operator binds it to a
|
|
# customer, and the hub delivers the credentials ONCE — then the box completes day-0 exactly like a
|
|
# direct-mode box. Direct mode (--bootstrap-env, secret-bearing, operator-prepped) is unchanged.
|
|
#
|
|
# 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.
|
|
#
|
|
# R-38 GRUB SLICE (v1.22.0) — every ISO is now REPACKED after prepare-iso to carry the Felhom boot
|
|
# screen and, more importantly, a menu with exactly ONE entry ("Felhom telepítés", default, 5s). The
|
|
# stock PVE menu offers Graphical/Terminal-UI/serial installers plus an Advanced Options submenu with
|
|
# debug/nomodeset/rescue entries — every one of them a route into the MANUAL installer, whose first
|
|
# question is which disk to wipe. A customer, or their helpful nephew, must not be able to reach it
|
|
# from a boot menu; those entries are not hidden, they are not emitted. The kernel/append line is
|
|
# lifted verbatim from the ISO's own automated entry, so boot behavior is unchanged. See iso-repack.sh.
|
|
# (--no-brand exists for debugging the stock menu; it is not a shipping mode.)
|
|
#
|
|
# SLICE B — --loader shim|mkimage (default shim). shim = the stock output (MS-signed shim→GRUB chain,
|
|
# keeps Secure Boot working on compliant firmware, spike S2b). mkimage = replace the ISO's UEFI boot
|
|
# path with a monolithic grub-mkimage-built BOOTX64.EFI built from the ISO's OWN GRUB modules — the
|
|
# workaround PROVEN LIVE during the N100 run (VALIDATION-n100-baremetal F1: cheap AMI AN3PLUS-class
|
|
# firmware can't relocate the ISO's signed GRUB from USB, `relocation 0x0`). The mkimage loader is
|
|
# UNSIGNED → the target board MUST have Secure Boot OFF (documented in the n100 profile's prep). The
|
|
# loader surgery runs AFTER prepare-iso (the assistant's answer/first-boot payload is provably
|
|
# untouched except the loader path).
|
|
#
|
|
# 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 + the mkimage surgery to the
|
|
# felhom-iso-assistant container (which carries proxmox-auto-install-assistant, xorriso, grub-mkimage,
|
|
# and mtools).
|
|
#===============================================================================
|
|
set -euo pipefail
|
|
|
|
# R-128 RULING (2026-07-31): the previous comment here claimed this "aligns with felhom-host-install
|
|
# SCRIPT_VERSION". That was a COMMENT, not an assertion — `SCRIPT_VERSION` appeared nowhere in
|
|
# scripts/iso/ except inside that sentence, so nothing ever evaluated it, and the two had drifted
|
|
# (1.25.0 vs 1.22.0). The fix is NOT to turn it into a real assertion, because the coupling it claimed
|
|
# does not exist: the ISO is a frozen artifact, while felhom-host-install.sh is fetched at RUN TIME
|
|
# from the website's git-sync of `main` (R-94/R-110), so whatever version an ISO carries, the script a
|
|
# box runs is always current. Coupling them would invent a constraint. The claim is corrected instead.
|
|
ISO_VERSION="1.26.1" # the ISO's own version. INDEPENDENT of felhom-host-install.sh's SCRIPT_VERSION,
|
|
# which is fetched at run time from main and is not frozen into the image.
|
|
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; }
|
|
|
|
# ver_ge A B → true iff version A >= version B (dotted numeric, sort -V).
|
|
ver_ge() { [[ "$1" == "$2" ]] && return 0; [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" == "$1" ]]; }
|
|
|
|
# R-71 STANDING GATE (v1.25.0): every ISO build asserts the golden it will install is >= the managed
|
|
# controller floor. If the floor exceeds golden, a freshly-installed box boots BELOW the floor and the
|
|
# day-0 managed update fires — racing the offsite apply-bridge exactly as it did on demo-hp
|
|
# (DIAG-f10-demo-hp-offsite-2026-07-23 / R-71). The build must fail loudly rather than ship that gap.
|
|
# Inputs: FELHOM_ASSERT_GOLDEN + FELHOM_ASSERT_FLOOR (the hub's current artifact_golden_version and
|
|
# min_controller_version — resolve them operator-side and pass in; the publish-train runbook covers
|
|
# how). Both printed. If either is unset the gate is UNENFORCED and says so LOUDLY (never a silent pass).
|
|
assert_golden_ge_floor() {
|
|
local golden="${FELHOM_ASSERT_GOLDEN:-}" floor="${FELHOM_ASSERT_FLOOR:-}"
|
|
if [[ -z "$golden" || -z "$floor" ]]; then
|
|
log_warn "R-71 golden>=floor gate UNENFORCED — pass FELHOM_ASSERT_GOLDEN + FELHOM_ASSERT_FLOOR to enforce (golden='${golden:-unset}' floor='${floor:-unset}')"
|
|
return 0
|
|
fi
|
|
if ver_ge "$golden" "$floor"; then
|
|
log_success "R-71 gate OK: golden $golden >= managed floor $floor"
|
|
else
|
|
die "R-71 BUILD GATE FAILED: golden $golden < managed floor $floor — a fresh box would boot below the floor and the day-0 update would race the offsite apply-bridge. Republish golden >= $floor (and vouch it) before building this ISO."
|
|
fi
|
|
}
|
|
|
|
PVE_ISO=""; ISO_SHA256=""; PROFILE=""; BOOTSTRAP_ENV=""; OUT_DIR="${FELHOM_ISO_OUT:-/mnt/5_hdd/felhom.eu/felhom-iso/out}"; PVE_VERSION=""; DRY_RUN=false
|
|
LOADER_CLI="" # --loader override; empty = fall back to the profile, then the shim default.
|
|
PAIRING=false # --pairing: build the GENERIC secret-free ISO (slice C); no --bootstrap-env.
|
|
RELEASE=false # --release: the PUBLIC image. No answer.toml at all, an INTERACTIVE two-entry menu,
|
|
# and day-0 delivered by a .deb rather than the answer file's [first-boot] hook.
|
|
BRAND=true # R-38: Felhom boot screen + single-entry menu. --no-brand is a debugging escape only.
|
|
BRAND_IMAGE="" # --brand-image override; empty = the website's og-image_2.png (the single source).
|
|
usage() {
|
|
cat <<EOF
|
|
Usage (direct): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
|
|
Usage (generic): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --pairing [options]
|
|
Usage (release): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --release [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
|
|
|
|
Mode (exactly one):
|
|
--bootstrap-env FILE DIRECT mode: the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval
|
|
passphrase). Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
|
|
--pairing PAIRING mode (slice C): the GENERIC, SECRET-FREE universal ISO. The box registers
|
|
itself as an unclaimed appliance at the hub; the operator binds it; the hub
|
|
delivers the customer-id + passphrase ONCE. No customer secret is baked in. The
|
|
hub URL comes from the profile (FELHOM_HUB_URL) or the default.
|
|
Options:
|
|
--loader shim|mkimage UEFI boot loader (default: shim, or the profile's FELHOM_LOADER; --loader wins).
|
|
shim = stock MS-signed chain (Secure Boot OK on compliant firmware).
|
|
mkimage = monolithic grub-mkimage loader for cheap AMI boards that can't boot
|
|
the ISO's GRUB from USB (F1). UNSIGNED -> the target board needs Secure Boot OFF.
|
|
--no-brand DEBUG ONLY: keep the stock PVE boot menu (all installer entries reachable).
|
|
Shipping ISOs are always branded: Felhom boot screen + exactly ONE entry.
|
|
--brand-image FILE override the boot-screen card (default: website/assets/og-image_2.png)
|
|
--out DIR output directory (default: the DooPlex build root
|
|
/mnt/5_hdd/felhom.eu/felhom-iso/out; override via \$FELHOM_ISO_OUT for other hosts)
|
|
--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 ;;
|
|
--pairing) PAIRING=true; shift ;;
|
|
--release) RELEASE=true; shift ;;
|
|
--loader) LOADER_CLI="$2"; shift 2 ;;
|
|
--no-brand) BRAND=false; shift ;;
|
|
--brand-image) BRAND_IMAGE="$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
|
|
[[ -z "$LOADER_CLI" || "$LOADER_CLI" == "shim" || "$LOADER_CLI" == "mkimage" ]] \
|
|
|| die "--loader must be 'shim' or 'mkimage' (got '$LOADER_CLI')"
|
|
|
|
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
|
|
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
|
|
[[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO"
|
|
if $RELEASE; then
|
|
# A release image has no answer file, so it has no fqdn and no [disk-setup] to take from a profile.
|
|
# Accepting one would imply a disk selection the image deliberately does not make.
|
|
[[ -z "$PROFILE" ]] || die "--release takes no --profile (a release image bakes no disk selection)"
|
|
[[ -z "$BOOTSTRAP_ENV" ]] || die "--release and --bootstrap-env are mutually exclusive"
|
|
$PAIRING && die "--release and --pairing are mutually exclusive"
|
|
$BRAND || die "--release requires branding (the two-entry menu IS the release menu)"
|
|
else
|
|
[[ -n "$PROFILE" ]] || die "--profile is required"
|
|
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
|
|
fi
|
|
# Mode: exactly one of --bootstrap-env (direct), --pairing (generic) or --release (public image).
|
|
if $RELEASE; then
|
|
: # validated above; a release image has no bootstrap-env by design
|
|
elif $PAIRING; then
|
|
[[ -z "$BOOTSTRAP_ENV" ]] || die "--pairing and --bootstrap-env are mutually exclusive"
|
|
else
|
|
[[ -n "$BOOTSTRAP_ENV" ]] || die "one of --bootstrap-env (direct), --pairing (generic) or --release is required"
|
|
[[ -f "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env not found: $BOOTSTRAP_ENV"
|
|
fi
|
|
|
|
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 ----------------------------------------------------------------------
|
|
if ! $RELEASE; then
|
|
log_step "loading profile: $PROFILE"
|
|
FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY=""; FELHOM_LOADER=""; FELHOM_HUB_URL=""; FELHOM_INSTALL_URL=""
|
|
# 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}\"]"
|
|
else
|
|
log_info "release mode — no profile, no fqdn, no [disk-setup], no root key"
|
|
FELHOM_FQDN="(none — interactive install)"
|
|
fi
|
|
|
|
# --- resolve the loader mode: --loader wins over the profile's FELHOM_LOADER over the shim default ---
|
|
LOADER="${LOADER_CLI:-${FELHOM_LOADER:-shim}}"
|
|
[[ "$LOADER" == "shim" || "$LOADER" == "mkimage" ]] \
|
|
|| die "profile FELHOM_LOADER must be 'shim' or 'mkimage' (got '$FELHOM_LOADER')"
|
|
if [[ "$LOADER" == "mkimage" ]]; then
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
log_warn "LOADER MODE = mkimage — the UEFI boot path is a monolithic grub-mkimage loader (F1 fix)."
|
|
log_warn "This loader is UNSIGNED: the target board MUST have Secure Boot OFF. shim/SB is bypassed."
|
|
log_warn "The ISO filename gains '-mkimage'; never confuse it with a shim (SB-capable) build."
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
else
|
|
log_info "loader mode = shim (stock MS-signed chain; Secure Boot works on compliant firmware)"
|
|
fi
|
|
|
|
# --- resolve the boot-screen card (R-38) ------------------------------------------------------------
|
|
# Single source of truth: the website asset. NOT copied into scripts/iso/ — a second copy of a brand
|
|
# asset is a second thing to forget to update.
|
|
if $BRAND; then
|
|
[[ -n "$BRAND_IMAGE" ]] || BRAND_IMAGE="$HERE/../../website/assets/og-image_2.png"
|
|
[[ -f "$BRAND_IMAGE" ]] || die "brand image not found: $BRAND_IMAGE (pass --brand-image, or run from the repo checkout)"
|
|
log_info "boot-screen card = $BRAND_IMAGE (menu: exactly ONE entry, 'Felhom telepítés')"
|
|
else
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
log_warn "--no-brand — the STOCK PVE boot menu ships: Graphical/Terminal-UI/serial installers and"
|
|
log_warn "the Advanced Options submenu are all reachable, i.e. a manual 'which disk do I wipe'"
|
|
log_warn "installer is one keypress away. DEBUGGING ONLY — never hand this ISO to a customer."
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
fi
|
|
|
|
# --- mode: DIRECT validates the secret-bearing env; PAIRING is secret-free (env generated below) -----
|
|
if $RELEASE; then
|
|
SECRET_BEARING="no"
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
log_info "RELEASE MODE — the PUBLIC image. No answer.toml, no auto-installer-mode.toml, no root"
|
|
log_info "password, no SSH key, no disk profile. Two INTERACTIVE menu entries; the person"
|
|
log_info "installing chooses the disk. Day-0 rides felhom-bootstrap.deb in /proxmox/packages/."
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
elif $PAIRING; then
|
|
SECRET_BEARING="no"
|
|
PAIR_HUB_URL="${FELHOM_HUB_URL:-https://hub.felhom.eu}"
|
|
PAIR_INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
log_info "PAIRING MODE — building the GENERIC, SECRET-FREE universal ISO (slice C)."
|
|
log_info "The box registers as an unclaimed appliance; the operator binds it; the hub delivers the"
|
|
log_info "customer-id + passphrase ONCE. Baked env carries only the hub URL ($PAIR_HUB_URL) — no secret."
|
|
echo -e "${YELLOW}==================================================================================${NC}"
|
|
else
|
|
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"
|
|
fi
|
|
|
|
# R-71 build gate — before any workspace/mint/build work commits (fails fast + loud).
|
|
assert_golden_ge_floor
|
|
|
|
# --- workspace ------------------------------------------------------------------------------------
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")"
|
|
# chmod first: the mkimage surgery's osirrox extract leaves read-only file modes that rm can't clear.
|
|
# FELHOM_ISO_KEEP_WORK=1 is a DEBUG/TEST escape: keep $WORK for post-mortem (the rootpw-emission
|
|
# harness cross-checks the emitted plaintext against the rendered answer's hash). Never a build mode.
|
|
cleanup() {
|
|
if [[ "${FELHOM_ISO_KEEP_WORK:-0}" == "1" ]]; then
|
|
log_warn "FELHOM_ISO_KEEP_WORK=1 — workspace KEPT at $WORK (debug/test only; delete it yourself)"
|
|
return 0
|
|
fi
|
|
chmod -R u+w "$WORK" 2>/dev/null || true; rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT
|
|
mkdir -p "$OUT_DIR" "$WORK/tmp"
|
|
ISO_DIR="$(cd "$(dirname "$PVE_ISO")" && pwd)"; ISO_BASE="$(basename "$PVE_ISO")"
|
|
|
|
# PAIRING: generate the SECRET-FREE env the stub bakes — only the hub URL, no customer/passphrase.
|
|
# (The bootstrap detects the absent customer-id/passphrase and enters pairing mode.)
|
|
if $PAIRING; then
|
|
BOOTSTRAP_ENV="$WORK/pairing.env"
|
|
cat > "$BOOTSTRAP_ENV" <<EOF
|
|
# GENERIC secret-free pairing env (R-21 slice C). NO customer-id, NO passphrase — the box registers
|
|
# as an unclaimed appliance and the hub delivers the credentials once, after the operator binds it.
|
|
FELHOM_HUB_URL=$PAIR_HUB_URL
|
|
FELHOM_INSTALL_URL=$PAIR_INSTALL_URL
|
|
EOF
|
|
log_info "generated secret-free pairing env (hub=$PAIR_HUB_URL)"
|
|
fi
|
|
|
|
# --- resolve the output ISO name (needed by the mint below: the rootpw sibling file is named after
|
|
# the ISO). Rule 4: the loader mode is loud in the filename — a '-mkimage' ISO implies
|
|
# Secure-Boot-off prep. -----------------------------------------------------------------------
|
|
LOADER_SUFFIX=""; [[ "$LOADER" != "shim" ]] && LOADER_SUFFIX="-${LOADER}"
|
|
MODE_SUFFIX=""; $PAIRING && MODE_SUFFIX="-generic" # the secret-free universal ISO is unmistakable
|
|
if $RELEASE; then
|
|
# Named so it cannot be mistaken for one of the lab artifacts in out/ (canary, probe, nested-vm,
|
|
# demo, hp, n100). "felhom-installer" + the version is what a tester quotes in a bug report.
|
|
OUT_ISO="$OUT_DIR/felhom-installer-${ISO_VERSION}-pve${PVE_VERSION}${LOADER_SUFFIX}.iso"
|
|
else
|
|
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}${MODE_SUFFIX}${LOADER_SUFFIX}.iso"
|
|
fi
|
|
|
|
# --- mint fresh THROWAWAY root hash ---------------------------------------------------------------
|
|
if $RELEASE; then
|
|
log_info "release mode — NOT minting a root password (release gate G2: none is baked, and no"
|
|
log_info " .rootpw.txt is emitted). The installer prompts the person doing the install."
|
|
ROOT_HASH=""
|
|
else
|
|
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")"
|
|
# R-61 slice 1: the baked root password becomes KNOWABLE. The plaintext is written to a 0600 sibling
|
|
# file next to the ISO — the SINGLE record of truth — and nowhere else: never stdout/stderr/logs,
|
|
# never the manifest (manifests get pasted into committed REPORTs; a separate file cannot ride along
|
|
# by accident). A fixed well-known password stays REJECTED (operator ruling 2026-07-21): a
|
|
# pre-pairing box sits on a stranger's LAN. Note: an aborted build can leave a rootpw file for an
|
|
# ISO that was never produced; the next build of the same name overwrites it.
|
|
( umask 077; printf '%s %s built %s\n' "$ROOT_PLAIN" "$(basename "$OUT_ISO")" "$(date -Is)" > "$OUT_ISO.rootpw.txt" )
|
|
unset ROOT_PLAIN
|
|
[[ -n "$ROOT_HASH" ]] || die "failed to mint root hash"
|
|
log_info "throwaway root hash written to the answer file; plaintext in $(basename "$OUT_ISO").rootpw.txt (0600, operator-only — value not logged)"
|
|
fi
|
|
|
|
# --- render answer.toml (pure bash param-expansion; no metachar hazards) ---------------------------
|
|
if ! $RELEASE; then
|
|
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"
|
|
fi # end: non-release answer/stub rendering
|
|
|
|
# --- RELEASE: build the felhom-bootstrap .deb from COMMITTED source (release gate G7/G9/G10) --------
|
|
RELEASE_DEB=""
|
|
if $RELEASE; then
|
|
log_step "building the felhom-bootstrap package"
|
|
[[ -x "$HERE/pkg/build-deb.sh" ]] || die "pkg/build-deb.sh not found next to build-felhom-iso.sh"
|
|
RELEASE_DEB="$("$HERE/pkg/build-deb.sh" "$ISO_VERSION" "$WORK/pkg")" \
|
|
|| die "package build FAILED (its own G8/G9 self-assertions gate it)"
|
|
log_success "package: $(basename "$RELEASE_DEB") ($(stat -c%s "$RELEASE_DEB") bytes)"
|
|
fi
|
|
|
|
# --- prepare-iso ----------------------------------------------------------------------------------
|
|
# (OUT_ISO is resolved above the mint — the rootpw sibling file is named after it.)
|
|
GRUB_VERSION="" # populated by the mkimage surgery (the grub-mkimage build used)
|
|
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"
|
|
$BRAND && log_dry "docker run … (repack/brand) generate 1024x768 boot card from $(basename "$BRAND_IMAGE") → install felhomtheme → rewrite grub.cfg to ONE entry ('Felhom telepítés', 5s), kernel line lifted from the stock automated entry"
|
|
[[ "$LOADER" == "mkimage" ]] && log_dry "docker run … (repack/mkimage) grub-mkimage from the ISO's own modules → swap BOOTX64.EFI in the EFI tree + efi.img"
|
|
( $BRAND || [[ "$LOADER" == "mkimage" ]] ) && log_dry "docker run … (repack) xorriso re-master → /work/final.iso"
|
|
log_info "DRY-RUN: no ISO produced"
|
|
exit 0
|
|
fi
|
|
if $RELEASE; then
|
|
# NO prepare-iso. That step is what creates /answer.toml and /auto-installer-mode.toml, and their
|
|
# absence is release-gate G1 — it also removes the Automated menu entry by construction, since the
|
|
# stock grub.cfg emits it only inside `if [ -f auto-installer-mode.toml ]`.
|
|
log_info "release mode — skipping prepare-iso entirely (no answer file is created)"
|
|
cp "$PVE_ISO" "$WORK/out.iso"
|
|
else
|
|
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
|
|
fi
|
|
[[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output"
|
|
|
|
# --- REPACK: R-38 GRUB branding + single-entry menu, and (slice B) the mkimage loader surgery. Both
|
|
# need the same extract -> modify -> re-master cycle, so iso-repack.sh does them in ONE pass. The
|
|
# assistant's answer/first-boot payload is untouched; only the GRUB menu/theme and (mkimage) the
|
|
# EFI boot path change. The mkimage recipe is the N100 run's proven workaround (VALIDATION F1) —
|
|
# do NOT re-derive it. See iso-repack.sh for the full rationale. -----------------------------------
|
|
if $BRAND || [[ "$LOADER" == "mkimage" ]]; then
|
|
REPACK_WHAT=""
|
|
$BRAND && REPACK_WHAT="branding"
|
|
[[ "$LOADER" == "mkimage" ]] && REPACK_WHAT="${REPACK_WHAT:+$REPACK_WHAT + }mkimage loader"
|
|
log_step "repacking ISO ($REPACK_WHAT)"
|
|
[[ -f "$HERE/iso-repack.sh" ]] || die "iso-repack.sh not found next to build-felhom-iso.sh"
|
|
cp "$HERE/iso-repack.sh" "$WORK/iso-repack.sh"
|
|
if $BRAND; then
|
|
mkdir -p "$WORK/brand"
|
|
cp "$HERE/grub/grub.cfg.tmpl" "$HERE/grub/felhom-theme.txt" \
|
|
"$HERE/grub/generate-grub-background.sh" "$WORK/brand/"
|
|
cp "$BRAND_IMAGE" "$WORK/brand/card.png"
|
|
$RELEASE && cp "$HERE/grub/grub-release.cfg.tmpl" "$WORK/brand/"
|
|
fi
|
|
REPACK_MENU=single; $RELEASE && REPACK_MENU=release
|
|
REPACK_DEB=""
|
|
# Keep the package's REAL filename inside the ISO: it is how the shipped version is visible from
|
|
# the image alone (release gate G7), and how a tester can name what they installed.
|
|
if [[ -n "${RELEASE_DEB:-}" ]]; then
|
|
cp "$RELEASE_DEB" "$WORK/$(basename "$RELEASE_DEB")"
|
|
REPACK_DEB="/work/$(basename "$RELEASE_DEB")"
|
|
fi
|
|
docker run --rm -v "$WORK":/work \
|
|
-e FELHOM_LOADER="$LOADER" -e FELHOM_BRAND="$($BRAND && echo 1 || echo 0)" \
|
|
-e FELHOM_MENU="$REPACK_MENU" -e FELHOM_DEB="$REPACK_DEB" \
|
|
"$IMAGE" bash /work/iso-repack.sh 2>&1 | sed 's/^/ [repack] /'
|
|
[[ -f "$WORK/final.iso" ]] || die "repack produced no output (see [repack] log above)"
|
|
GRUB_VERSION="$(cat "$WORK/grub-version.txt" 2>/dev/null || echo unknown)"
|
|
cp "$WORK/final.iso" "$OUT_ISO"
|
|
log_success "repack applied ($REPACK_WHAT)"
|
|
else
|
|
cp "$WORK/out.iso" "$OUT_ISO"
|
|
fi
|
|
|
|
# --- 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"
|
|
LOADER_NOTE="shim (stock MS-signed chain; Secure Boot OK on compliant firmware)"
|
|
[[ "$LOADER" == "mkimage" ]] && LOADER_NOTE="mkimage (monolithic grub-mkimage UEFI loader, F1 fix — UNSIGNED; target board MUST have Secure Boot OFF)"
|
|
BRAND_NOTE="FELHOM (single entry 'Felhom telepítés', default, 5s; stock installer entries not emitted)"
|
|
$RELEASE && BRAND_NOTE="FELHOM release menu — TWO INTERACTIVE entries ('Felhom telepítés' graphical = default, 'Felhom telepítés (szöveges mód)' = Terminal UI), timeout 15s"
|
|
$BRAND || BRAND_NOTE="STOCK PVE menu (--no-brand; DEBUG BUILD — manual installer reachable, do not ship)"
|
|
MODE_NOTE="direct (env-baked customer-id + retrieval passphrase; secret-bearing)"
|
|
$PAIRING && MODE_NOTE="pairing (GENERIC secret-free universal ISO — box self-registers, operator binds, hub delivers once)"
|
|
$RELEASE && MODE_NOTE="release (PUBLIC image — NO answer.toml, NO baked credential, interactive disk selection)"
|
|
cat > "$OUT_ISO.manifest.txt" <<EOF
|
|
Felhom bare-metal ISO build manifest (R-21 slice A+B+C)
|
|
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 : $( $RELEASE && echo 'none (a release image bakes no disk selection)' || echo "${PROFILE_NAME}" )
|
|
fqdn : ${FELHOM_FQDN}
|
|
mode : ${MODE_NOTE}
|
|
loader : ${LOADER_NOTE}
|
|
grub-mkimage : ${GRUB_VERSION:-n/a (shim mode; loader unchanged)}
|
|
boot-menu : ${BRAND_NOTE}
|
|
$( if $RELEASE; then
|
|
echo "menu-entries : 2 (graphical default + Terminal UI; timeout 15s)"
|
|
echo "menu-removed : debug variants, Rescue Boot, memtest86+, UEFI Firmware Settings"
|
|
echo "automated-entry : NOT PRESENT — no auto-installer-mode.toml, so the stock grub.cfg does"
|
|
echo " not emit it. Disk selection is INTERACTIVE by construction."
|
|
else
|
|
$BRAND && cat "$WORK/brand-report.txt" 2>/dev/null || true
|
|
fi )
|
|
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}$( $RELEASE && echo ' (PUBLIC image — carries NO credential of any kind)' || { $PAIRING && echo ' (GENERIC ISO — carries NO customer secret)' || echo ' (embeds the customer retrieval passphrase — supervised/single-use, delete after the run)'; } )
|
|
root-password : $( $RELEASE && echo 'NONE — not baked. The installer prompts the person installing (release gate G2).' || echo "see $(basename "$OUT_ISO").rootpw.txt (operator-only; NEVER commit or paste into REPORTs)" )
|
|
answer-file : $( $RELEASE && echo 'NONE — no answer.toml, no auto-installer-mode.toml (release gate G1)' || echo 'baked (/answer.toml)' )
|
|
felhom-package : $( [[ -n "$RELEASE_DEB" ]] && echo "$(basename "$RELEASE_DEB") sha256=$(sha256sum "$RELEASE_DEB" | cut -d\ -f1)" || echo 'n/a (day-0 rides the answer file first-boot hook)' )
|
|
repo-commit : $(cd "$HERE" && git rev-parse HEAD 2>/dev/null || echo unknown)
|
|
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"
|
|
if $RELEASE; then
|
|
log_info "root-pw : NONE — no password is baked and no .rootpw.txt is emitted (release gate G2)"
|
|
else
|
|
log_info "root-pw : $OUT_ISO.rootpw.txt (0600, operator-only — the console credential for this build; never commit/paste)"
|
|
fi
|
|
if $RELEASE; then
|
|
log_success "PUBLIC RELEASE image — no answer file, no baked credential, interactive disk selection."
|
|
log_info "Run the release gate before publishing: documentation/runbooks/iso-release-gate.md"
|
|
elif $PAIRING; then
|
|
log_success "GENERIC secret-free ISO — carries NO customer secret. Bind the box on the hub after it registers."
|
|
else
|
|
log_warn "SECRET-BEARING ISO (embeds the retrieval passphrase). Supervised/single-use; never distribute; delete after the run."
|
|
fi
|