#!/usr/bin/env bash # build-iso.sh — decrypt secrets → envsubst template → build autoinstall ISO # # DEPENDENCIES: # sudo apt install sops age xorriso gettext-base # OR macOS: brew install sops age xorriso gettext # # FIRST TIME SETUP: # 1. age-keygen -o ~/.config/sops/age/keys.txt # 2. Paste the public key (age1...) into .sops.yaml # 3. Fill in secrets.yaml, then encrypt: # sops -e secrets.yaml > secrets.sops.yaml && rm secrets.yaml # 4. git add .sops.yaml secrets.sops.yaml user-data.tmpl build-iso.sh .gitignore scripts/ # # USAGE: # ./build-iso.sh # ./build-iso.sh --ubuntu-iso ~/Downloads/ubuntu-24.04.4-live-server-amd64.iso set -euo pipefail UBUNTU_ISO="${UBUNTU_ISO:-}" UBUNTU_VERSION="24.04.4" UBUNTU_ISO_URL="https://releases.ubuntu.com/${UBUNTU_VERSION}/ubuntu-${UBUNTU_VERSION}-live-server-amd64.iso" WORK_DIR="$(mktemp -d /tmp/autoinstall-build.XXXXXX)" OUTPUT_ISO="autoinstall-$(date +%Y%m%d-%H%M).iso" SOPS_FILE="secrets.sops.yaml" TEMPLATE_DIR="templates" TARGET_USER=$(yq -r .autoinstall.identity.username templates/user-data.tmpl) export TARGET_USER NOCLOUD_DIR="$WORK_DIR/iso/nocloud" mkdir -p "$NOCLOUD_DIR" GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[✗]${NC} $*" exit 1 } function envsubst_in_place() { set -x local _filename=$1 envsubst "\$TARGET_USER" <"$_filename" >$_filename.tmp && mv $_filename.tmp $_filename set +x } while [[ $# -gt 0 ]]; do case $1 in --ubuntu-iso) UBUNTU_ISO="$2" shift 2 ;; *) error "Unknown argument: $1" ;; esac done cleanup() { info "Cleaning up..." echo "$WORK_DIR" rm -rf "$WORK_DIR" } trap cleanup EXIT for cmd in sops envsubst xorriso; do command -v "$cmd" &>/dev/null || error "'$cmd' not found. Install it first." done export SOPS_AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$HOME/.config/sops/age/keys.txt}" [[ -f "$SOPS_AGE_KEY_FILE" ]] || error "age key not found at $SOPS_AGE_KEY_FILE" # ── Decrypt secrets → render template ───────────────────────────────────────── info "Decrypting secrets and rendering template..." sops exec-env "$SOPS_FILE" "envsubst < $TEMPLATE_DIR/user-data.tmpl > $NOCLOUD_DIR/user-data" sops exec-env "$SOPS_FILE" "envsubst '\$WIFI_HOUSE_PASSWORD \$NOKIA_WIFI_KEY_PASSWORD' < $TEMPLATE_DIR/user-data-wifi.tmpl > $NOCLOUD_DIR/user-data-wifi.config" envsubst_in_place "$NOCLOUD_DIR/user-data" envsubst_in_place "$NOCLOUD_DIR/user-data-wifi.config" info "Template rendered." # ── Get Ubuntu ISO ───────────────────────────────────────────────────────────── if [[ -n "$UBUNTU_ISO" ]]; then [[ -f "$UBUNTU_ISO" ]] || error "ISO not found: $UBUNTU_ISO" info "Using provided ISO: $UBUNTU_ISO" else UBUNTU_ISO="ubuntu-${UBUNTU_VERSION}-live-server-amd64.iso" if [[ ! -f "$UBUNTU_ISO" ]]; then info "Downloading Ubuntu ${UBUNTU_VERSION} server ISO..." curl -L --fail --progress-bar -o "$UBUNTU_ISO" "$UBUNTU_ISO_URL" || { rm -f "$UBUNTU_ISO" error "Download failed (check URL or network): $UBUNTU_ISO_URL" } else info "Found cached ISO: $UBUNTU_ISO" fi fi # ── Extract ISO ──────────────────────────────────────────────────────────────── info "Extracting ISO..." xorriso -osirrox on -indev "$UBUNTU_ISO" -extract / "$WORK_DIR/iso" 2>/dev/null chmod -R u+w "$WORK_DIR/iso" # Extract MBR template directly from the original ISO (first 432 bytes) # boot_hybrid.img is NOT present in the extracted filesystem on 24.04 dd if="$UBUNTU_ISO" bs=1 count=432 of="$WORK_DIR/mbr_template.bin" 2>/dev/null # ── Inject autoinstall files ─────────────────────────────────────────────────── info "Injecting autoinstall config and post-install script..." cp deployment/* "$NOCLOUD_DIR/" touch "$NOCLOUD_DIR/meta-data" envsubst_in_place "$NOCLOUD_DIR/post-install.sh" cp -r $NOCLOUD_DIR /tmp/ # ── Patch GRUB ──────────────────────────────────────────────────────────────── GRUB_CFG="$WORK_DIR/iso/boot/grub/grub.cfg" if [[ -f "$GRUB_CFG" ]]; then info "Patching GRUB for unattended boot..." cat >"$WORK_DIR/grub_prepend.cfg" <<'GRUBENTRY' set default=0 set timeout=1 menuentry "Autoinstall Ubuntu" { set gfxpayload=keep linux /casper/vmlinuz quiet autoinstall ds=nocloud\;s=/cdrom/nocloud/ --- initrd /casper/initrd } GRUBENTRY # Prepend our entry, then append the original (so manual install is still reachable) cat "$WORK_DIR/grub_prepend.cfg" "$GRUB_CFG" >"$WORK_DIR/grub_merged.cfg" mv "$WORK_DIR/grub_merged.cfg" "$GRUB_CFG" fi EFI_LINE=$(fdisk -l "$UBUNTU_ISO" | grep "EFI System") EFI_START=$(echo "$EFI_LINE" | awk '{print $(NF-5)}') EFI_END=$(echo "$EFI_LINE" | awk '{print $(NF-4)}') EFI_COUNT=$((EFI_END - EFI_START + 1)) # ── Repack ISO ───────────────────────────────────────────────────────────────── xorriso -as mkisofs \ -r -V "Ubuntu-AutoInstall" -o "$OUTPUT_ISO" \ -J -joliet-long \ --grub2-mbr --interval:local_fs:0s-15s:zero_mbrpt,zero_gpt:"$UBUNTU_ISO" \ --protective-msdos-label \ -partition_offset 16 \ --mbr-force-bootable \ -append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b \ --interval:local_fs:${EFI_START}d-${EFI_END}d::"$UBUNTU_ISO" \ -appended_part_as_gpt \ -iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \ -c '/boot.catalog' \ -b '/boot/grub/i386-pc/eltorito.img' \ -no-emul-boot -boot-load-size 4 -boot-info-table \ --grub2-boot-info \ -eltorito-alt-boot \ -e "--interval:appended_partition_2_start_${EFI_START}s_size_${EFI_COUNT}d:all::" \ -no-emul-boot \ -boot-load-size $EFI_COUNT \ "$WORK_DIR/iso" info "Done! ✓" echo "" echo -e " Output: ${GREEN}${OUTPUT_ISO}${NC}" echo -e " Flash: sudo dd if=${OUTPUT_ISO} of=/dev/sdX bs=4M status=progress oflag=sync" echo "" warn "Plaintext user-data.yaml deleted. secrets.yaml never hit disk."