diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md index 2bfc333..062cd5f 100644 --- a/deploy/nixos/README.md +++ b/deploy/nixos/README.md @@ -33,9 +33,19 @@ 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 d68cad2..e2e138c 100644 --- a/deploy/nixos/hardware/douro.nix +++ b/deploy/nixos/hardware/douro.nix @@ -20,12 +20,24 @@ 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" @@ -37,6 +49,9 @@ "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 29f44b1..118fe30 100644 --- a/flake.nix +++ b/flake.nix @@ -261,6 +261,61 @@ }) ]; }; + + # 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) ────────── @@ -293,35 +348,22 @@ sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix; batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix; - # 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. + # 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. batm3-usb = 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 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; - }) - ]; + 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 ]; }; }; @@ -455,53 +497,16 @@ 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 - # 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 - ''; + # 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; # Backwards compat iso = self.nixosConfigurations.douro.config.system.build.isoImage;