#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