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:
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-host.sh — Sysadmin Chronicles host prerequisite checker
|
||||
#
|
||||
# Run this before first-run-setup.sh to see what's missing.
|
||||
# Safe to run any number of times — read-only checks only.
|
||||
#
|
||||
# Exit code 0 = all required dependencies present.
|
||||
# Exit code 1 = one or more required dependencies missing.
|
||||
|
||||
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}"
|
||||
|
||||
PASS="✓"
|
||||
FAIL="✗"
|
||||
WARN="⚠"
|
||||
|
||||
errors=0
|
||||
warnings=0
|
||||
|
||||
run_virsh_quick() {
|
||||
timeout 5 virsh "$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_cmd() {
|
||||
local cmd="$1"
|
||||
local label="${2:-$cmd}"
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
echo " $PASS $label"
|
||||
else
|
||||
echo " $FAIL $label — NOT FOUND"
|
||||
((errors+=1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_file() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
local required="${3:-true}"
|
||||
if [ -e "$path" ]; then
|
||||
echo " $PASS $label ($path)"
|
||||
else
|
||||
if [ "$required" = "true" ]; then
|
||||
echo " $FAIL $label — NOT FOUND ($path)"
|
||||
((errors+=1))
|
||||
else
|
||||
echo " $WARN $label — not found ($path) [optional]"
|
||||
((warnings+=1))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_group() {
|
||||
local group="$1"
|
||||
if id -nG "$OWNER_USER" | grep -qw "$group"; then
|
||||
echo " $PASS User is in group: $group"
|
||||
else
|
||||
echo " $WARN Not in group '$group' — libvirt access may require sudo or group add"
|
||||
((warnings+=1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_kvm_access() {
|
||||
if id -nG "$OWNER_USER" | grep -qw kvm; then
|
||||
echo " $PASS User is in group: kvm"
|
||||
elif [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
|
||||
echo " $PASS /dev/kvm is accessible to this session"
|
||||
else
|
||||
echo " $WARN Not in group 'kvm' — KVM acceleration may require sudo or group add"
|
||||
((warnings+=1))
|
||||
fi
|
||||
}
|
||||
|
||||
libvirt_ready=false
|
||||
socket_ready=false
|
||||
|
||||
if run_virsh_quick -q list --all; then
|
||||
libvirt_ready=true
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet libvirtd.socket 2>/dev/null || \
|
||||
systemctl is-active --quiet virtqemud.socket 2>/dev/null; then
|
||||
socket_ready=true
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Sysadmin Chronicles — Host Prerequisite Check"
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
echo "── Virtualization ─────────────────────────────────"
|
||||
check_file "/dev/kvm" "KVM device node"
|
||||
check_cmd "virsh" "virsh (libvirt CLI)"
|
||||
check_cmd "qemu-system-x86_64" "QEMU (x86_64)"
|
||||
|
||||
# libvirt runtime
|
||||
if [ "$libvirt_ready" = "true" ]; then
|
||||
echo " $PASS libvirt responds to virsh"
|
||||
elif [ "$socket_ready" = "true" ]; then
|
||||
echo " $WARN libvirt socket activation is available, but this user cannot reach $LIBVIRT_DEFAULT_URI"
|
||||
echo " Add yourself to the libvirt group or use sudo for setup."
|
||||
((warnings+=1))
|
||||
else
|
||||
echo " $FAIL libvirt is not reachable — start socket activation or the daemon"
|
||||
echo " Example: sudo systemctl enable --now libvirtd.socket"
|
||||
((errors+=1))
|
||||
fi
|
||||
|
||||
check_group "libvirt"
|
||||
check_kvm_access
|
||||
|
||||
echo ""
|
||||
echo "── Storage ────────────────────────────────────────"
|
||||
check_cmd "qemu-img" "qemu-img (disk image tool)"
|
||||
# Storage pool exists check
|
||||
if run_virsh_quick pool-info sc-images; then
|
||||
echo " $PASS sc-images storage pool exists"
|
||||
else
|
||||
echo " $WARN sc-images storage pool not yet created (run first-run-setup.sh)"
|
||||
((warnings+=1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── Networking ─────────────────────────────────────"
|
||||
if run_virsh_quick net-info sc-internal; then
|
||||
echo " $PASS sc-internal network exists"
|
||||
if timeout 5 virsh net-dumpxml sc-internal 2>/dev/null | grep -q "<forward mode=['\"]nat['\"]"; then
|
||||
echo " $PASS sc-internal has NAT egress for VM provisioning"
|
||||
else
|
||||
echo " $WARN sc-internal is missing NAT egress — rebuilds may fail package installation"
|
||||
echo " Run: sudo bash tools/setup/first-run-setup.sh"
|
||||
((warnings+=1))
|
||||
fi
|
||||
else
|
||||
echo " $WARN sc-internal network not yet created (run first-run-setup.sh)"
|
||||
((warnings+=1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── SSH Keys ───────────────────────────────────────"
|
||||
check_file "$OWNER_HOME/.ssh/sc_host_key" "Host→Guest SSH key" "false"
|
||||
check_file "$OWNER_HOME/.ssh/sc_host_key.pub" "Host→Guest SSH public key" "false"
|
||||
|
||||
echo ""
|
||||
echo "── Runtime Tools ──────────────────────────────────"
|
||||
check_cmd "ssh" "ssh"
|
||||
check_cmd "node" "node (Node.js, for content validation)"
|
||||
check_cmd "godot" "godot (Godot 4 engine binary)" || true # warn only
|
||||
|
||||
echo ""
|
||||
echo "── VM Images ──────────────────────────────────────"
|
||||
for vm in sc-workstation sc-web-server sc-build-machine; do
|
||||
if run_virsh_quick dominfo "$vm"; then
|
||||
echo " $PASS Domain exists: $vm"
|
||||
else
|
||||
echo " $WARN Domain not yet created: $vm (run seed-vms.sh)"
|
||||
((warnings+=1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
||||
echo " ✅ All checks passed. Ready to play."
|
||||
elif [ "$errors" -eq 0 ]; then
|
||||
echo " ⚠ $warnings warning(s). Run first-run-setup.sh if this is a fresh install."
|
||||
else
|
||||
echo " ❌ $errors error(s), $warnings warning(s). Run first-run-setup.sh to resolve."
|
||||
fi
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
exit $([ "$errors" -eq 0 ] && echo 0 || echo 1)
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env bash
|
||||
# first-run-setup.sh — Sysadmin Chronicles one-time host setup
|
||||
#
|
||||
# This script creates the libvirt resources (networks, storage pool) and SSH
|
||||
# keys required for the game. It is safe to re-run — all actions are idempotent.
|
||||
#
|
||||
# What this does:
|
||||
# 1. Creates sc-internal libvirt network (private NAT, game-only)
|
||||
# 2. Creates sc-images storage pool for qcow2 VM images
|
||||
# 3. Generates the host→guest SSH key pair (sc_host_key)
|
||||
# 4. Checks libvirtd is running and user has access
|
||||
#
|
||||
# What this does NOT do:
|
||||
# - Build VM images (that's seed-vms.sh)
|
||||
# - Modify /etc/libvirt or system-wide config beyond the named libvirt resources
|
||||
# - Require broad sudo during normal gameplay
|
||||
#
|
||||
# AGENT RULES: Never run provisioning scripts against VMs from here.
|
||||
# This script only creates host infrastructure.
|
||||
|
||||
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}"
|
||||
|
||||
DRY_RUN=false
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
echo "[DRY-RUN] No changes will be made."
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
run() {
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo " [would run] $*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
step() { echo ""; echo "── $* ───────────────────────"; }
|
||||
ok() { echo " ✓ $*"; }
|
||||
info() { echo " → $*"; }
|
||||
|
||||
libvirt_reachable() {
|
||||
virsh -q list --all >/dev/null 2>&1
|
||||
}
|
||||
|
||||
libvirt_socket_available() {
|
||||
systemctl is-active --quiet libvirtd.socket 2>/dev/null || \
|
||||
systemctl is-active --quiet virtqemud.socket 2>/dev/null
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Sysadmin Chronicles — First-Run Setup"
|
||||
echo "══════════════════════════════════════════════════"
|
||||
|
||||
# Sanity: check libvirt access
|
||||
step "Checking libvirt access"
|
||||
if libvirt_reachable; then
|
||||
ok "libvirt responds to virsh"
|
||||
elif libvirt_socket_available; then
|
||||
ok "libvirt socket activation is available"
|
||||
else
|
||||
echo " ERROR: libvirt is not reachable."
|
||||
echo " Run: sudo systemctl enable --now libvirtd.socket"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
step "Creating sc-internal libvirt network"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NETWORK_XML="$SCRIPT_DIR/../vm/network-sc-internal.xml"
|
||||
|
||||
write_network_xml() {
|
||||
local target="$1"
|
||||
cat > "$target" << 'EOF'
|
||||
<network>
|
||||
<name>sc-internal</name>
|
||||
<forward mode='nat'/>
|
||||
<bridge name='sc-br0' stp='on' delay='0'/>
|
||||
<ip address='10.42.0.1' netmask='255.255.255.0'>
|
||||
<dhcp>
|
||||
<range start='10.42.0.10' end='10.42.0.50'/>
|
||||
</dhcp>
|
||||
</ip>
|
||||
</network>
|
||||
EOF
|
||||
}
|
||||
|
||||
network_has_nat() {
|
||||
virsh net-dumpxml sc-internal 2>/dev/null | grep -q "<forward mode=['\"]nat['\"]"
|
||||
}
|
||||
|
||||
define_network() {
|
||||
local label="$1"
|
||||
local tmpxml
|
||||
tmpxml=$(mktemp /tmp/sc-internal-XXXXXX.xml)
|
||||
if [ -f "$NETWORK_XML" ]; then
|
||||
cp "$NETWORK_XML" "$tmpxml"
|
||||
else
|
||||
write_network_xml "$tmpxml"
|
||||
fi
|
||||
run virsh net-define "$tmpxml"
|
||||
run virsh net-autostart sc-internal
|
||||
run virsh net-start sc-internal
|
||||
rm -f "$tmpxml"
|
||||
ok "$label"
|
||||
}
|
||||
|
||||
if virsh net-list --all | grep -q "sc-internal"; then
|
||||
if network_has_nat; then
|
||||
ok "sc-internal network already exists with NAT"
|
||||
else
|
||||
info "Recreating sc-internal with NAT egress for guest provisioning..."
|
||||
run virsh net-destroy sc-internal >/dev/null 2>&1 || true
|
||||
run virsh net-undefine sc-internal
|
||||
define_network "sc-internal network recreated and started"
|
||||
fi
|
||||
else
|
||||
info "Creating sc-internal (private NAT, game-scoped network)..."
|
||||
define_network "sc-internal network created and started"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
step "Creating sc-images storage pool"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ "${LIBVIRT_DEFAULT_URI}" = "qemu:///system" ]; then
|
||||
IMAGES_DIR="/var/lib/libvirt/images/sysadmin-chronicles"
|
||||
else
|
||||
IMAGES_DIR="$OWNER_HOME/.local/share/sysadmin-chronicles/images"
|
||||
fi
|
||||
|
||||
if virsh pool-list --all | grep -q "sc-images"; then
|
||||
CURRENT_POOL_PATH="$(virsh pool-dumpxml sc-images 2>/dev/null | sed -n 's:.*<path>\(.*\)</path>.*:\1:p' | head -n1)"
|
||||
if [ "$CURRENT_POOL_PATH" = "$IMAGES_DIR" ]; then
|
||||
ok "sc-images pool already exists"
|
||||
else
|
||||
info "Recreating sc-images pool at $IMAGES_DIR (was $CURRENT_POOL_PATH)..."
|
||||
run mkdir -p "$IMAGES_DIR"
|
||||
run virsh pool-destroy sc-images >/dev/null 2>&1 || true
|
||||
run virsh pool-undefine sc-images
|
||||
run virsh pool-define-as sc-images dir --target "$IMAGES_DIR"
|
||||
run virsh pool-autostart sc-images
|
||||
run virsh pool-start sc-images
|
||||
ok "sc-images pool recreated"
|
||||
fi
|
||||
else
|
||||
info "Creating sc-images pool at $IMAGES_DIR..."
|
||||
run mkdir -p "$IMAGES_DIR"
|
||||
run virsh pool-define-as sc-images dir --target "$IMAGES_DIR"
|
||||
run virsh pool-autostart sc-images
|
||||
run virsh pool-start sc-images
|
||||
ok "sc-images pool created"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
step "Generating SSH key pair"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
KEY_PATH="$OWNER_HOME/.ssh/sc_host_key"
|
||||
if [ -f "$KEY_PATH" ]; then
|
||||
run chown "$OWNER_USER:$OWNER_USER" "$KEY_PATH" "${KEY_PATH}.pub" >/dev/null 2>&1 || true
|
||||
run chmod 600 "$KEY_PATH" >/dev/null 2>&1 || true
|
||||
run chmod 644 "${KEY_PATH}.pub" >/dev/null 2>&1 || true
|
||||
ok "SSH key already exists: $KEY_PATH"
|
||||
else
|
||||
info "Generating ed25519 key: $KEY_PATH"
|
||||
run mkdir -p "$(dirname "$KEY_PATH")"
|
||||
run ssh-keygen -t ed25519 -N "" -C "sysadmin-chronicles-host" -f "$KEY_PATH"
|
||||
run chmod 600 "$KEY_PATH"
|
||||
run chmod 644 "${KEY_PATH}.pub"
|
||||
run chown "$OWNER_USER:$OWNER_USER" "$KEY_PATH" "${KEY_PATH}.pub"
|
||||
ok "SSH key generated"
|
||||
info "Public key (add to VM authorized_keys during build):"
|
||||
if [ "$DRY_RUN" = "false" ]; then
|
||||
cat "${KEY_PATH}.pub"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
step "Verifying group membership"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if id -nG | grep -qw "libvirt"; then
|
||||
ok "User is in libvirt group"
|
||||
else
|
||||
info "Consider adding yourself to the libvirt group for passwordless VM control:"
|
||||
info " sudo usermod -aG libvirt \$USER && newgrp libvirt"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Setup complete."
|
||||
echo " Next step: bash tools/setup/seed-vms.sh"
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generates a self-signed CA and server certificate for Sysadmin Chronicles TLS.
|
||||
# Idempotent — skips if certs already exist.
|
||||
# Run this before building VMs. Called by install.sh automatically.
|
||||
set -euo pipefail
|
||||
|
||||
SC_CERT_DIR="${SC_CERT_DIR:-$HOME/.local/share/sysadmin-chronicles/certs}"
|
||||
mkdir -p "$SC_CERT_DIR"
|
||||
chmod 700 "$SC_CERT_DIR"
|
||||
|
||||
if [[ -f "$SC_CERT_DIR/server.crt" && -f "$SC_CERT_DIR/server.key" && -f "$SC_CERT_DIR/ca.crt" ]]; then
|
||||
echo "TLS certs already exist at $SC_CERT_DIR — skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Generating Axiom Works internal CA..."
|
||||
openssl genrsa -out "$SC_CERT_DIR/ca.key" 4096 2>/dev/null
|
||||
openssl req -new -x509 -days 3650 \
|
||||
-key "$SC_CERT_DIR/ca.key" \
|
||||
-out "$SC_CERT_DIR/ca.crt" \
|
||||
-subj "/CN=Axiom Works Internal CA/O=Axiom Works" 2>/dev/null
|
||||
|
||||
echo "Generating server certificate..."
|
||||
openssl genrsa -out "$SC_CERT_DIR/server.key" 4096 2>/dev/null
|
||||
openssl req -new \
|
||||
-key "$SC_CERT_DIR/server.key" \
|
||||
-out "$SC_CERT_DIR/server.csr" \
|
||||
-subj "/CN=portal.axiomworks.internal/O=Axiom Works" 2>/dev/null
|
||||
|
||||
cat > "$SC_CERT_DIR/server.ext" <<'EXTEOF'
|
||||
subjectAltName=DNS:portal.axiomworks.internal,DNS:sage.axiomworks.internal,DNS:axiomworks.corp,DNS:www.axiomworks.corp,DNS:*.axiomworks.internal,DNS:*.axiomworks.corp
|
||||
EXTEOF
|
||||
|
||||
openssl x509 -req -days 3650 \
|
||||
-in "$SC_CERT_DIR/server.csr" \
|
||||
-CA "$SC_CERT_DIR/ca.crt" \
|
||||
-CAkey "$SC_CERT_DIR/ca.key" \
|
||||
-CAcreateserial \
|
||||
-out "$SC_CERT_DIR/server.crt" \
|
||||
-extfile "$SC_CERT_DIR/server.ext" 2>/dev/null
|
||||
|
||||
chmod 600 "$SC_CERT_DIR/ca.key" "$SC_CERT_DIR/server.key"
|
||||
rm -f "$SC_CERT_DIR/server.csr" "$SC_CERT_DIR/server.ext"
|
||||
|
||||
echo "TLS certs generated at $SC_CERT_DIR"
|
||||
echo " CA cert: $SC_CERT_DIR/ca.crt"
|
||||
echo " Server cert: $SC_CERT_DIR/server.crt"
|
||||
echo " Server key: $SC_CERT_DIR/server.key"
|
||||
@@ -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
|
||||
|
||||
# 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 ""
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# uninstall.sh — Remove all Sysadmin Chronicles game-owned host resources.
|
||||
#
|
||||
# Removes:
|
||||
# - sc- prefixed libvirt VM domains (after confirmation)
|
||||
# - sc- prefixed libvirt networks
|
||||
# - sc-images storage pool and its directory
|
||||
# - SSH key pair (~/.ssh/sc_host_key)
|
||||
#
|
||||
# Does NOT remove:
|
||||
# - The game directory itself (remove manually if desired)
|
||||
# - Any system-wide libvirt config
|
||||
# - Any resources not prefixed with sc-
|
||||
#
|
||||
# Usage:
|
||||
# bash tools/setup/uninstall.sh
|
||||
# bash tools/setup/uninstall.sh --dry-run
|
||||
# bash tools/setup/uninstall.sh --yes (skip confirmation)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=false
|
||||
ASSUME_YES=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--yes) ASSUME_YES=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
run() {
|
||||
if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] $*"; else "$@"; fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Sysadmin Chronicles — Uninstall"
|
||||
echo "══════════════════════════════════════════════════"
|
||||
[ "$DRY_RUN" = "true" ] && echo " [DRY-RUN mode — no changes]"
|
||||
echo ""
|
||||
echo " This will PERMANENTLY remove all sc- prefixed VMs,"
|
||||
echo " networks, storage, and SSH keys."
|
||||
echo ""
|
||||
|
||||
if [ "$ASSUME_YES" = "false" ] && [ "$DRY_RUN" = "false" ]; then
|
||||
read -rp " Type YES to confirm uninstall: " confirm
|
||||
if [ "$confirm" != "YES" ]; then
|
||||
echo " Aborted."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove VMs
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "── Removing VM domains ──────────────────────────"
|
||||
for domain in $(virsh list --all --name 2>/dev/null | grep "^sc-" || true); do
|
||||
echo " Removing $domain..."
|
||||
# Stop if running
|
||||
if virsh domstate "$domain" 2>/dev/null | grep -q "running"; then
|
||||
run virsh destroy "$domain"
|
||||
fi
|
||||
# Remove all snapshots
|
||||
for snap in $(virsh snapshot-list "$domain" --name 2>/dev/null || true); do
|
||||
run virsh snapshot-delete "$domain" "$snap"
|
||||
done
|
||||
run virsh undefine "$domain" --remove-all-storage
|
||||
echo " ✓ $domain removed"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove networks
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "── Removing sc- networks ─────────────────────────"
|
||||
for net in $(virsh net-list --all --name 2>/dev/null | grep "^sc-" || true); do
|
||||
echo " Removing network $net..."
|
||||
if virsh net-info "$net" 2>/dev/null | grep -q "Active:.*yes"; then
|
||||
run virsh net-destroy "$net"
|
||||
fi
|
||||
run virsh net-undefine "$net"
|
||||
echo " ✓ $net removed"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove storage pool
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "── Removing sc-images storage pool ──────────────"
|
||||
if virsh pool-list --all | grep -q "sc-images"; then
|
||||
POOL_PATH=$(virsh pool-dumpxml sc-images 2>/dev/null | grep -oP '(?<=<path>)[^<]+' || echo "")
|
||||
if virsh pool-info sc-images 2>/dev/null | grep -q "State:.*running"; then
|
||||
run virsh pool-destroy sc-images
|
||||
fi
|
||||
run virsh pool-undefine sc-images
|
||||
if [ -n "$POOL_PATH" ] && [ -d "$POOL_PATH" ]; then
|
||||
run rm -rf "$POOL_PATH"
|
||||
fi
|
||||
echo " ✓ sc-images pool removed"
|
||||
else
|
||||
echo " (sc-images pool not found — skipping)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove SSH keys
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "── Removing SSH keys ─────────────────────────────"
|
||||
KEY="$HOME/.ssh/sc_host_key"
|
||||
if [ -f "$KEY" ]; then
|
||||
run rm -f "$KEY" "${KEY}.pub"
|
||||
echo " ✓ SSH keys removed"
|
||||
else
|
||||
echo " (No sc_host_key found — skipping)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Uninstall complete."
|
||||
echo " Game files (this directory) were not removed."
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user