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:
Executable
+3
@@ -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" "$@"
|
||||
Executable
+140
@@ -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"
|
||||
Executable
+3
@@ -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" "$@"
|
||||
Executable
+3
@@ -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" "$@"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
Executable
+103
@@ -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
|
||||
}
|
||||
Executable
+158
@@ -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
|
||||
}
|
||||
Executable
+734
@@ -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
|
||||
}
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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)"
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
Executable
+311
@@ -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 1–7 or q."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
Executable
+113
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user