diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-07 17:26:42 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-07 17:26:42 +0300 |
| commit | 1aa5eb37c626f48bd725578637e5db4ae870ebfe (patch) | |
| tree | ed0f2507a276947dc5cb9e285c8558873e4ea186 | |
| parent | 213c92a36424d55f7a4650851ceb45c8a2283926 (diff) | |
ychat: bump image to deacb3f (fix chat color picker / POST body parsing)
| -rw-r--r-- | f3s/pi-netbsd/README.md | 323 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/bake/bake-golden.sh | 81 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/bake/config.exp | 41 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/bake/setup.sh | 69 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/flash/95netbsdflash/flash.sh | 82 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/flash/95netbsdflash/module-setup.sh | 20 | ||||
| -rwxr-xr-x | f3s/pi-netbsd/flash/build-flasher.sh | 30 | ||||
| -rw-r--r-- | f3s/ychat/helm-chart/Chart.yaml | 2 | ||||
| -rw-r--r-- | f3s/ychat/helm-chart/templates/deployment.yaml | 2 |
9 files changed, 648 insertions, 2 deletions
diff --git a/f3s/pi-netbsd/README.md b/f3s/pi-netbsd/README.md new file mode 100644 index 0000000..1166018 --- /dev/null +++ b/f3s/pi-netbsd/README.md @@ -0,0 +1,323 @@ +# Remote in-place NetBSD install on a Raspberry Pi 3B+ (no SD removal) + +Runbook + scripts to replace **Rocky Linux 9** with **NetBSD 10.1** on an f3s +Raspberry Pi **3 Model B+**, entirely over SSH, **without pulling the microSD +card**. Proven on `pi0` (2026-07-03); this document is written so the same can +be done to `pi1` (or a rebuild of `pi0`) by changing two values. + +> **TL;DR of the hard-won lesson:** `kexec` is compiled out of the Rocky RPi +> kernel and the systemd shutdown-pivot does not fire on these boxes, so the +> only remote RAM-flasher that works is one loaded by the **Pi firmware** as an +> `initramfs` via a one-shot `config.txt`. See [Why the obvious methods +> fail](#why-the-obvious-methods-fail). + +--- + +## Contents + +- [Outcome / definition of done](#outcome--definition-of-done) +- [Hardware & facts](#hardware--facts) +- [Why the obvious methods fail](#why-the-obvious-methods-fail) +- [How it works (the method that does)](#how-it-works-the-method-that-does) +- [Prerequisites](#prerequisites) +- [Stage A — bake the golden image (on `earth`)](#stage-a--bake-the-golden-image-on-earth) +- [Stage B — remote in-place flash (on the Pi)](#stage-b--remote-in-place-flash-on-the-pi) +- [Doing pi1 specifically](#doing-pi1-specifically) +- [Verification](#verification) +- [Rollback / recovery](#rollback--recovery) +- [Troubleshooting & gotchas](#troubleshooting--gotchas) +- [Cleanup](#cleanup) +- [Post-install (deferred)](#post-install-deferred) +- [File manifest](#file-manifest) + +--- + +## Outcome / definition of done + +`ssh paul@<pi-ip>` lands on a **NetBSD 10.1 (GENERIC64) evbarm/aarch64** system: + +- login as `paul` via **SSH key** (member of `wheel`); password auth also on as a backup +- hostname correct (`piN.lan.buetow.org`) +- **static IP on `mue0`** (the 3B+ onboard LAN78xx NIC in NetBSD), default route + DNS working +- root filesystem auto-resized to the whole card on first boot + +## Hardware & facts + +| Item | pi0 | pi1 | +|------|-----|-----| +| Board | Raspberry Pi 3 Model B+ | Raspberry Pi 3 Model B+ | +| Was running | Rocky Linux 9.7 aarch64 | Rocky Linux 9.x aarch64 | +| LAN IP | 192.168.1.125 | **192.168.1.126** | +| Hostname | pi0.lan.buetow.org | **pi1.lan.buetow.org** | +| WireGuard | 192.168.2.203 | 192.168.2.204 | +| RAM | 909 MiB | 909 MiB | +| SD | `/dev/mmcblk0` (30 GB): p1 `/boot` vfat, p2 swap, p3 `/` ext4 | same | +| NIC (Linux → NetBSD) | `lan78xx` → **`mue0`** | `lan78xx` → **`mue0`** | +| Gateway / DNS | 192.168.1.1 | 192.168.1.1 | + +Confirmed on the RPi Rocky kernel (`6.1.31-v8.1.el9.altarch`): `CONFIG_KEXEC` +disabled, `CONFIG_KEXEC_FILE` unset; **boots with no initramfs** (kernel mounts +the ext4 root directly); `ext4`, `vfat`, `nls_cp437`, `nls_ascii`, `mmc_block`, +`sdhci`, `tmpfs` are all **built-in** (`=y`) — so a flasher initramfs needs no +extra modules to mount `/boot`, the root, or a tmpfs. + +NetBSD base image used: **NetBSD 10.1 evbarm-aarch64 gzimg** +`https://cdn.netbsd.org/pub/NetBSD/NetBSD-10.1/evbarm-aarch64/binary/gzimg/arm64.img.gz` +(GPT: EFI System partition + NetBSD FFS root; boots RPi 3/4/5). + +## Why the obvious methods fail + +Three RAM-flasher mechanisms were tried on `pi0`. Only the third works here: + +1. **`kexec` into a RAM flasher** (what most guides assume). ❌ Impossible: + `kexec_load(2)` returns `ENOSYS` and `kexec_file_load(2)` is not built + (`CONFIG_KEXEC_FILE` unset). `kexec-tools` is installed but the syscalls are + not in the kernel. +2. **systemd shutdown-pivot** (`/run/initramfs/shutdown` switch-root at reboot). + ❌ Does **not fire** on these boxes — proven with a 3-channel evidence probe + over several test reboots; `systemd-shutdown` never switch-roots into + `/run/initramfs` here (the box was not booted from an initrd, and + `dracut-shutdown` is a no-op because `.need_shutdown` is never created). + *Also_ a red herring along the way: mounting a **separate tmpfs** at + `/run/initramfs` gets unmounted during shutdown — always populate it as a + plain dir on `/run` if you ever revisit this. +3. **Pi-firmware `initramfs`** (this runbook). ✅ Works. The VideoCore firmware + loads a small dracut initramfs right after the kernel; a dracut *pre-mount* + hook runs in RAM before the real root is mounted and does the flash. + +## How it works (the method that does) + +``` + EARTH (x86_64 laptop) piN (RPi 3B+, Rocky) + ───────────────────── ───────────────────── + Stage A: bake golden image Stage B: remote flash + qemu-system-aarch64 -M virt (TCG) 1. build flasher.img (dracut, + boots stock NetBSD arm64.img pre-mount hook) -> /boot + -> configure headless (sshd, 2. arm: write /boot/config.txt + static mue0, user+key, pw) = "initramfs flasher.img + -> sync + poweroff followkernel" + trigger + gzip -> netbsd-piN-golden.img.gz file /boot/netbsd-flash-mode + │ 3. systemctl reboot + └── scp to piN:/home/paul/ ───────────► ── firmware loads flasher.img ──┐ + ▼ + dracut pre-mount hook (95netbsdflash): + - DISARM: rm /boot/config.txt + trigger + (so any later failure self-heals to Rocky) + - mount ext4 root ro, copy golden gz -> tmpfs (RAM) + - dryrun: gunzip|wc -c (validate, reboot to Rocky) + - real: gunzip|dd of=/dev/mmcblk0 (reboot to NetBSD) + ▼ + NetBSD 10.1 boots headless, static .12x, sshd + ▼ + ssh paul@<ip> ✅ ACCEPTANCE +``` + +Key safety property: the hook **removes `config.txt` before touching anything +else**, so the flasher is strictly one-shot and any failure *after the hook +starts* leaves a card that boots normally (Rocky, or — once flashed — NetBSD). +The **dry-run** proves the entire boot+stage+decompress path non-destructively +before the real `dd`. + +## Prerequisites + +On **earth** (the flashing workstation): + +- `qemu-system-aarch64`, `qemu-img`, `edk2` AAVMF firmware, `expect`, passwordless `sudo` + (`sudo dnf install -y qemu-system-aarch64 qemu-img edk2-aarch64 expect`). +- Note: earth is x86_64, so the aarch64 guest runs under **TCG emulation** (slow + but fine — a bake is a handful of minutes). +- Your SSH public key (`~/.ssh/id_rsa.pub`) — it gets baked into the image. + +On the **Pi**: + +- Reachable over SSH as `paul` with **passwordless sudo** (the f3s default). +- `dracut` present (it is on Rocky), `/boot` is the vfat firmware partition, + ~500 MB free on `/` to stage the image. +- Its twin Pi still serving the shared role (so downtime is a non-event). + +## Stage A — bake the golden image (on `earth`) + +Scripts: [`bake/`](bake). Editing the NetBSD FFS root from Linux is unsafe, so +we configure the image **from inside a real NetBSD** running under qemu. + +1. **Edit the two per-host values** at the top of [`bake/setup.sh`](bake/setup.sh): + `HOSTNAME` and `IPADDR` (see [Doing pi1](#doing-pi1-specifically)). +2. Run the orchestrator: + ```bash + cd f3s/pi-netbsd/bake + ./bake-golden.sh piN # e.g. ./bake-golden.sh pi1 + ``` + It downloads `arm64.img.gz` (if absent), serves `setup.sh` over a localhost + HTTP server, boots the image in qemu, blind-drives the console via + `config.exp` to fetch+run `setup.sh`, verifies the on-disk result, then + `sync`+powers off and produces `netbsd-piN-golden.img.gz`. +3. A random backup password is generated and printed (also saved to + `piN-cred.txt`). The **SSH key is the primary login**; change the password + after install. + +What `setup.sh` configures inside the image: `sshd=YES`, `hostname`, +`ifconfig_mue0="inet <ip> netmask 0xffffff00"`, `defaultroute`, `dhcpcd=NO`, +`/etc/resolv.conf`, user `paul` (+`wheel`) with your `authorized_keys` and an +argon2id password (root too), `sshd_config` pubkey+password auth, and an +`/etc/rc.local` fallback that puts the static IP on the first real ethernet +interface if `mue0` is ever named differently. + +> **The qemu-automation gotchas** (already handled in the scripts, documented so +> you understand them): add a **virtio-rng** device or NetBSD stalls on entropy +> and never generates ssh host keys; run `expect` under **`LC_ALL=C`** or Tcl +> chokes on the serial control bytes; **blind-drive** the login/commands with +> fixed sleeps and judge success by markers written to the log rather than +> matching the flaky console; and **`sync` then wait for a clean poweroff** — +> killing qemu before the FFS is flushed corrupts `rc.conf`/`pwd.db`. + +## Stage B — remote in-place flash (on the Pi) + +Scripts: [`flash/`](flash). Copy this whole `pi-netbsd/` tree (or at least +`flash/`) to the Pi first, e.g. `scp -r f3s/pi-netbsd paul@piN.lan.buetow.org:`. + +1. **Stage the golden image on the Pi** (on its ext4 root): + ```bash + scp netbsd-piN-golden.img.gz paul@piN.lan.buetow.org:/home/paul/ + ``` + The flasher hook globs for **`/home/paul/netbsd-*-golden.img.gz`** (or under + `/root/`), so any `netbsd-<pi>-golden.img.gz` name works — no rename needed. + Verify integrity: `gzip -t` and compare `sha256sum` against earth. +2. **Build the flasher initramfs** (module name is `netbsdflash`, no numeric prefix): + ```bash + ssh paul@piN.lan.buetow.org 'sudo /home/paul/pi-netbsd/flash/build-flasher.sh' + ``` + Produces `/boot/flasher.img` (~38 MB) and prints an `lsinitrd` sanity check. +3. **Dry-run first (non-destructive):** + ```bash + ssh paul@piN.lan.buetow.org ' + sudo rm -f /boot/flash-evidence.txt + printf "initramfs flasher.img followkernel\n" | sudo tee /boot/config.txt + printf "dryrun\n" | sudo tee /boot/netbsd-flash-mode + sync; sudo systemctl reboot' + ``` + The Pi goes down ~90 s (staging + decompress), then returns as **Rocky**. + Read the proof: + ```bash + ssh paul@piN.lan.buetow.org 'sudo cat /boot/flash-evidence.txt' + # EXPECT: + # FLASHER-RAN mode=dryrun up=... + # DRYRUN-RESULT bytes=1586495488 rc=0 + ``` + `bytes=1586495488 rc=0` and `config.txt` gone ⇒ the whole path works. + **Do not proceed to the real flash unless the dry-run shows this.** +4. **Real flash (destructive, irreversible):** + ```bash + ssh paul@piN.lan.buetow.org ' + sudo rm -f /boot/flash-evidence.txt + printf "initramfs flasher.img followkernel\n" | sudo tee /boot/config.txt + printf "real\n" | sudo tee /boot/netbsd-flash-mode + sync; sudo systemctl reboot' + ``` + The Pi stages to RAM, `dd`s the 1.59 GB image onto `/dev/mmcblk0`, then + reboots into **NetBSD**. Allow several minutes (SD write + first boot + + root resize). + +## Doing pi1 specifically + +Only two things differ from pi0. In [`bake/setup.sh`](bake/setup.sh): + +```sh +HOSTNAME="pi1.lan.buetow.org" +IPADDR="192.168.1.126" +``` + +Everything else is identical: same board, same NIC (`mue0`), same gateway/DNS, +same flasher. WireGuard peer for pi1 is `192.168.2.204` (configured later, +out of scope here). Then run Stage A (`./bake-golden.sh pi1`) and Stage B as +above against `pi1.lan.buetow.org` / `192.168.1.126`. + +> Keep pi0 (or the other twin) up while flashing pi1 so the static +> `f3s.buetow.org` backend stays served. + +## Verification + +```bash +ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null paul@192.168.1.126 ' + uname -a # NetBSD piN.lan.buetow.org 10.1 ... evbarm + hostname # piN.lan.buetow.org + id # uid=1000(paul) ... groups ... wheel + ifconfig mue0 # inet 192.168.1.126 + netstat -rn -f inet | grep default # default 192.168.1.1 ... mue0 + host cdn.netbsd.org # DNS resolves + df -h / # root grown to ~29G' +``` + +NetBSD regenerates its own SSH **host keys**, so your `known_hosts` entry for the +Pi will change — remove the old line or use the relaxed flags above. + +## Rollback / recovery + +- **Before the real flash:** nothing is destroyed. Any failure once the hook has + started leaves the card booting normally (the hook removes `config.txt` + first). If the initramfs never runs the hook (rare — a malformed + `flasher.img`), the Pi boot-loops on the missing/failed initramfs until you + fix it physically: pull the SD, delete `config.txt`, reinsert. Mitigation is + the dry-run, which proves the initramfs boots. +- **After the real flash:** the card is NetBSD. To go back to Rocky, reflash the + Rocky image (keep a known-good one handy) — physically or by the same method + in reverse. **Keep a Rocky SD image before starting** as the ultimate fallback + (physical access is "inconvenient but possible" for these boxes). + +## Troubleshooting & gotchas + +- **Dry-run returns fast (~20 s) with no `flash-evidence.txt`:** the initramfs + did not run the hook — check `/boot/config.txt` really says + `initramfs flasher.img followkernel`, that `/boot/flasher.img` exists, and that + the trigger file `/boot/netbsd-flash-mode` contains exactly `dryrun`. +- **`dracut module 'netbsdflash' cannot be found`:** use `--add netbsdflash` + (module name = directory name **without** the `95` prefix). +- **`bytes=` is not `1586495488` or `rc!=0`:** the staged/decompressed image is + bad — re-check the `scp`/`sha256sum` of the golden gz on the Pi. +- **NetBSD boots but is unreachable:** the NIC came up under a name other than + `mue0`; the `rc.local` fallback should still put `.12x` on the first real + ethernet iface — attach a console (HDMI/serial) to inspect if needed. +- **qemu bake stalls / login never proceeds:** see the Stage A gotchas box + (virtio-rng, `LC_ALL=C`, blind-drive, sync-before-poweroff). + +## Cleanup + +On the Pi (once NetBSD is confirmed, these are on the wiped card anyway for a +real flash; relevant only if you leave a Pi on Rocky after dry-runs): + +```bash +sudo rm -f /boot/config.txt /boot/netbsd-flash-mode /boot/flash-evidence.txt \ + /boot/flasher.img /home/paul/netbsd-pi0-golden.img.gz +# optional: restore the untouched initramfs backup +sudo mv /boot/initramfs-*.img.orig /boot/initramfs-$(uname -r).img # if present +``` + +On earth: stop any leftover `python3 -m http.server` from the bake; the +`*.img`/`*.img.gz` work files can be deleted or kept for the next Pi. + +## Post-install (deferred) + +Base OS + networking + SSH is the whole scope of this runbook. Re-provisioning +the Pi's role on NetBSD is separate work: + +- **lighttpd static site** (`f3s.buetow.org`): `pkgin install lighttpd`, recreate + `/var/www` + the Host-based vhost config. relayd backends keep the same IPs, so + the OpenBSD frontends need no change. +- **DTail `dserver`**: needs a **`netbsd/arm64`** build (the Pis previously ran + `linux/arm64`). +- **WireGuard** peer, monitoring agents, etc. + +## File manifest + +``` +f3s/pi-netbsd/ +├── README.md # this runbook +├── bake/ +│ ├── bake-golden.sh # earth: orchestrate the qemu bake +│ ├── setup.sh # image customization (EDIT HOSTNAME/IPADDR) +│ └── config.exp # earth: expect driver for the qemu console +└── flash/ + ├── build-flasher.sh # Pi: build /boot/flasher.img via dracut + └── 95netbsdflash/ # dracut module (name: "netbsdflash") + ├── module-setup.sh + └── flash.sh # the pre-mount flasher hook +``` diff --git a/f3s/pi-netbsd/bake/bake-golden.sh b/f3s/pi-netbsd/bake/bake-golden.sh new file mode 100755 index 0000000..538a8a3 --- /dev/null +++ b/f3s/pi-netbsd/bake/bake-golden.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Stage A orchestrator — run on `earth`. Produces netbsd-<piN>-golden.img.gz. +# Usage: ./bake-golden.sh piN [workdir] +# Requires: qemu-system-aarch64, qemu-img, edk2 AAVMF, expect, ~/.ssh/id_rsa.pub. +# EDIT bake/setup.sh (HOSTNAME/IPADDR) for the target Pi BEFORE running this. +set -euo pipefail + +PIN="${1:?usage: $0 piN [workdir]}" +HERE="$(cd "$(dirname "$0")" && pwd)" +WORK="${2:-$HOME/Downloads}" +BASE="$WORK/NetBSD-10.1-evbarm-aarch64-arm64.img.gz" +URL="https://cdn.netbsd.org/pub/NetBSD/NetBSD-10.1/evbarm-aarch64/binary/gzimg/arm64.img.gz" +CODE=/usr/share/AAVMF/AAVMF_CODE.fd +VARSRC=/usr/share/AAVMF/AAVMF_VARS.fd +PUBKEY="${SSHKEY_FILE:-$HOME/.ssh/id_rsa.pub}" + +command -v qemu-system-aarch64 >/dev/null || { echo "install qemu-system-aarch64"; exit 1; } +[ -f "$CODE" ] && [ -f "$VARSRC" ] || { echo "install edk2 AAVMF firmware"; exit 1; } +[ -f "$PUBKEY" ] || { echo "missing $PUBKEY"; exit 1; } +mkdir -p "$WORK" + +echo "== base image ==" +[ -f "$BASE" ] || curl -fSL "$URL" -o "$BASE" +gzip -t "$BASE" + +echo "== fresh work image + varstore ==" +IMG="$WORK/netbsd-$PIN-work.img" +VARS="$WORK/AAVMF_VARS_$PIN.fd" +gunzip -kc "$BASE" > "$IMG" +cp "$VARSRC" "$VARS" + +echo "== per-host values from the piN argument ==" +case "$PIN" in + pi0) IPADDR=192.168.1.125 ;; + pi1) IPADDR=192.168.1.126 ;; + pi2) IPADDR=192.168.1.127 ;; + pi3) IPADDR=192.168.1.128 ;; + *) IPADDR="${IPADDR:?unknown $PIN — set IPADDR env}" ;; +esac +HOSTNAME_FQDN="$PIN.lan.buetow.org" +echo " $PIN -> $HOSTNAME_FQDN / $IPADDR" + +echo "== render setup.sh (inject host/ip + key + generated password) into a served dir ==" +SEED="$WORK/seed-$PIN"; mkdir -p "$SEED" +PW=$(openssl rand -base64 9 | tr -d '/+=' | cut -c1-12) +printf '%s\n' "$PW" > "$WORK/$PIN-cred.txt"; chmod 600 "$WORK/$PIN-cred.txt" +KEY=$(cat "$PUBKEY") +# substitute host/ip lines + placeholders; awk keeps the multiline key intact +awk -v key="$KEY" -v pw="$PW" -v host="$HOSTNAME_FQDN" -v ip="$IPADDR" ' + /^HOSTNAME=/ { print "HOSTNAME=\"" host "\""; next } + /^IPADDR=/ { print "IPADDR=\"" ip "\""; next } + { gsub(/__SSHKEY__/,key); gsub(/__PW__/,pw); print } +' "$HERE/setup.sh" > "$SEED/setup.sh" + +echo "== serve setup.sh on 127.0.0.1:8000 (guest reaches it at 10.0.2.2) ==" +python3 -m http.server 8000 --bind 127.0.0.1 --directory "$SEED" >"$WORK/httpd-$PIN.log" 2>&1 & +HTTPD=$! +trap 'kill "$HTTPD" 2>/dev/null || true' EXIT +until grep -q . "$WORK/httpd-$PIN.log" 2>/dev/null || curl -fsS http://127.0.0.1:8000/setup.sh -o /dev/null 2>/dev/null; do sleep 0.3; done + +echo "== boot + configure via qemu/expect (TCG, ~4 min) ==" +LOG="$WORK/config-$PIN.log"; : > "$LOG" +NBIMG="$IMG" NBVARS="$VARS" NBLOG="$LOG" NBHTTP="http://10.0.2.2:8000/setup.sh" \ + LC_ALL=C LANG=C expect -f "$HERE/config.exp" || true + +echo "== verify markers ==" +if ! grep -qa 'SETUP_OK' "$LOG" || ! grep -qa 'halt: halted' "$LOG"; then + echo "!! bake did not complete cleanly; inspect $LOG"; exit 1 +fi +echo "-- on-disk verification block --" +sed -n '/^VBEGIN/,/^VEND/p' <(tr -d '\r' < "$LOG") + +echo "== gzip -> golden ==" +GOLDEN="$WORK/netbsd-$PIN-golden.img.gz" +gzip -c "$IMG" > "$GOLDEN" +gzip -t "$GOLDEN" +echo +echo "DONE: $GOLDEN" +echo "sha256: $(sha256sum "$GOLDEN" | awk '{print $1}')" +echo "backup password for paul/root: $PW (also in $WORK/$PIN-cred.txt)" +echo "Next: scp \"$GOLDEN\" paul@$PIN.lan.buetow.org:/home/paul/netbsd-pi0-golden.img.gz (see README Stage B)" diff --git a/f3s/pi-netbsd/bake/config.exp b/f3s/pi-netbsd/bake/config.exp new file mode 100755 index 0000000..1a3fe61 --- /dev/null +++ b/f3s/pi-netbsd/bake/config.exp @@ -0,0 +1,41 @@ +#!/usr/bin/expect -f +# Blind-drive the NetBSD-in-qemu console to fetch+run setup.sh, verify, sync, +# and power off. Paths come from the environment (set by bake-golden.sh): +# NBIMG - raw work image (edited in place, then gzipped to the golden) +# NBVARS - writable AAVMF (edk2) varstore +# NBLOG - session log file +# NBHTTP - host-side URL of setup.sh (reachable from the guest as 10.0.2.2) +# +# Lessons baked in: virtio-rng (entropy, else boot stalls / no ssh host keys), +# run under LC_ALL=C (Tcl chokes on the serial's control bytes), blind login +# with fixed sleeps + a quote-trick shell probe (expect's LIVE matching over the +# TCG serial is unreliable), and judge success by the &&-chained command's +# markers in NBLOG rather than by intermediate console matches. +set timeout 700 +log_file -a $env(NBLOG) + +spawn qemu-system-aarch64 -M virt -cpu cortex-a72 -m 2048 -smp 2 -accel tcg \ + -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-device,rng=rng0 \ + -drive if=pflash,format=raw,readonly=on,file=/usr/share/AAVMF/AAVMF_CODE.fd \ + -drive if=pflash,format=raw,file=$env(NBVARS) \ + -drive file=$env(NBIMG),format=raw,if=virtio,cache=writeback \ + -nic user -display none -serial stdio -monitor none + +# Fully blind login (root has no password on the stock image). expect's LIVE +# console matching over the TCG serial is unreliable, so we do NOT probe for a +# prompt — we just type, with generous settle time, and let bake-golden.sh judge +# success from the SETUP_OK / halt markers in NBLOG afterward. Sending root twice +# is harmless: the first logs in, a second lands as "root: not found" at a shell. +sleep 135 +send "\r"; sleep 2 +send "root\r"; sleep 6 +send "\r"; sleep 2 +send "root\r"; sleep 6 + +# one &&-chained command: fetch -> setup -> verify dump -> sync -> poweroff. +# setup.sh prints SETUP_OK; the VBEGIN..VEND block is the on-disk verification. +set timeout 600 +send "ftp -o /tmp/setup.sh $env(NBHTTP) && sh /tmp/setup.sh && echo VBEGIN && id paul && grep '^paul:' /etc/master.passwd && grep -E '^(hostname|ifconfig_mue0|defaultroute|dhcpcd|sshd)=' /etc/rc.conf && cat /home/paul/.ssh/authorized_keys && echo VEND && sync && sync && halt -p\r" +expect { eof {} timeout { puts "\n!!TIMEOUT no poweroff" } } +puts "\n==SESSION ENDED==" +exit 0 diff --git a/f3s/pi-netbsd/bake/setup.sh b/f3s/pi-netbsd/bake/setup.sh new file mode 100755 index 0000000..dc63064 --- /dev/null +++ b/f3s/pi-netbsd/bake/setup.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# NetBSD headless image customization. Fetched over qemu user-net and run as +# root INSIDE the NetBSD-in-qemu VM by config.exp during a bake. +# +# `bake-golden.sh` substitutes __SSHKEY__ and __PW__ before serving this file. +# EDIT the two per-host values below for the target Pi (see README "Doing pi1"). +set -eu + +HOSTNAME="pi0.lan.buetow.org" # <-- EDIT for the target Pi (e.g. pi1.lan.buetow.org) +IPADDR="192.168.1.125" # <-- EDIT for the target Pi (e.g. 192.168.1.126) +GW="192.168.1.1" +DNS="192.168.1.1" + +RC=/etc/rc.conf + +# 1) rc.conf: static networking + hostname + sshd; strip conflicting keys first. +tmp=$(mktemp) +grep -vE '^(hostname|ifconfig_mue0|defaultroute|dhcpcd|sshd)=' "$RC" > "$tmp" +mv "$tmp" "$RC" +cat >> "$RC" <<EOF +hostname="$HOSTNAME" +sshd=YES +dhcpcd=NO +ifconfig_mue0="inet $IPADDR netmask 0xffffff00" +defaultroute="$GW" +EOF + +# 2) resolv.conf +echo "nameserver $DNS" > /etc/resolv.conf + +# 3) rc.local fallback: if the LAN78xx iface is not named mue0, put the static IP +# on the first real ethernet interface so the box stays reachable. +cat > /etc/rc.local <<EOF +#!/bin/sh +if ! ifconfig mue0 >/dev/null 2>&1; then + iface=\$(ifconfig -l | tr ' ' '\n' | grep -E '^(mue|ure|axe|cdce|vioif)[0-9]' | head -1) + if [ -n "\$iface" ]; then + ifconfig "\$iface" inet $IPADDR netmask 0xffffff00 up + route add default $GW + fi +fi +EOF +chmod 0755 /etc/rc.local + +# 4) user paul (+wheel) + authorized key +if ! id paul >/dev/null 2>&1; then useradd -m -G wheel -s /bin/sh paul; fi +mkdir -p /home/paul/.ssh +cat > /home/paul/.ssh/authorized_keys <<'EOF' +__SSHKEY__ +EOF +chmod 0700 /home/paul/.ssh +chmod 0600 /home/paul/.ssh/authorized_keys +chown -R paul:users /home/paul/.ssh + +# 5) passwords (native NetBSD argon2id hash), same for paul + root (backup login) +H=$(pwhash '__PW__') +usermod -p "$H" paul +usermod -p "$H" root + +# 6) sshd: ensure key + password auth (appended lines win over commented defaults) +cat >> /etc/ssh/sshd_config <<'EOF' +PubkeyAuthentication yes +PasswordAuthentication yes +EOF + +rm -f /tmp/setup.sh +# Flush all writes to the image before qemu is stopped (soft-dep FFS). +sync; sync +echo SETUP_OK diff --git a/f3s/pi-netbsd/flash/95netbsdflash/flash.sh b/f3s/pi-netbsd/flash/95netbsdflash/flash.sh new file mode 100755 index 0000000..c15bb71 --- /dev/null +++ b/f3s/pi-netbsd/flash/95netbsdflash/flash.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# dracut pre-mount hook (SOURCED by dracut init — must not call exit/return at +# top level in a way that aborts init; we keep all action inside an `if`). +# +# Trigger: a file `netbsd-flash-mode` (contents "dryrun" or "real") on the FAT +# /boot partition. If absent, this hook does nothing and normal boot continues. +# +# SAFETY: the very first thing we do once triggered is DISARM — delete +# config.txt and the trigger from the FAT partition — so that if anything later +# fails, a power-cycle boots normally back into Rocky (config.txt gone => the Pi +# firmware loads no initramfs). + +FATDEV=/dev/mmcblk0p1 +ROOTDEV=/dev/mmcblk0p3 +DISK=/dev/mmcblk0 + +nf_log() { echo "netbsdflash: $*"; echo "netbsdflash: $*" > /dev/kmsg 2>/dev/null; } +nf_reboot() { sync; sleep 2; reboot -f 2>/dev/null; sleep 2; echo b > /proc/sysrq-trigger 2>/dev/null; sleep 15; } + +mkdir -p /nf_fat /nf_root /nf_ram 2>/dev/null +NF_MODE="" +if mount -t vfat "$FATDEV" /nf_fat 2>/dev/null; then + [ -f /nf_fat/netbsd-flash-mode ] && NF_MODE=$(tr -d ' \r\n' < /nf_fat/netbsd-flash-mode 2>/dev/null) +fi + +if [ -n "$NF_MODE" ]; then + nf_log "TRIGGERED mode=$NF_MODE" + # --- DISARM FIRST (self-heal on any later failure) --- + rm -f /nf_fat/config.txt /nf_fat/netbsd-flash-mode 2>/dev/null + sync + echo "FLASHER-RAN mode=$NF_MODE up=$(cat /proc/uptime 2>/dev/null)" > /nf_fat/flash-evidence.txt 2>/dev/null + sync + nf_log "disarmed; evidence written" + + # --- stage golden image from ext4 root into RAM --- + NF_OK=1 + if ! mount -t ext4 -o ro "$ROOTDEV" /nf_root 2>/dev/null; then + nf_log "FATAL mount root"; echo "FATAL mount-root" >> /nf_fat/flash-evidence.txt; NF_OK=0 + fi + GZ="" + for cand in /nf_root/home/paul/netbsd-*-golden.img.gz /nf_root/root/netbsd-*-golden.img.gz; do + [ -f "$cand" ] && { GZ="$cand"; break; } + done + if [ "$NF_OK" = 1 ] && [ -z "$GZ" ]; then + nf_log "FATAL image-missing"; echo "FATAL image-missing" >> /nf_fat/flash-evidence.txt; NF_OK=0 + fi + if [ "$NF_OK" = 1 ]; then + mount -t tmpfs -o size=550m tmpfs /nf_ram 2>/dev/null + if cp "$GZ" /nf_ram/g.gz 2>/dev/null; then + nf_log "staged $(wc -c < /nf_ram/g.gz 2>/dev/null) bytes in RAM" + else + nf_log "FATAL copy-to-RAM"; echo "FATAL copy-RAM" >> /nf_fat/flash-evidence.txt; NF_OK=0 + fi + fi + umount /nf_root 2>/dev/null + sync + + if [ "$NF_OK" = 1 ] && [ "$NF_MODE" = dryrun ]; then + bytes=$(gzip -dc /nf_ram/g.gz 2>/dev/null | wc -c); rc=$? + nf_log "DRYRUN bytes=$bytes rc=$rc" + echo "DRYRUN-RESULT bytes=$bytes rc=$rc" >> /nf_fat/flash-evidence.txt + sync; umount /nf_fat 2>/dev/null + nf_log "DRYRUN done; rebooting to Rocky" + nf_reboot + elif [ "$NF_OK" = 1 ]; then + echo "REAL-START up=$(cat /proc/uptime 2>/dev/null)" >> /nf_fat/flash-evidence.txt + sync; umount /nf_fat 2>/dev/null + nf_log "REAL: writing image onto $DISK" + gzip -dc /nf_ram/g.gz 2>/dev/null | dd of="$DISK" bs=4M conv=fsync 2>&1 | tail -2 | while read -r l; do nf_log "dd: $l"; done + sync + nf_log "REAL flash complete; rebooting into NetBSD" + nf_reboot + else + # a FATAL occurred; config.txt already removed, so reboot -> Rocky + sync; umount /nf_fat 2>/dev/null + nf_log "aborted; rebooting to Rocky" + nf_reboot + fi +else + # not triggered: clean up and let the normal boot proceed + umount /nf_fat 2>/dev/null +fi diff --git a/f3s/pi-netbsd/flash/95netbsdflash/module-setup.sh b/f3s/pi-netbsd/flash/95netbsdflash/module-setup.sh new file mode 100755 index 0000000..4d82ad9 --- /dev/null +++ b/f3s/pi-netbsd/flash/95netbsdflash/module-setup.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# dracut module: 95netbsdflash (Pi-firmware initramfs variant) +# Loaded by the Pi firmware via config.txt `initramfs flasher.img followkernel`. +# A pre-mount hook (runs in the initramfs, before the real root is mounted) +# reads a trigger file from the FAT /boot partition and flashes NetBSD onto the +# SD from a RAM copy of the golden image staged on the ext4 root. + +check() { return 0; } +depends() { return 0; } + +installkernel() { + # vfat to read /boot (config.txt/trigger), ext4 to read the staged image, + # mmc drivers for the SD. + instmods vfat nls_cp437 nls_ascii ext4 mmc_block sdhci sdhci-iproc +} + +install() { + inst_multiple gzip gunzip dd sync sleep sh cat wc mount umount mkdir rm ls cp grep tr + inst_hook pre-mount 99 "$moddir/flash.sh" +} diff --git a/f3s/pi-netbsd/flash/build-flasher.sh b/f3s/pi-netbsd/flash/build-flasher.sh new file mode 100755 index 0000000..80f66fe --- /dev/null +++ b/f3s/pi-netbsd/flash/build-flasher.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Stage B, step 2 — run on the Pi (as root via sudo). Installs the 95netbsdflash +# dracut module and builds a standalone /boot/flasher.img that the Pi firmware +# loads via config.txt. Does NOT arm or reboot (see README Stage B steps 3-4). +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +KVER=$(uname -r) +MODDIR=/usr/lib/dracut/modules.d/95netbsdflash +OUT=/boot/flasher.img + +echo "== install dracut module 'netbsdflash' ==" +mkdir -p "$MODDIR" +install -m0755 "$HERE/95netbsdflash/module-setup.sh" "$MODDIR/module-setup.sh" +install -m0755 "$HERE/95netbsdflash/flash.sh" "$MODDIR/flash.sh" + +echo "== keep an untouched backup of the default initramfs (first time only) ==" +cp -n "/boot/initramfs-$KVER.img" "/boot/initramfs-$KVER.img.orig" 2>/dev/null || true + +echo "== build $OUT for kernel $KVER ==" +# ext4/vfat/mmc/nls are built-in on the RPi kernel, but --add-drivers is harmless +dracut --force --no-hostonly \ + --add netbsdflash \ + --add-drivers "vfat nls_cp437 nls_ascii ext4 mmc_block sdhci sdhci-iproc" \ + "$OUT" "$KVER" + +ls -la "$OUT" +echo "== sanity: hook present in initramfs ==" +lsinitrd "$OUT" | grep -E 'pre-mount/99-flash.sh' && echo "hook OK" || { echo "HOOK MISSING"; exit 1; } +echo "== done. Arm with a /boot/config.txt + /boot/netbsd-flash-mode (see README). ==" diff --git a/f3s/ychat/helm-chart/Chart.yaml b/f3s/ychat/helm-chart/Chart.yaml index d85b4aa..ff3d105 100644 --- a/f3s/ychat/helm-chart/Chart.yaml +++ b/f3s/ychat/helm-chart/Chart.yaml @@ -3,4 +3,4 @@ name: ychat description: yChat revival (SQLite-backed, persistent registered accounts) for f3s type: application version: 0.8.0 -appVersion: "1f28ff5"
\ No newline at end of file +appVersion: "deacb3f"
\ No newline at end of file diff --git a/f3s/ychat/helm-chart/templates/deployment.yaml b/f3s/ychat/helm-chart/templates/deployment.yaml index 3bfbfa2..b838adf 100644 --- a/f3s/ychat/helm-chart/templates/deployment.yaml +++ b/f3s/ychat/helm-chart/templates/deployment.yaml @@ -43,7 +43,7 @@ spec: - name: ychat # SQLite is now mandatory (see ychat/DOCKER.md); the image tag tracks # the ychat repo commit that build was cut from. - image: registry.lan.buetow.org:30001/ychat:1f28ff5 + image: registry.lan.buetow.org:30001/ychat:deacb3f imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false |
