From 332497500cd7f0ac5f7408bdd2cea6b0d908b0f6 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Wed, 29 Jul 2026 17:47:00 +0200 Subject: [PATCH] feat(deploy): USB-bootable batm3 test image (disk-image-batm3-usb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a USB-bootable BATM3 disk-image target plus the batm3 hardware changes that make a dd'd USB stick boot reliably on the Dell 9030 AIO. flake.nix — new `disk-image-batm3-usb` target: - Distinct partition labels (nixos-usb / ESP-USB) so stage-1 by-label resolution can't latch onto an internal SATA drive that already holds a generic nixos/ESP-labelled install. Post-build mlabel relabels the ESP FAT volume to ESP-USB (bootloader files untouched; UEFI still loads /EFI/BOOT/BOOTX64.EFI). - /boot mounted nofail + short device-timeout: the firmware already loaded the bootloader before Linux; without nofail a slow/late ESP-USB enumeration drops to emergency mode with root locked — a dead end. - NO growPartition/autoResize on the USB image: sfdisk rewriting the partition table on first boot is the single most bus-stressing write, and flaky USB bridges drop off the bus mid-rewrite (sfdisk wedges in uninterruptible D-state and ESP-USB vanishes with the device, so /boot times out too). Persistent state is a few MB and the image already ships ~2GB free in root. The internal-SATA disk-image-batm3 keeps growPartition — a real AHCI SSD won't drop the bus. - autoUpgrade off (test image, not a managed fleet member). batm3.nix — USB-boot reliability: - Add usb_storage to initrd.availableKernelModules so stage-1 binds the stick and /dev/disk/by-label/* appears. - Blacklist uas + usbcore.autosuspend=-1: force the slower-but-reliable Bulk-Only Transport path and stop the boot medium being power-suspended mid-I/O — both were causing "device offline error" bus drops. Co-Authored-By: Claude Opus 4.8 --- deploy/nixos/hardware/batm3.nix | 17 ++++++++ flake.nix | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/deploy/nixos/hardware/batm3.nix b/deploy/nixos/hardware/batm3.nix index 2a83f2d..ce8e1fe 100644 --- a/deploy/nixos/hardware/batm3.nix +++ b/deploy/nixos/hardware/batm3.nix @@ -17,8 +17,22 @@ "ahci" "usbhid" "sd_mod" + # USB mass-storage: required to boot the dd'd image from a USB stick + # (stage-1 must bind the flash drive as a SCSI disk so + # /dev/disk/by-label/nixos appears). Harmless on the internal-SATA + # install, where ahci+sd_mod already cover the root device. + # + # NOTE: deliberately NO "uas" here. Many USB sticks/bridges advertise + # UAS but drop off the bus ("device offline error, dev sdb") under the + # sustained write load of first-boot growPartition/journal/swapfile. + # Blacklisting uas below forces the slower-but-reliable usb-storage + # (Bulk-Only Transport) path. SATA/eMMC installs don't use uas anyway. + "usb_storage" ]; + # Keep the USB flash drive off the flaky UAS driver (see note above). + blacklistedKernelModules = [ "uas" ]; + kernelModules = [ "kvm-intel" "usbtouchscreen" @@ -27,6 +41,9 @@ kernelParams = [ "quiet" "splash" + # Disable USB autosuspend so the boot medium (and kiosk peripherals) + # aren't power-suspended mid-I/O — another cause of "device offline". + "usbcore.autosuspend=-1" ]; }; diff --git a/flake.nix b/flake.nix index 3aa61e9..3fe1bba 100644 --- a/flake.nix +++ b/flake.nix @@ -424,6 +424,83 @@ printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true ''; + # USB-bootable BATM3 TEST image with DISTINCT partition labels + # (nixos-usb / ESP-USB). The plain disk-image-batm3 reuses the generic + # nixos/ESP labels, so a USB stick carrying it, booted on a batm3 whose + # internal SATA drive ALREADY holds a nixos/ESP-labelled install, makes + # stage-1's by-label/nixos resolve to the internal drive (larger fs, + # journal recovers) instead of the stick — the stage-2 init path baked + # into the USB's boot entry isn't on that root, so stage 1 aborts. + # Distinct labels make stage-1 pick the stick unambiguously WITHOUT + # touching the internal drive. Unlike disk-image-sintra-usb this keeps + # systemd-boot: the batm3 firmware UEFI-USB-boots fine via the ESP's + # /EFI/BOOT/BOOTX64.EFI removable fallback, so no GRUB/hybrid-table + # change is needed — only the label disambiguation here plus the + # usb_storage/uas initrd modules (in batm3.nix). Does NOT grow to fill + # the stick (see the growPartition note below — sfdisk on first boot + # wedges flaky USB bridges); auto-upgrade off (test image, not a managed + # fleet member — also stops scheduled bootloader writes landing on the + # internal drive's ESP). + disk-image-batm3-usb = + let + cfg = self.nixosConfigurations.batm3-installed.extendModules { + modules = [ + ({ lib, ... }: { + fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb"; + fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB"; + # /boot must NOT be a hard boot dependency on the USB test + # image. The firmware already loaded the bootloader from the + # ESP before Linux started; /boot is only remounted so the OS + # can *update* the bootloader — which this image never does + # (autoUpgrade off, no nixos-rebuild on the stick). Without + # nofail, a slow/late ESP-USB enumeration (BOT is slower than + # UAS) blows past systemd's 90s device-timeout and drops to + # emergency mode — with root locked, an unrecoverable dead end. + # nofail + a short timeout lets the (already-mounted) root carry + # the boot to completion; /boot mounts if/when the ESP shows up. + fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ]; + # DELIBERATELY NO growPartition/autoResize on the USB image. + # growPartition runs sfdisk to rewrite the stick's partition + # table on first boot — the single most bus-stressing write of + # the boot. Flaky USB bridges drop off the bus mid-rewrite + # (sfdisk hangs forever as an uninterruptible D-state task) and, + # worse, partition 1 (ESP-USB) vanishes with the device, so + # /boot times out too. The kiosk's persistent state (state.db, + # .env, wifi.conf, logs) is a few MB and the built image already + # carries ~2GB free inside root — growing to fill the stick buys + # nothing and costs reliability. The internal-SATA target + # (disk-image-batm3) keeps growPartition: a real AHCI SSD won't + # drop the bus and there filling the disk is worth it. + system.autoUpgrade.enable = lib.mkForce false; + }) + ]; + }; + baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { + inherit pkgs lib; + config = cfg.config; + format = "raw"; + partitionTableType = "efi"; + diskSize = "auto"; + label = "nixos-usb"; # ext4 root label (make-disk-image -L) + }; + in + pkgs.runCommand "nixos-disk-image-batm3-usb" + { nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; } + '' + mkdir -p $out + cp --sparse=always ${baseImage}/nixos.img $out/nixos.img + chmod +w $out/nixos.img + # make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the + # volume to ESP-USB so /boot (by-label/ESP-USB) can't resolve to an + # internal drive's ESP. Volume label only — bootloader files are + # untouched, and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless. + espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}') + echo "ESP partition starts at byte $espStart — relabelling to ESP-USB" + export MTOOLS_SKIP_CHECK=1 + mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB + printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true + ''; + # Backwards compat iso = self.nixosConfigurations.douro.config.system.build.isoImage; };