chore: bootstrap lean sysadmin-chronicles repo

Import the runnable game code, content, docs, scripts, and repo guidance while leaving local agent state, dependency installs, build output, and backup copies out of the published tree.
This commit is contained in:
2026-05-02 11:49:07 -04:00
commit 0265afa054
252 changed files with 37574 additions and 0 deletions
+327
View File
@@ -0,0 +1,327 @@
#!/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
# Q002Q004 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 ""