#!/usr/bin/env bash # seed-vms.sh — Build all game VMs and create baseline snapshots. # # This script orchestrates the full VM provisioning pipeline: # 1. Build base VM images (cloud-init or manual install) # 2. Install guest helper binaries # 3. Run quest-prep scripts for each Tier 1 quest # 4. Take named baseline snapshots # # Prerequisites: Run first-run-setup.sh first (creates networks + pool). # # Usage: # bash tools/setup/seed-vms.sh # Build all VMs # bash tools/setup/seed-vms.sh --dry-run # Preview only # bash tools/setup/seed-vms.sh --vm workstation # One VM only # bash tools/setup/seed-vms.sh --skip-build # Prep scripts + snapshots only # # AGENT RULES: # - Never run quest-prep scripts against live player VMs. # - All prep scripts must be idempotent (safe to run twice). # - Snapshots are only taken after prep scripts complete successfully. set -euo pipefail OWNER_USER="${SUDO_USER:-$USER}" OWNER_HOME="$(getent passwd "$OWNER_USER" | cut -d: -f6)" OWNER_HOME="${OWNER_HOME:-$HOME}" export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}" export SC_OWNER_USER="$OWNER_USER" export SC_OWNER_HOME="$OWNER_HOME" export SC_SSH_KEY="${SC_SSH_KEY:-$OWNER_HOME/.ssh/sc_host_key}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" VM_TOOLS="$PROJECT_ROOT/tools/vm" QUEST_PREP="$VM_TOOLS/quest-prep" source "$PROJECT_ROOT/tools/lib/config.sh" config_read || true normalize_dir_path() { local path="${1:-}" while [[ "$path" == *//* ]]; do path="${path//\/\//\/}" done while [ "$path" != "/" ] && [ "${path%/}" != "$path" ]; do path="${path%/}" done printf '%s\n' "$path" } if [ -n "${SC_IMAGES_DIR:-}" ]; then SC_IMAGES_DIR="$(normalize_dir_path "$SC_IMAGES_DIR")" export SC_IMAGE_ROOT="$SC_IMAGES_DIR" fi export SC_POOL_NAME="${SC_POOL_NAME:-sc-images}" export SC_NETWORK_NAME="${SC_NETWORK_NAME:-sc-internal}" DRY_RUN=false SKIP_BUILD=false SINGLE_VM="" while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true; shift ;; --skip-build) SKIP_BUILD=true; shift ;; --vm) SINGLE_VM="$2"; shift 2 ;; *) echo "Unknown argument: $1"; exit 1 ;; esac done run() { if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] $*" else "$@" fi } step() { echo ""; echo "── $* ───────────────────────────────────────"; } ok() { echo " ✓ $*"; } info() { echo " → $*"; } fail() { echo " ✗ $*"; exit 1; } vm_selected() { local key="$1" [ -z "$SINGLE_VM" ] || [ "$SINGLE_VM" = "$key" ] } domain_selected() { local domain="$1" case "$domain" in sc-workstation) vm_selected "workstation" ;; sc-web-server) vm_selected "web_server" ;; sc-build-machine) vm_selected "build_machine" ;; *) return 1 ;; esac } require_file() { local path="$1" local label="$2" if [ ! -f "$path" ]; then fail "$label is missing: $path" fi } echo "" echo "══════════════════════════════════════════════════" echo " Sysadmin Chronicles — VM Seed Pipeline" echo "══════════════════════════════════════════════════" [ "$DRY_RUN" = "true" ] && echo " [DRY-RUN mode]" echo "" step "Validating provisioning toolchain" require_file "$QUEST_PREP/Q001-prep.sh" "Q001 prep script" require_file "$QUEST_PREP/Q002-prep.sh" "Q002 prep script" require_file "$QUEST_PREP/Q003-prep.sh" "Q003 prep script" require_file "$QUEST_PREP/Q004-prep.sh" "Q004 prep script" require_file "$QUEST_PREP/Q006-prep.sh" "Q006 prep script" require_file "$QUEST_PREP/Q006-post-clean.sh" "Q006 post-clean script" if [ "$SKIP_BUILD" = "false" ]; then missing_scripts=() for script in \ "$VM_TOOLS/build-workstation.sh" \ "$VM_TOOLS/build-web-server.sh" \ "$VM_TOOLS/build-build-machine.sh" \ "$VM_TOOLS/install-guest-helper.sh" \ "$VM_TOOLS/suppress-maintenance-noise.sh" do if [ ! -f "$script" ]; then missing_scripts+=("$script") fi done if [ "${#missing_scripts[@]}" -gt 0 ]; then echo " ✗ VM provisioning pipeline is incomplete in this repo checkout." echo " Missing files:" for script in "${missing_scripts[@]}"; do echo " - $script" done echo "" echo " Current state:" echo " - The Godot game and authored content are present." echo " - The VM image build/provision helper scripts are not." echo "" echo " Real VM seeding cannot complete until those scripts are added." exit 1 fi fi # --------------------------------------------------------------------------- # STEP 1 — Build base images # --------------------------------------------------------------------------- if [ "$SKIP_BUILD" = "false" ]; then step "Building VM base images" info "NOTE: VM image builds require cloud-init ISO or manual install." info " See docs/ARCHITECTURE.md §5.3.1 for workstation profile guidance." info " See tools/vm/build-*.sh scripts for per-VM build details." echo "" if vm_selected "workstation"; then info "Building workstation (ares) — Debian XFCE desktop..." run bash "$VM_TOOLS/build-workstation.sh" $([ "$DRY_RUN" = "true" ] && echo "--dry-run") fi if vm_selected "web_server"; then info "Building web_server (hermes) — headless Debian..." run bash "$VM_TOOLS/build-web-server.sh" $([ "$DRY_RUN" = "true" ] && echo "--dry-run") fi if vm_selected "build_machine"; then info "Building build_machine (vulcan) — headless Arch..." run bash "$VM_TOOLS/build-build-machine.sh" $([ "$DRY_RUN" = "true" ] && echo "--dry-run") fi fi # --------------------------------------------------------------------------- # STEP 1b — Verify baseline connectivity # --------------------------------------------------------------------------- if [ "$SKIP_BUILD" = "false" ] && [ "$DRY_RUN" = "false" ]; then step "Verifying baseline connectivity" for dom_host in "sc-web-server:hermes" "sc-build-machine:vulcan"; do dom="${dom_host%%:*}" host="${dom_host##*:}" addr="$(virsh domifaddr "$dom" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | head -n1 || true)" if [ -z "$addr" ]; then info "$dom: no IP yet — skipping connectivity check" continue fi result="$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10 -i "$SC_SSH_KEY" "player@$addr" hostname 2>/dev/null || echo FAIL)" if [ "$result" = "FAIL" ] || [ -z "$result" ]; then fail "$dom ($host): 'hostname' failed — check inetutils and shell PATH provisioning" fi ok "$dom ($host): hostname=$result" done fi # --------------------------------------------------------------------------- # STEP 2 — Suppress guest maintenance noise # --------------------------------------------------------------------------- step "Suppressing guest maintenance noise" info "Tuning base images to suppress package manager notices..." for dom in sc-workstation sc-web-server sc-build-machine; do domain_selected "$dom" || continue if virsh dominfo "$dom" &>/dev/null 2>&1 || [ "$DRY_RUN" = "true" ]; then run bash "$VM_TOOLS/suppress-maintenance-noise.sh" "$dom" \ $([ "$DRY_RUN" = "true" ] && echo "--dry-run") ok "$dom: maintenance noise suppressed" fi done # --------------------------------------------------------------------------- # STEP 3 — Install guest helpers # --------------------------------------------------------------------------- step "Installing guest helpers" info "Guest helpers are non-authoritative — advisory signals only." for dom in sc-workstation sc-web-server sc-build-machine; do domain_selected "$dom" || continue if virsh dominfo "$dom" &>/dev/null 2>&1 || [ "$DRY_RUN" = "true" ]; then run bash "$VM_TOOLS/install-guest-helper.sh" "$dom" \ $([ "$DRY_RUN" = "true" ] && echo "--dry-run") ok "$dom: guest helper installed" fi done # --------------------------------------------------------------------------- # STEP 4 — Run quest-prep scripts and snapshot # --------------------------------------------------------------------------- step "Running quest-prep scripts and snapshotting" run_prep_and_snapshot() { local quest_id="$1" local domain="$2" local snapshot_name="$3" local prep_script="$QUEST_PREP/${quest_id}-prep.sh" if [ ! -f "$prep_script" ]; then echo " ⚠ No prep script found for $quest_id — skipping" return fi info "Running $quest_id prep on $domain..." run bash "$prep_script" "$domain" $([ "$DRY_RUN" = "true" ] && echo "--dry-run") info "Taking snapshot '$snapshot_name' on $domain..." run virsh snapshot-delete "$domain" "$snapshot_name" >/dev/null 2>&1 || true run virsh snapshot-create-as "$domain" "$snapshot_name" \ --description "${quest_id} baseline — created by seed-vms.sh" \ --atomic ok "$domain → $snapshot_name" } run_post_clean_and_snapshot() { local quest_id="$1" local domain="$2" local snapshot_name="$3" local clean_script="$QUEST_PREP/${quest_id}-post-clean.sh" if [ ! -f "$clean_script" ]; then echo " ⚠ No post-clean script found for $quest_id — skipping" return fi info "Applying ${quest_id} clean branch state on $domain..." run bash "$clean_script" "$domain" $([ "$DRY_RUN" = "true" ] && echo "--dry-run") info "Taking snapshot '$snapshot_name' on $domain..." run virsh snapshot-delete "$domain" "$snapshot_name" >/dev/null 2>&1 || true run virsh snapshot-create-as "$domain" "$snapshot_name" \ --description "${quest_id} clean branch baseline — created by seed-vms.sh" \ --atomic ok "$domain → $snapshot_name" } # Q001: workstation day-one state if vm_selected "workstation"; then run_prep_and_snapshot "Q001" "sc-workstation" "baseline.day-one" fi # Q002–Q004 share hermes clean baseline; prep scripts layer on top if vm_selected "web_server"; then run_prep_and_snapshot "Q002" "sc-web-server" "baseline.clean" run_prep_and_snapshot "Q003" "sc-web-server" "baseline.post-q002" run_prep_and_snapshot "Q004" "sc-web-server" "baseline.post-q003" fi # Q005 and Q007 use post-q004 baseline if vm_selected "web_server"; then info "Creating baseline.post-q004 snapshot (used by Q005, Q007)..." run virsh snapshot-delete "sc-web-server" "baseline.post-q004" >/dev/null 2>&1 || true run virsh snapshot-create-as "sc-web-server" "baseline.post-q004" \ --description "Post-Q004 baseline" --atomic || true fi # Q006: build machine broken baseline, then authored clean handoff for later quests if vm_selected "build_machine"; then run_prep_and_snapshot "Q006" "sc-build-machine" "baseline.clean" run_post_clean_and_snapshot "Q006" "sc-build-machine" "baseline.post-q006" fi info "Q008 remains a multi-VM authored-state gap and is not provisioned by seed-vms.sh yet." # Take recovery snapshots (always available as fallback) step "Creating recovery snapshots" for dom in sc-workstation sc-web-server sc-build-machine; do domain_selected "$dom" || continue info "Creating baseline.recovery on $dom..." run virsh snapshot-delete "$dom" "baseline.recovery" >/dev/null 2>&1 || true run virsh snapshot-create-as "$dom" "baseline.recovery" \ --description "Recovery fallback — created by seed-vms.sh" \ --atomic || true ok "$dom → baseline.recovery" done # --------------------------------------------------------------------------- echo "" echo "══════════════════════════════════════════════════" echo " Seed pipeline complete." echo " Verify with: bash tools/setup/check-host.sh" echo " Run content validation: bash tools/dev/test-content.sh" echo "══════════════════════════════════════════════════" echo ""