mirror of
https://github.com/theupriser/steamify-cachyos-dev.git
synced 2026-10-05 12:45:56 +02:00
224 lines
13 KiB
Bash
Executable File
224 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# Install a fresh, ssh-ready test VM unattended: CachyOS (KDE Plasma,
|
|
# plasma-login-manager, btrfs, Limine, linux-cachyos + -lts) from the ISO's
|
|
# headless installer, then sshd with your key, passwordless sudo and the test
|
|
# autologin into Plasma. Ends with the snapshots `clean` and `ssh-ready`.
|
|
# scripts/vminstall.sh [--force] [--iso <file>] [run.sh flags...] e.g. --fremont
|
|
# --force replace an existing disk.qcow2 in $VM_DIR (asks first)
|
|
# --iso <file> the live ISO (default $VM_DIR/cachyos.iso, e.g. a
|
|
# Steamify CachyOS build later; it must be archiso-based and
|
|
# have cachyos-installer)
|
|
# Env: VM_DIR (see common.sh), VM_USER (default: your host username; theupriser when that is root),
|
|
# VM_PASSWORD (steamify), VM_SSH_KEY (private key to authorize; default
|
|
# ~/.ssh/steamify-vm_ed25519, asked once when that doesn't exist), REPO (shared as 9p `repo`, default ../steamify-cachyos),
|
|
# VM_INSTALL_MEM (RAM of the live installer VM only; default VM_MEM, else run.sh's 8G),
|
|
# VM_CACHE (host dir for the packages, default ~/vms/pkg-cache; empty = none),
|
|
# VM_STEAMIFY (a Steamify ISO also runs steamify-install: the Steamify page's ids,
|
|
# empty = defaults, skip = plain CachyOS), VM_BOOTLOADER (limine; or systemd-boot, grub), VM_TIMEZONE (the host's), VM_HOST_IP (the host as the guest sees it,
|
|
# 10.0.2.2 with QEMU's user networking).
|
|
# The VM's user and key are recorded in $VM_DIR (vm-user, ssh-key) for the
|
|
# other scripts. Never touches your existing SSH keys or ~/.ssh/known_hosts.
|
|
set -euo pipefail
|
|
[[ -n "${CI:-}" ]] && export VM_HEADLESS=1 # no display in CI (QEMU fails with "OpenGL is not supported by the display")
|
|
here="$(cd "$(dirname "$0")" && pwd)"
|
|
repo="$(cd "$here/.." && pwd)"
|
|
[[ -n "${VM_USER:-}" ]] || VM_USER="$(id -un)"
|
|
[[ "$VM_USER" != root ]] || VM_USER=theupriser # root can't be the guest user (WSL runs as root)
|
|
export VM_USER
|
|
. "$here/common.sh"
|
|
# The pacman package cache, on the host and shared by every VM you install
|
|
# (9p tag `cache`, see run.sh): a second install downloads nothing. VM_CACHE= (empty) turns it off.
|
|
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}"
|
|
|
|
force=false iso="" run_flags=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--force) force=true; shift ;;
|
|
--iso) iso="${2:?--iso needs a file}"; shift 2 ;;
|
|
*) run_flags+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
password="${VM_PASSWORD:-steamify}"
|
|
timezone="${VM_TIMEZONE:-$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo UTC)}"
|
|
hostname=steamify-vm
|
|
REPO="${REPO:-$(dirname "$repo")/steamify-cachyos}"
|
|
[[ -d "$REPO" ]] || { echo "REPO $REPO doesn't exist (the Steamify checkout shared into the VM); set REPO=." >&2; exit 1; }
|
|
export REPO
|
|
|
|
mkdir -p "$VM_DIR"
|
|
iso="${iso:-$VM_DIR/cachyos.iso}"
|
|
[[ -f "$iso" ]] || { echo "No ISO at $iso: run get-iso.sh (in $VM_DIR) or pass --iso <file>." >&2; exit 1; }
|
|
iso="$(cd "$(dirname "$iso")" && pwd)/$(basename "$iso")"
|
|
pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null && { echo "A VM is running on port $VM_PORT; power it off first." >&2; exit 1; }
|
|
if [[ -f "$VM_DIR/disk.qcow2" ]]; then
|
|
[[ "$force" == true ]] || { echo "$VM_DIR already has a disk.qcow2; use --force to replace it, or another VM_DIR." >&2; exit 1; }
|
|
read -r -p "Replace $VM_DIR/disk.qcow2 and all its snapshots? Type YES: " answer < /dev/tty
|
|
[[ "$answer" == YES ]] || { echo "Nothing changed."; exit 1; }
|
|
fi
|
|
|
|
# --- The SSH key to authorize: a new one just for the VM, or one of yours.
|
|
# The VM's own key is made once and then always used: asked only when it
|
|
# doesn't exist yet.
|
|
dedicated="$HOME/.ssh/steamify-vm_ed25519"
|
|
[[ -z "${VM_SSH_KEY:-}" && -f "$dedicated" ]] && VM_SSH_KEY="$dedicated"
|
|
if [[ -z "${VM_SSH_KEY:-}" ]]; then
|
|
mapfile -t pubs < <(ls "$HOME"/.ssh/*.pub 2>/dev/null | grep -vxF "$dedicated.pub")
|
|
echo "Which SSH key should log in to the VM?"
|
|
echo " 1) a new key just for the VM, used from now on: $dedicated (no passphrase)"
|
|
for i in "${!pubs[@]}"; do echo " $((i + 2))) your key ${pubs[$i]%.pub}"; done
|
|
read -r -p "Choice [1]: " choice < /dev/tty
|
|
choice="${choice:-1}"
|
|
if [[ "$choice" == 1 ]]; then
|
|
VM_SSH_KEY="$dedicated"
|
|
# Never overwrite a key: it doesn't exist here.
|
|
ssh-keygen -q -t ed25519 -N "" -C "steamify-vm" -f "$dedicated"
|
|
elif [[ "$choice" =~ ^[0-9]+$ && -n "${pubs[$((choice - 2))]:-}" ]]; then
|
|
VM_SSH_KEY="${pubs[$((choice - 2))]%.pub}"
|
|
else
|
|
echo "Unknown choice: $choice" >&2; exit 1
|
|
fi
|
|
fi
|
|
[[ -f "$VM_SSH_KEY.pub" ]] || { echo "No public key $VM_SSH_KEY.pub." >&2; exit 1; }
|
|
pubkey="$(cat "$VM_SSH_KEY.pub")"
|
|
|
|
echo "VM: $VM_DIR user: $VM_USER password: $password hostname: $hostname timezone: $timezone"
|
|
echo "ISO: $iso key: $VM_SSH_KEY"
|
|
|
|
# --- The ISO's kernel and initramfs, to boot it with our parameters.
|
|
boot="$VM_DIR/iso-boot"
|
|
mkdir -p "$boot"
|
|
# One loop setup at a time: parallel installs (vmtest.sh --install) otherwise get "Device or resource busy".
|
|
exec 9> /tmp/vminstall-iso.lock; flock 9
|
|
if command -v bsdtar >/dev/null; then
|
|
# No loop device, no udisks, no root: libarchive reads the ISO9660 image directly (a CI container has no
|
|
# loop partition nodes, so the mounts below can't work there).
|
|
mnt="$(mktemp -d)"
|
|
cleanup_loop() { chmod -R u+rwX "$mnt" 2>/dev/null || true; rm -rf "$mnt" 2>/dev/null || true; } # the ISO's files come out read-only
|
|
trap cleanup_loop EXIT
|
|
bsdtar -xf "$iso" -C "$mnt" arch/boot/x86_64/vmlinuz-linux-cachyos arch/boot/x86_64/initramfs-linux-cachyos.img boot EFI 2>/dev/null || true
|
|
[[ -f "$mnt/arch/boot/x86_64/vmlinuz-linux-cachyos" ]] || { echo "bsdtar found no kernel in the ISO." >&2; exit 1; }
|
|
elif ! command -v udisksctl >/dev/null && [[ $EUID -eq 0 ]]; then
|
|
# No udisks (the WSL box, as root): a plain loop device and mount.
|
|
loop="$(losetup -f -r -P --show "$iso")"
|
|
mnt="$(mktemp -d)"
|
|
cleanup_loop() { umount "$mnt" 2>/dev/null || true; rmdir "$mnt" 2>/dev/null || true; losetup -d "$loop" 2>/dev/null || true; }
|
|
trap cleanup_loop EXIT
|
|
mount -r "${loop}p1" "$mnt" 2>/dev/null || mnt=""
|
|
else
|
|
loop="$(udisksctl loop-setup --no-user-interaction -r -f "$iso" | grep -o '/dev/loop[0-9]*')"
|
|
cleanup_loop() { udisksctl unmount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; udisksctl loop-delete --no-user-interaction -b "$loop" >/dev/null 2>&1 || true; }
|
|
trap cleanup_loop EXIT
|
|
mnt=""
|
|
for _ in $(seq 10); do
|
|
mnt="$(findmnt -nro TARGET "${loop}p1" 2>/dev/null || true)"
|
|
[[ -n "$mnt" ]] && break
|
|
udisksctl mount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; sleep 1
|
|
done
|
|
fi
|
|
[[ -n "$mnt" ]] || { echo "Couldn't mount the ISO." >&2; exit 1; }
|
|
rm -f "$boot"/* # copied read-only from the ISO
|
|
install -m 644 "$mnt/arch/boot/x86_64/vmlinuz-linux-cachyos" "$mnt/arch/boot/x86_64/initramfs-linux-cachyos.img" "$boot/"
|
|
search="$(grep -rhoE 'archisosearchuuid=[^ ]+' "$mnt/boot" "$mnt/EFI" 2>/dev/null | head -1)"
|
|
[[ -n "$search" ]] || { echo "No archisosearchuuid in the ISO's boot menu." >&2; exit 1; }
|
|
cleanup_loop; trap - EXIT
|
|
flock -u 9; exec 9>&-
|
|
|
|
# --- What the live system fetches from the host.
|
|
www="$VM_DIR/install-www"
|
|
rm -rf "$www"; mkdir -p "$www"
|
|
# QEMU's user networking (-nic user in run.sh) always shows the host to the
|
|
# guest at 10.0.2.2, whatever network the host is on; only a bridged/tap
|
|
# setup would need another address.
|
|
host_ip="${VM_HOST_IP:-10.0.2.2}"
|
|
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1])')"
|
|
sed -e "s/@PORT@/$port/" -e "s/@HOST_IP@/$host_ip/" "$repo/share/vminstall-live.sh" > "$www/vminstall-live.sh"
|
|
cp "$repo/share/vminstall-post.sh" "$www/"
|
|
{
|
|
printf 'VM_USER=%q\nVM_HOSTNAME=%q\nVM_PUBKEY=%q\nDEVICE=/dev/vda\nVM_STEAMIFY=%q\n' "$VM_USER" "$hostname" "$pubkey" "${VM_STEAMIFY:-}"
|
|
} > "$www/vminstall.env"
|
|
python3 - "$www/settings.json" "$VM_USER" "$password" "$timezone" "$hostname" "${VM_BOOTLOADER:-limine}" << 'PY'
|
|
import json, sys
|
|
out, user, pw, tz, host, bootloader = sys.argv[1:]
|
|
json.dump({
|
|
"install_type": "simple", "headless_mode": True,
|
|
"device": "/dev/vda", "fs_name": "btrfs", "subvolumes": "default",
|
|
"partitions": [
|
|
{"name": "/dev/vda1", "mountpoint": "/boot", "size": "2G", "fs_name": "vfat", "type": "boot"},
|
|
{"name": "/dev/vda2", "mountpoint": "/", "size": "100%", "type": "root"},
|
|
],
|
|
"hostname": host, "locale": "en_US.UTF-8", "xkbmap": "us", "timezone": tz,
|
|
"user_name": user, "user_pass": pw, "user_shell": "/bin/fish", "root_pass": pw,
|
|
"kernel": "linux-cachyos linux-cachyos-lts", "desktop": "kde", "bootloader": bootloader,
|
|
}, open(out, "w"), indent=2)
|
|
PY
|
|
python3 "$here/vminstall-server.py" "$www" "$port" &
|
|
server=$!
|
|
trap 'kill $server 2>/dev/null || true' EXIT
|
|
|
|
# --- Install.
|
|
cd "$VM_DIR"
|
|
cp "$repo/run.sh" run.sh # always this repo's: the install needs its kernel/serial options
|
|
[[ -e share ]] || ln -s "$repo/share" share
|
|
rm -f disk.qcow2 vars.fd vars.clean.fd vars.ssh-ready.fd known_hosts
|
|
qemu-img create -q -f qcow2 disk.qcow2 60G
|
|
echo "$VM_USER" > vm-user
|
|
echo "$REPO" > repo-path
|
|
echo "$VM_SSH_KEY" > ssh-key
|
|
# systemd.run= only generates kernel-command-line.service; systemd.wants=
|
|
# starts it next to the normal boot (desktop, network). It fetches and runs
|
|
# the live script. (Root logs in on the serial console without a password:
|
|
# VM_SERIAL.sock, for debugging.)
|
|
append="$search archisobasedir=arch cow_spacesize=10G module_blacklist=pcspkr console=ttyS0,115200 console=tty0"
|
|
append+=" systemd.run=\"/usr/bin/bash -c 'curl -fsS --retry 150 --retry-all-errors --retry-delay 2 http://$host_ip:$port/vminstall-live.sh | bash'\""
|
|
append+=" systemd.run_success_action=none systemd.run_failure_action=none systemd.wants=kernel-command-line.service"
|
|
echo "Installing: watch the VM's window, or follow the console with tail -f $VM_DIR/serial.log (install log: $www/install.log)..."
|
|
VM_MEM="${VM_INSTALL_MEM:-${VM_MEM:-}}" VM_SERIAL="$VM_DIR/serial.log" VM_ISO="$iso" VM_KERNEL="$boot/vmlinuz-linux-cachyos" VM_INITRD="$boot/initramfs-linux-cachyos.img" VM_APPEND="$append" \
|
|
./run.sh install "${run_flags[@]}" > vm-install.log 2>&1 &
|
|
qemu=$!
|
|
# 90 minutes; a failure reported by the live system ends it at once (it stays up after one)
|
|
for _ in $(seq 1080); do
|
|
kill -0 $qemu 2>/dev/null || break
|
|
[[ -s "$www/status" && "$(cat "$www/status")" != ok ]] && { sleep 5; kill $qemu 2>/dev/null; wait $qemu 2>/dev/null; break; }
|
|
sleep 5
|
|
done
|
|
kill -0 $qemu 2>/dev/null && { echo "The install didn't finish in 90 minutes; see $www/install.log." >&2; kill $qemu; exit 1; }
|
|
status="$(cat "$www/status" 2>/dev/null || echo "no status (the live system never reported back)")"
|
|
cp "$www/install.log" install.log 2>/dev/null || true
|
|
if [[ "$status" != ok ]]; then
|
|
echo "Install failed: $status. Log: $VM_DIR/install.log" >&2
|
|
# In CI only the job log survives: show why.
|
|
for f in vm-install.log serial.log install.log; do [[ -s "$f" ]] && { echo "--- $f (last 40 lines)" >&2; tail -n 40 "$f" | sed 's/\x1b\[[0-9;]*[A-Za-z]//g' >&2; }; done
|
|
exit 1
|
|
fi
|
|
qemu-img snapshot -c clean disk.qcow2 && cp vars.fd vars.clean.fd
|
|
echo "Installed; snapshot 'clean' taken."
|
|
|
|
# --- First boot: check it's ssh-ready, then snapshot.
|
|
nohup ./run.sh "${run_flags[@]}" > vm.log 2>&1 &
|
|
vm=$!
|
|
for _ in $(seq 100); do vm_ssh -o ConnectTimeout=3 true 2>/dev/null && break; sleep 3; done
|
|
vm_ssh true || { echo "No SSH after the first boot; see $VM_DIR/vm.log." >&2; exit 1; }
|
|
# The poweroff at the end may drop the connection (ssh exits 255): judge by the ssh-ready line instead.
|
|
out="$(vm_ssh bash -s 2>&1 << 'REMOTE'
|
|
set -e
|
|
if [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]]; then
|
|
# Steamify's gaming mode (gamescope, no Plasma, and it doesn't render in the VM): the login manager has to be up.
|
|
for _ in $(seq 60); do systemctl is-active --quiet display-manager && break; sleep 3; done
|
|
systemctl is-active --quiet display-manager || { echo "The login manager didn't start." >&2; exit 1; }
|
|
else
|
|
for _ in $(seq 60); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
|
pgrep -u "$USER" -x plasmashell >/dev/null || { echo "Plasma didn't start." >&2; exit 1; }
|
|
fi
|
|
sudo -n true
|
|
echo "ssh-ready: $(id -un)@$(hostname), $(uname -r), $(localectl status | sed -n 's/.*LANG=//p'), $(timedatectl show -p Timezone --value)"
|
|
sudo systemctl poweroff
|
|
REMOTE
|
|
)" || true
|
|
printf '%s\n' "$out" | grep -v '^Connection to .* closed'
|
|
grep -q '^ssh-ready: ' <<< "$out" || { echo "The first boot check failed; see above and $VM_DIR/vm.log." >&2; kill $vm 2>/dev/null; exit 1; }
|
|
while kill -0 $vm 2>/dev/null; do sleep 2; done # this VM only: others may run in parallel
|
|
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
|
|
rm -rf "$www"
|
|
echo "Done: snapshots 'clean' and 'ssh-ready' in $VM_DIR."
|
|
echo "Log in by hand as $VM_USER / $password (root: $password)."
|