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
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Wrapper — delegates to the modular build-vm.sh driver.
exec "$(dirname "$0")/build-vm.sh" "$(dirname "$0")/profiles/build-machine.sh" "$@"
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# build-vm.sh — Modular VM builder. Sources a profile file that declares VM
# variables and a generate_user_data() function, then runs the common build
# pipeline against it.
#
# Usage:
# ./build-vm.sh <profile> [--dry-run] [--force]
#
# Example:
# ./build-vm.sh profiles/web-server.sh --force
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <profile> [--dry-run] [--force]"
exit 1
fi
PROFILE_ARG="$1"; shift
DRY_RUN=false
FORCE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--force) FORCE=true; shift ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
done
source "$PROJECT_ROOT/tools/lib/config.sh"
config_read || true
if [ -n "${SC_IMAGES_DIR:-}" ]; then
SC_IMAGE_ROOT="${SC_IMAGE_ROOT:-$SC_IMAGES_DIR}"
export SC_IMAGE_ROOT
fi
if [ -n "${SC_LIBVIRT_URI:-}" ]; then
LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-$SC_LIBVIRT_URI}"
export LIBVIRT_DEFAULT_URI
fi
source "$SCRIPT_DIR/lib/common.sh"
# Resolve profile path: bare name (e.g. "web-server") or explicit path.
if [[ -f "$PROFILE_ARG" ]]; then
PROFILE="$PROFILE_ARG"
elif [[ -f "$SCRIPT_DIR/profiles/${PROFILE_ARG}.sh" ]]; then
PROFILE="$SCRIPT_DIR/profiles/${PROFILE_ARG}.sh"
elif [[ -f "$SCRIPT_DIR/profiles/${PROFILE_ARG}" ]]; then
PROFILE="$SCRIPT_DIR/profiles/${PROFILE_ARG}"
else
echo "Profile not found: $PROFILE_ARG"
echo "Available profiles:"
ls "$SCRIPT_DIR/profiles/"
exit 1
fi
source "$PROFILE"
# Validate required profile variables.
for var in DOMAIN HOSTNAME RAM_MB VCPUS DISK_SIZE GRAPHICS BASE_URL BASE_IMAGE; do
[[ -n "${!var:-}" ]] || { echo "Profile must set $var"; exit 1; }
done
declare -f generate_user_data >/dev/null || { echo "Profile must define generate_user_data()"; exit 1; }
GAME_HOST_IP="${SC_GAME_HOST_IP:-10.42.0.1}"
POOL_DIR="$(pool_path)"
DISK_PATH="$POOL_DIR/${DOMAIN}.qcow2"
SEED_ISO="$SC_SEED_DIR/${DOMAIN}-seed.iso"
PUBKEY="$(<"${SC_SSH_KEY}.pub")"
export DOMAIN HOSTNAME RAM_MB VCPUS DISK_SIZE GRAPHICS BASE_URL BASE_IMAGE
export GAME_HOST_IP POOL_DIR DISK_PATH SEED_ISO PUBKEY
ensure_vm_tooling
echo ""
echo "══════════════════════════════════════════════════"
echo " Building VM: $DOMAIN ($HOSTNAME)"
echo " Profile: $(basename "$PROFILE")"
echo " RAM: ${RAM_MB} MB vCPUs: ${VCPUS} Disk: ${DISK_SIZE}"
echo "══════════════════════════════════════════════════"
if domain_exists "$DOMAIN" && [ "$FORCE" = "false" ]; then
ok "$DOMAIN already exists. Use --force to rebuild it."
exit 0
fi
step "Preparing base image"
download_if_missing "$BASE_URL" "$BASE_IMAGE"
step "Preparing cloud-init seed"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
generate_user_data > "$tmpdir/user-data"
cat > "$tmpdir/meta-data" <<EOF
instance-id: ${DOMAIN}
local-hostname: ${HOSTNAME}
EOF
create_seed_iso "$tmpdir/user-data" "$tmpdir/meta-data" "$SEED_ISO"
step "Building domain"
destroy_domain "$DOMAIN"
create_backing_disk "$BASE_IMAGE" "$DISK_PATH" "$DISK_SIZE"
build_import_domain "$DOMAIN" "$DISK_PATH" "$SEED_ISO" "$RAM_MB" "$VCPUS" "$GRAPHICS"
step "Waiting for guest networking"
guest_addr=""
if [ "$DRY_RUN" = "false" ]; then
if guest_addr="$(wait_for_agent_ip "$DOMAIN" 300)"; then
ok "$DOMAIN is reachable at $guest_addr"
else
info "Guest IP not available yet. First boot may still be running cloud-init."
fi
fi
if [ "$DRY_RUN" = "false" ] && [ -n "${READY_COMMAND:-}" ]; then
step "Waiting for guest readiness"
if [ -n "$guest_addr" ]; then
if [ -n "${READY_WATCH_TEMPLATE:-}" ]; then
watch_command="${READY_WATCH_TEMPLATE//\{ADDR\}/$guest_addr}"
info "Watch live progress in another terminal:"
info "$watch_command"
fi
fi
if wait_for_guest_command "$DOMAIN" "${READY_TIMEOUT:-600}" "$READY_COMMAND" "${READY_PROGRESS_COMMAND:-}" "${READY_PROGRESS_EVERY_SEC:-30}"; then
ok "$DOMAIN passed readiness check"
detach_seed_iso "$DOMAIN" "$SEED_ISO"
else
fail "$DOMAIN did not pass readiness check within ${READY_TIMEOUT:-600}s"
fi
fi
ok "$DOMAIN build complete"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Wrapper — delegates to the modular build-vm.sh driver.
exec "$(dirname "$0")/build-vm.sh" "$(dirname "$0")/profiles/web-server.sh" "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Wrapper — delegates to the modular build-vm.sh driver.
exec "$(dirname "$0")/build-vm.sh" "$(dirname "$0")/profiles/workstation.sh" "$@"
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# install-guest-helper.sh — Install the advisory guest helper onto a VM.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DRY_RUN=false
DOMAIN="${1:-}"
if [ -z "$DOMAIN" ]; then
echo "Usage: bash tools/vm/install-guest-helper.sh <domain> [--dry-run]"
exit 1
fi
if [[ "${2:-}" == "--dry-run" ]]; then
DRY_RUN=true
fi
source "$SCRIPT_DIR/lib/common.sh"
helper_name=""
case "$DOMAIN" in
sc-workstation) helper_name="atlas-index" ;;
sc-web-server) helper_name="yardd" ;;
sc-build-machine) helper_name="ops-telemetry-cache" ;;
*) echo "Unknown domain: $DOMAIN"; exit 1 ;;
esac
ensure_vm_tooling
tmp_script="$(mktemp)"
cat > "$tmp_script" <<EOF
cat > /usr/local/bin/${helper_name} <<'HELPER'
#!/usr/bin/env bash
set -euo pipefail
printf '{"helper":"%s","hostname":"%s","timestamp":"%s"}\n' \
"${helper_name}" \
"\$(hostname)" \
"\$(date -Iseconds)"
HELPER
chmod 755 /usr/local/bin/${helper_name}
EOF
info "Installing guest helper ${helper_name} on ${DOMAIN}"
guest_run_sudo_script "$DOMAIN" "$tmp_script"
rm -f "$tmp_script"
ok "${DOMAIN}: helper ${helper_name} installed"
+427
View File
@@ -0,0 +1,427 @@
#!/usr/bin/env bash
set -euo pipefail
COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VM_TOOLS_DIR="$(cd "$COMMON_DIR/.." && pwd)"
PROJECT_ROOT="$(cd "$VM_TOOLS_DIR/../.." && pwd)"
SC_OWNER_USER="${SC_OWNER_USER:-${SUDO_USER:-$USER}}"
SC_OWNER_HOME="${SC_OWNER_HOME:-$(getent passwd "$SC_OWNER_USER" | cut -d: -f6)}"
SC_OWNER_HOME="${SC_OWNER_HOME:-$HOME}"
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
if [ "${LIBVIRT_DEFAULT_URI}" = "qemu:///system" ]; then
SC_HOME="${SC_HOME:-/var/lib/libvirt/sysadmin-chronicles}"
SC_IMAGE_ROOT="${SC_IMAGE_ROOT:-/var/lib/libvirt/images/sysadmin-chronicles}"
else
SC_HOME="${SC_HOME:-$SC_OWNER_HOME/.local/share/sysadmin-chronicles}"
SC_IMAGE_ROOT="${SC_IMAGE_ROOT:-$SC_HOME/images}"
fi
SC_BASE_DIR="$SC_IMAGE_ROOT/base"
SC_SEED_DIR="$SC_IMAGE_ROOT/seed"
SC_POOL_NAME="${SC_POOL_NAME:-sc-images}"
SC_NETWORK_NAME="${SC_NETWORK_NAME:-sc-internal}"
SC_SSH_KEY="${SC_SSH_KEY:-$SC_OWNER_HOME/.ssh/sc_host_key}"
DRY_RUN="${DRY_RUN:-false}"
_virsh() {
if [ "${SC_VIRSH_SUDO:-false}" = true ]; then
sudo virsh "$@"
else
virsh "$@"
fi
}
_virt_install() {
if [ "${SC_VIRSH_SUDO:-false}" = true ]; then
sudo virt-install "$@"
else
virt-install "$@"
fi
}
step() { echo ""; echo "── $* ───────────────────────────────────────"; }
ok() { echo "$*"; }
info() { echo "$*"; }
fail() { echo "$*"; exit 1; }
ssh_login_user() {
local domain="${1:-}"
case "$domain" in
sc-workstation) printf '%s\n' "opsbridge" ;;
*) printf '%s\n' "player" ;;
esac
}
run() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN] $*"
else
"$@"
fi
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "Required command not found: $cmd"
}
ensure_vm_tooling() {
require_cmd virsh
require_cmd qemu-img
require_cmd curl
require_cmd virt-install
if ! command -v cloud-localds >/dev/null 2>&1 \
&& ! command -v genisoimage >/dev/null 2>&1 \
&& ! command -v mkisofs >/dev/null 2>&1 \
&& ! command -v xorriso >/dev/null 2>&1; then
fail "Need cloud-localds, genisoimage, mkisofs, or xorriso to build NoCloud seed images"
fi
[ -f "$SC_SSH_KEY" ] || fail "Missing SSH private key: $SC_SSH_KEY"
[ -f "${SC_SSH_KEY}.pub" ] || fail "Missing SSH public key: ${SC_SSH_KEY}.pub"
run mkdir -p "$SC_BASE_DIR" "$SC_SEED_DIR"
_virsh pool-info "$SC_POOL_NAME" >/dev/null 2>&1 || fail "Missing libvirt pool: $SC_POOL_NAME"
_virsh net-info "$SC_NETWORK_NAME" >/dev/null 2>&1 || fail "Missing libvirt network: $SC_NETWORK_NAME"
}
pool_path() {
local path
path="$(_virsh pool-dumpxml "$SC_POOL_NAME" | sed -n 's:.*<path>\(.*\)</path>.*:\1:p' | head -n1)"
[ -n "$path" ] || fail "Could not determine pool path for $SC_POOL_NAME"
printf '%s\n' "$path"
}
domain_exists() {
local domain="$1"
_virsh dominfo "$domain" >/dev/null 2>&1
}
download_if_missing() {
local url="$1"
local dest="$2"
if [ -f "$dest" ]; then
ok "Using cached base image: $(basename "$dest")"
return
fi
info "Downloading $(basename "$dest")"
run curl -L --fail --output "$dest" "$url"
}
create_backing_disk() {
local base_image="$1"
local target_disk="$2"
local disk_size="${3:-}"
run mkdir -p "$(dirname "$target_disk")"
run rm -f "$target_disk"
if [ -n "$disk_size" ]; then
run qemu-img create -f qcow2 -F qcow2 -b "$base_image" "$target_disk" "$disk_size"
else
run qemu-img create -f qcow2 -F qcow2 -b "$base_image" "$target_disk"
fi
}
create_seed_iso() {
local user_data="$1"
local meta_data="$2"
local output_iso="$3"
local seed_dir
seed_dir="$(mktemp -d)"
cp "$user_data" "$seed_dir/user-data"
cp "$meta_data" "$seed_dir/meta-data"
run rm -f "$output_iso"
if command -v cloud-localds >/dev/null 2>&1; then
run cloud-localds "$output_iso" "$seed_dir/user-data" "$seed_dir/meta-data"
elif command -v genisoimage >/dev/null 2>&1; then
run genisoimage -quiet -output "$output_iso" -volid cidata -joliet -rock "$seed_dir/user-data" "$seed_dir/meta-data"
elif command -v mkisofs >/dev/null 2>&1; then
run mkisofs -quiet -output "$output_iso" -volid cidata -joliet -rock "$seed_dir/user-data" "$seed_dir/meta-data"
else
run xorriso -as mkisofs -quiet -output "$output_iso" -volid cidata -joliet -rock "$seed_dir/user-data" "$seed_dir/meta-data"
fi
rm -rf "$seed_dir"
}
destroy_domain() {
local domain="$1"
if ! domain_exists "$domain"; then
return
fi
info "Removing existing domain definition: $domain"
run _virsh destroy "$domain" >/dev/null 2>&1 || true
run _virsh undefine "$domain" --nvram --snapshots-metadata >/dev/null 2>&1 \
|| run _virsh undefine "$domain" --snapshots-metadata >/dev/null 2>&1 \
|| run _virsh undefine "$domain" --nvram >/dev/null 2>&1 \
|| run _virsh undefine "$domain" >/dev/null 2>&1 \
|| true
}
build_import_domain() {
local domain="$1"
local disk_path="$2"
local seed_iso="$3"
local ram_mb="$4"
local vcpus="$5"
local graphics_mode="$6"
local args=(
--name "$domain"
--memory "$ram_mb"
--vcpus "$vcpus"
--import
--disk "path=$disk_path,format=qcow2,bus=virtio"
--disk "path=$seed_iso,device=cdrom"
--network "network=$SC_NETWORK_NAME,model=virtio"
--channel "unix,target_type=virtio,name=org.qemu.guest_agent.0"
--rng /dev/urandom
--osinfo detect=on,require=off
--noautoconsole
)
case "$graphics_mode" in
none)
args+=(--graphics none --console pty,target_type=serial)
;;
vnc)
args+=(--graphics vnc,listen=127.0.0.1)
;;
spice)
args+=(
--graphics spice,listen=127.0.0.1
--video virtio
--channel "spicevmc,target_type=virtio,name=com.redhat.spice.0"
)
;;
spice-qxl)
args+=(
--graphics spice,listen=127.0.0.1
--video qxl
--channel "spicevmc,target_type=virtio,name=com.redhat.spice.0"
)
;;
*)
fail "Unknown graphics mode: $graphics_mode"
;;
esac
run _virt_install "${args[@]}"
run _virsh autostart "$domain"
}
seed_cdrom_target() {
local domain="$1"
local seed_iso="$2"
_virsh dumpxml "$domain" 2>/dev/null \
| awk -v seed="$seed_iso" '
/<disk / { in_disk=1; target="" }
in_disk && /<source file=/ && index($0, seed) { matched=1 }
in_disk && /<target dev=/ {
line=$0
sub(/.*<target dev=.?/, "", line)
sub(/[ '\'"'"'"].*/, "", line)
target=line
}
in_disk && /<\/disk>/ {
if (matched && target != "") {
print target
exit
}
in_disk=0
matched=0
target=""
}
'
}
detach_seed_iso() {
local domain="$1"
local seed_iso="$2"
local target
target="$(seed_cdrom_target "$domain" "$seed_iso" || true)"
if [ -z "$target" ]; then
info "No cloud-init seed ISO attached to $domain"
return 0
fi
info "Detaching cloud-init seed ISO from $domain ($target)"
if _virsh domstate "$domain" 2>/dev/null | grep -qi running; then
run _virsh detach-disk "$domain" "$target" --config >/dev/null 2>&1 || true
run _virsh detach-disk "$domain" "$target" --live >/dev/null 2>&1 || true
if seed_cdrom_target "$domain" "$seed_iso" >/dev/null 2>&1; then
info "Restarting $domain to apply cloud-init seed ISO detach"
run _virsh shutdown "$domain" >/dev/null 2>&1 || true
local waited=0
while [ "$waited" -lt 60 ] && _virsh domstate "$domain" 2>/dev/null | grep -qi running; do
sleep 2
waited=$((waited + 2))
done
if _virsh domstate "$domain" 2>/dev/null | grep -qi running; then
run _virsh destroy "$domain" >/dev/null 2>&1 || true
fi
run _virsh start "$domain" >/dev/null 2>&1
wait_for_agent_ip "$domain" 180 >/dev/null || true
fi
else
run _virsh detach-disk "$domain" "$target" --config >/dev/null 2>&1 || true
fi
}
domain_mac() {
local domain="$1"
_virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\([^']*\)'.*/\1/p" | head -n1
}
dhcp_lease_ip() {
local domain="$1"
local mac
mac="$(domain_mac "$domain")"
[ -n "$mac" ] || return 1
_virsh net-dhcp-leases "$SC_NETWORK_NAME" 2>/dev/null \
| awk -v mac="$mac" '$0 ~ mac {print $5}' \
| cut -d/ -f1 \
| head -n1
}
valid_guest_ip() {
local addr="${1:-}"
[[ -n "$addr" ]] || return 1
[[ "$addr" != 127.* ]] || return 1
[[ "$addr" != "0.0.0.0" ]] || return 1
return 0
}
wait_for_agent_ip() {
local domain="$1"
local timeout_sec="${2:-300}"
local waited=0
while [ "$waited" -lt "$timeout_sec" ]; do
local addr
addr="$(_virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | head -n1 || true)"
if ! valid_guest_ip "$addr"; then
addr=""
fi
if [ -z "$addr" ]; then
addr="$(dhcp_lease_ip "$domain" || true)"
fi
if valid_guest_ip "$addr"; then
printf '%s\n' "$addr"
return 0
fi
sleep 5
waited=$((waited + 5))
done
return 1
}
wait_for_ssh() {
local domain="$1"
local timeout_sec="${2:-180}"
local waited=0
local login_user
login_user="$(ssh_login_user "$domain")"
while [ "$waited" -lt "$timeout_sec" ]; do
local addr
addr="$(wait_for_agent_ip "$domain" 10 || true)"
if [ -n "$addr" ]; then
if ssh_base_args "$addr" "$login_user" true >/dev/null 2>&1; then
return 0
fi
fi
sleep 5
waited=$((waited + 5))
done
return 1
}
wait_for_guest_command() {
local domain="$1"
local timeout_sec="$2"
local command="$3"
local progress_command="${4:-}"
local progress_every_sec="${5:-30}"
local waited=0
local last_progress=-9999
local login_user
login_user="$(ssh_login_user "$domain")"
while [ "$waited" -lt "$timeout_sec" ]; do
local addr
addr="$(wait_for_agent_ip "$domain" 10 || true)"
if [ -n "$addr" ]; then
if ssh_base_args "$addr" "$login_user" "$command" >/dev/null 2>&1; then
return 0
fi
if [ -n "$progress_command" ] && [ $((waited - last_progress)) -ge "$progress_every_sec" ]; then
last_progress="$waited"
info "Guest progress for $domain:"
ssh_base_args "$addr" "$login_user" "$progress_command" 2>/dev/null || true
fi
fi
sleep 5
waited=$((waited + 5))
done
return 1
}
ssh_base_args() {
local host="$1"
local login_user="${2:-player}"
shift
shift || true
ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o BatchMode=yes \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
-i "$SC_SSH_KEY" \
"${login_user}@${host}" \
"$@"
}
guest_run() {
local domain="$1"
shift
wait_for_ssh "$domain" 180 >/dev/null || fail "SSH did not become ready for $domain"
local addr
addr="$(wait_for_agent_ip "$domain" 120)" || fail "Could not resolve IP for $domain"
local login_user
login_user="$(ssh_login_user "$domain")"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN][SSH $domain@$addr] $*"
return 0
fi
ssh_base_args "$addr" "$login_user" "$@"
}
guest_run_sudo_script() {
local domain="$1"
local script_file="$2"
wait_for_ssh "$domain" 180 >/dev/null || fail "SSH did not become ready for $domain"
local addr
addr="$(wait_for_agent_ip "$domain" 120)" || fail "Could not resolve IP for $domain"
local login_user
login_user="$(ssh_login_user "$domain")"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN][SSH $domain@$addr] sudo bash -s < $script_file"
return 0
fi
ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o BatchMode=yes \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
-i "$SC_SSH_KEY" \
"${login_user}@${addr}" "sudo bash -s" < "$script_file"
}
+14
View File
@@ -0,0 +1,14 @@
<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'/>
<!-- Fixed reservations — must match /etc/hosts in each VM profile -->
<host mac='52:54:00:49:9b:64' name='hermes' ip='10.42.0.40'/>
<host mac='52:54:00:5e:9f:b9' name='vulcan' ip='10.42.0.24'/>
<host mac='52:54:00:bd:aa:29' name='ares' ip='10.42.0.36'/>
</dhcp>
</ip>
</network>
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Profile: sc-build-machine (vulcan)
# Role: Arch Linux build machine — compiles AxiomFlow artifacts, runs scheduled
# jobs, deploys to hermes. Intentionally different distro from Debian servers.
# Distro: Arch Linux cloud image
DOMAIN="sc-build-machine"
HOSTNAME="vulcan"
RAM_MB=768
VCPUS=2
DISK_SIZE="10G"
GRAPHICS="vnc"
BASE_URL="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
BASE_IMAGE="$SC_BASE_DIR/Arch-Linux-x86_64-cloudimg.qcow2"
generate_user_data() {
cat <<EOF
#cloud-config
hostname: ${HOSTNAME}
fqdn: ${HOSTNAME}.axiomworks.internal
manage_etc_hosts: false
ssh_pwauth: false
users:
- default
- name: player
gecos: Axiom Works Builder
groups: [wheel]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${PUBKEY}
write_files:
- path: /etc/hosts
owner: root:root
permissions: '0644'
content: |
127.0.0.1 localhost
127.0.1.1 vulcan vulcan.axiomworks.internal
${GAME_HOST_IP} axiomworks.internal portal.axiomworks.internal
10.42.0.40 hermes hermes.axiomworks.internal
- path: /etc/sudoers.d/99-player
owner: root:root
permissions: '0440'
content: |
player ALL=(ALL) NOPASSWD:ALL
- path: /etc/sysctl.d/99-sc-vulcan.conf
owner: root:root
permissions: '0644'
content: |
vm.swappiness=10
vm.vfs_cache_pressure=50
vm.dirty_ratio=25
vm.dirty_background_ratio=5
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
- path: /home/player/.bashrc
owner: root:root
permissions: '0644'
content: |
[ -z "\$PS1" ] && return
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
export TERM=xterm-256color
export EDITOR=vim
PS1='\[\e[0;35m\]\u@\h\[\e[0m\]:\[\e[0;34m\]\w\[\e[0m\]\$ '
HISTSIZE=5000
HISTFILESIZE=10000
HISTCONTROL=ignoredups:erasedups
shopt -s histappend
alias ll='ls -lh --color=auto'
alias la='ls -lha --color=auto'
alias grep='grep --color=auto'
alias ..='cd ..'
alias pacs='pacman -Ss'
alias paci='sudo pacman -S'
alias pacq='pacman -Qi'
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
fi
- path: /home/player/.bash_profile
owner: root:root
permissions: '0644'
content: |
[[ -f ~/.bashrc ]] && . ~/.bashrc
runcmd:
- pacman -Sy --noconfirm archlinux-keyring
- pacman -Su --noconfirm
- pacman -S --noconfirm --needed sudo openssh qemu-guest-agent base-devel git inetutils iproute2 curl wget rsync vim nano htop python python-pip jq less tree unzip tcpdump lsof strace bind-tools openbsd-netcat bash-completion
- systemctl enable qemu-guest-agent sshd
- systemctl start qemu-guest-agent sshd
- mkdir -p /srv/repo /srv/builds /var/log/axiomworks
- printf 'vulcan — AxiomFlow build machine\n' > /srv/repo/README.txt
- dd if=/dev/zero of=/swapfile bs=1M count=1024 status=progress
- chmod 600 /swapfile
- mkswap /swapfile
- swapon /swapfile
- echo '/swapfile none swap sw 0 0' >> /etc/fstab
- sysctl -p /etc/sysctl.d/99-sc-vulcan.conf
- chown -R player:player /home/player /srv/repo /srv/builds
- systemctl disable ModemManager || true
- systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
final_message: "Vulcan build machine is ready."
EOF
}
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Profile: sc-web-server (hermes)
# Role: nginx web/app server — staging and demo environment for AxiomFlow.
# Distro: Debian 12 (bookworm) cloud image
DOMAIN="sc-web-server"
HOSTNAME="hermes"
RAM_MB=512
VCPUS=1
DISK_SIZE="8G"
GRAPHICS="vnc"
BASE_URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
BASE_IMAGE="$SC_BASE_DIR/debian-12-genericcloud-amd64.qcow2"
generate_user_data() {
cat <<EOF
#cloud-config
hostname: ${HOSTNAME}
fqdn: ${HOSTNAME}.axiomworks.internal
manage_etc_hosts: false
ssh_pwauth: false
package_update: true
package_upgrade: false
packages:
- qemu-guest-agent
- openssh-server
- sudo
- nginx
- logrotate
- rsync
- curl
- wget
- git
- python3
- jq
- vim
- nano
- htop
- procps
- psmisc
- iproute2
- iputils-ping
- dnsutils
- netcat-openbsd
- tcpdump
- lsof
- strace
- less
- tree
- unzip
- bash-completion
users:
- default
- name: player
gecos: Axiom Works Operator
groups: [sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${PUBKEY}
write_files:
- path: /etc/hosts
owner: root:root
permissions: '0644'
content: |
127.0.0.1 localhost
127.0.1.1 hermes hermes.axiomworks.internal
${GAME_HOST_IP} axiomworks.internal portal.axiomworks.internal
- path: /etc/sudoers.d/99-player
owner: root:root
permissions: '0440'
content: |
player ALL=(ALL) NOPASSWD:ALL
- path: /etc/nginx/sites-available/axiomworks.conf
owner: root:root
permissions: '0644'
content: |
server {
listen 80;
server_name hermes hermes.axiomworks.internal _;
root /var/www/axiomworks;
index index.html;
access_log /var/log/nginx/axiomworks.access.log;
error_log /var/log/nginx/axiomworks.error.log;
location / {
try_files \$uri \$uri/ =404;
}
}
- path: /var/www/axiomworks/index.html
owner: root:root
permissions: '0644'
content: |
<!doctype html>
<html><head><title>AxiomFlow</title></head>
<body><h1>AxiomFlow Staging</h1><p>Build not yet deployed.</p></body>
</html>
- path: /opt/deploy/deploy.sh
owner: root:root
permissions: '0755'
content: |
#!/usr/bin/env bash
set -euo pipefail
SRC="\${1:-/home/player/build/dist}"
rsync -av --delete "\$SRC/" /var/www/axiomworks/
echo "\$(date) Deploy from \$SRC complete." >> /var/log/axiomworks/deploy.log
- path: /home/player/.bashrc
owner: root:root
permissions: '0644'
content: |
[ -z "\$PS1" ] && return
export TERM=xterm-256color
export EDITOR=vim
PS1='\[\e[0;33m\]\u@\h\[\e[0m\]:\[\e[0;34m\]\w\[\e[0m\]\$ '
HISTSIZE=5000
HISTFILESIZE=10000
HISTCONTROL=ignoredups:erasedups
shopt -s histappend
alias ll='ls -lh --color=auto'
alias la='ls -lha --color=auto'
alias grep='grep --color=auto'
alias ..='cd ..'
alias nginx-test='nginx -t'
alias nginx-reload='systemctl reload nginx'
alias logs='journalctl -f'
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
fi
- path: /etc/sysctl.d/99-sc-hermes.conf
owner: root:root
permissions: '0644'
content: |
vm.swappiness=10
vm.vfs_cache_pressure=50
vm.dirty_ratio=15
vm.dirty_background_ratio=3
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
runcmd:
- ln -sf /etc/nginx/sites-available/axiomworks.conf /etc/nginx/sites-enabled/axiomworks.conf
- rm -f /etc/nginx/sites-enabled/default
- mkdir -p /var/www/axiomworks /var/log/axiomworks /opt/deploy
- chown -R www-data:www-data /var/www/axiomworks
- touch /var/log/axiomworks/deploy.log
- chown www-data:www-data /var/log/axiomworks/deploy.log
- chown -R player:player /home/player
- fallocate -l 512M /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile && echo '/swapfile none swap sw 0 0' >> /etc/fstab
- sysctl -p /etc/sysctl.d/99-sc-hermes.conf
- systemctl enable --now qemu-guest-agent ssh nginx
- systemctl disable --now unattended-upgrades || true
- systemctl disable --now apt-daily.timer apt-daily-upgrade.timer || true
- systemctl disable --now ModemManager || true
- systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
final_message: "Hermes web server is ready."
EOF
}
+734
View File
@@ -0,0 +1,734 @@
#!/usr/bin/env bash
# Profile: sc-workstation (ares)
# Role: XFCE desktop workstation — where the player works.
# Distro: Debian 12 (bookworm) cloud image
DOMAIN="sc-workstation"
HOSTNAME="ares"
RAM_MB=2048
VCPUS=2
DISK_SIZE="20G"
GRAPHICS="${SC_WORKSTATION_GRAPHICS:-spice}"
BASE_URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
BASE_IMAGE="$SC_BASE_DIR/debian-12-genericcloud-amd64.qcow2"
READY_TIMEOUT=1200
READY_COMMAND='cloud-init status 2>/dev/null | grep -q "status: done" && ! uname -r | grep -q cloud && test -e /dev/dri/card0 && systemctl is-active --quiet lightdm'
READY_PROGRESS_COMMAND='cloud-init status --long; echo "---"; tail -n 12 /var/log/cloud-init-output.log'
READY_WATCH_TEMPLATE='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=5 -o LogLevel=ERROR -i ~/.ssh/sc_host_key opsbridge@{ADDR} "sudo tail -f /var/log/cloud-init-output.log"'
# Extra variables used in user-data
WALLPAPER_PATH="${SC_WALLPAPER_PATH:-$PROJECT_ROOT/server/public/wallpaper.png}"
WALLPAPER_B64=""
if [ -f "$WALLPAPER_PATH" ]; then
WALLPAPER_B64="$(base64 -w 0 "$WALLPAPER_PATH")"
fi
WALLPAPER_B64_INDENT="$(printf '%s\n' "$WALLPAPER_B64" | fold -w 76 | sed 's/^/ /')"
PRIVKEY_INDENT="$(sed 's/^/ /' "$SC_SSH_KEY")"
source "$PROJECT_ROOT/tools/lib/internal-https.sh"
sc_ensure_internal_certs "$PROJECT_ROOT"
sc_export_internal_https_env
SC_CERT_DIR="$(sc_cert_dir)"
HUD_URL="$(sc_hud_url)"
SAGE_URL="$(sc_sage_url)"
COMPANY_URL="$(sc_company_url)"
_SC_CA_CERT_PEM=""
_SC_SERVER_CERT_PEM=""
_SC_SERVER_KEY_PEM=""
if [[ -f "$(sc_ca_cert)" && -f "$(sc_tls_cert)" && -f "$(sc_tls_key)" ]]; then
_SC_CA_CERT_PEM="$(cat "$SC_CERT_DIR/ca.crt")"
_SC_SERVER_CERT_PEM="$(cat "$SC_CERT_DIR/server.crt")"
_SC_SERVER_KEY_PEM="$(cat "$SC_CERT_DIR/server.key")"
fi
_SC_CA_CERT_INDENT="$(printf '%s\n' "$_SC_CA_CERT_PEM" | sed 's/^/ /')"
_SC_SERVER_CERT_INDENT="$(printf '%s\n' "$_SC_SERVER_CERT_PEM" | sed 's/^/ /')"
_SC_SERVER_KEY_INDENT="$(printf '%s\n' "$_SC_SERVER_KEY_PEM" | sed 's/^/ /')"
_SC_CA_CERT_JSON="$(printf '%s' "$_SC_CA_CERT_PEM" | tr '\n' '|' | sed 's/|/\\n/g')"
PLAYER_SSH_CONFIG="$(cat <<'EOF'
Host hermes
HostName 10.42.0.40
User player
IdentityFile ~/.ssh/sc_host_key
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
ConnectTimeout 5
LogLevel ERROR
Host vulcan
HostName 10.42.0.24
User player
IdentityFile ~/.ssh/sc_host_key
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
ConnectTimeout 5
LogLevel ERROR
Host 10.42.0.*
User player
IdentityFile ~/.ssh/sc_host_key
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
ConnectTimeout 5
LogLevel ERROR
EOF
)"
PLAYER_SSH_CONFIG_INDENT="$(printf '%s\n' "$PLAYER_SSH_CONFIG" | sed 's/^/ /')"
_nginx_config() {
printf '%s\n' \
' server {' \
' listen 443 ssl;' \
' server_name axiomworks.corp www.axiomworks.corp;' \
' ssl_certificate /etc/nginx/certs/server.crt;' \
' ssl_certificate_key /etc/nginx/certs/server.key;' \
' location / {' \
" proxy_pass https://${GAME_HOST_IP}:3000/company/;" \
' proxy_ssl_verify off;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $remote_addr;' \
' }' \
' }' \
' server {' \
' listen 80;' \
' server_name axiomworks.corp www.axiomworks.corp;' \
' return 301 https://$host$request_uri;' \
' }'
}
_cert_write_files() {
if [[ -z "$_SC_CA_CERT_PEM" ]]; then return; fi
printf '%s\n' ' - path: /usr/local/share/ca-certificates/axiomworks-ca.crt'
printf '%s\n' ' owner: root:root'
printf '%s\n' " permissions: '0644'"
printf '%s\n' ' content: |'
printf '%s\n' "$_SC_CA_CERT_INDENT"
printf '%s\n' ' - path: /etc/nginx/certs/server.crt'
printf '%s\n' ' owner: root:root'
printf '%s\n' " permissions: '0644'"
printf '%s\n' ' content: |'
printf '%s\n' "$_SC_SERVER_CERT_INDENT"
printf '%s\n' ' - path: /etc/nginx/certs/server.key'
printf '%s\n' ' owner: root:root'
printf '%s\n' " permissions: '0600'"
printf '%s\n' ' content: |'
printf '%s\n' "$_SC_SERVER_KEY_INDENT"
printf '%s\n' ' - path: /etc/chromium/policies/managed/axiomworks-ca.json'
printf '%s\n' ' owner: root:root'
printf '%s\n' " permissions: '0644'"
printf '%s\n' ' content: |'
printf '%s\n' ' {'
printf '%s\n' ' "AdditionalTrustAnchors": ['
printf '%s\n' " \"$_SC_CA_CERT_JSON\""
printf '%s\n' ' ]'
printf '%s\n' ' }'
}
generate_user_data() {
cat <<EOF
#cloud-config
hostname: ${HOSTNAME}
fqdn: ${HOSTNAME}.internal
manage_etc_hosts: false
ssh_pwauth: false
package_update: true
package_upgrade: false
packages:
- qemu-guest-agent
- spice-vdagent
- accountsservice
- openssh-server
- sudo
- bash-completion
- xfce4
- xfce4-goodies
- lightdm
- lightdm-gtk-greeter
- tilix
- chromium
- thunar
- gvfs
- libglib2.0-bin
- libnss3-tools
- dbus-x11
- geany
- meld
- fonts-hack
- fonts-firacode
- vim
- nano
- htop
- tmux
- curl
- wget
- rsync
- git
- jq
- python3
- openssh-client
- nmap
- netcat-openbsd
- dnsutils
- traceroute
- mtr
- tcpdump
- strace
- lsof
- openssl
- whois
- iperf3
- logwatch
- gnome-themes-extra
- avahi-daemon
- libnss-mdns
- nginx
users:
- default
- name: opsbridge
gecos: Axiom Works Ops Bridge
groups: [sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${PUBKEY}
- name: player
gecos: Axiom Works Player
groups: [sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${PUBKEY}
write_files:
- path: /etc/hosts
owner: root:root
permissions: '0644'
content: |
127.0.0.1 localhost axiomworks.corp www.axiomworks.corp
127.0.1.1 ares ares.axiomworks.internal
${GAME_HOST_IP} axiomworks.internal portal.axiomworks.internal sage.axiomworks.internal www.axiomworks.internal
10.42.0.40 hermes hermes.axiomworks.internal
10.42.0.24 vulcan vulcan.axiomworks.internal
- path: /etc/axiom/onboarding
owner: root:root
permissions: '0644'
content: |
Welcome to Axiom Works.
Hostname: ares
User: player
Portal: ${HUD_URL}
Knowledge base: ${SAGE_URL}
SSH targets: hermes.axiomworks.internal vulcan.axiomworks.internal
Open Tilix for terminal work. Use ssh hermes or ssh vulcan once your SSH key is configured.
- path: /etc/sudoers.d/99-player
owner: root:root
permissions: '0440'
content: |
player ALL=(ALL) NOPASSWD:ALL
- path: /etc/sudoers.d/99-opsbridge
owner: root:root
permissions: '0440'
content: |
opsbridge ALL=(ALL) NOPASSWD:ALL
- path: /etc/lightdm/lightdm.conf.d/50-autologin.conf
owner: root:root
permissions: '0644'
content: |
[Seat:*]
autologin-user=player
autologin-user-timeout=0
- path: /home/player/.config/chromium/Default/Bookmarks
owner: root:root
permissions: '0644'
content: |
{
"checksum": "",
"roots": {
"bookmark_bar": {
"children": [
{
"date_added": "13369637600000000",
"guid": "1a2b3c4d-0001-0001-0001-000000000001",
"id": "2",
"name": "Axiom Works Portal",
"type": "url",
"url": "${HUD_URL}"
},
{
"date_added": "13369637600000000",
"guid": "1a2b3c4d-0001-0001-0001-000000000002",
"id": "3",
"name": "Sage (KB)",
"type": "url",
"url": "${SAGE_URL}"
},
{
"date_added": "13369637600000000",
"guid": "1a2b3c4d-0001-0001-0001-000000000003",
"id": "6",
"name": "Axiom Works Website",
"type": "url",
"url": "${COMPANY_URL}"
}
],
"date_added": "13369637600000000",
"date_modified": "13369637600000000",
"guid": "0bc5d13f-2cba-48a8-9801-375a6731a4b8",
"id": "1",
"name": "Bookmarks bar",
"type": "folder"
},
"other": {
"children": [],
"date_added": "13369637600000000",
"date_modified": "0",
"guid": "82b081ec-3dd3-493c-b8d3-c1c01c3ce438",
"id": "4",
"name": "Other bookmarks",
"type": "folder"
},
"synced": {
"children": [],
"date_added": "13369637600000000",
"date_modified": "0",
"guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f",
"id": "5",
"name": "Mobile bookmarks",
"type": "folder"
}
},
"version": 1
}
- path: /usr/local/bin/open-portal
owner: root:root
permissions: '0755'
content: |
#!/bin/bash
# Wait for game server before opening Chromium — avoids "site can't be reached" on fast VM boot.
until curl -sf --max-time 2 "${HUD_URL}" >/dev/null 2>&1; do sleep 2; done
exec chromium --no-first-run --no-default-browser-check --new-window "${HUD_URL}"
- path: /usr/local/share/axiomworks-wallpaper.png
owner: root:root
permissions: '0644'
encoding: b64
content: |
${WALLPAPER_B64_INDENT}
- path: /usr/share/backgrounds/wallpaper.png
owner: root:root
permissions: '0644'
encoding: b64
content: |
${WALLPAPER_B64_INDENT}
- path: /home/player/Desktop/Portal.desktop
owner: root:root
permissions: '0755'
content: |
[Desktop Entry]
Type=Application
Name=Axiom Works Portal
Exec=chromium --no-first-run --no-default-browser-check --new-window ${HUD_URL}
Icon=chromium
Terminal=false
- path: /home/player/Desktop/Terminal.desktop
owner: root:root
permissions: '0755'
content: |
[Desktop Entry]
Type=Application
Name=Terminal
Exec=tilix
Icon=utilities-terminal
Terminal=false
Path=/home/player
- path: /usr/local/bin/trust-desktop-launchers
owner: root:root
permissions: '0755'
content: |
#!/bin/bash
# Trust every player desktop launcher from the real player login session.
set -u
PATH=/usr/local/bin:/usr/bin:/bin
player_uid="\$(id -u player)"
desktop_dir=/home/player/Desktop
export HOME=/home/player
export USER=player
export LOGNAME=player
export DISPLAY="\${DISPLAY:-:0}"
export XAUTHORITY="\${XAUTHORITY:-/home/player/.Xauthority}"
export XDG_RUNTIME_DIR="/run/user/\$player_uid"
if [ -S "\$XDG_RUNTIME_DIR/bus" ]; then
export DBUS_SESSION_BUS_ADDRESS="unix:path=\$XDG_RUNTIME_DIR/bus"
fi
metadata_daemon=""
for candidate in /usr/libexec/gvfsd-metadata /usr/lib/gvfs/gvfsd-metadata /usr/lib/x86_64-linux-gnu/gvfs/gvfsd-metadata; do
if [ -x "\$candidate" ]; then
metadata_daemon="\$candidate"
break
fi
done
if [ -n "\$metadata_daemon" ] && ! /usr/bin/pgrep -u "\$player_uid" -x gvfsd-metadata >/dev/null 2>&1; then
"\$metadata_daemon" >/dev/null 2>&1 &
sleep 1
fi
for i in \$(/usr/bin/seq 1 20); do
trusted_any=false
failed=false
for launcher in "\$desktop_dir"/*.desktop; do
[ -e "\$launcher" ] || continue
chmod 0755 "\$launcher" 2>/dev/null || true
checksum="\$(/usr/bin/sha256sum "\$launcher" | /usr/bin/awk '{print \$1}')" || {
failed=true
continue
}
if /usr/bin/gio set -t string "\$launcher" metadata::xfce-exe-checksum "\$checksum" 2>/dev/null; then
actual_checksum="\$(/usr/bin/gio info -a metadata::xfce-exe-checksum "\$launcher" 2>/dev/null | /usr/bin/awk -F': ' '/metadata::xfce-exe-checksum:/ {print \$2; exit}')"
owner_mode="\$(/usr/bin/stat -c '%U:%G %a' "\$launcher" 2>/dev/null || true)"
if [ "\$actual_checksum" != "\$checksum" ] || [ "\$owner_mode" != "player:player 755" ]; then
failed=true
continue
fi
trusted_any=true
else
failed=true
fi
done
if [ "\$trusted_any" = true ] && [ "\$failed" = false ]; then
/usr/bin/xfdesktop --reload >/dev/null 2>&1 || /usr/bin/pkill -HUP xfdesktop 2>/dev/null || true
rm -f /home/player/.config/autostart/trust-launchers.desktop
exit 0
fi
sleep 1
done
# gvfsd not ready — will retry next login
exit 1
- path: /home/player/.local/bin/trust-desktop-launchers.sh
owner: root:root
permissions: '0755'
content: |
#!/bin/bash
exec /usr/local/bin/trust-desktop-launchers
- path: /home/player/.config/autostart/trust-launchers.desktop
owner: root:root
permissions: '0644'
content: |
[Desktop Entry]
Type=Application
Name=Trust Desktop Launchers
Exec=/usr/local/bin/trust-desktop-launchers
Terminal=false
X-GNOME-Autostart-enabled=true
Hidden=false
NoDisplay=true
- path: /home/player/Desktop/VIEWER_HELP.txt
owner: root:root
permissions: '0644'
content: |
Workstation Viewer — Quick Reference
=====================================
Toggle fullscreen: F11
Release mouse/kb: Shift+F12 (or Ctrl+Alt on some builds)
Scale display: View → Zoom (or Ctrl+scroll)
Copy from guest: Select text, then right-click → Copy
Paste to guest: Right-click input field → Paste
Switch USB redirect: Input → USB Device Redirection
- path: /home/player/.config/xfce4/desktop/icons.screen0-1264x757.rc
owner: root:root
permissions: '0644'
content: |
[xfdesktop-version-4.10.3+-rcfile_format]
4.10.3+=true
[/home/player/Desktop/VIEWER_HELP.txt]
row=6
col=0
[/home/player/Desktop/Terminal.desktop]
row=0
col=6
[/home/player/Desktop/Portal.desktop]
row=0
col=7
[Trash]
row=6
col=11
[/]
row=0
col=4
[/home/player]
row=0
col=5
- path: /home/player/.config/xfce4/desktop/icons.screen.latest.rc
owner: root:root
permissions: '0644'
content: |
[xfdesktop-version-4.10.3+-rcfile_format]
4.10.3+=true
[/home/player/Desktop/VIEWER_HELP.txt]
row=6
col=0
[/home/player/Desktop/Terminal.desktop]
row=0
col=6
[/home/player/Desktop/Portal.desktop]
row=0
col=7
[Trash]
row=6
col=11
[/]
row=0
col=4
[/home/player]
row=0
col=5
- path: /home/player/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml
owner: root:root
permissions: '0644'
content: |
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfwm4" version="1.0">
<property name="general" type="empty">
<property name="use_compositing" type="bool" value="false"/>
</property>
</channel>
- path: /home/player/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-screensaver.xml
owner: root:root
permissions: '0644'
content: |
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-screensaver" version="1.0">
<property name="saver" type="empty">
<property name="enabled" type="bool" value="false"/>
</property>
<property name="lock" type="empty">
<property name="enabled" type="bool" value="false"/>
</property>
</channel>
- path: /home/player/.ssh/sc_host_key
owner: root:root
permissions: '0600'
content: |
${PRIVKEY_INDENT}
- path: /home/player/.ssh/config
owner: root:root
permissions: '0600'
content: |
${PLAYER_SSH_CONFIG_INDENT}
- path: /home/player/.config/chromium/Default/Preferences
owner: root:root
permissions: '0644'
content: |
{
"bookmark_bar": { "show_on_all_tabs": true },
"browser": {
"check_default_browser": false,
"show_home_button": false
},
"background_mode": { "enabled": false },
"signin": { "allowed": false },
"metrics": { "reporting_enabled": false },
"safebrowsing": { "enabled": false },
"translate": { "enabled": false }
}
- path: /home/player/.config/chromium/First Run
owner: root:root
permissions: '0644'
content: ''
- path: /home/player/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml
owner: root:root
permissions: '0644'
content: |
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xsettings" version="1.0">
<property name="Net" type="empty">
<property name="ThemeName" type="string" value="Adwaita-dark"/>
<property name="IconThemeName" type="string" value="Adwaita"/>
</property>
<property name="Gtk" type="empty">
<property name="CursorThemeName" type="string" value="Adwaita"/>
</property>
</channel>
- path: /home/player/.config/xfce4/helpers.rc
owner: root:root
permissions: '0644'
content: |
TerminalEmulator=tilix
- path: /home/player/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml
owner: root:root
permissions: '0644'
content: |
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-desktop" version="1.0">
<property name="backdrop" type="empty">
<property name="screen0" type="empty">
<property name="monitorVirtual-0" type="empty">
<property name="workspace0" type="empty">
<property name="color-style" type="int" value="0"/>
<property name="rgba1" type="array">
<value type="uint" value="16"/>
<value type="uint" value="22"/>
<value type="uint" value="30"/>
<value type="uint" value="255"/>
</property>
<property name="image-style" type="int" value="2"/>
<property name="last-image" type="string" value="/usr/local/share/axiomworks-wallpaper.png"/>
</property>
</property>
<property name="monitorVirtual-1" type="empty">
<property name="workspace0" type="empty">
<property name="color-style" type="int" value="1"/>
<property name="image-style" type="int" value="5"/>
<property name="last-image" type="string" value="/usr/share/backgrounds/wallpaper.png"/>
</property>
<property name="workspace1" type="empty">
<property name="color-style" type="int" value="1"/>
<property name="image-style" type="int" value="5"/>
<property name="last-image" type="string" value="/usr/share/backgrounds/wallpaper.png"/>
</property>
<property name="workspace2" type="empty">
<property name="color-style" type="int" value="1"/>
<property name="image-style" type="int" value="5"/>
<property name="last-image" type="string" value="/usr/share/backgrounds/wallpaper.png"/>
</property>
<property name="workspace3" type="empty">
<property name="color-style" type="int" value="1"/>
<property name="image-style" type="int" value="5"/>
<property name="last-image" type="string" value="/usr/share/backgrounds/wallpaper.png"/>
</property>
</property>
</property>
</property>
<property name="desktop-icons" type="empty">
<property name="show-removable" type="bool" value="false"/>
<property name="show-device-icons" type="bool" value="false"/>
<property name="show-network-removable" type="bool" value="false"/>
<property name="show-trash" type="bool" value="true"/>
<property name="show-home" type="bool" value="true"/>
<property name="show-filesystem" type="bool" value="true"/>
</property>
</channel>
- path: /home/player/.bashrc
owner: root:root
permissions: '0644'
content: |
[ -z "\$PS1" ] && return
export TERM=xterm-256color
export EDITOR=nano
PS1='\[\e[0;32m\]\u@\h\[\e[0m\]:\[\e[0;34m\]\w\[\e[0m\]\$ '
HISTSIZE=5000
HISTFILESIZE=10000
HISTCONTROL=ignoredups:erasedups
shopt -s histappend
alias ll='ls -lh --color=auto'
alias la='ls -lha --color=auto'
alias l='ls -CF --color=auto'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'
export LS_COLORS='di=0;34:ln=0;36:ex=0;32:'
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
fi
- path: /etc/sysctl.d/99-sc-workstation.conf
owner: root:root
permissions: '0644'
content: |
vm.swappiness=10
vm.vfs_cache_pressure=50
vm.dirty_ratio=20
vm.dirty_background_ratio=5
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
- path: /etc/udev/rules.d/90-sysadmin-chronicles-hide-system-disk.rules
owner: root:root
permissions: '0644'
content: |
# Hide the internal VirtIO system disk from desktop file-manager device lists.
KERNEL=="vd[a-z]", ENV{UDISKS_IGNORE}="1"
KERNEL=="vd[a-z][0-9]*", ENV{UDISKS_IGNORE}="1"
- path: /etc/nginx/sites-available/axiomworks
owner: root:root
permissions: '0644'
content: |
$(_nginx_config)
$(_cert_write_files)
runcmd:
- mkdir -p /home/player/Desktop /home/player/projects /home/player/.ssh /home/player/.config/autostart /home/player/.config/xfce4/desktop /home/player/.config/xfce4/xfconf/xfce-perchannel-xml /home/player/.config/chromium/Default /home/opsbridge/.ssh /home/player/.local/bin
- chown -R player:player /home/player
- chown -R opsbridge:opsbridge /home/opsbridge
- passwd -d player
- chmod 700 /home/player/.ssh
- chmod 700 /home/opsbridge/.ssh
- touch /home/player/.ssh/authorized_keys
- touch /home/opsbridge/.ssh/authorized_keys
- chown player:player /home/player/.ssh/authorized_keys
- chown opsbridge:opsbridge /home/opsbridge/.ssh/authorized_keys
- chmod 600 /home/player/.ssh/authorized_keys
- chmod 600 /home/opsbridge/.ssh/authorized_keys
- printf '%s\n' 'Axiom Works workstation ready.' > /home/player/notes.txt
- chown player:player /home/player/notes.txt
- mkdir -p /var/lib/lightdm/data
- chown lightdm:lightdm /var/lib/lightdm/data || chown 108:114 /var/lib/lightdm/data || true
- test -f /swapfile || fallocate -l 1G /swapfile
- chmod 600 /swapfile
- mkswap -f /swapfile
- swapon /swapfile || true
- grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
- ln -sf /etc/nginx/sites-available/axiomworks /etc/nginx/sites-enabled/axiomworks
- mkdir -p /etc/nginx/certs
- test -f /usr/local/share/ca-certificates/axiomworks-ca.crt && update-ca-certificates || true
- mkdir -p /etc/chromium/policies/managed
- |
if [ -f /usr/local/share/ca-certificates/axiomworks-ca.crt ]; then
mkdir -p /home/player/.pki/nssdb
certutil -d sql:/home/player/.pki/nssdb -N --empty-password 2>/dev/null || true
certutil -d sql:/home/player/.pki/nssdb -A -t "CT,," -n "Axiom Works CA" -i /usr/local/share/ca-certificates/axiomworks-ca.crt 2>/dev/null || true
chown -R player:player /home/player/.pki
fi
- rm -f /etc/nginx/sites-enabled/default
- systemctl enable --now nginx
- systemctl enable --now qemu-guest-agent ssh spice-vdagent
- systemctl enable lightdm
- systemctl set-default graphical.target
- DEBIAN_FRONTEND=noninteractive apt-get purge -y plymouth plymouth-label || true
- DEBIAN_FRONTEND=noninteractive apt-get install -y linux-image-amd64
- cloud_kernels="\$(dpkg-query -W -f='\${Package}\\n' 'linux-image-*-cloud-amd64' 2>/dev/null | tr '\\n' ' ')"; if [ -n "\$cloud_kernels" ]; then DEBIAN_FRONTEND=noninteractive apt-get purge -y linux-image-cloud-amd64 \$cloud_kernels; fi
- update-grub || true
- update-alternatives --set x-www-browser /usr/bin/chromium || true
- update-alternatives --set x-terminal-emulator /usr/bin/tilix || true
- sysctl -p /etc/sysctl.d/99-sc-workstation.conf
- udevadm control --reload-rules || true
- udevadm trigger --subsystem-match=block || true
- systemctl enable --now avahi-daemon
- "sed -i 's/^hosts:.*/hosts: files mdns4_minimal [NOTFOUND=return] dns/' /etc/nsswitch.conf"
- systemctl disable --now unattended-upgrades || true
- systemctl disable --now apt-daily.timer apt-daily-upgrade.timer || true
- systemctl disable --now ModemManager || true
- systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
- rm -f /home/player/.config/autostart/game-hud.desktop
- rm -f /home/player/.Xauthority /home/player/.ICEauthority
- find /home/player/Desktop -maxdepth 1 -type f -name '*.desktop' -exec chmod 0755 {} +
- chown -R player:player /home/player
power_state:
mode: reboot
timeout: 30
condition: true
final_message: "Ares XFCE workstation is ready."
EOF
}
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Q001-prep.sh — Workstation baseline: SSH key missing
#
# Prepares the workstation VM for Q001 "Welcome Aboard".
# The player's SSH key was never added during provisioning.
#
# What this does:
# - Ensures the player account exists
# - Removes /home/player/.ssh/authorized_keys (key not provisioned)
# - Leaves /var/log/auth.log with a "Permission denied (publickey)" entry
#
# Idempotent: safe to run multiple times.
# AGENT RULES: Never run against a live player session.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-workstation}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_USER="${SSH_USER:-opsbridge}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
VM_IP="$(get_vm_ip "$DOMAIN")"
SSH="ssh $SSH_OPTS $SSH_USER@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
$SSH "sudo $*"
fi
}
echo "Q001-prep: Preparing $DOMAIN for 'Welcome Aboard'..."
run_in_vm "bash -lc 'mkdir -p /home/player/.ssh; touch /var/log/auth.log; ts=\$(date +\"%b %d %H:%M:%S\"); echo \"\$ts ares sshd[1234]: Failed publickey for player from 10.42.0.1 port 22 ssh2\" >> /var/log/auth.log; rm -f /home/player/.ssh/authorized_keys; echo Q001-prep: authorized_keys removed'"
echo "Q001-prep: Done."
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Q002-prep.sh — hermes baseline: nginx config syntax error
#
# Prepares sc-web-server for Q002 "Syntax Error in Aisle Four".
# Introduces a deliberate nginx config syntax error that breaks the service.
#
# What this does:
# - Installs nginx if not present
# - Writes a broken /etc/nginx/sites-enabled/axiomworks.conf
# (missing semicolon on the server_name line)
# - Stops nginx so the player finds it down
# - Adds error log evidence
#
# Idempotent: safe to run multiple times.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-web-server}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
VM_IP=$(get_vm_ip "$DOMAIN")
SSH="ssh $SSH_OPTS player@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
printf '%s\n' "$*" | $SSH "sudo bash -se"
fi
}
echo "Q002-prep: Preparing $DOMAIN for 'Syntax Error in Aisle Four'..."
run_in_vm "mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available"
# Write broken nginx config (missing semicolon after server_name)
run_in_vm "cat > /etc/nginx/sites-enabled/axiomworks.conf <<'NGINX_CONF'
server {
listen 80;
server_name axiomworks.internal # <-- MISSING SEMICOLON: this is the bug
root /var/www/axiomworks;
index index.html;
location / {
try_files \$uri \$uri/ =404;
}
}
NGINX_CONF"
# Disable the default site to make this the only relevant config
run_in_vm "rm -f /etc/nginx/sites-enabled/default"
# Stop nginx (it fails to start with bad config)
run_in_vm "systemctl stop nginx || true"
# Populate nginx error log with the kind of evidence a player would find
run_in_vm "mkdir -p /var/log/nginx && echo '[emerg] unexpected \";\" in /etc/nginx/sites-enabled/axiomworks.conf:3' >> /var/log/nginx/error.log"
# Create the web root (nginx would serve from here if config were valid)
run_in_vm "mkdir -p /var/www/axiomworks && echo '<h1>Axiom Works</h1>' > /var/www/axiomworks/index.html"
echo "Q002-prep: Done. nginx is stopped with broken config on $DOMAIN."
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Q003-prep.sh — hermes baseline: logrotate missing, nginx access log ballooning
#
# Prepares sc-web-server for Q003 "The Log That Ate the Disk".
# Assumes Q002 is already resolved (nginx is running, config is clean).
#
# What this does:
# - Removes /etc/logrotate.d/nginx (log rotation not configured)
# - Grows /var/log/nginx/access.log to ~80% disk pressure
# - Disk usage should read >85% on /var so player sees the pressure
#
# Idempotent: safe to run multiple times.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-web-server}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
VM_IP=$(get_vm_ip "$DOMAIN")
SSH="ssh $SSH_OPTS player@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
printf '%s\n' "$*" | $SSH "sudo bash -se"
fi
}
echo "Q003-prep: Preparing $DOMAIN for 'The Log That Ate the Disk'..."
# Remove logrotate config for nginx
run_in_vm "rm -f /etc/logrotate.d/nginx"
# Generate a large access log (~500MB of fake log entries, enough to fill a 6GB VM)
# Use truncate for speed rather than generating real content
run_in_vm "mkdir -p /var/log/nginx"
run_in_vm "truncate -s 500M /var/log/nginx/access.log"
# Write real-looking last few lines so tail shows something plausible
run_in_vm "echo '10.42.0.1 - - [\$(date +\"%d/%b/%Y:%H:%M:%S +0000\")] \"GET / HTTP/1.1\" 200 612 \"-\" \"Mozilla/5.0\"' >> /var/log/nginx/access.log"
echo "Q003-prep: Done. /var/log/nginx/access.log inflated on $DOMAIN."
echo " Check disk pressure with: df -h (on the VM)"
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Q004-prep.sh — hermes baseline: web root owned by root, deploy script in place
#
# Prepares sc-web-server for Q004 "Not My Files".
# A bad deploy re-ran as root and chowned the web root to root.
# The deploy script itself is in /opt/deploy/deploy.sh.
#
# What this does:
# - Chowns /var/www/axiomworks and all contents to root:root
# - Places a deploy script at /opt/deploy/deploy.sh (chowned player:player)
# - Ensures nginx is running (deploy will fail but nginx serves stale content)
#
# Idempotent: safe to run multiple times.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-web-server}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
VM_IP=$(get_vm_ip "$DOMAIN")
SSH="ssh $SSH_OPTS player@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
printf '%s\n' "$*" | $SSH "sudo bash -se"
fi
}
echo "Q004-prep: Preparing $DOMAIN for 'Not My Files'..."
# Ensure web root exists and is owned by root (the bug)
run_in_vm "mkdir -p /var/www/axiomworks && chown -R root:root /var/www/axiomworks"
# Create the deploy script as player:player (this is correct — player runs it)
run_in_vm "mkdir -p /opt/deploy"
run_in_vm "cat > /opt/deploy/deploy.sh <<'DEPLOY_SCRIPT'
#!/usr/bin/env bash
# deploy.sh — Axiom Works web deploy
# Copies build artifacts to /var/www/axiomworks/
set -e
SRC=\"\${1:-/home/player/build/dist}\"
rsync -av \"\$SRC/\" /var/www/axiomworks/
echo 'Deploy complete.'
DEPLOY_SCRIPT"
run_in_vm "chown player:player /opt/deploy/deploy.sh && chmod 755 /opt/deploy/deploy.sh"
# Ensure nginx is running (serves stale content with root-owned files)
run_in_vm "systemctl start nginx || true"
echo "Q004-prep: Done. /var/www/axiomworks is owned by root on $DOMAIN."
echo " Player must: sudo chown -R player:player /var/www/axiomworks"
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Q006-post-clean.sh — vulcan clean branch state after Q006
#
# Applies the authored clean outcome of Q006 so seed-vms.sh can materialize
# baseline.post-q006 for later quests.
#
# What this does:
# - Enables and starts systemd-timesyncd
# - Verifies archlinux-keyring is installed
# - Replaces pacman.log failure evidence with a healthy update trail
#
# Idempotent: safe to run multiple times.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-build-machine}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
VM_IP="$(get_vm_ip "$DOMAIN")"
SSH="ssh $SSH_OPTS player@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
printf '%s\n' "$*" | $SSH "sudo bash -se"
fi
}
echo "Q006-post-clean: Applying clean Q006 outcome on $DOMAIN..."
run_in_vm "pacman -Q archlinux-keyring >/dev/null"
run_in_vm "timedatectl set-ntp true || true"
run_in_vm "systemctl enable --now systemd-timesyncd"
run_in_vm "cat > /var/log/pacman.log <<'PACMAN_LOG'
[2026-04-23T09:02:14-0400] [PACMAN] synchronizing package lists
[2026-04-23T09:02:19-0400] [ALPM] transaction started
[2026-04-23T09:02:19-0400] [ALPM] upgraded archlinux-keyring (20260401-1 -> 20260420-1)
[2026-04-23T09:02:20-0400] [ALPM] transaction completed
PACMAN_LOG"
run_in_vm "cat > /var/log/axiomworks/time-drift.note <<'NOTE'
Time sync restored.
systemd-timesyncd is enabled and active.
archlinux-keyring is present and package operations are healthy.
NOTE"
echo "Q006-post-clean: Done. systemd-timesyncd is active and baseline.post-q006 is ready on $DOMAIN."
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Q006-prep.sh — vulcan baseline: time sync disabled, pacman signature errors logged
#
# Prepares sc-build-machine for Q006 "Time Is A Flat Circle".
# The machine clock is drifting because time sync was disabled, which surfaces
# as pacman signature verification failures.
#
# What this does:
# - Disables and stops common NTP services
# - Seeds pacman.log with realistic signature failure evidence
# - Leaves a small operator note pointing at time drift symptoms
#
# Idempotent: safe to run multiple times.
set -euo pipefail
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
DOMAIN="${1:-sc-build-machine}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
get_vm_ip() {
local domain="$1"
local addr=""
addr="$(virsh domifaddr "$domain" --source agent 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
if [ -n "$addr" ]; then
printf '%s\n' "$addr"
return 0
fi
local mac=""
mac="$(virsh dumpxml "$domain" 2>/dev/null | sed -n "s/.*<mac address='\\([^']*\\)'.*/\\1/p" | head -n1)"
[ -n "$mac" ] || return 1
addr="$(virsh net-dhcp-leases sc-internal 2>/dev/null | awk -v mac="$mac" '$0 ~ mac {print $5}' | cut -d/ -f1 | grep -v '^127\.' | head -n1 || true)"
[ -n "$addr" ] || return 1
printf '%s\n' "$addr"
}
SC_SSH_KEY="${SC_SSH_KEY:-${HOME}/.ssh/sc_host_key}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=10 -o LogLevel=ERROR -i $SC_SSH_KEY"
VM_IP="$(get_vm_ip "$DOMAIN")"
SSH="ssh $SSH_OPTS player@$VM_IP"
run_in_vm() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN in $DOMAIN] $*"
else
printf '%s\n' "$*" | $SSH "sudo bash -se"
fi
}
echo "Q006-prep: Preparing $DOMAIN for 'Time Is A Flat Circle'..."
run_in_vm "timedatectl set-ntp false || true"
run_in_vm "systemctl stop systemd-timesyncd ntpd chronyd 2>/dev/null || true"
run_in_vm "systemctl disable systemd-timesyncd ntpd chronyd 2>/dev/null || true"
run_in_vm "mkdir -p /var/log/axiomworks /srv/repo /srv/builds"
run_in_vm "cat > /var/log/pacman.log <<'PACMAN_LOG'
[2026-04-23T08:10:51-0400] [PACMAN] synchronizing package lists
[2026-04-23T08:10:57-0400] [ALPM] transaction started
[2026-04-23T08:10:58-0400] [ALPM] warning: Public keyring not found; have you run 'pacman-key --init'?
[2026-04-23T08:10:58-0400] [ALPM] error: archlinux-keyring: signature from \"Arch Linux Master Key\" is invalid
[2026-04-23T08:10:58-0400] [ALPM] error: failed to commit transaction (invalid or corrupted package (PGP signature))
[2026-04-23T08:10:58-0400] [ALPM] transaction failed
PACMAN_LOG"
run_in_vm "cat > /var/log/axiomworks/time-drift.note <<'NOTE'
Builds started failing after the machine clock fell behind.
Symptoms:
- pacman reports invalid or corrupted package (PGP signature)
- signed packages appear to come from the future
- timedatectl shows NTP inactive
NOTE"
echo "Q006-prep: Done. NTP is disabled and pacman signature failures are seeded on $DOMAIN."
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env bash
# Rebuild or revert game virtual machines.
#
# Usage:
# rebuild-vms.sh Interactive menu
# rebuild-vms.sh --vm workstation Rebuild a single VM (interactive)
# rebuild-vms.sh --revert Revert all VMs to baseline snapshot
# rebuild-vms.sh --revert --vm workstation
# rebuild-vms.sh --snapshot --vm workstation --name before-risky-thing
# rebuild-vms.sh --snapshot --all --name pre-shift-4
# rebuild-vms.sh --revert --name before-risky-thing --vm workstation
# rebuild-vms.sh --dry-run [other flags]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
source "$PROJECT_ROOT/tools/lib/ui.sh"
source "$PROJECT_ROOT/tools/lib/config.sh"
source "$PROJECT_ROOT/tools/lib/libvirt.sh"
source "$PROJECT_ROOT/tools/lib/vm.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_GAME_DIR:-}" ]; then
normalized_game_dir="$(_normalize_dir_path "$SC_GAME_DIR")"
if [ "$normalized_game_dir" != "$SC_GAME_DIR" ]; then
SC_GAME_DIR="$normalized_game_dir"
config_write SC_GAME_DIR "$SC_GAME_DIR"
fi
fi
if [ -n "${SC_IMAGES_DIR:-}" ]; then
normalized_images_dir="$(_normalize_dir_path "$SC_IMAGES_DIR")"
elif [ -n "${SC_GAME_DIR:-}" ]; then
normalized_images_dir="$SC_GAME_DIR/images"
else
normalized_images_dir=""
fi
if [ -n "$normalized_images_dir" ]; then
if [ "${SC_IMAGES_DIR:-}" != "$normalized_images_dir" ]; then
SC_IMAGES_DIR="$normalized_images_dir"
config_write SC_IMAGES_DIR "$SC_IMAGES_DIR"
fi
export SC_IMAGE_ROOT="$SC_IMAGES_DIR"
fi
export LIBVIRT_DEFAULT_URI="${SC_LIBVIRT_URI:-${LIBVIRT_DEFAULT_URI:-qemu:///system}}"
export SC_POOL_NAME="${SC_POOL_NAME:-sc-images}"
export SC_NETWORK_NAME="${SC_NETWORK_NAME:-sc-internal}"
# VM display names
declare -A VM_LABEL=(
[sc-workstation]="workstation"
[sc-web-server]="web server"
[sc-build-machine]="build server"
)
declare -A VM_PROFILE=(
[sc-workstation]=workstation
[sc-web-server]=web-server
[sc-build-machine]=build-machine
)
ALL_VMS=(sc-workstation sc-web-server sc-build-machine)
DRY_RUN=false
MODE=""
SINGLE_VM=""
SNAP_NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--revert) MODE=revert; shift ;;
--snapshot) MODE=snapshot; shift ;;
--vm) SINGLE_VM="sc-$2"; shift 2 ;;
--name) SNAP_NAME="$2"; shift 2 ;;
--all) SINGLE_VM=""; shift ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
done
run() {
if [ "$DRY_RUN" = true ]; then
echo " [dry-run] $*"
else
"$@"
fi
}
# Revert one VM to its newest baseline snapshot; prints result.
_revert_to_baseline() {
local vm="$1"
local label="${VM_LABEL[$vm]:-$vm}"
local candidate local_snap=""
for candidate in "baseline.recovery" "baseline.day-one" "baseline.clean"; do
if snapshot_exists "$vm" "$candidate" 2>/dev/null; then
local_snap="$candidate"
break
fi
done
if [ -n "$local_snap" ]; then
sc_info "Reverting $label to $local_snap..."
run snapshot_revert "$vm" "$local_snap"
sc_ok "$label reverted to $local_snap"
else
sc_warn "No baseline snapshot found for $vm — skipping"
fi
}
_target_vms() {
if [ -n "$SINGLE_VM" ]; then
echo "$SINGLE_VM"
else
printf '%s\n' "${ALL_VMS[@]}"
fi
}
# ---------------------------------------------------------------------------
# Non-interactive flag modes
# ---------------------------------------------------------------------------
if [ "$MODE" = "revert" ] && [ -n "$SNAP_NAME" ]; then
sc_header "REVERTING TO SNAPSHOT: $SNAP_NAME"
while IFS= read -r vm; do
label="${VM_LABEL[$vm]:-$vm}"
sc_info "Reverting $label..."
if snapshot_exists "$vm" "$SNAP_NAME"; then
run snapshot_revert "$vm" "$SNAP_NAME"
sc_ok "$label reverted to $SNAP_NAME"
else
sc_warn "Snapshot '$SNAP_NAME' not found on $vm — skipping"
fi
done < <(_target_vms)
exit 0
fi
if [ "$MODE" = "snapshot" ]; then
[ -n "$SNAP_NAME" ] || { echo " --snapshot requires --name"; exit 1; }
while IFS= read -r vm; do
label="${VM_LABEL[$vm]:-$vm}"
sc_info "Snapshotting $label as '$SNAP_NAME'..."
run vm_snapshot_create "$vm" "$SNAP_NAME"
sc_ok "$label$SNAP_NAME"
done < <(_target_vms)
exit 0
fi
if [ "$MODE" = "revert" ]; then
sc_header "REVERTING TO BASELINE"
while IFS= read -r vm; do
_revert_to_baseline "$vm"
done < <(_target_vms)
exit 0
fi
# ---------------------------------------------------------------------------
# Interactive menu
# ---------------------------------------------------------------------------
sc_header "SYSADMIN CHRONICLES — VM TOOLS"
while true; do
echo " What would you like to do?"
echo ""
echo " 1) Revert all VMs to last known good (fast — ~30s)"
echo " 2) Rebuild workstation (~8 min)"
echo " 3) Rebuild web server (~4 min)"
echo " 4) Rebuild build server (~5 min)"
echo " 5) Rebuild everything (~20 min)"
echo " 6) Take a snapshot"
echo " 7) Revert to a named snapshot"
echo ""
echo " q) Cancel"
echo ""
printf " > " >/dev/tty
read -r choice </dev/tty
echo ""
case "$choice" in
q|Q)
echo " Cancelled."
exit 0
;;
1)
sc_section "Reverting all VMs to baseline"
for vm in "${ALL_VMS[@]}"; do
_revert_to_baseline "$vm"
done
break
;;
2|3|4|5)
case "$choice" in
2) targets=(sc-workstation) ;;
3) targets=(sc-web-server) ;;
4) targets=(sc-build-machine) ;;
5) targets=("${ALL_VMS[@]}") ;;
esac
overall_status=0
for vm in "${targets[@]}"; do
label="${VM_LABEL[$vm]:-$vm}"
profile="${VM_PROFILE[$vm]}"
echo ""
sc_warn "This will permanently rebuild $label."
sc_warn "Quest progress on this VM will be lost."
echo ""
if sc_confirm "Back up save data first?" "Y"; then
BACKUP="$HOME/.local/share/sysadmin-chronicles/saves/pre-rebuild-$(date +%Y%m%d-%H%M%S).json"
[ -f "$HOME/.local/share/sysadmin-chronicles/saves/autosave.json" ] \
&& cp "$HOME/.local/share/sysadmin-chronicles/saves/autosave.json" "$BACKUP" \
&& sc_ok "Save backed up to $BACKUP" || sc_info "(no autosave found)"
fi
if ! sc_confirm "Rebuild $label now?" "N"; then
sc_info "Skipping $label."
continue
fi
sc_section "Rebuilding $label"
logfile="$HOME/.local/share/sysadmin-chronicles/rebuild-${profile}.log"
printf " Rebuilding %-18s " "$label"
start_ts="$(date +%s)"
if run vm_rebuild "$profile" $( [ "$DRY_RUN" = true ] && echo "--dry-run" ) \
> "$logfile" 2>&1; then
elapsed=$(( $(date +%s) - start_ts ))
printf "✓ %dm %02ds\n" $(( elapsed / 60 )) $(( elapsed % 60 ))
else
printf "✗\n"
sc_warn "Rebuild failed — see $logfile"
overall_status=1
continue
fi
# Re-run quest prep and re-snapshot
sc_info "Re-running quest prep for $vm..."
if run bash "$PROJECT_ROOT/tools/setup/seed-vms.sh" --skip-build --vm "${profile//-/_}" \
>> "$logfile" 2>&1; then
sc_ok "$label rebuild complete"
else
sc_warn "Quest prep had errors — see $logfile"
overall_status=1
fi
done
[ "$overall_status" -eq 0 ] || exit "$overall_status"
break
;;
6)
echo " Take a snapshot"
echo ""
echo " Which VM?"
for i in "${!ALL_VMS[@]}"; do
vm="${ALL_VMS[$i]}"
printf " %d) %s\n" $(( i + 1 )) "${VM_LABEL[$vm]:-$vm}"
done
printf " > " >/dev/tty
read -r vm_choice </dev/tty
echo ""
vm="${ALL_VMS[$(( vm_choice - 1 ))]:-}"
[ -n "$vm" ] || { sc_warn "Invalid choice"; continue; }
printf " Snapshot name (letters, numbers, hyphens): " >/dev/tty
read -r snap_name </dev/tty
echo ""
run vm_snapshot_create "$vm" "$snap_name" \
&& sc_ok "Snapshot created: $snap_name on ${VM_LABEL[$vm]:-$vm}" \
|| sc_warn "Snapshot failed."
break
;;
7)
echo " Revert to a named snapshot"
echo ""
echo " Which VM?"
for i in "${!ALL_VMS[@]}"; do
vm="${ALL_VMS[$i]}"
printf " %d) %s\n" $(( i + 1 )) "${VM_LABEL[$vm]:-$vm}"
done
printf " > " >/dev/tty
read -r vm_choice </dev/tty
echo ""
vm="${ALL_VMS[$(( vm_choice - 1 ))]:-}"
[ -n "$vm" ] || { sc_warn "Invalid choice"; continue; }
echo " Available snapshots on ${VM_LABEL[$vm]:-$vm}:"
virsh snapshot-list "$vm" --name 2>/dev/null | grep -v '^$' | sed 's/^/ /' || true
echo ""
printf " Snapshot name to revert to: " >/dev/tty
read -r snap_name </dev/tty
echo ""
snap_date="$(virsh snapshot-info "$vm" "$snap_name" 2>/dev/null | grep 'Creation Time' | awk '{print $3, $4}' || echo "")"
[ -n "$snap_date" ] && sc_info "Snapshot date: $snap_date"
if sc_confirm "Revert ${VM_LABEL[$vm]:-$vm} to '$snap_name'?" "N"; then
run vm_snapshot_revert "$vm" "$snap_name" \
&& sc_ok "Reverted to $snap_name" \
|| sc_warn "Revert failed."
fi
break
;;
*)
sc_warn "Invalid choice — enter 17 or q."
;;
esac
done
echo ""
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# Repair trusted desktop launcher metadata in an existing sc-workstation VM.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
DOMAIN="${SC_WORKSTATION_DOMAIN:-sc-workstation}"
tmp_script="$(mktemp)"
trap 'rm -f "$tmp_script"' EXIT
cat > "$tmp_script" <<'GUESTEOF'
set -euo pipefail
install -d -o player -g player /home/player/Desktop /home/player/.local/bin /home/player/.config/autostart
find /home/player/Desktop -maxdepth 1 -type f -name '*.desktop' -exec chown player:player {} +
find /home/player/Desktop -maxdepth 1 -type f -name '*.desktop' -exec chmod 0755 {} +
if [ -f /home/player/.config/chromium/Default/Bookmarks ]; then
sudo -u player sed -i 's#http://www\.axiomworks\.corp/#https://www.axiomworks.corp/#g' /home/player/.config/chromium/Default/Bookmarks
fi
cat > /usr/local/bin/trust-desktop-launchers <<'SCRIPTEOF'
#!/bin/bash
set -u
PATH=/usr/local/bin:/usr/bin:/bin
player_uid="$(id -u player)"
desktop_dir=/home/player/Desktop
export HOME=/home/player
export USER=player
export LOGNAME=player
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-/home/player/.Xauthority}"
export XDG_RUNTIME_DIR="/run/user/$player_uid"
if [ -S "$XDG_RUNTIME_DIR/bus" ]; then
export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
fi
metadata_daemon=""
for candidate in /usr/libexec/gvfsd-metadata /usr/lib/gvfs/gvfsd-metadata /usr/lib/x86_64-linux-gnu/gvfs/gvfsd-metadata; do
if [ -x "$candidate" ]; then
metadata_daemon="$candidate"
break
fi
done
if [ -n "$metadata_daemon" ] && ! /usr/bin/pgrep -u "$player_uid" -x gvfsd-metadata >/dev/null 2>&1; then
"$metadata_daemon" >/dev/null 2>&1 &
sleep 1
fi
for i in $(/usr/bin/seq 1 20); do
trusted_any=false
failed=false
for launcher in "$desktop_dir"/*.desktop; do
[ -e "$launcher" ] || continue
chmod 0755 "$launcher" 2>/dev/null || true
checksum="$(/usr/bin/sha256sum "$launcher" | /usr/bin/awk '{print $1}')" || {
failed=true
continue
}
if /usr/bin/gio set -t string "$launcher" metadata::xfce-exe-checksum "$checksum" 2>/dev/null; then
actual_checksum="$(/usr/bin/gio info -a metadata::xfce-exe-checksum "$launcher" 2>/dev/null | /usr/bin/awk -F': ' '/metadata::xfce-exe-checksum:/ {print $2; exit}')"
owner_mode="$(/usr/bin/stat -c '%U:%G %a' "$launcher" 2>/dev/null || true)"
if [ "$actual_checksum" != "$checksum" ] || [ "$owner_mode" != "player:player 755" ]; then
failed=true
continue
fi
trusted_any=true
else
failed=true
fi
done
if [ "$trusted_any" = true ] && [ "$failed" = false ]; then
/usr/bin/xfdesktop --reload >/dev/null 2>&1 || /usr/bin/pkill -HUP xfdesktop 2>/dev/null || true
rm -f /home/player/.config/autostart/trust-launchers.desktop
exit 0
fi
sleep 1
done
exit 1
SCRIPTEOF
chmod 0755 /usr/local/bin/trust-desktop-launchers
cat > /home/player/.local/bin/trust-desktop-launchers.sh <<'SCRIPTEOF'
#!/bin/bash
exec /usr/local/bin/trust-desktop-launchers
SCRIPTEOF
chown player:player /home/player/.local/bin/trust-desktop-launchers.sh
chmod 0755 /home/player/.local/bin/trust-desktop-launchers.sh
cat > /home/player/.config/autostart/trust-launchers.desktop <<'DESKTOPEOF'
[Desktop Entry]
Type=Application
Name=Trust Desktop Launchers
Exec=/usr/local/bin/trust-desktop-launchers
Terminal=false
X-GNOME-Autostart-enabled=true
Hidden=false
NoDisplay=true
DESKTOPEOF
chown player:player /home/player/.config/autostart/trust-launchers.desktop
chmod 0644 /home/player/.config/autostart/trust-launchers.desktop
if [ -S "/run/user/$(id -u player)/bus" ]; then
sudo -u player env HOME=/home/player /usr/local/bin/trust-desktop-launchers
else
echo "Player DBus session is not active; repair will retry on next graphical login." >&2
fi
GUESTEOF
guest_run_sudo_script "$DOMAIN" "$tmp_script"
ok "Desktop launcher repair applied to $DOMAIN"
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# snapshot-all.sh — Snapshot or revert all game VMs at once.
#
# Usage:
# bash tools/vm/snapshot-all.sh --snapshot <name> Create named snapshot on all VMs
# bash tools/vm/snapshot-all.sh --revert-to <name> Revert all VMs to named snapshot
# bash tools/vm/snapshot-all.sh --list List all snapshots per VM
# bash tools/vm/snapshot-all.sh --dry-run --revert-to ... Dry run (no state changes)
#
# SAFETY:
# - Only operates on sc- prefixed domains.
# - Always prints a summary before modifying state.
# - --revert-to requires explicit confirmation (skipped with --yes flag).
# - This script is for developer use only. It is NOT available in-game.
#
# AGENT RULES:
# - Never run --revert-to without explicit user instruction.
# - Never run against domains that don't start with sc-.
set -euo pipefail
VMS=("sc-workstation" "sc-web-server" "sc-build-machine")
DRY_RUN=false
ASSUME_YES=false
SNAPSHOT_NAME=""
REVERT_NAME=""
LIST_MODE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--snapshot) SNAPSHOT_NAME="$2"; shift 2 ;;
--revert-to) REVERT_NAME="$2"; shift 2 ;;
--list) LIST_MODE=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--yes) ASSUME_YES=true; shift ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
done
run() {
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN] $*"
else
"$@"
fi
}
guard_prefix() {
local dom="$1"
if [[ "$dom" != sc-* ]]; then
echo "SAFETY: refusing to operate on non-game domain: $dom"
exit 1
fi
}
# ---------------------------------------------------------------------------
if [ "$LIST_MODE" = "true" ]; then
echo ""
echo "── Snapshots per VM ─────────────────────────────"
for dom in "${VMS[@]}"; do
echo ""
echo " $dom:"
if virsh dominfo "$dom" &>/dev/null 2>&1; then
virsh snapshot-list "$dom" --name 2>/dev/null | sed 's/^/ /' || echo " (none)"
else
echo " (domain does not exist)"
fi
done
echo ""
exit 0
fi
# ---------------------------------------------------------------------------
# SNAPSHOT
# ---------------------------------------------------------------------------
if [ -n "$SNAPSHOT_NAME" ]; then
echo ""
echo "Creating snapshot '$SNAPSHOT_NAME' on all game VMs..."
[ "$DRY_RUN" = "true" ] && echo "[DRY-RUN mode]"
echo ""
for dom in "${VMS[@]}"; do
guard_prefix "$dom"
if virsh dominfo "$dom" &>/dev/null 2>&1; then
echo " Snapshotting $dom..."
run virsh snapshot-create-as "$dom" "$SNAPSHOT_NAME" \
--description "Created by snapshot-all.sh" \
--atomic
echo "$dom$SNAPSHOT_NAME"
else
echo "$dom not found — skipping"
fi
done
echo ""
echo "Done."
exit 0
fi
# ---------------------------------------------------------------------------
# REVERT
# ---------------------------------------------------------------------------
if [ -n "$REVERT_NAME" ]; then
echo ""
echo "══════════════════════════════════════════════════"
echo " REVERT ALL VMs TO: $REVERT_NAME"
echo "══════════════════════════════════════════════════"
echo " VMs: ${VMS[*]}"
echo " This will DISCARD all unsaved VM state."
[ "$DRY_RUN" = "true" ] && echo " [DRY-RUN mode — no changes will be made]"
echo ""
if [ "$ASSUME_YES" = "false" ] && [ "$DRY_RUN" = "false" ]; then
read -rp " Type YES to confirm revert: " confirm
if [ "$confirm" != "YES" ]; then
echo " Aborted."
exit 0
fi
fi
for dom in "${VMS[@]}"; do
guard_prefix "$dom"
if virsh dominfo "$dom" &>/dev/null 2>&1; then
echo " Reverting $dom..."
# Stop VM first if running
if virsh domstate "$dom" 2>/dev/null | grep -q "running"; then
run virsh destroy "$dom"
fi
run virsh snapshot-revert "$dom" "$REVERT_NAME" --running
echo "$dom$REVERT_NAME"
else
echo "$dom not found — skipping"
fi
done
echo ""
echo "Revert complete."
exit 0
fi
# No mode selected
echo "Usage:"
echo " bash snapshot-all.sh --snapshot <name>"
echo " bash snapshot-all.sh --revert-to <name>"
echo " bash snapshot-all.sh --list"
echo " Add --dry-run to preview without changes."
exit 1
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# suppress-maintenance-noise.sh — Reduce guest maintenance output noise.
#
# Suppresses on Debian/Ubuntu guests:
# - APT periodic background updates
# - MOTD dynamic scripts (package counts, landscape-sysinfo, news)
# - PAM motd modules (dynamic MOTD printed at login)
# - "X updates can be applied immediately" login banner
#
# Suppresses on Arch guests:
# - pkgfile update timer (if present)
# - quiet-mode marker for game to detect
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DRY_RUN=false
DOMAIN="${1:-}"
if [ -z "$DOMAIN" ]; then
echo "Usage: bash tools/vm/suppress-maintenance-noise.sh <domain> [--dry-run]"
exit 1
fi
if [[ "${2:-}" == "--dry-run" ]]; then
DRY_RUN=true
fi
source "$SCRIPT_DIR/lib/common.sh"
ensure_vm_tooling
tmp_script="$(mktemp)"
cat > "$tmp_script" <<'EOF'
# --- Debian/Ubuntu ---
if command -v apt-get >/dev/null 2>&1; then
# Disable APT background periodic tasks
mkdir -p /etc/apt/apt.conf.d
cat > /etc/apt/apt.conf.d/99sysadmin-chronicles-quiet <<'APT'
APT::Periodic::Enable "0";
APT::Periodic::Update-Package-Lists "0";
APT::Periodic::Unattended-Upgrade "0";
APT::Periodic::Download-Upgradeable-Packages "0";
Acquire::Languages "none";
APT
# Disable dynamic MOTD scripts that show update counts, ads, news
if [ -d /etc/update-motd.d ]; then
chmod -x /etc/update-motd.d/* 2>/dev/null || true
# Preserve a minimal placeholder so PAM doesn't error
printf '#!/bin/sh\n' > /etc/update-motd.d/00-sysadmin-chronicles
chmod +x /etc/update-motd.d/00-sysadmin-chronicles
fi
# Remove static /etc/motd content if present
if [ -f /etc/motd ]; then
printf '' > /etc/motd
fi
# Disable PAM dynamic motd in sshd PAM config (suppresses update counts at login)
for pam_file in /etc/pam.d/sshd /etc/pam.d/login; do
if [ -f "$pam_file" ]; then
sed -i 's/^session\s\+optional\s\+pam_motd\.so/#&/' "$pam_file"
fi
done
# Suppress "X updates can be applied" from landscape-sysinfo / update-notifier
if [ -f /etc/landscape/client.conf ]; then
sed -i '/sysinfo/d' /etc/landscape/client.conf 2>/dev/null || true
fi
# Disable landscape-sysinfo if installed
if command -v landscape-sysinfo >/dev/null 2>&1; then
if [ -f /etc/landscape/client.conf ]; then
grep -q 'include_sysinfo_plugins' /etc/landscape/client.conf || \
printf '[sysinfo]\ninclude_sysinfo_plugins =\n' >> /etc/landscape/client.conf
else
mkdir -p /etc/landscape
printf '[sysinfo]\ninclude_sysinfo_plugins =\n' > /etc/landscape/client.conf
fi
fi
# Disable update-notifier login hint (Debian/Ubuntu)
if [ -d /etc/profile.d ]; then
for f in /etc/profile.d/update-notifier.sh /etc/profile.d/motd-news.sh; do
[ -f "$f" ] && chmod -x "$f" 2>/dev/null || true
done
fi
fi
# --- Arch Linux ---
if command -v pacman >/dev/null 2>&1; then
# Disable pkgfile update timer if present (produces periodic output)
if systemctl list-unit-files pkgfile-update.timer &>/dev/null; then
systemctl disable --now pkgfile-update.timer 2>/dev/null || true
fi
# Quiet-mode marker for game to detect
mkdir -p /etc/sysadmin-chronicles
printf 'managed=true\n' > /etc/sysadmin-chronicles/quiet-mode.conf
fi
EOF
info "Suppressing maintenance noise on ${DOMAIN}"
guest_run_sudo_script "$DOMAIN" "$tmp_script"
rm -f "$tmp_script"
ok "${DOMAIN}: maintenance noise suppressed"