chore: bootstrap lean sysadmin-chronicles repo
Import the runnable game code, content, docs, scripts, and repo guidance while leaving local agent state, dependency installs, build output, and backup copies out of the published tree.
This commit is contained in:
@@ -0,0 +1,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"
|
||||
}
|
||||
Reference in New Issue
Block a user