Initial Commit

This commit is contained in:
2026-09-15 15:24:03 -04:00
commit 4dc7086e7e
605 changed files with 101380 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/bin/bash
# =====================================================================
# Puts installation media where build.sh can find it, having checked it.
#
# sudo ./fetch.sh ubuntu # download the pinned release
# sudo ./fetch.sh ubuntu ~/Downloads/x.iso # use one already downloaded
# sudo ./fetch.sh windows ~/Downloads/x.iso
#
# EITHER WAY IT IS CHECKSUMMED
#
# A local file is not trusted more than a downloaded one - it is the same
# 6 GB of installer that becomes the machine every session runs from. For
# Ubuntu the hash is compared against Canonical's published SHA256SUMS
# for that series, fetched at the time, rather than a constant pasted in
# here that goes stale at the next point release and gets deleted.
#
# Windows media has no published list to check against, so its hash is
# only RECORDED. That is a real difference and it is not papered over:
# the manifest says where the file came from and what it hashed to, and
# that is all anyone can honestly claim about it.
#
# WHY UBUNTU IS NOT FETCHED WITH quickget
#
# quickget was adopted for image acquisition because the Windows side -
# locating an ISO, pairing it with the right virtio-win build - is
# genuinely fiddly and well solved there. Ubuntu is not that. Acquisition
# is one download and one checksum, and doing it directly keeps a
# 2,400-line bash dependency off the path that builds the image this
# suite runs on every session.
# =====================================================================
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
ISO_DIR=/var/lib/owuivm/iso
[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; }
install -d -o owuivmadm -g owuivmadm -m 0755 "$ISO_DIR"
# The series to check against, and the release to download when no local
# file is given. Bump both together.
UBUNTU_SERIES="26.04"
UBUNTU_ISO="ubuntu-26.04.1-desktop-amd64.iso"
UBUNTU_BASE="https://releases.ubuntu.com/${UBUNTU_SERIES}"
verify_against_canonical() { # verify_against_canonical <path> <basename>
local path="$1" name="$2" want got
echo "==> checking $name against Canonical's SHA256SUMS"
want=$(curl -fsSL "$UBUNTU_BASE/SHA256SUMS" | awk -v f="*$name" '$2==f{print $1}')
[ -n "$want" ] || {
echo "$name is not in the published list for $UBUNTU_SERIES." >&2
echo "Either it is from another series, or the name has been changed." >&2
exit 1
}
got=$(sha256sum "$path" | awk '{print $1}')
[ "$want" = "$got" ] || {
echo "CHECKSUM MISMATCH - refusing to use this file." >&2
echo " want $want" >&2
echo " got $got" >&2
exit 1
}
echo " ok $got"
printf '%s %s verified against %s/SHA256SUMS\n' "$name" "$got" "$UBUNTU_BASE" \
> "$ISO_DIR/ubuntu.manifest"
}
fetch_ubuntu() {
local supplied="${1:-}"
if [ -n "$supplied" ]; then
[ -f "$supplied" ] || { echo "no such file: $supplied" >&2; exit 1; }
local name; name=$(basename "$supplied")
verify_against_canonical "$supplied" "$name"
# Copied rather than symlinked: the source is in somebody's Downloads
# folder, and a base image build that fails halfway through because
# the ISO was tidied away is a bad afternoon.
echo "==> copying into $ISO_DIR"
install -o owuivmadm -g owuivmadm -m 0644 "$supplied" "$ISO_DIR/$name"
else
local dest="$ISO_DIR/$UBUNTU_ISO"
if [ -f "$dest" ]; then
echo "already present: $dest"
else
echo "==> downloading $UBUNTU_ISO"
curl -fL --retry 3 --continue-at - -o "$dest.part" "$UBUNTU_BASE/$UBUNTU_ISO"
mv -f "$dest.part" "$dest"
chown owuivmadm:owuivmadm "$dest"
fi
verify_against_canonical "$dest" "$UBUNTU_ISO"
fi
echo
cat "$ISO_DIR/ubuntu.manifest"
}
# The virtio driver disk. Windows setup cannot see a virtio-blk disk
# without it and will tell you there are no drives to install to, which
# reads like a broken VM rather than a missing driver. Fetched from the
# same place every Windows-on-KVM guide uses; it is not signed by anyone
# whose signature we can check, so the hash is recorded rather than
# verified - exactly like the Windows media itself.
VIRTIO_URL="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
fetch_virtio() {
local dest="$ISO_DIR/virtio-win.iso"
if [ -f "$dest" ]; then
echo "already present: $dest"
else
echo "==> downloading virtio-win.iso"
curl -fL --retry 3 --continue-at - -o "$dest.part" "$VIRTIO_URL"
mv -f "$dest.part" "$dest"
chown owuivmadm:owuivmadm "$dest"; chmod 0644 "$dest"
fi
printf 'virtio-win.iso %s recorded only - from %s\n' \
"$(sha256sum "$dest" | awk '{print $1}')" "$VIRTIO_URL" > "$ISO_DIR/virtio.manifest"
cat "$ISO_DIR/virtio.manifest"
}
fetch_windows() {
local supplied="${1:-}"
[ -n "$supplied" ] || { echo "Windows media must be supplied: $0 windows /path/to.iso" >&2; exit 2; }
[ -f "$supplied" ] || { echo "no such file: $supplied" >&2; exit 1; }
local name got
name=$(basename "$supplied")
# The maintenance boot picks Windows media by name, so a file that does
# not look like Windows media would either be ignored or - worse - the
# Ubuntu ISO would be picked instead. Checked here, once, where it can
# still be fixed by renaming rather than debugged from a boot failure.
case "$name" in
[Ww]in*.iso|*[Ww]indows*.iso) ;;
*) echo "refusing '$name': Windows media must have a name starting with Win or containing Windows," >&2
echo "because that is how the maintenance boot tells it apart from the Ubuntu ISO." >&2
exit 1 ;;
esac
got=$(sha256sum "$supplied" | awk '{print $1}')
install -o owuivmadm -g owuivmadm -m 0644 "$supplied" "$ISO_DIR/$name"
# RECORDED, not verified. See the header.
printf '%s %s recorded only - no published checksum to compare against\n' "$name" "$got" \
> "$ISO_DIR/windows.manifest"
cat "$ISO_DIR/windows.manifest"
echo
fetch_virtio
}
case "${1:-}" in
ubuntu) fetch_ubuntu "${2:-}" ;;
windows) fetch_windows "${2:-}" ;;
virtio) fetch_virtio ;;
*) echo "usage: $0 {ubuntu|windows|virtio} [/path/to/local.iso]" >&2; exit 2 ;;
esac
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# =====================================================================
# Import a prebuilt base image instead of building one.
#
# sudo ./vm/images/import.sh ubuntu
# sudo ./vm/images/import.sh windows
# sudo ./vm/images/import.sh both
#
# WHY THIS EXISTS
#
# Building Ubuntu takes about 25 unattended minutes. Windows has no
# unattended path at all - it is a person clicking through setup in a VNC
# console, loading a virtio driver by hand, and knowing when to stop. For
# anyone who just wants the sub-agent working, downloading a finished
# image is the difference between an afternoon and a coffee.
#
# WHY IT IS A SCRIPT AND NOT A LIST OF STEPS
#
# Three things have to be exactly right or the result fails later, in
# ways that do not point at their cause:
#
# * the owner must be owuivmadm, or the maintenance tool cannot thaw it
# * the mode must be 0444, because FROZEN IS THE OWNER WRITE BIT and
# nothing else - a 0644 base means vmd refuses every session with
# "freeze it and try again"
# * the .sha256 must land beside the disk, or every saved fast-start
# machine is treated as stale and sessions silently boot from cold
#
# A checklist gets those wrong. This does not.
# =====================================================================
set -euo pipefail
BASE=${OWUIVM_BASE_DIR:-/var/lib/owuivm/base}
# Override to host your own. Any web server will do: the script only ever
# does a ranged GET, and it verifies what it gets.
SOURCE=${OWUIVM_IMAGE_SOURCE:-https://nextcloud.grantlan.com/s/OWUIToolsVMDisks}
[ "$(id -u)" -eq 0 ] || { echo "import.sh must run as root (it writes $BASE)" >&2; exit 1; }
command -v curl >/dev/null || { echo "FAILED: curl is not installed" >&2; exit 1; }
getent passwd owuivmadm >/dev/null || {
echo "FAILED: there is no owuivmadm user - run vm/host/install.sh first" >&2; exit 1; }
# Roughly what each one needs, so a download does not die at 95% with the
# filesystem full. qcow2 is sparse but these are not thin.
declare -A NEED=( [ubuntu]=11 [windows]=44 )
fetch() { # fetch <remote-name> <destination>
local name="$1" dest="$2"
# Nextcloud's public-share download endpoint. `-C -` resumes, which
# matters: windows.qcow2 is 40 GB and a dropped connection three
# quarters of the way through should not mean starting again.
curl -fSL --retry 3 --retry-delay 5 -C - \
-o "$dest" "$SOURCE/download?path=%2F&files=$name"
}
import_one() {
local name="$1"
local disk="$BASE/$name.qcow2" vars="$BASE/$name-OVMF_VARS.fd" sums="$BASE/$name.sha256"
# A FINISHED IMAGE IS ONE THAT IS FROZEN. A part-downloaded one is not.
#
# The obvious guard - refuse if the file exists - contradicts the promise
# that this is resumable: a download interrupted at 30 GB could never be
# re-run. So the test is the same one everything else here uses, the
# owner write bit. import.sh sets it 0444 only after the checksum has
# passed, so a writable disk is by definition an unfinished one.
if [ -e "$disk" ] && [ "$(( 8#$(stat -c %a "$disk") & 8#200 ))" -eq 0 ]; then
echo "FAILED: $disk is already here and frozen." >&2
echo " Importing would replace a base image you may have changed. Delete it" >&2
echo " first if that is what you want - and note that any saved fast-start" >&2
echo " machine for it becomes stale." >&2
exit 1
fi
if [ -e "$disk" ]; then
echo "==> $name: an unfinished download is here; resuming it"
fi
local free_gb
free_gb=$(df -BG --output=avail "$BASE" | tail -1 | tr -dc '0-9')
if [ "$free_gb" -lt "${NEED[$name]}" ]; then
echo "FAILED: $name needs about ${NEED[$name]} GB and $BASE has ${free_gb} GB free" >&2
exit 1
fi
echo "==> $name: downloading (${NEED[$name]} GB or so; resumable, ^C and re-run is safe)"
install -d -o owuivmadm -g owuivmadm -m 0775 "$BASE"
fetch "$name.sha256" "$sums"
fetch "$name-OVMF_VARS.fd" "$vars"
fetch "$name.qcow2" "$disk"
echo "==> $name: verifying"
# BY BASENAME, NOT `sha256sum -c`.
#
# The manifest carries absolute paths - /var/lib/owuivm/base/... - because
# that is where the freeze wrote it. `sha256sum -c` would therefore check
# the files at THOSE paths, which are the ones already installed, not the
# ones just downloaded. At the default location they are the same file and
# it passes for the wrong reason; with OWUIVM_BASE_DIR set it verifies
# somebody else's disk, or fails on a file that is not there.
#
# Measured: a test import into a scratch directory reported both files OK
# while the scratch copy was a quarter downloaded.
local bad=0 want got file
while read -r want file; do
[ -n "$want" ] || continue
got=$(sha256sum "$BASE/$(basename "$file")" | awk '{print $1}')
if [ "$want" = "$got" ]; then
echo " $(basename "$file"): OK"
else
echo " $(basename "$file"): MISMATCH" >&2
bad=1
fi
done < "$sums"
if [ "$bad" -ne 0 ]; then
echo "FAILED: the download is corrupt or incomplete." >&2
echo " Re-run to resume, or delete $BASE/$name.* and start again." >&2
exit 1
fi
echo "==> $name: freezing"
chown owuivmadm:owuivmadm "$disk" "$vars" "$sums"
# 0444 IS the freeze. See the header.
chmod 0444 "$disk" "$vars"
chmod 0644 "$sums"
echo " $name imported and frozen"
}
case "${1:-}" in
ubuntu|windows) import_one "$1" ;;
both) import_one ubuntu; import_one windows ;;
*) echo "usage: import.sh {ubuntu|windows|both}" >&2; exit 64 ;;
esac
echo
echo "Done. Open Tool Server VM Maintenance and the row should read 'frozen'."
echo "Sessions boot these from cold; press 'Save fast start' to make them resume"
echo "in seconds instead."
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# =====================================================================
# Builds the frozen Ubuntu base image, unattended, from the stock ISO.
#
# sudo ./build.sh # ~25 minutes, mostly apt
#
# HOW THE INSTALL IS MADE UNATTENDED
#
# Subiquity will read an autoinstall config from a NoCloud volume, but it
# stops to ask for confirmation unless `autoinstall` is on the kernel
# command line. There is no way to pass a kernel argument to a CD-ROM, so
# the ISO is repacked with that one word added to its grub menu entry.
# The alternative - booting the ISO's kernel directly with -kernel - was
# rejected because it bypasses OVMF, and an image installed outside UEFI
# will not boot under owuivm-launch, which uses it.
#
# The repack is `xorriso ... -boot_image any replay`, which copies the
# original boot records across verbatim. Anything else produces an ISO
# that is subtly not bootable, usually only under UEFI, usually only on
# the second attempt.
#
# THE BUILD RUNS AS owuivm ON PURPOSE
#
# Same uid, same egress filter, same everything as a real session. If the
# sandbox's network rules would break `apt`, this is where that is found
# - not three phases later with an agent in the loop to confuse it.
# =====================================================================
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; }
ISO_DIR=/var/lib/owuivm/iso
BASE_DIR=/var/lib/owuivm/base
WORK=$(mktemp -d /var/tmp/owuivm-build.XXXXXX)
trap 'rm -rf "$WORK"' EXIT
SRC_ISO="$ISO_DIR/$(awk '{print $1}' "$ISO_DIR/ubuntu.manifest")"
AUTO_ISO="$WORK/ubuntu-autoinstall.iso"
SEED_ISO="$WORK/seed.iso"
DISK="$BASE_DIR/ubuntu.qcow2"
VARS="$BASE_DIR/ubuntu-OVMF_VARS.fd"
LOG=/var/log/owuivm-build-ubuntu.log
[ -f "$SRC_ISO" ] || { echo "no source ISO - run ../fetch.sh ubuntu first" >&2; exit 1; }
if [ -e "$DISK" ]; then
echo "$DISK already exists."
echo "Refusing to overwrite a base image: unfreeze and edit it through the"
echo "maintenance tool, or delete it deliberately and re-run."
exit 1
fi
echo "==> patching the ISO's boot menu"
xorriso -osirrox on -indev "$SRC_ISO" -extract /boot/grub/grub.cfg "$WORK/grub.cfg" >/dev/null 2>&1
# `autoinstall` - skips subiquity's confirmation prompt.
# `console=ttyS0`- so the install is watchable in $LOG instead of being a
# 25-minute black box that either works or does not.
sed -i -e 's|^\(\s*linux\s*/casper/vmlinuz\)\s*---|\1 autoinstall console=ttyS0,115200 ---|' \
-e 's|^set timeout=.*|set timeout=1|' "$WORK/grub.cfg"
grep -q 'autoinstall' "$WORK/grub.cfg" || { echo "grub.cfg patch did not apply" >&2; exit 1; }
xorriso -indev "$SRC_ISO" -outdev "$AUTO_ISO" \
-boot_image any replay \
-map "$WORK/grub.cfg" /boot/grub/grub.cfg \
-commit >/dev/null 2>&1
[ -s "$AUTO_ISO" ] || { echo "repack produced nothing" >&2; exit 1; }
echo "==> building the NoCloud seed"
# cloud-init finds this by volume label alone; no ds= argument needed.
mkdir -p "$WORK/seed" && cp user-data meta-data "$WORK/seed/"
xorriso -as mkisofs -o "$SEED_ISO" -V CIDATA -J -r "$WORK/seed" >/dev/null 2>&1
echo "==> creating the disk"
install -d -o owuivmadm -g owuivmadm -m 0755 "$BASE_DIR"
qemu-img create -f qcow2 "$DISK" 40G >/dev/null
cp /usr/share/OVMF/OVMF_VARS_4M.fd "$VARS"
chown owuivm:owuivm "$DISK" "$VARS"
chmod 0644 "$DISK" "$VARS"
chmod 0777 "$WORK"; chmod 0644 "$AUTO_ISO" "$SEED_ISO"
echo "==> installing (watch: tail -f $LOG)"
: > "$LOG"; chown owuivm:owuivm "$LOG"
# -no-reboot: subiquity reboots when it is done, and that is our signal
# that the install finished. Without it the fresh system boots and the
# build never returns.
setpriv --reuid=owuivm --regid=owuivm --groups=kvm --inh-caps=-all -- \
qemu-system-x86_64 \
-no-user-config -nodefaults -no-reboot \
-machine q35,accel=kvm -cpu host -smp 4 -m 4096 \
-drive "if=pflash,format=raw,unit=0,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd" \
-drive "if=pflash,format=raw,unit=1,file=$VARS" \
-drive "file=$DISK,if=none,id=disk0,format=qcow2,cache=unsafe" \
-device virtio-blk-pci,drive=disk0,bootindex=1 \
-drive "file=$AUTO_ISO,if=none,id=cd0,media=cdrom,readonly=on" \
-device ide-cd,drive=cd0,bus=ide.0,bootindex=2 \
-drive "file=$SEED_ISO,if=none,id=cd1,media=cdrom,readonly=on" \
-device ide-cd,drive=cd1,bus=ide.1 \
-netdev user,id=net0,ipv6=off -device virtio-net-pci,netdev=net0,romfile= \
-device VGA -display none \
-serial "file:$LOG"
echo "==> freezing"
# The write bit IS the freeze. owuivm is neither the owner nor in the
# group, so the uid that runs every session cannot clear it; only the
# maintenance unit, which runs as owuivmadm behind a polkit check, can.
chown owuivmadm:owuivmadm "$DISK" "$VARS"
chmod 0444 "$DISK" "$VARS"
sha256sum "$DISK" "$VARS" > "$BASE_DIR/ubuntu.sha256"
chown owuivmadm:owuivmadm "$BASE_DIR/ubuntu.sha256"
echo
echo "base image built and frozen:"
ls -l "$DISK" "$VARS"
cat "$BASE_DIR/ubuntu.sha256"
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
# =====================================================================
# Everything the autoinstall does to a base image, for an image that was
# installed by hand instead.
#
# run INSIDE the Ubuntu guest, as root, during a maintenance boot:
# sudo bash by-hand.sh
#
# WHY THIS EXISTS
#
# user-data builds an image unattended and this file is its late-commands
# written out longhand. An image installed interactively - which is a
# perfectly reasonable way to build one, and how this host's first Ubuntu
# image was made - misses every one of them, and most of the misses are
# silent: the machine works, and something about it is wrong once a
# session is running and nobody is watching.
#
# Idempotent on purpose. It is meant to be run again after the operator
# installs something else, without having to remember what it already did.
# =====================================================================
set -uo pipefail
[ "$(id -u)" -eq 0 ] || { echo "run this with sudo" >&2; exit 1; }
say() { printf '\n== %s\n' "$*"; }
user=$(id -nu 1000 2>/dev/null || echo "")
say "the control channel"
# THE ONE THING WITHOUT WHICH NOTHING WORKS.
# It is how every command reaches this machine. No agent, no session -
# and the failure looks exactly like a machine that did not boot.
apt-get update -qq
apt-get install -y -qq qemu-guest-agent
systemctl enable --now qemu-guest-agent
say "what scripts under test tend to reach for"
apt-get install -y -qq python3 python3-pip python3-venv git curl jq unzip ca-certificates
say "the project share"
# The session mounts this itself over the guest channel, so the fstab
# line is belt and braces - it makes the mount happen on a boot nobody
# is driving, and `nofail` is what keeps a machine with no share from
# dropping into an emergency shell where nothing can reach it.
mkdir -p /mnt/project
grep -q '^project /mnt/project virtiofs' /etc/fstab \
|| printf 'project /mnt/project virtiofs rw,nofail,x-systemd.device-timeout=5 0 0\n' >> /etc/fstab
say "two minutes of every boot"
# MEASURED on this image: systemd-networkd-wait-online 2min 0.072s, with
# graphical.target reached at 2min 3s. The desktop is rendered by
# NetworkManager, so networkd has no links to wait for and sits out its
# entire timeout. The guest agent answers at multi-user, so a CLI session
# never noticed - which is why this stayed invisible until a GUI session
# spent two minutes photographing a black screen.
systemctl mask systemd-networkd-wait-online.service NetworkManager-wait-online.service
say "a desktop with nobody at the keyboard"
if [ -n "$user" ]; then
mkdir -p /etc/gdm3
cat > /etc/gdm3/custom.conf <<EOF
[daemon]
AutomaticLoginEnable=true
AutomaticLogin=$user
EOF
# MEASURED ON 26.04: `WaylandEnable=false` used to be here, with an
# xorg.conf.d file pinning the mode, because the agent clicks at
# absolute coordinates and a screen that changes size makes every
# learned position wrong.
#
# This release has no Xorg to fall back to - xserver-xorg-core is not
# installed and /usr/share/xsessions is empty - so that line does not
# choose X, it leaves GDM with no session to log in to at all, and an
# autologin into nothing looks exactly like a machine that failed to
# boot a desktop.
#
# Wayland costs nothing here: the agent's eyes and hands are QMP
# `screendump` and `input-send-event`, which are the virtual hardware
# and know nothing about the display server. The mode comes from the
# virtio-vga device instead of from a config file, and it is stable
# for the same reason the netdev is - the qemu argv is a constant.
rm -f /etc/X11/xorg.conf.d/10-owuivm-resolution.conf
# Every session IS the first one - the disk is restored from the same
# frozen base every time - so the first-run wizard would open a modal
# dialog over the desktop in front of the agent, every single run.
install -d -o 1000 -g 1000 "/home/$user/.config"
echo yes > "/home/$user/.config/gnome-initial-setup-done"
chown 1000:1000 "/home/$user/.config/gnome-initial-setup-done"
else
echo " no uid 1000, skipping the desktop settings"
fi
say "sources of background change"
# Every session starts from this frozen disk. Anything that updates
# itself in the background makes two runs of the same script differ, and
# the difference shows up as a script that is flaky rather than as a
# machine that changed underneath it.
systemctl disable --now unattended-upgrades.service 2>/dev/null
systemctl disable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null
# SNAPD IS HELD, NOT DISABLED, AND THAT IS A DELIBERATE DIFFERENCE.
#
# The autoinstall disables it outright. On a desktop image that also
# takes Firefox away, because Firefox IS a snap here - and a GUI session
# whose agent cannot open a browser has lost something real. What was
# actually wanted is "stop changing underneath me", and holding refreshes
# says exactly that while leaving the snaps that are installed working.
snap refresh --hold >/dev/null 2>&1 && echo " snap refreshes held" \
|| echo " could not hold snap refreshes (old snapd?); check 'snap refresh --hold'"
# A remote desktop server inside the sandbox is a listening socket with
# no route to it and no reason to exist; the operator's console comes
# from QEMU's own VNC.
systemctl disable --now gnome-remote-desktop.service 2>/dev/null
[ -d /etc/cloud ] && touch /etc/cloud/cloud-init.disabled
say "what this machine looks like now"
printf ' guest agent : %s\n' "$(systemctl is-active qemu-guest-agent)"
printf ' autologin : %s\n' "$(grep -h '^AutomaticLogin=' /etc/gdm3/custom.conf 2>/dev/null || echo 'not set')"
printf ' fstab : %s\n' "$(grep -h '^project ' /etc/fstab 2>/dev/null || echo 'no share line')"
printf ' resolver : %s\n' "$(awk '/^nameserver/{printf "%s ", $2}' /etc/resolv.conf)"
printf ' internet : %s\n' "$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 https://example.com/ 2>&1 || echo unreachable)"
echo
echo "Shut the machine down from Tool Server VM Maintenance, then press Freeze."
+2
View File
@@ -0,0 +1,2 @@
instance-id: owuivm-base
local-hostname: owuivm
+213
View File
@@ -0,0 +1,213 @@
#cloud-config
# =====================================================================
# Unattended install for the Ubuntu base image.
#
# Read with vm/images/ubuntu/build.sh, which repacks the stock server ISO
# with `autoinstall` on the kernel command line and hands this file over
# on a second volume labelled CIDATA.
#
# Everything here exists to make the guest behave identically on every
# session. A base image that drifts is a base image that makes a failing
# script look like a failing agent.
# =====================================================================
autoinstall:
version: 1
interactive-sections: []
locale: en_US.UTF-8
keyboard:
layout: us
# STATIC, not DHCP, and this is load-bearing.
#
# SLIRP hands out 10.0.2.3 as the DNS server, which is a forwarder
# inside the qemu process that relays to THIS HOST's resolver. The
# sandbox's egress filter denies 127.0.0.0/8, so that relay is dead by
# design - and correctly so, since the host's resolver is a LAN service.
# A guest that took DHCP would therefore get a nameserver that never
# answers, and every agent session would open with a confusing "no
# internet". Pinning a public resolver here makes the guest's DNS a
# thing the guest owns, which is what the isolation model assumes.
network:
version: 2
ethernets:
primary:
match:
name: "en*"
addresses: [10.0.2.15/24]
routes:
- to: default
via: 10.0.2.2
nameservers:
addresses: [1.1.1.1, 9.9.9.9]
storage:
layout:
name: direct
identity:
hostname: owuivm
realname: VM User
username: vmuser
# The password is "vmuser", and it is documented on purpose.
#
# There is no network path to this machine - no hostfwd, no sshd, no
# bridge - so it guards a console nobody but the operator can reach,
# through a VNC socket that exists only in maintenance mode. It is
# written down rather than hidden because hiding it would invite
# treating it as a secret, which it is not.
#
# It is still a KNOWN password, and this file is published, so if you
# ever give this machine a listening socket, change it first:
#
# openssl passwd -6 'your-password'
#
# and replace the hash below, before running build.sh.
password: "$6$owuivm$eqPoBTdzXEA8WHYyXtQOlQfw4uOYGw2N4vC.Hl9KVdKOFuKQIEc0zvyVmy6t5XgVLhKr5Q9sdaWLOWWjBCaRZ1"
# No sshd. The agent reaches this machine over virtio-serial, which
# needs no listening socket and keeps working when a script breaks the
# guest's networking - which is exactly when the agent needs it most.
ssh:
install-server: false
packages:
- qemu-guest-agent
- ubuntu-desktop-minimal
- python3
- python3-pip
- python3-venv
- git
- curl
- ca-certificates
- jq
- unzip
# Updates during a session would make two runs of the same script
# differ for reasons that have nothing to do with the script.
package_update: false
package_upgrade: false
late-commands:
# The guest agent is the whole control channel. Without it the
# machine boots and is unreachable, which looks identical to a
# machine that failed to boot.
- curtin in-target -- systemctl enable qemu-guest-agent.service
# Debian and its derivatives have historically shipped
# /etc/default/qemu-guest-agent restricting guest-exec and
# guest-file-*. Those two ARE the control channel here, so the file
# is written explicitly rather than left to the package default.
- |
cat > /target/etc/default/qemu-guest-agent <<'EOF'
# BLACKLIST_RPC is deliberately empty: guest-exec and guest-file-*
# are how this machine is driven. There is no network path in, so
# the RPC surface is reachable only from the host process that owns
# the VM.
DAEMON_ARGS=""
EOF
# GUI mode drives whatever is on the screen, so something has to be
# on it. Auto-login means a session exists from boot with no human,
# and it is the same arrangement the Windows image uses so both
# behave the same way.
- |
mkdir -p /target/etc/gdm3
cat > /target/etc/gdm3/custom.conf <<'EOF'
[daemon]
AutomaticLoginEnable=true
AutomaticLogin=vmuser
EOF
# NO `WaylandEnable=false`, AND NO xorg.conf, ON PURPOSE.
#
# Both were here. The reasoning was sound - the agent clicks at
# absolute coordinates, so the screen must be the same size every
# session - and the mechanism stopped existing: 26.04 ships no Xorg
# (xserver-xorg-core is not installed, /usr/share/xsessions is
# empty), so turning Wayland off leaves GDM with nothing to log in
# to. Measured on this host's image before it was frozen.
#
# The size is fixed anyway, one layer down: it comes from the
# virtio-vga device in a qemu argv that is a constant, not from a
# display server`s config file.
# NETPLAN IS WRITTEN HERE, BY HAND, AND NOT LEFT TO THE INSTALLER.
#
# Subiquity does not write /etc/netplan itself - it hands the network
# config to cloud-init, which renders it on FIRST BOOT. This image
# disables cloud-init a few lines below (it must not re-run on every
# boot of a disk restored from a frozen base thousands of times), so
# the netplan was never written at all and the guest silently fell
# back to DHCP.
#
# That is not a cosmetic difference. SLIRP's DHCP hands out 10.0.2.3
# as the nameserver, which is a forwarder inside the qemu process
# that relays to THIS HOST's resolver - and the sandbox's egress
# filter denies 127.0.0.0/8, correctly, because the host's resolver
# is a LAN service. Measured: the guest came up with working TCP to
# 1.1.1.1 and no name resolution whatsoever, which reads as "the
# internet is broken" rather than "DNS is misconfigured".
- |
cat > /target/etc/netplan/00-owuivm.yaml <<'EOF'
network:
version: 2
ethernets:
primary:
match:
name: "en*"
dhcp4: false
dhcp6: false
addresses: [10.0.2.15/24]
routes:
- to: default
via: 10.0.2.2
nameservers:
addresses: [1.1.1.1, 9.9.9.9]
EOF
chmod 0600 /target/etc/netplan/00-owuivm.yaml
# The project share. `nofail` because a session may legitimately run
# without one, and a missing share must not drop the machine into an
# emergency shell where nothing can reach it.
- |
mkdir -p /target/mnt/project
printf 'project /mnt/project virtiofs rw,nofail,x-systemd.device-timeout=5 0 0\n' >> /target/etc/fstab
# MEASURED: this one service cost two minutes of every boot.
#
# `systemd-analyze blame` on the first build: systemd-networkd-wait-online
# 2min 0.072s, and graphical.target reached after 2min 3s. The desktop is
# rendered by NetworkManager here (netplan's default renderer for a
# desktop install), so networkd has no links to wait for and sits out its
# full timeout before failing. The guest agent answers at multi-user, so a
# CLI session was unaffected and fast - which is exactly why this was
# invisible until a GUI session took a screenshot of a black screen for
# two minutes.
- curtin in-target -- systemctl mask systemd-networkd-wait-online.service
- curtin in-target -- systemctl mask NetworkManager-wait-online.service
# The first-run wizard opens a modal dialog over the desktop on every
# session, because every session is the first one - the disk is restored
# from the same frozen base every time. An agent driving the screen would
# have to dismiss it before it could do anything, every single time.
- |
mkdir -p /target/home/vmuser/.config
echo yes > /target/home/vmuser/.config/gnome-initial-setup-done
chown -R 1000:1000 /target/home/vmuser/.config
# A remote desktop server inside the sandbox is a listening socket with
# no way to reach it and no reason to exist. The operator's console comes
# from QEMU's own VNC in maintenance mode, not from inside the guest.
- curtin in-target -- systemctl disable gnome-remote-desktop.service || true
# Three sources of background change, all of which would make two
# runs of the same script differ.
- curtin in-target -- systemctl disable unattended-upgrades.service || true
- curtin in-target -- systemctl disable apt-daily.timer apt-daily-upgrade.timer || true
- curtin in-target -- systemctl disable snapd.service snapd.socket snapd.seeded.service || true
# cloud-init has done its job; leaving it enabled means it re-runs on
# every boot of a disk that is restored from the same frozen base
# thousands of times.
- touch /target/etc/cloud/cloud-init.disabled
+114
View File
@@ -0,0 +1,114 @@
# The Windows base image
Unlike Ubuntu, this one is installed by hand. There is no unattended path here
and that is deliberate: Windows licensing, edition choice and the out-of-box
experience are all decisions for the operator, not for a script.
## What is already prepared
/var/lib/owuivm/iso/Win11_25H2_English_x64_v2.iso the media you supplied
/var/lib/owuivm/iso/virtio-win.iso the driver disc
Both are recorded in `windows.manifest` and `virtio.manifest` with their
SHA-256. **Recorded, not verified** - unlike Ubuntu, there is no published
checksum list to compare a Windows ISO against, so the manifest says what the
file hashed to and nothing stronger. That difference is real and is not papered
over.
## Three things Windows needs that Ubuntu does not
All three are already in the launcher, applied only when the image is named
`windows`:
| Requirement | What is provided | What happens without it |
|---|---|---|
| **TPM 2.0** | `swtpm` on a unix socket, `tpm-tis` device | Setup stops at "This PC can't run Windows 11" |
| **Secure Boot** | `OVMF_CODE_4M.secboot.fd` + a variable store seeded from `OVMF_VARS_4M.ms.fd`, with `property=secure,value=on` so the guest cannot rewrite it | Same message. Microsoft's keys have to be *enrolled*, not just the firmware present |
| **virtio drivers** | `virtio-win.iso` attached as a second CD during the install only | Setup reports no drives to install to, which looks like a broken VM |
| **Hyper-V enlightenments** | fourteen `hv-*` flags on `-cpu`, Windows only | The machine is simply slow, and the host looks busy while the guest looks idle - measured at 347% CPU on an idle installer with the cgroup neither short of memory nor throttled |
| **`-rtc base=localtime`** | Windows only | The clock is hours out, so TLS verification fails and every download breaks in a way that reads as a network fault |
`swtpm` also needed an AppArmor exception: Ubuntu's shipped profile permits
libvirt's paths and nothing else, so the socket was refused with
`Could not open UnixIO socket: Permission denied` - a policy denial wearing a
permission error's clothes, with nothing in dmesg. See
`vm/host/apparmor-local-swtpm`.
## Installing it
1. Finish the Ubuntu image first, or close it. Only one maintenance machine
runs at a time and the prepare step refuses the second.
Starting a *frozen* image is always safe, at any time: it boots a throwaway
copy and cannot change what is on disk. That is the way to check that an
image still comes up before handing it to the agent.
2. **Tool Server VM Maintenance** on the desktop -> the Windows row ->
**Install from ISO**.
A blank 60 GB disk is created, the media is attached, and the console opens.
3. **Two prompts eat keystrokes before Windows does, in this order.** OVMF shows
its own "press a key for boot options" window for about five seconds - any
key there drops you into the firmware menu, not the installer (if that
happens: **Boot Manager** -> **UEFI QEMU DVD-ROM QM00001**, which is the
Windows ISO; QM00003 is virtio-win). Then the ISO itself shows "Press any
key to boot from CD or DVD" for about five seconds, and *that* one has to be
answered or the machine falls through to an EFI shell. So: let the firmware
window pass untouched, then press a key.
4. When setup asks where to install and shows no drives, choose **Load driver**,
browse the second CD, and take `amd64\w11` (or `amd64\2k25`). The disk
appears immediately afterwards.
5. Install normally. A local account is simplest - the machine has no way to
reach a Microsoft account sign-in that needs a phone, and nothing about this
image should depend on one.
## Before you shut down
The equivalents of what the Ubuntu image needed, and for the same reasons:
- **`qemu-guest-agent`** from the driver disc (`virtio-win-guest-tools.exe`, or
`guest-agent\qemu-ga-x86_64.msi`). This is the entire control channel; without
it the machine boots and is unreachable, which looks identical to a machine
that failed to boot.
- **Static networking**: address `10.0.2.15/24`, gateway `10.0.2.2`, DNS
`1.1.1.1`. DHCP hands out SLIRP's own resolver at `10.0.2.3`, which relays to
*this host's* resolver and is correctly blocked by the sandbox filter - giving
a machine with working TCP and no name resolution.
- **Auto-logon** for the admin account, so a GUI session has a desktop from boot
with no human. `netplwiz`, or `Set-LocalUser` plus the `AutoAdminLogon`
registry values.
- **The rest of virtio-win**, not just the storage driver setup needed: NetKVM
for the network, and the display driver in particular. Until it is installed
`virtio-vga` is a plain VGA framebuffer to Windows and every redraw is a full
repaint - which is most of what "the VM feels slow" turns out to be once the
enlightenments are on.
- **Turn off Windows Update's automatic reboots** and anything else that changes
the machine between sessions. Every session starts from this frozen disk, so
background change here is change in every future run.
## Then
Press **Shut down**. The guest is asked to power off over ACPI and systemd waits
for it, so this is the machine shutting itself down rather than the plug coming
out. The variable store is copied back on the way - a UEFI boot entry added by
setup is otherwise lost and the disk will not boot.
Then press **Freeze**, which is the operator's own button and is not done for
you: nothing re-freezes an image behind your back any more. Freezing sets `0444`
and records the checksum, and until it is done no agent session will start -
vmd refuses to open one on a writable base and says so.
Then press **Installed — remove disc**, which appears on the row while an
install is in progress and wakes up once the machine is stopped. That is what
says the install is finished: until it is pressed the installer media is still
attached at every boot, deliberately, because a machine shut down halfway
through setup needs its disc to be finished and nothing here can tell the two
cases apart. The disc first, the disk second - the boot order tries the disk and
falls through to the media, so an installed system boots itself either way and
the only cost of leaving it attached is the ISO staying open.
## Not yet done
Nothing on this page has been run. The Ubuntu path was measured end to end;
this one is written from the same code paths plus the documented Windows
requirements, and the first real install will find something. The argv it will
be given *is* asserted, by `tests/test_vm_launch_argv.sh`, which checks that the
Windows machine gets secure-boot firmware, a TPM and a protected variable store
while the Ubuntu one gets none of them.