Initial Commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user