From 0b9f931d16745a29d01c4ec4e995a73b440d4e34 Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Sun, 27 Sep 2026 02:48:25 -0400 Subject: [PATCH 1/5] feat(install): one script installs a Holoport, rehearsed under SeaBIOS scripts/holoport-install.sh partitions the named disk the ADR-017 way (GPT, 1 MiB bios_grub, vfat ESP 'boot', ext4 root 'nixos', swap 'swap'), mounts root at /mnt and the ESP at /mnt/efi-boot, runs nixos-install and then grub-install --target=i386-pc. It refuses to run without an explicit disk and asks for the disk name back. The flake exposes it as packages.x86_64-linux.holoport-install. checks.x86_64-linux.vmTestHoloportInstall runs that package on an installer VM against an empty AHCI disk, then boots the disk under SeaBIOS and asserts the conductor, the fleet's three hApps and Grafana on edgenode-01. CI runs it in its own job because the closure is about 10 GiB. --- .github/workflows/ci.yml | 38 ++++++ flake.nix | 239 ++++++++++++++++++++++++++++++++++++ scripts/holoport-install.sh | 161 ++++++++++++++++++++++++ 3 files changed, 438 insertions(+) create mode 100755 scripts/holoport-install.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f86ffc..9547934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,3 +126,41 @@ jobs: .#checks.x86_64-linux.vmTestGateway \ .#checks.x86_64-linux.vmTestWindtunnel \ .#checks.x86_64-linux.conductorMetricsJq + + # The Holoport install rehearsal copies the event node's whole closure (about + # 10 GiB, most of it the Plasma desktop) onto a virtual disk, so the runner + # holds it twice: once in its store, once in the qcow2. That does not fit + # next to the other VM tests on a stock runner, so it has its own job and + # clears the preinstalled toolchains it never uses first. + holoport-install: + name: Holoport install (SeaBIOS) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Free disk space + run: | + df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/share/boost + sudo docker image prune --all --force + df -h / + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Enable KVM group permissions + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Install on an empty disk and boot it under SeaBIOS + run: nix build -L .#checks.x86_64-linux.vmTestHoloportInstall diff --git a/flake.nix b/flake.nix index e693a15..f3af396 100644 --- a/flake.nix +++ b/flake.nix @@ -455,6 +455,76 @@ assert_installed_once("after reboot") ''; }; + + # ---- Holoport install (ADR-017) -------------------------------------- + # + # Legacy BIOS means i386-pc GRUB and SeaBIOS, so the package and its + # check exist on x86_64-linux only: an attribute named null is left out. + onX86 = name: + if system == "x86_64-linux" + then name + else null; + # + # scripts/holoport-install.sh is the whole install sequence; this pins + # every tool it calls, so the stock NixOS ISO and the workshop ISO run + # the same binaries the VM check does. `nix` and `udevadm` come from + # the live system it runs on. x86_64 only: the BIOS half is i386-pc GRUB. + holoportInstall = pkgs.writeShellApplication { + name = "holoport-install"; + runtimeInputs = with pkgs; [ + coreutils + dosfstools + e2fsprogs + gawk + gnugrep + grub2 + iproute2 + nixos-install + parted + util-linux + ]; + text = builtins.readFile ./scripts/holoport-install.sh; + }; + + # The Sensorica event node exactly as examples/sensorica-fleet builds + # it (same modules, same host file, same 0.6 line and hApp bundles), + # on this flake's nixpkgs rather than the fleet's own lock. The one + # addition is the test driver's backdoor, which nixpkgs' + # installer tests also put into the system they install. + holoportTarget = inputs.nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = {inherit inputs;}; + modules = [ + self.nixosModules.holochain-edgenode + self.nixosModules.holochain-grafana + self.nixosModules.holochain-windtunnel + { + _module.args = { + fleetLine = { + holochain = holonix06.holochain; + hc = holonix06.hc; + }; + fleetHapps = import ./examples/sensorica-fleet/happs.nix { + inherit pkgs; + hc = holonix06.hc; + }; + }; + } + ./examples/sensorica-fleet/hosts/edgenode-01/configuration.nix + "${inputs.nixpkgs}/nixos/modules/testing/test-instrumentation.nix" + ]; + }; + + # The Holoport's disk as QEMU sees it: SATA behind AHCI, so it is + # /dev/sda and the initrd reaches it through `ahci` from the fleet's + # own hardware-configuration.nix, not through a virtio driver the real + # box never loads. Both machines attach the same image; the test script + # creates it and exports its path. + holoportDisk = [ + "-device ahci,id=holoport-ahci" + "-drive if=none,id=holoport-sda,format=qcow2,cache=writeback,werror=report,file=\"$HOLOPORT_DISK\"" + "-device ide-hd,drive=holoport-sda,bus=holoport-ahci.0,bootindex=1" + ]; in { packages = { holochain-0_6 = holonix06.holochain; @@ -470,6 +540,10 @@ options-doc = pkgs.runCommand "module-options.md" {} '' cat ${optionsDocHeader} ${optionsDoc.optionsCommonMark} > $out ''; + + # docs/deployment.md § "Installing on a Holoport (legacy BIOS)". + # x86_64 only; a null name leaves the attribute out elsewhere. + ${onX86 "holoport-install"} = holoportInstall; }; devShells.default = pkgs.mkShell { @@ -811,6 +885,171 @@ happ = kandoHapp; nodeExtra = on06; }; + + # The Holoport install rehearsed end to end. An installer machine runs + # the holoport-install package against an empty SATA disk; the same + # disk then boots under QEMU's default firmware, SeaBIOS, which is + # legacy BIOS like the Holoport (no OVMF anywhere), and the event node + # has to come up on it with its conductor and the fleet's three hApps. + # + # One deviation from docs/deployment.md, as in nixpkgs' + # nixos/tests/installer.nix: the sandbox has no network, so the + # script gets the prebuilt system's store path instead of a flake + # reference and runs `nixos-install --system` where the doc's + # command runs `nixos-install --flake`. Everything before and after + # that one call is the documented path. + ${onX86 "vmTestHoloportInstall"} = pkgs.testers.nixosTest { + name = "holoport-install"; + # The whole closure is copied onto the target disk, and three + # hApps compile their wasm on the first boot. + globalTimeout = 2 * 60 * 60; + + nodes.installer = { + lib, + modulesPath, + ... + }: { + imports = ["${modulesPath}/profiles/installation-device.nix"]; + environment.systemPackages = [holoportInstall]; + virtualisation = { + # A tmpfs root, so the only disk in this machine is the + # Holoport's and nothing else carries a `nixos` label. + diskImage = null; + cores = 4; + memorySize = 4096; + additionalPaths = [holoportTarget.config.system.build.toplevel]; + qemu.options = holoportDisk; + }; + nix.settings.substituters = lib.mkForce []; + # The installer profile's empty root password next to the test + # driver's own root password file makes NixOS warn at every + # evaluation; the driver's shell needs neither. + users.users.root.initialHashedPassword = lib.mkForce null; + }; + + nodes.target = { + virtualisation = { + # No kernel handed to QEMU and no firmware other than its + # default: the disk's own boot code is all there is. + useBootLoader = true; + useDefaultFilesystems = false; + diskImage = null; + cores = 4; + memorySize = 4096; + qemu.options = holoportDisk; + # Never used: the system that boots is the one on the disk. + fileSystems."/" = { + device = "/dev/disk/by-label/not-this-one"; + fsType = "ext4"; + }; + }; + }; + + testScript = let + toplevel = holoportTarget.config.system.build.toplevel; + hc = "${holoportTarget.config.services.holochain-edgenode.hcPackage}/bin/hc"; + in '' + import os + import subprocess + import tempfile + + SYSTEM = "${toplevel}" + disk = os.path.join(tempfile.mkdtemp(), "holoport-sda.qcow2") + subprocess.run( + ["${pkgs.qemu_test}/bin/qemu-img", "create", "-f", "qcow2", disk, "40G"], + check=True, + ) + os.environ["HOLOPORT_DISK"] = disk + + installer.start() + installer.wait_for_unit("multi-user.target") + installer.succeed("udevadm settle") + + with subtest("the script never picks a disk by itself"): + status, _ = installer.execute("holoport-install") + assert status == 2, f"no arguments: expected exit 2, got {status}" + status, _ = installer.execute(f"holoport-install {SYSTEM}") + assert status == 2, f"one argument: expected exit 2, got {status}" + installer.fail(f"echo /dev/sda | holoport-install /dev/sda1 {SYSTEM}") + + with subtest("a confirmation that does not name the disk erases nothing"): + installer.fail(f"echo yes | holoport-install /dev/sda {SYSTEM}") + parts = installer.succeed("lsblk -nro NAME /dev/sda").split() + assert parts == ["sda"], f"the disk was touched: {parts}" + + with subtest("install"): + installer.succeed( + f"echo /dev/sda | holoport-install /dev/sda {SYSTEM} >&2", + timeout=3600, + ) + + with subtest("the ADR-017 layout"): + table = installer.succeed("${pkgs.parted}/bin/parted -s /dev/sda unit MiB print") + installer.log(table) + installer.succeed("${pkgs.parted}/bin/parted -s /dev/sda print | grep -E '^ 1 .*bios_grub'") + installer.succeed("${pkgs.parted}/bin/parted -s /dev/sda print | grep -E '^ 2 .*esp'") + for dev, label, fstype in [ + ("/dev/sda2", "boot", "vfat"), + ("/dev/sda3", "nixos", "ext4"), + ("/dev/sda4", "swap", "swap"), + ]: + got = installer.succeed(f"blkid -o value -s LABEL {dev}").strip() + assert got == label, f"{dev}: label {got!r}, expected {label!r}" + got = installer.succeed(f"blkid -o value -s TYPE {dev}").strip() + assert got == fstype, f"{dev}: type {got!r}, expected {fstype!r}" + + # docs/deployment.md: edgenode-01's Grafana password file is + # written under /mnt before the first boot. + installer.succeed( + "install -d -m 0700 /mnt/var/lib/secrets", + "echo vmtest > /mnt/var/lib/secrets/grafana-admin-password", + "chmod 0400 /mnt/var/lib/secrets/grafana-admin-password", + "umount -R /mnt", + "swapoff -a", + "sync", + ) + installer.shutdown() + + target.start() + + with subtest("SeaBIOS boots the disk through the BIOS GRUB"): + # Without a boot sector SeaBIOS finds nothing to run and the + # kernel never prints; this fails in minutes instead of + # waiting out the global timeout for a shell that never comes. + target.wait_for_console_text("Linux version", timeout=600) + target.wait_for_unit("multi-user.target") + + with subtest("the installed system, not a test fixture"): + assert target.succeed("hostname").strip() == "edgenode-01" + assert target.succeed("findmnt -no SOURCE /").strip() == "/dev/sda3" + assert target.succeed("readlink -f /run/booted-system").strip() == SYSTEM + target.succeed("test -d /boot/grub/i386-pc") + target.succeed("test -f /efi-boot/EFI/BOOT/BOOTX64.EFI") + target.succeed("swapon --show=NAME --noheadings | grep -x /dev/sda4") + + with subtest("the conductor"): + target.wait_for_unit("holochain-conductor.service", timeout=900) + state = target.succeed("systemctl is-active holochain-conductor.service").strip() + assert state == "active", f"expected active, got {state}" + + with subtest("the fleet's hApps are installed once and enabled"): + target.wait_for_unit("holochain-happ-installer.service", timeout=3600) + apps = target.succeed("${hc} sandbox call --running 4444 list-apps") + for app in ["hrea", "kando", "requests-and-offers"]: + key = f'"installed_app_id":"{app}"' + assert apps.count(key) == 1, f"{app}: listed {apps.count(key)} times:\n{apps}" + enabled = apps.count('"status":{"type":"enabled"}') + assert enabled == 3, f"expected 3 enabled apps, got {enabled}:\n{apps}" + + with subtest("Grafana on the event node, with the password written before boot"): + target.wait_for_unit("grafana.service") + target.wait_for_open_port(3000) + search = target.succeed( + "curl -sf -u admin:vmtest 'http://localhost:3000/api/search?query=Holochain'" + ) + assert '"uid":"holochain-fleet"' in search, search + ''; + }; }; }; }; diff --git a/scripts/holoport-install.sh b/scripts/holoport-install.sh new file mode 100755 index 0000000..6d4b05d --- /dev/null +++ b/scripts/holoport-install.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# holoport-install: erase one disk, lay it out the ADR-017 way, install a NixOS +# system on it and make it boot on legacy BIOS (a Holoport) as well as UEFI. +# +# This file is the single source of the install sequence. The flake exposes it +# as `packages.x86_64-linux.holoport-install` with every tool it calls pinned, +# docs/deployment.md § "Installing on a Holoport (legacy BIOS)" runs that, and +# `checks.x86_64-linux.vmTestHoloportInstall` runs the same package under +# SeaBIOS. Layout and commands follow holochain/wind-tunnel-runner +# (`installer.nix`, `base-install.nix`). +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: holoport-install DISK SOURCE + + DISK the whole disk to erase, for example /dev/sda. Never guessed: a + HoloPort+ has two disks, and this erases the one you name. + SOURCE what to install, either + a flake reference ending in #, built on this machine with + the Holochain binary cache, for example + github:Sensorica/nixos-holochain?dir=examples/sensorica-fleet#edgenode-01 + or + a /nix/store/...-nixos-system-* path built elsewhere. If it is not + in this machine's store, the script prints the `nix copy` command + to run on the machine that built it and waits for the copy. + +Layout (GPT): 1 MiB bios_grub, 510 MiB vfat ESP labelled `boot`, ext4 root +labelled `nixos`, swap labelled `swap` (SWAP_SIZE, default 8GiB) at the end. +Root is mounted at /mnt and the ESP at /mnt/efi-boot, then nixos-install, then +`grub-install --target=i386-pc` for the BIOS half. /mnt stays mounted so files +the system needs before its first boot can be written under it. +EOF +} + +die() { + echo "holoport-install: $*" >&2 + exit 1 +} + +if [ "$#" -ne 2 ]; then + usage >&2 + exit 2 +fi + +disk=$1 +source=$2 +swap_size=${SWAP_SIZE:-8GiB} + +holochain_cache=( + --option extra-substituters https://holochain-ci.cachix.org + --option extra-trusted-public-keys holochain-ci.cachix.org-1:5IUSkZc0aoRS53rfkvH9Kid40NpyjwCMCzwRTXy+QN8= +) + +[ "$(id -u)" -eq 0 ] || die "run as root (sudo)" +[ -b "$disk" ] || die "$disk is not a block device" +[ "$(lsblk -dno TYPE "$disk")" = disk ] || die "$disk is not a whole disk; name the disk, not a partition" + +case $source in + /nix/store/*) mode=system ;; + *'#'?*) mode=flake ;; + *) die "SOURCE must be a flake reference ending in # or a /nix/store path to a built system, not '$source'" ;; +esac + +if lsblk -nro MOUNTPOINTS "$disk" | grep -q .; then + die "something on $disk is mounted or in use as swap; this is not the disk to erase, or release it first" +fi +if mountpoint -q /mnt; then + die "/mnt is already a mount point; unmount it first (umount -R /mnt)" +fi + +# The installed system mounts by label, so a second disk that already carries +# one of these labels (a HoloPort+ whose other disk held an earlier install) +# would make the first boot pick between two roots. +for label in nixos boot swap; do + while read -r dev; do + [ -n "$dev" ] || continue + [ "$dev" = "$disk" ] && continue + [ "/dev/$(lsblk -no PKNAME "$dev")" = "$disk" ] && continue + die "$dev, outside $disk, is labelled '$label' and would clash with the new install; wipe it first (wipefs -a $dev)" + done < <(blkid -c /dev/null -t "LABEL=$label" -o device || true) +done + +part() { + case $disk in + *[0-9]) echo "${disk}p$1" ;; + *) echo "$disk$1" ;; + esac +} + +echo "This ERASES everything on $disk:" +lsblk -o NAME,SIZE,TYPE,FSTYPE,LABEL,MODEL,SERIAL "$disk" +echo +echo "Disks left untouched:" +lsblk -dno NAME,SIZE,MODEL | grep -v "^$(basename "$disk") " || echo " (none)" +echo +printf 'Type %s to erase it and install %s: ' "$disk" "$source" +read -r answer || true +[ "$answer" = "$disk" ] || die "answer did not match $disk; nothing was changed" + +echo "==> partitioning $disk" +wipefs -a "$disk" +parted -s "$disk" -- mklabel gpt \ + mkpart bios 1MiB 2MiB \ + set 1 bios_grub on \ + mkpart boot fat32 2MiB 512MiB \ + set 2 esp on \ + mkpart nixos ext4 512MiB "-$swap_size" \ + mkpart swap linux-swap "-$swap_size" 100% +udevadm settle +for n in 2 3 4; do + for _ in $(seq 30); do + [ -b "$(part "$n")" ] && break + sleep 1 + done + [ -b "$(part "$n")" ] || die "$(part "$n") did not appear after partitioning" +done + +echo "==> formatting" +mkfs.fat -F 32 -n boot "$(part 2)" +mkfs.ext4 -F -L nixos "$(part 3)" +mkswap -L swap "$(part 4)" + +echo "==> mounting" +mkdir -p /mnt +mount "$(part 3)" /mnt +mkdir -p /mnt/efi-boot +mount -o umask=077 "$(part 2)" /mnt/efi-boot +swapon "$(part 4)" + +echo "==> installing $source" +if [ "$mode" = flake ]; then + # The target has no nix.conf yet, so the Holochain cache is passed here or + # the conductor is compiled from source on the machine. + nixos-install --no-channel-copy "${holochain_cache[@]}" --flake "$source" +else + if ! nix-store --check-validity "$source" 2>/dev/null && + ! nix-store --store /mnt --check-validity "$source" 2>/dev/null; then + address=$(ip -4 -o addr show scope global | awk '{ sub(/\/.*/, "", $4); print $4; exit }') + echo "$source is not in this machine's store. On the machine that built it, run:" + echo + echo " nix copy --to 'ssh://root@${address:-}?remote-store=/mnt' $source" + echo + echo "Waiting for the copy to land in /mnt (Ctrl-C to stop; /mnt stays mounted)..." + until nix-store --store /mnt --check-validity "$source" 2>/dev/null; do + sleep 10 + done + fi + nixos-install --no-channel-copy --system "$source" +fi + +# NixOS installs the UEFI half (device = "nodev", efiInstallAsRemovable) during +# nixos-install; the BIOS half has to be written by hand, once, into the MBR and +# the bios_grub partition. Its modules live in /boot/grub on the ext4 root, next +# to the grub.cfg NixOS regenerates on every switch. +echo "==> installing GRUB for legacy BIOS" +grub-install --target=i386-pc --boot-directory=/mnt/boot "$disk" + +echo +echo "Installed on $disk. /mnt is still mounted: write anything the system needs before its first boot under /mnt now, then run" +echo " umount -R /mnt && swapoff $(part 4) && reboot" From f0b465d931b587137db5c2267d4fd4ecc21ef37b Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Sun, 27 Sep 2026 02:48:25 -0400 Subject: [PATCH 2/5] docs(deployment): install runbook for a Holoport on legacy BIOS Boot an installer, get network, run holoport-install from the box or from a laptop that copies the closure straight into /mnt, write the Grafana password before the first boot, verify. The fleet README, the fleet template and the common.nix comment now point at the script instead of saying the sequence is unwritten. --- docs/deployment.md | 99 +++++++++++++++++++++++ examples/sensorica-fleet/README.md | 2 +- examples/sensorica-fleet/hosts/common.nix | 5 +- templates/fleet/README.md | 2 +- 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 4e2afde..f21f76a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -64,6 +64,105 @@ sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress sync ``` +## Installing on a Holoport (legacy BIOS) + +A Holoport boots legacy BIOS only, so the NixOS graphical installer's default UEFI layout does not boot on it. One script, [`scripts/holoport-install.sh`](../scripts/holoport-install.sh), does the whole sequence of ADR-017: GPT with a 1 MiB `bios_grub` partition, a vfat ESP labelled `boot`, an ext4 root labelled `nixos` and 8 GiB of swap labelled `swap` at the end; root mounted at `/mnt` and the ESP at `/mnt/efi-boot`; `nixos-install`; then `grub-install --target=i386-pc` for the BIOS half. The same disk also boots on UEFI, because NixOS writes the EFI half from `hosts/common.nix`. The flake publishes the script as `packages.x86_64-linux.holoport-install` with every tool it calls pinned, and `checks.x86_64-linux.vmTestHoloportInstall` runs that package under SeaBIOS. + +The script erases exactly the disk you name and nothing else. It refuses to run without one, shows that disk and the disks it will leave alone, and waits for you to type the disk's name back. It also refuses when another disk already carries one of its three labels, because the installed system mounts by label. + +### The machines + +| | HoloPort | HoloPort+ | +|---|---|---| +| CPU, RAM | dual-core Pentium 3.5 GHz, 8 GB | quad-core i7, 16 GB | +| Disks | 1 TB HDD at `/dev/sda` | 128 GB SSD at `/dev/sda`, 2 TB HDD at `/dev/sdb` | +| Install on | `/dev/sda` | `/dev/sda` (the SSD); `/dev/sdb` stays as it is | + +Both have Ethernet and no Wi-Fi, HDMI and a USB keyboard, no DMI data, and legacy BIOS. The key for the BIOS setup is not known yet: try Del or F2; the boot menu is on F7, F8, F11 or F12. + +### 1. Boot an installer and get network + +Write the workshop ISO (above) or the stock NixOS 26.05 minimal ISO to a USB stick with `dd`, plug the Holoport into the lab router with Ethernet, and boot the stick from the boot menu. Then, in a root shell (`sudo -i` on either ISO): + +```bash +ip -br -4 a +nix --extra-experimental-features nix-command store info --store https://cache.nixos.org +lsblk -d -o NAME,SIZE,ROTA,MODEL +``` + +The first line shows the address the router gave the box; the second prints `Store URL: https://cache.nixos.org` once the binary cache is reachable; the third confirms which disk is which before anything is erased. + +### 2a. Install from the Holoport itself + +Nearly everything is downloaded rather than built: packages come from cache.nixos.org and from the Holochain cache, which the script passes to `nixos-install` since the target has no `nix.conf` yet; only small derivations such as the unpacked Requests & Offers bundle and the configuration files are built on the box. Clone the repository, put your SSH public key in the `operatorKeys` list at the top of `examples/sensorica-fleet/hosts/common.nix`, then run the script against your checkout: + +```bash +git clone https://github.com/Sensorica/nixos-holochain /root/nixos-holochain +cd /root/nixos-holochain +nano examples/sensorica-fleet/hosts/common.nix +nix --extra-experimental-features 'nix-command flakes' run --accept-flake-config .#holoport-install -- /dev/sda ./examples/sensorica-fleet#edgenode-01 +``` + +Type `/dev/sda` when it asks. It asks once more at the end, for a root password for the console. On the base HoloPort, with its slow disk and two cores, expect this to take a while; path 2b moves the work to a laptop. + +### 2b. Install from a laptop + +The laptop builds the system and copies it straight onto the Holoport's new root partition over SSH, so the Holoport only partitions, receives and writes the boot loader. The laptop needs the Holochain cache in its own `nix.conf` (see [Trying it without hardware](#trying-it-without-hardware)), or it compiles the conductor. + +On the laptop, from a checkout with your key already in `operatorKeys`: + +```bash +nix build ./examples/sensorica-fleet#nixosConfigurations.edgenode-01.config.system.build.toplevel --out-link edgenode-01-system +``` + +On the Holoport, give root a password for the installer session and start its SSH server (the installer ships one but does not start it): + +```bash +passwd +systemctl start sshd +``` + +Back on the laptop, with `HOLOPORT_IP` being the address from step 1, send your key, then start the install over SSH: + +```bash +ssh-copy-id root@HOLOPORT_IP +ssh -t root@HOLOPORT_IP "nix --extra-experimental-features 'nix-command flakes' run --accept-flake-config github:Sensorica/nixos-holochain#holoport-install -- /dev/sda $(readlink -f edgenode-01-system)" +``` + +The script partitions the disk, sees that the system is not on the Holoport yet, prints the exact `nix copy` command and waits. Run it in a second terminal on the laptop; it has this shape: + +```bash +nix copy --to "ssh://root@HOLOPORT_IP?remote-store=/mnt" "$(readlink -f edgenode-01-system)" +``` + +`remote-store=/mnt` writes into the new root partition rather than the installer's own store, which lives in RAM and is too small for the event node's closure (about 10 GiB, most of it the desktop). The script carries on by itself once the copy lands. + +### 3. Before the first boot + +`/mnt` is still mounted when the script ends. edgenode-01 runs Grafana with its admin password read from a file, which has to exist before Grafana first starts; `NEW_PASSWORD` is the one you choose: + +```bash +install -d -m 0700 /mnt/var/lib/secrets +install -m 0400 /dev/null /mnt/var/lib/secrets/grafana-admin-password +printf '%s' 'NEW_PASSWORD' > /mnt/var/lib/secrets/grafana-admin-password +umount -R /mnt && swapoff -a && reboot +``` + +Remove the USB stick while the Holoport restarts. It boots from its disk through the BIOS GRUB. + +### 4. Verify + +On the Holoport, or over SSH as `sensorica` or root with the key from `operatorKeys`: + +```bash +systemctl is-active holochain-conductor +systemctl status holochain-happ-installer +journalctl -u holochain-happ-installer --no-pager | grep 'Enabled app' +curl -s -u "admin:NEW_PASSWORD" 'localhost:3000/api/search?query=Holochain' +``` + +The first boot compiles three hApps, so `holochain-happ-installer` can take several minutes to finish on a Holoport (about a minute in the VM check). The conductor should answer `active`; the journal should end with `hc-sandbox: Enabled app: "hrea"`, `"kando"` and `"requests-and-offers"`; and the last line should return the **Holochain Fleet** dashboard, which is also at `http://HOLOPORT_IP:3000` from a laptop on the same network. [Verifying the deployment](#verifying-the-deployment) has the metrics checks. + ## Trying it without hardware The root flake ships a single-node configuration so you can run the module on a laptop before touching a Holoport: diff --git a/examples/sensorica-fleet/README.md b/examples/sensorica-fleet/README.md index b2ea7a4..8efcf29 100644 --- a/examples/sensorica-fleet/README.md +++ b/examples/sensorica-fleet/README.md @@ -50,7 +50,7 @@ nix flake check --no-build --override-input nixos-holochain "$(git rev-parse --s ## Hardware configuration -Each host ships a placeholder `hardware-configuration.nix` so the fleet evaluates before any machine exists. It is not a bare stub: it carries the Holoport disk layout of ADR-017, so a machine partitioned that way boots on this file as written. The partitioning and `grub-install` sequence follows holochain/wind-tunnel-runner and is not yet written up in `docs/deployment.md` (tracked in #6). +Each host ships a placeholder `hardware-configuration.nix` so the fleet evaluates before any machine exists. It is not a bare stub: it carries the Holoport disk layout of ADR-017, so a machine partitioned that way boots on this file as written. The partitioning and `grub-install` sequence follows holochain/wind-tunnel-runner; `scripts/holoport-install.sh` runs it, and `docs/deployment.md` § "Installing on a Holoport (legacy BIOS)" is the runbook. GPT with a 1 MiB `bios_grub` partition *and* a vfat ESP labelled `boot`, an ext4 root labelled `nixos`, swap labelled `swap`; GRUB installed twice, the UEFI half by NixOS (`device = "nodev"`, `efiSupport`, `efiInstallAsRemovable`, ESP at `/efi-boot`) and the BIOS half by one `grub-install --target=i386-pc` in the runbook. A Holoport boots legacy BIOS only; the laptops the fleet is installed from are usually UEFI; this serves both. diff --git a/examples/sensorica-fleet/hosts/common.nix b/examples/sensorica-fleet/hosts/common.nix index 2e89e7c..448d300 100644 --- a/examples/sensorica-fleet/hosts/common.nix +++ b/examples/sensorica-fleet/hosts/common.nix @@ -16,10 +16,11 @@ in { # ADR-017: the Holoport is a legacy-BIOS x86_64 box, and the same tree has to # install on a UEFI laptop, so the disk is GPT with a 1 MiB `bios_grub` # partition *and* an ESP, and GRUB is installed twice. NixOS writes the EFI - # half from this block; the install runbook runs + # half from this block; the install script (scripts/holoport-install.sh in + # nixos-holochain, docs/deployment.md § "Installing on a Holoport") runs # grub-install --target=i386-pc --boot-directory=/mnt/boot /dev/sda # for the BIOS half. `device = "nodev"` is what leaves that half to the - # runbook. `efiInstallAsRemovable` writes EFI/BOOT/BOOTX64.EFI, which firmware + # script. `efiInstallAsRemovable` writes EFI/BOOT/BOOTX64.EFI, which firmware # that keeps no boot variables still finds. Layout and both commands follow # holochain/wind-tunnel-runner (`base-install.nix`, `installer.nix`). boot.loader.grub = { diff --git a/templates/fleet/README.md b/templates/fleet/README.md index 4a32290..2756e2c 100644 --- a/templates/fleet/README.md +++ b/templates/fleet/README.md @@ -47,7 +47,7 @@ The placeholder targets a machine that may boot **legacy BIOS or UEFI**, because - the UEFI half by NixOS from `boot.loader.grub` in `hosts/common.nix` (`device = "nodev"`, `efiSupport`, `efiInstallAsRemovable`, ESP mounted at `/efi-boot`); - the BIOS half by one command in the install runbook, `grub-install --target=i386-pc --boot-directory=/mnt/boot /dev/sda`. -`efiInstallAsRemovable` writes `EFI/BOOT/BOOTX64.EFI`, so firmware that keeps no boot variables still finds it. If your machines are UEFI only you can drop the `bios_grub` partition and the `i386-pc` command; if they are BIOS only, the ESP and the EFI half are what you drop. Layout and both commands after holochain/wind-tunnel-runner (`base-install.nix`, `installer.nix`); a written partitioning runbook is tracked upstream in Sensorica/nixos-holochain#6. +`efiInstallAsRemovable` writes `EFI/BOOT/BOOTX64.EFI`, so firmware that keeps no boot variables still finds it. If your machines are UEFI only you can drop the `bios_grub` partition and the `i386-pc` command; if they are BIOS only, the ESP and the EFI half are what you drop. Layout and both commands after holochain/wind-tunnel-runner (`base-install.nix`, `installer.nix`); the whole partition, install and `grub-install` sequence is one command, `nix run github:Sensorica/nixos-holochain#holoport-install -- DISK FLAKE#HOST`, written up in the upstream `docs/deployment.md` § "Installing on a Holoport (legacy BIOS)". ## Deploy From fcaf1735d784f055a8cdaf08b52fe69934aefd9a Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Mon, 28 Sep 2026 00:29:58 -0400 Subject: [PATCH 3/5] fix(install): settle udev before mounting the new root, resolve a by-id disk A VM run once stopped at the root mount with 'wrong fs type' right after mkfs. The script now waits for udev after formatting and mounts the root as ext4 explicitly. A DISK given as a /dev/disk/by-id link is resolved to its kernel node first, so partition names and the label check compare /dev/sdX names. --- scripts/holoport-install.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/holoport-install.sh b/scripts/holoport-install.sh index 6d4b05d..8afc38b 100755 --- a/scripts/holoport-install.sh +++ b/scripts/holoport-install.sh @@ -43,7 +43,9 @@ if [ "$#" -ne 2 ]; then exit 2 fi -disk=$1 +# A /dev/disk/by-id/... link resolves to its /dev/sdX node, so partition names +# and the label check below compare kernel names. +disk=$(readlink -f "$1") source=$2 swap_size=${SWAP_SIZE:-8GiB} @@ -120,10 +122,13 @@ echo "==> formatting" mkfs.fat -F 32 -n boot "$(part 2)" mkfs.ext4 -F -L nixos "$(part 3)" mkswap -L swap "$(part 4)" +# Let udev finish probing the new filesystems before mounting; without this a +# VM run once failed here with "wrong fs type". +udevadm settle echo "==> mounting" mkdir -p /mnt -mount "$(part 3)" /mnt +mount -t ext4 "$(part 3)" /mnt mkdir -p /mnt/efi-boot mount -o umask=077 "$(part 2)" /mnt/efi-boot swapon "$(part 4)" From 0c9b423f1da79bd2b9e325c01049964d8877fc10 Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Mon, 28 Sep 2026 00:29:58 -0400 Subject: [PATCH 4/5] docs(deployment): what the first real Holoport install showed Esc opens the base HoloPort's boot menu, the stick reads reliably only from a USB 2 port, and the graphical desktop freezes on the HD 610 (session log 2026-09-27). The root password prompt at the end of nixos-install appears only when its input is a terminal. --- docs/deployment.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 30aef0d..a3cb8c9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -78,11 +78,11 @@ The script erases exactly the disk you name and nothing else. It refuses to run | Disks | 1 TB HDD at `/dev/sda` | 128 GB SSD at `/dev/sda`, 2 TB HDD at `/dev/sdb` | | Install on | `/dev/sda` | `/dev/sda` (the SSD); `/dev/sdb` stays as it is | -Both have Ethernet and no Wi-Fi, HDMI and a USB keyboard, no DMI data, and legacy BIOS. The key for the BIOS setup is not known yet: try Del or F2; the boot menu is on F7, F8, F11 or F12. +Both have Ethernet and no Wi-Fi, HDMI and a USB keyboard, no DMI data, and legacy BIOS. On the base HoloPort, tapping Esc at power-on opens the firmware boot menu; pick the stick there, because the GRUB menu on the internal disk belongs to HoloOS and never lists it. The key for the BIOS setup, and the HoloPort+'s keys, are not known yet: try Del or F2 for setup, and F7, F8, F11 or F12 for the boot menu. ### 1. Boot an installer and get network -Write the workshop ISO (above) or the stock NixOS 26.05 minimal ISO to a USB stick with `dd`, plug the Holoport into the lab router with Ethernet, and boot the stick from the boot menu. Then, in a root shell (`sudo -i` on either ISO): +Write the workshop ISO (above) or the stock NixOS 26.05 minimal ISO to a USB stick with `dd`, plug the Holoport into the lab router with Ethernet, and boot the stick from the boot menu. Use a USB 2 port: from a USB 3 port the base HoloPort's live system fails with `SQUASHFS error: Unable to read page` and freezes. Prefer a text console to a graphical ISO, whose desktop freezes on the base HoloPort's Intel HD 610; if you booted one and it froze, Ctrl+Alt+F1 then Ctrl+Alt+F3 reaches a console logged in as `nixos`, and `sudo systemctl stop display-manager` stops the frozen session. Then, in a root shell (`sudo -i` on either ISO): ```bash ip -br -4 a @@ -103,7 +103,7 @@ nano examples/sensorica-fleet/hosts/common.nix nix --extra-experimental-features 'nix-command flakes' run --accept-flake-config .#holoport-install -- /dev/sda ./examples/sensorica-fleet#edgenode-01 ``` -Type `/dev/sda` when it asks. It asks once more at the end, for a root password for the console. On the base HoloPort, with its slow disk and two cores, expect this to take a while; path 2b moves the work to a laptop. +Type `/dev/sda` when it asks. It asks once more at the end, for a root password for the console, but only when it runs in a terminal: `nixos-install` skips that prompt when its input is not one (a background or piped SSH session), and then `passwd`, run in the shell `nixos-enter --root /mnt` opens, sets it before the reboot. On the base HoloPort, with its slow disk and two cores, expect this to take a while; path 2b moves the work to a laptop. ### 2b. Install from a laptop From c8c91f7cdf2821864bb7c3b0cfecf47ff1563a62 Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Sun, 27 Sep 2026 12:21:45 -0400 Subject: [PATCH 5/5] fix(tests): the Holoport install target takes its hApps from sensorica-event-node Once #59 and #61 are both in, hosts/common.nix no longer reads fleetLine and fleetHapps: the fleet imports nixosModules.sensorica-event-node for them. holoportTarget still passed the old arguments, so the installed system had no hApps, no installer unit, and vmTestHoloportInstall failed ("holochain-happ-installer.service is inactive and there are no pending jobs"). It now imports the module the fleet imports, and the check passes: hrea, kando and requests-and-offers enabled 20 to 43 s after boot. --- flake.nix | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/flake.nix b/flake.nix index f6411df..9657863 100644 --- a/flake.nix +++ b/flake.nix @@ -663,18 +663,9 @@ self.nixosModules.holochain-edgenode self.nixosModules.holochain-grafana self.nixosModules.holochain-windtunnel - { - _module.args = { - fleetLine = { - holochain = holonix06.holochain; - hc = holonix06.hc; - }; - fleetHapps = import ./examples/sensorica-fleet/happs.nix { - inherit pkgs; - hc = holonix06.hc; - }; - }; - } + # The line, hApps and seed, from the same export the fleet's + # `fleetModules` import (#33). + self.nixosModules.sensorica-event-node ./examples/sensorica-fleet/hosts/edgenode-01/configuration.nix "${inputs.nixpkgs}/nixos/modules/testing/test-instrumentation.nix" ];