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
+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