diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md index 062cd5f..2bfc333 100644 --- a/deploy/nixos/README.md +++ b/deploy/nixos/README.md @@ -33,19 +33,9 @@ Each ATM model has two flake outputs: | `nixosConfigurations.-installed` | installed | full GPT + systemd-boot install, ext4 root, supports `nixos-rebuild switch` | | `packages.x86_64-linux.iso-` | ISO | ISO image of the live variant | | `packages.x86_64-linux.disk-image-` | raw image | dd-able full disk image of the installed variant | -| `nixosConfigurations.-usb` | installed, on a stick | `-installed` hardened to run from a USB stick: `nixos-usb`/`ESP-USB` labels, `nofail` `/boot`, no partition growing, auto-upgrade off (`batm3`, `douro` only) | -| `packages.x86_64-linux.disk-image--usb` | raw image | dd-able image of the `-usb` variant — flash, plug in, boot; no installer step | Models: `douro`, `tejo`, `sintra`, `batm3`. -**Run-from-USB deployments** (`batm3` today, `douro` since the LNbits cutover) -skip the Alpine + dd-to-internal-disk procedure below entirely: the stick *is* -the system. Flash `disk-image--usb` with balenaEtcher (it verifies the -write — a truncated or bad copy fails stage-1 fsck on first boot) onto a stick -of 16 GB or more, plug it in, power on. Updates go in-place against the -`-usb` config (`nix copy` the toplevel + `switch-to-configuration`), -which keeps pairing and `/var/lib/bitspire`. - ```bash # Build a Sintra disk image nix build .#disk-image-sintra diff --git a/deploy/nixos/hardware/douro.nix b/deploy/nixos/hardware/douro.nix index e2e138c..d68cad2 100644 --- a/deploy/nixos/hardware/douro.nix +++ b/deploy/nixos/hardware/douro.nix @@ -20,24 +20,12 @@ initrd.availableKernelModules = [ "xhci_pci" "ahci" - # 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/* appears). Harmless on the internal install. - # - # NOTE: deliberately NO "uas" here. Many USB sticks/bridges advertise - # UAS but drop off the bus ("device offline error, dev sdb") under - # sustained write load. Blacklisting uas below forces the slower-but- - # reliable usb-storage (Bulk-Only Transport) path. SATA/mSATA installs - # don't use uas anyway. (Same hardening as batm3.nix.) "usb_storage" "sd_mod" "sdhci_pci" "i915" ]; - # Keep the USB flash drive off the flaky UAS driver (see note above). - blacklistedKernelModules = [ "uas" ]; - kernelModules = [ "kvm-intel" "i2c-dev" @@ -49,9 +37,6 @@ "vt.handoff=7" # Bay Trail: preserve BIOS display init "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 038a599..85895d9 100644 --- a/flake.nix +++ b/flake.nix @@ -349,61 +349,6 @@ }) ]; }; - - # Module that turns an -installed config into the one a dd'd USB - # stick actually runs. Shared by every *-usb variant so the USB-boot - # hazards are solved once: - # - distinct fs labels (nixos-usb / ESP-USB) so stage-1 can't latch an - # internal drive that already holds a generic nixos/ESP-labelled install; - # - nofail /boot: the firmware already loaded the bootloader before Linux; - # without nofail a slow/late ESP-USB enumeration (BOT is slower than UAS) - # blows past systemd's 90s device-timeout into emergency mode with root - # locked — a dead end. nofail + short timeout lets the already-mounted - # root carry the boot; /boot mounts if/when it shows; - # - NO growPartition/autoResize: 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 D-state and - # ESP-USB vanishes with the device). Persistent state is a few MB and - # the image ships ~2GB free. The internal-disk images keep it; - # - autoUpgrade off: no scheduled nix-store churn or bootloader writes on - # the stick. Updates go in-place via `nix copy` + switch-to-configuration - # against the named -usb config (preserves pairing + /var/lib). - usbBootModule = { lib, ... }: { - fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb"; - fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB"; - fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ]; - system.autoUpgrade.enable = lib.mkForce false; - }; - - # dd-able USB image of a -usb config. make-disk-image gives the - # ext4 root the nixos-usb label directly (-L) but hardcodes the ESP FAT - # label to "ESP", so the volume is relabelled to ESP-USB afterwards — - # volume label only; bootloader files are untouched and UEFI loads - # /EFI/BOOT/BOOTX64.EFI regardless. Keeps systemd-boot: both the batm3 - # and douro firmware UEFI-USB-boot fine via that removable fallback. - mkUsbDiskImage = machineModel: usbConfig: - let - baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { - inherit pkgs lib; - config = usbConfig.config; - format = "raw"; - partitionTableType = "efi"; - diskSize = "auto"; - label = "nixos-usb"; # ext4 root label (make-disk-image -L) - }; - in - pkgs.runCommand "nixos-disk-image-${machineModel}-usb" - { nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; } - '' - mkdir -p $out - cp --sparse=always ${baseImage}/nixos.img $out/nixos.img - chmod +w $out/nixos.img - 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 - ''; in { # ── NixOS Configurations (top-level, not per-system) ────────── @@ -436,22 +381,35 @@ sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix; batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix; - # USB-bootable variants of -installed (see usbBootModule for - # what changes). These are the configs a flashed stick actually runs. - # Exposed as named configs (not just inline in the disk-image targets) - # so their system closures can be built here and deployed in-place with - # `nix copy` + `switch-to-configuration` — updating the app on a running - # stick WITHOUT reflashing (preserves pairing + /var/lib state). - # disk-image--usb builds its filesystem image from the same config. + # USB-bootable variant of batm3-installed. This is the config the + # flashed USB stick actually runs — distinct fs labels so stage-1 can't + # latch the internal drive, nofail /boot, no growPartition, autoUpgrade + # off. Exposed as a named config (not just inline in the disk-image + # target) so its system closure can be built here and deployed in-place + # with `nix copy` + `switch-to-configuration` — updating the app on a + # running stick WITHOUT reflashing (preserves pairing + /var/lib state). + # disk-image-batm3-usb builds its filesystem image from this same config. batm3-usb = self.nixosConfigurations.batm3-installed.extendModules { - modules = [ usbBootModule ]; - }; - # douro: the production unit's internal drive is not NixOS, so the - # label disambiguation is moot today, but the nofail /boot and the - # uas/autosuspend hardening in douro.nix are what make a stick a - # reliable boot medium on the Bay Trail box. Same in-place update flow. - douro-usb = self.nixosConfigurations.douro-installed.extendModules { - modules = [ usbBootModule ]; + 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 image. The + # firmware already loaded the bootloader before Linux; without + # nofail, a slow/late ESP-USB enumeration (BOT is slower than UAS) + # blows past systemd's 90s device-timeout into emergency mode with + # root locked — a dead end. nofail + short timeout lets the + # already-mounted root carry the boot; /boot mounts if/when it shows. + fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ]; + # NO growPartition/autoResize: 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 D-state + # and ESP-USB vanishes with the device). Persistent state is a few + # MB and the image ships ~2GB free. The internal-SATA disk-image- + # batm3 keeps growPartition (a real AHCI SSD won't drop the bus). + system.autoUpgrade.enable = lib.mkForce false; + }) + ]; }; }; @@ -585,16 +543,53 @@ printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true ''; - # USB-bootable images (see usbBootModule / mkUsbDiskImage in the let - # block). Flash with dd or balenaEtcher, boot the stick, done — no - # installer step. The plain disk-image- reuses the generic - # nixos/ESP labels, so a stick carrying it, booted on a machine whose - # internal drive ALREADY holds a nixos/ESP-labelled install, makes - # stage-1's by-label/nixos resolve to the internal drive 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. These variants can't hit that. - disk-image-batm3-usb = mkUsbDiskImage "batm3" self.nixosConfigurations.batm3-usb; - disk-image-douro-usb = mkUsbDiskImage "douro" self.nixosConfigurations.douro-usb; + # 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 + # Filesystem image of the batm3-usb config (defined in + # nixosConfigurations). Same config that in-place deploys target, so + # a reflash and a `switch-to-configuration` converge on one system. + baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { + inherit pkgs lib; + config = self.nixosConfigurations.batm3-usb.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;