diff --git a/emmc-provisioning/cloud-init/first-boot.md b/emmc-provisioning/cloud-init/first-boot.md index 176024a..6273192 100644 --- a/emmc-provisioning/cloud-init/first-boot.md +++ b/emmc-provisioning/cloud-init/first-boot.md @@ -17,7 +17,7 @@ This script runs once on first boot via cloud-init (see `user-data-remote-gnss.e 9. **reTerminal DM drivers** — Seeed repo clone and `reTerminal.sh`. 10. **Re-apply splash** — Set `disable_splash=0`, Plymouth theme to `custom` only, `update-initramfs`. 11. **Dark theme** — Set GTK dark theme for user `pi`: `~/.config/gtk-3.0/settings.ini` with `gtk-application-prefer-dark-theme=1` and `gtk-theme-name=PiXnoir` (Raspberry Pi OS dark theme). -12. **CM4 EEPROM enable** — On CM4, `rpi-eeprom-update` is disabled by default. First-boot enables it by: adding `RPI_EEPROM_USE_FLASHROM=1` and `CM4_ENABLE_RPI_EEPROM_UPDATE=1` to `/etc/default/rpi-eeprom-update`; adding a `[cm4]` block to `config.txt` with `dtparam=spi=on`, `dtoverlay=audremap`, `dtoverlay=spi-gpio40-45`. After reboot, `rpi-eeprom-update -l` works and boot order can be set. +12. **CM4 EEPROM enable** — On CM4, `rpi-eeprom-update` is disabled by default. First-boot enables it by adding `RPI_EEPROM_USE_FLASHROM=1` and `CM4_ENABLE_RPI_EEPROM_UPDATE=1` to `/etc/default/rpi-eeprom-update`. **No config.txt changes are needed** — `dtoverlay=audremap`/`dtoverlay=spi-gpio40-45` are for the flashrom method only and **must not be added** as they conflict with the reTerminal DM display backlight (GPIO13 PWM). The bootloader method (`pieeprom.upd`) is used instead. 13. **Boot order** — If `rpi-eeprom-config` is available, set `BOOT_ORDER=0x21` (network first, then eMMC/SD). On CM4 first boot this may be skipped (EEPROM not yet enabled); a one-shot systemd service runs after reboot to set boot order once. 14. **One-shots** — Download `set-rotation-once.sh` + `.desktop` from file server (wlr-randr for labwc). Wallpaper is set once via pcmanfm config during first-boot. 15. **Reboot.** @@ -70,11 +70,13 @@ Creates `/home/pi/.config/autostart` so that `.desktop` files placed there are s Downloads from `FILE_SERVER` (no local creation): - **`FILE_SERVER`** — Base URL for first-boot assets (default: `http://10.130.60.141:5000/files/first-boot`). All first-boot files are served from a **first-boot** subfolder on the file server. Change this if your server or path is different. -- **`start-chromium.sh`** — Downloaded to `/home/pi/start-chromium.sh`, made executable (755), owned by `pi`. This script waits for the desktop, starts Chromium in kiosk mode (e.g. `--app=...`), and uses `wmctrl` to force fullscreen. +- **`start-chromium.sh`** — Downloaded to `/home/pi/start-chromium.sh`, made executable (755), owned by `pi`. Waits for the desktop, then starts Chromium in fullscreen. When the session is Wayland (rpd-labwc), Chromium runs with `--ozone-platform=wayland` so **touch long-press produces right-click (context menu)** like the rest of the desktop; on X11 it falls back to `--ozone-platform=x11` and uses `wmctrl` to force fullscreen. - **`chromium-kiosk.desktop`** — Downloaded to `/home/pi/.config/autostart/chromium-kiosk.desktop`, mode 644, owned by `pi`. This autostart entry runs `start-chromium.sh` when `pi` logs in. Ensure the `.desktop` file on the server has `Exec=/home/pi/start-chromium.sh` (or the path you use on the device). +**Touch in Chromium:** Long-press on the touchscreen to open the context menu (right-click). This works when Chromium runs as a Wayland client (default under rpd-labwc). If you ever run under pure X11, long-press may not trigger the context menu; in that case you can use **evdev-right-click-emulation** (see e.g. [evdev-right-click-emulation](https://github.com/PeterCxy/evdev-right-click-emulation)) to inject right-click on long-press at the input layer. + --- ## Boot splash and wallpaper (single image from file server) @@ -134,7 +136,7 @@ First-boot sets a dark GTK theme for user **pi** via **`~/.config/gtk-3.0/settin ## CM4: enable rpi-eeprom-update (for boot order) -On **CM4**, first-boot enables `rpi-eeprom-update` by: (1) **`/etc/default/rpi-eeprom-update`**: **`RPI_EEPROM_USE_FLASHROM=1`**, **`CM4_ENABLE_RPI_EEPROM_UPDATE=1`**; (2) **config.txt** **`[cm4]`** block: **`dtparam=spi=on`**, **`dtoverlay=audremap`**, **`dtoverlay=spi-gpio40-45`**. After reboot, **`rpi-eeprom-update -l`** works. See: [usbboot](https://github.com/raspberrypi/usbboot/blob/master/Readme.md). +On **CM4**, first-boot enables `rpi-eeprom-update` by setting **`RPI_EEPROM_USE_FLASHROM=1`** and **`CM4_ENABLE_RPI_EEPROM_UPDATE=1`** in **`/etc/default/rpi-eeprom-update`**. **No dtparams are added to config.txt.** `dtoverlay=audremap` and `dtoverlay=spi-gpio40-45` are only needed for the *flashrom* (direct SPI) update method — they **must not** be added because `audremap` remaps audio to GPIO12/13, which conflicts with the reTerminal DM display backlight PWM on GPIO13, causing a blank screen. The bootloader file method (`pieeprom.upd`) works without these overlays. See: [usbboot](https://github.com/raspberrypi/usbboot/blob/master/Readme.md). ## Boot order (network first, then eMMC/SD) diff --git a/emmc-provisioning/cloud-init/start-chromium.sh b/emmc-provisioning/cloud-init/start-chromium.sh index f455d5c..ef7d1c3 100755 --- a/emmc-provisioning/cloud-init/start-chromium.sh +++ b/emmc-provisioning/cloud-init/start-chromium.sh @@ -1,43 +1,51 @@ #!/bin/bash # Disable keyring prompts export GNOME_KEYRING_CONTROL="" -export DISPLAY=:0 -# Force X11 instead of Wayland for better fullscreen support -export GDK_BACKEND=x11 -unset WAYLAND_DISPLAY +# Prefer Wayland when available so touch long-press produces right-click (context menu) +# like the rest of the desktop. X11/XWayland does not get that behavior for Chromium. +USE_WAYLAND=0 +if [ -n "$WAYLAND_DISPLAY" ] && [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" ]; then + USE_WAYLAND=1 +fi -# Wait for display and desktop environment to be ready -# Check if DISPLAY is accessible (wait up to 30 seconds) -for i in {1..60}; do - if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then - # Wait for desktop environment to be fully loaded - if pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1 || pgrep -x xfdesktop >/dev/null 2>&1; then - break - fi +if [ "$USE_WAYLAND" -eq 1 ]; then + # Native Wayland: fullscreen + touch-friendly (long-press = right-click) + export GDK_BACKEND=wayland + # Wait for compositor + for i in {1..60}; do + if [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" ]; then + pgrep -x labwc >/dev/null 2>&1 && break fi sleep 0.5 -done - -# Additional delay to ensure window manager is fully ready -sleep 5 - -# Start Chromium with flags to avoid keyring and ensure proper fullscreen -# Force X11 platform and add fullscreen-related flags -# Fullscreen mode (current active) -/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 & - -# Wait for Chromium window to appear and then force fullscreen -sleep 3 -# Try to find Chromium window and force it to fullscreen -for i in {1..10}; do + done + sleep 3 + /usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 & +else + # Fallback: X11 (e.g. no Wayland session) + export DISPLAY=:0 + export GDK_BACKEND=x11 + unset WAYLAND_DISPLAY + for i in {1..60}; do + if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then + if pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1 || pgrep -x xfdesktop >/dev/null 2>&1; then + break + fi + fi + sleep 0.5 + done + sleep 5 + /usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 & + sleep 3 + for i in {1..10}; do WINDOW_ID=$(wmctrl -l 2>/dev/null | grep -i chromium | head -1 | awk '{print $1}') if [ -n "$WINDOW_ID" ]; then - wmctrl -i -r "$WINDOW_ID" -b add,fullscreen 2>/dev/null - break + wmctrl -i -r "$WINDOW_ID" -b add,fullscreen 2>/dev/null + break fi sleep 0.5 -done + done +fi diff --git a/emmc-provisioning/docs/NETWORK-BOOT-DEPLOYMENT-FLOW.md b/emmc-provisioning/docs/NETWORK-BOOT-DEPLOYMENT-FLOW.md new file mode 100644 index 0000000..7ec0a63 --- /dev/null +++ b/emmc-provisioning/docs/NETWORK-BOOT-DEPLOYMENT-FLOW.md @@ -0,0 +1,97 @@ +# How network boot deployment works + +This describes the full flow from power-on to eMMC deploy/backup when using **network boot** with the provisioning LXC. + +--- + +## Overview + +1. **reTerminal** is set to try **network boot first** (EEPROM `BOOT_ORDER=0x21`). +2. It is connected to the **same LAN as the LXC’s eth1** (e.g. 10.20.50.0/24). +3. On power-on it gets an IP via **DHCP** and loads **boot files via TFTP** from the LXC. +4. The **netboot environment** (kernel + rootfs) runs **provisioning-client.sh**, which registers with the **dashboard** and polls for an action. +5. In the **dashboard** you see the device under “Device detected (Network)” and choose **Deploy** or **Backup**. +6. The device performs the action (download image → write eMMC, or read eMMC → upload), then you can reboot to run from eMMC. + +--- + +## Step-by-step + +### 1. LXC (provisioning server) + +- **eth0** = WAN (e.g. 10.130.60.141), internet for the LXC. +- **eth1** = LAN (e.g. 10.20.50.1/24): + - **dnsmasq**: DHCP on eth1 (e.g. 10.20.50.100–200) and **TFTP** with next-server = 10.20.50.1, boot file = `start4cd.elf`. + - **TFTP root** `/srv/tftpboot`: Raspberry Pi 4/CM4 boot files (from GitHub: start4cd.elf, fixup4cd.dat, kernel8.img, etc.). + - **NAT**: traffic from 10.20.50.0/24 is masqueraded out eth0 so netbooted devices have internet if needed. + +The **dashboard** (Flask) runs in the LXC and is reachable at e.g. `http://10.20.50.1:5000` from the LAN. The **golden image** for Deploy lives at `/var/lib/cm4-provisioning/golden.img` (same LXC or bind-mounted from host). + +### 2. reTerminal (device) + +- **EEPROM**: `BOOT_ORDER=0x21` (network first, then SD/eMMC). Can be set by cloud-init first-boot on an already-flashed device. +- **Network**: Ethernet connected to the same segment as the LXC’s **eth1** (e.g. same switch/VLAN as 10.20.50.0/24). +- On **power-on**: + 1. Pi 4/CM4 firmware does **DHCP** on the wired interface. + 2. DHCP reply gives: IP (e.g. 10.20.50.100), **next-server (TFTP)** = 10.20.50.1, **boot filename** = start4cd.elf. + 3. Device **TFTP**s boot files from the LXC (start4cd.elf, fixup4cd.dat, kernel, DTB, etc.). + 4. It boots the **kernel** (and optionally an initramfs or NFS root). That environment must have **network**, **curl**, and **provisioning-client.sh**. + +### 3. Netboot root / environment + +The **TFTP**-loaded kernel (and optional initramfs/NFS root) must end up in an environment where: + +- The device has an IP on the same LAN as the LXC (already from DHCP). +- **provisioning-client.sh** is present and run (e.g. from init, a login script, or a systemd service). +- **PROVISIONING_SERVER** is set to the dashboard URL on the LXC’s LAN IP, e.g. + `PROVISIONING_SERVER=http://10.20.50.1:5000` + +So the “netboot environment” is either: + +- A **custom initramfs** (recommended): build with **network-boot-initramfs/build.sh**, copy **initrd.img** to the TFTP root, and add `initramfs initrd.img followkernel` to **config.txt**. The initramfs brings up the network and runs the provisioning client. See **network-boot-initramfs/README.md**. +- A **minimal rootfs** (e.g. NFS) that runs the client script at boot, or +- Any other setup that gets the client running with network and the right `PROVISIONING_SERVER`. + +### 4. Provisioning client (on the device) + +- **provisioning-client.sh**: + 1. **Registers**: `POST /api/register-device` with MAC and IP. + 2. **Polls**: `GET /api/device-action-poll?mac=...` every few seconds. + 3. When the dashboard returns **action = deploy** (with **url**): + downloads the image from **url** and runs `dd of=/dev/mmcblk0`. + 4. When the dashboard returns **action = backup** (with **upload_url**): + runs `dd if=/dev/mmcblk0` and POSTs the stream to **upload_url**. + 5. Then exits (and you can reboot to eMMC after deploy). + +### 5. Dashboard (your actions) + +- You open the dashboard at `http://10.20.50.1:5000` (or the LXC’s WAN IP if you’re not on the provisioning LAN). +- Under **“Device detected (Network)”** you see the device (identified by MAC). +- You click **Deploy** or **Backup**. +- The dashboard sets the **action** (and URL/upload_url) for that MAC; the next **device-action-poll** returns it, and the client runs the corresponding dd + curl. + +--- + +## Data flow summary + +| Stage | Where | What happens | +|-------------|--------------|--------------| +| Boot | reTerminal | DHCP (get IP + next-server + boot file), then TFTP (load start4cd.elf, kernel, etc.). | +| Boot | reTerminal | Kernel (and netboot root) start; run **provisioning-client.sh** with `PROVISIONING_SERVER=http://10.20.50.1:5000`. | +| Register | Device → LXC | POST /api/register-device (MAC, IP). | +| Poll | Device → LXC | GET /api/device-action-poll?mac=... every 5 s. | +| Your choice | You → LXC | In dashboard: click Deploy or Backup for that device. | +| Deploy | LXC → device | Client GETs image URL, streams to `dd of=/dev/mmcblk0`. | +| Backup | Device → LXC | Client `dd if=/dev/mmcblk0` and POSTs to upload_url. | +| After | reTerminal | Reboot; if you deployed, it can now boot from eMMC. | + +--- + +## What you need in place + +- **LXC**: eth1 = 10.20.50.1/24, dnsmasq (DHCP + TFTP on eth1), `/srv/tftpboot` with RPi 4 boot files, NAT for 10.20.50.0/24 via eth0. Dashboard running, `golden.img` present for Deploy. + See **NETWORK-BOOT-LXC.md** and **setup-network-boot-on-lxc.sh**. +- **reTerminal**: EEPROM boot order = network first; Ethernet on 10.20.50.0/24; netboot environment that runs **provisioning-client.sh** with `PROVISIONING_SERVER=http://10.20.50.1:5000`. +- **Netboot root**: Must provide network, curl, and the client script (NFS, initramfs, or custom root). + +The **TFTP** setup only gets the Pi to boot a kernel (and optional root). The **provisioning** (Deploy/Backup) is done by that kernel’s environment running the **network-client** against the dashboard on the LXC. diff --git a/emmc-provisioning/network-boot-initramfs/README.md b/emmc-provisioning/network-boot-initramfs/README.md new file mode 100644 index 0000000..b233143 --- /dev/null +++ b/emmc-provisioning/network-boot-initramfs/README.md @@ -0,0 +1,77 @@ +# Provisioning initramfs for network boot + +Minimal initramfs that runs **provisioning-client.sh** after bringing up the network. Used with Raspberry Pi 4 / CM4 (reTerminal) when booting via TFTP from the provisioning LXC. + +## What it does + +1. Mounts `/proc`, `/sys`, `/dev`, `/dev/pts`. +2. Ensures an IP (reuses kernel DHCP or runs `udhcpc` on eth0). +3. Runs the provisioning client with `PROVISIONING_SERVER` (default `http://10.20.50.1:5000`, overridable via kernel cmdline). +4. The client registers with the dashboard and polls for **Deploy** or **Backup**; on action it performs the dd + curl and exits. + +## Build + +**On x86_64 (e.g. your laptop):** the script uses **Podman** or **Docker** with `--platform linux/arm64` to run an arm64 container and copy busybox + curl into the initramfs. Your host must be able to *run* arm64 containers (via QEMU emulation). + +- **Fedora:** one-time setup to enable arm64 containers: + ```bash + sudo dnf install -y qemu-user-static + ``` + Then run the build (Podman will use QEMU automatically): + ```bash + cd emmc-provisioning/network-boot-initramfs + ./build.sh + ``` +- If you don’t install `qemu-user-static`, the script will fail with an error and print the same instructions and an alternative (build on a Pi). + +**On a Raspberry Pi 4 or other aarch64 host:** no Docker. Install deps and run: + +```bash +sudo apt install -y busybox curl +./build.sh +``` + +Optional: pass an output path: + +```bash +./build.sh /path/to/initrd.img +``` + +## Deploy to TFTP root + +1. Copy **initrd.img** to the LXC TFTP root (e.g. `/srv/tftpboot`): + + ```bash + scp initrd.img root@10.130.60.141:/srv/tftpboot/ + ``` + +2. In the TFTP root, ensure **config.txt** (Raspberry Pi boot config) includes the initramfs line. If you use the stock `config.txt` from the RPi firmware repo, add: + + ``` + initramfs initrd.img followkernel + ``` + + So the firmware loads the kernel and then the initrd that “follows” it. The Pi will boot the kernel and run `/init` from the initrd. + +3. If your DHCP already points the Pi to this TFTP server and `start4cd.elf`, the Pi will load kernel + initrd from the same root. No NFS or extra server needed. + +## Kernel cmdline (optional) + +To override the provisioning server URL (e.g. if the dashboard is on another IP), add to **cmdline.txt** in the TFTP root (or append to the kernel command line): + +``` +provisioning_server=http://10.20.50.1:5000 +``` + +The init script reads `provisioning_server=` from `/proc/cmdline` and exports `PROVISIONING_SERVER` for the client. + +## Flow summary + +1. Pi does DHCP → gets IP and TFTP server (e.g. 10.20.50.1). +2. Pi loads via TFTP: start4cd.elf, fixup4cd.dat, config.txt, cmdline.txt, kernel8.img, **initrd.img**. +3. Kernel boots with initrd as root; runs `/init`. +4. Init mounts minimal fs, ensures network, runs `/provisioning-client.sh`. +5. Client registers and polls; you choose Deploy or Backup in the dashboard; client runs dd + curl and exits. +6. After deploy, power cycle the Pi so it boots from eMMC. + +See **docs/NETWORK-BOOT-DEPLOYMENT-FLOW.md** for the full deployment flow. diff --git a/emmc-provisioning/network-boot-initramfs/build.sh b/emmc-provisioning/network-boot-initramfs/build.sh new file mode 100755 index 0000000..69e914c --- /dev/null +++ b/emmc-provisioning/network-boot-initramfs/build.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Build provisioning initramfs for Raspberry Pi 4 / CM4 (aarch64). +# Produces initrd.img (gzip cpio) for TFTP boot (config.txt: initramfs initrd.img followkernel). +# +# On x86_64: tries Docker/Podman with --platform linux/arm64; if that fails (no +# arm64 emulation), downloads prebuilt static aarch64 busybox and curl. No sudo needed. +# On aarch64 (e.g. Raspberry Pi): uses local busybox and curl if installed. + +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT="${1:-$SCRIPT_DIR/initrd.img}" +BUILD_DIR=$(mktemp -d) +trap "rm -rf $BUILD_DIR" EXIT + +echo "Build dir: $BUILD_DIR" + +# Layout: /init, /provisioning-client.sh, /bin/busybox, /bin/sh, /usr/bin/curl, /lib/*.so +mkdir -p "$BUILD_DIR"/{bin,usr/bin,proc,sys,dev,dev/pts,lib} +cp "$SCRIPT_DIR/init" "$BUILD_DIR/init" +cp "$SCRIPT_DIR/provisioning-client.sh" "$BUILD_DIR/provisioning-client.sh" +chmod +x "$BUILD_DIR/init" "$BUILD_DIR/provisioning-client.sh" + +ARCH=$(uname -m 2>/dev/null) +if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ] || [ "$ARCH" = "armv8l" ]; then + if ! command -v busybox >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then + echo "On aarch64: install busybox and curl (apt install busybox curl) then re-run." + exit 1 + fi + echo "Copying busybox and curl from host (aarch64)..." + cp "$(command -v busybox)" "$BUILD_DIR/bin/busybox" + cp "$(command -v curl)" "$BUILD_DIR/usr/bin/curl" + chmod +x "$BUILD_DIR/bin/busybox" "$BUILD_DIR/usr/bin/curl" + for f in $(ldd "$(command -v curl)" 2>/dev/null | awk '/=>/{print $3}'); do + [ -f "$f" ] && cp "$f" "$BUILD_DIR/lib/" + done + cp /lib/ld-linux-aarch64.so.1 "$BUILD_DIR/lib/" 2>/dev/null || true +else + # x86_64: try container first; on "Exec format error" (no arm64 emulation) use static downloads + CONTAINER_RUNTIME="" + for cmd in docker podman; do + if command -v "$cmd" >/dev/null 2>&1; then + CONTAINER_RUNTIME="$cmd" + break + fi + done + + CONTAINER_OK=0 + if [ -n "$CONTAINER_RUNTIME" ]; then + echo "Trying $CONTAINER_RUNTIME (linux/arm64)..." + CNT_NAME="cm4-initramfs-build-$$" + # Use a container-internal dir (no bind mount); copy out with podman cp (works with rootless) + $CONTAINER_RUNTIME run --name "$CNT_NAME" --platform linux/arm64 debian:bookworm-slim bash -c ' + apt-get update -qq && apt-get install -y -qq busybox curl + mkdir -p /out/bin /out/usr/bin /out/lib + cp /bin/busybox /out/bin/busybox + cp /usr/bin/curl /out/usr/bin/curl + chmod +x /out/bin/busybox /out/usr/bin/curl + ldd /usr/bin/curl 2>/dev/null | awk "/=>/{print \$3}" | while read f; do [ -n "$f" ] && [ -f "$f" ] && cp "$f" /out/lib/; done + cp /lib/ld-linux-aarch64.so.1 /out/lib/ 2>/dev/null || true + ' 2>/dev/null || true + # Always copy from container (avoids rootless bind-mount issues) + $CONTAINER_RUNTIME cp "$CNT_NAME:/out/bin/busybox" "$BUILD_DIR/bin/" 2>/dev/null && \ + $CONTAINER_RUNTIME cp "$CNT_NAME:/out/usr/bin/curl" "$BUILD_DIR/usr/bin/" 2>/dev/null && \ + $CONTAINER_RUNTIME cp "$CNT_NAME:/out/lib/." "$BUILD_DIR/lib/" 2>/dev/null || true + $CONTAINER_RUNTIME rm -f "$CNT_NAME" 2>/dev/null + if [ -f "$BUILD_DIR/bin/busybox" ] && [ -f "$BUILD_DIR/usr/bin/curl" ]; then + CONTAINER_OK=1 + echo "Container build succeeded." + fi + fi + + if [ "$CONTAINER_OK" -ne 1 ]; then + echo "Using prebuilt static aarch64 binaries (no container/emulation needed)..." + DOWNLOAD_DIR=$(mktemp -d) + trap "rm -rf $BUILD_DIR $DOWNLOAD_DIR" EXIT + if command -v curl >/dev/null 2>&1; then + GET="curl -sL" + GET_O="curl -sL -o" + else + GET="wget -q -O -" + GET_O="wget -q -O" + fi + # Static busybox aarch64: try busybox.net, then Alpine busybox-static package + BB_OK=0 + $GET_O "$DOWNLOAD_DIR/busybox" "https://busybox.net/downloads/binaries/1.35.0-defconfig-multiarch-musl/busybox-armv8l" 2>/dev/null || true + if [ -f "$DOWNLOAD_DIR/busybox" ] && [ -s "$DOWNLOAD_DIR/busybox" ]; then + BB_OK=1 + fi + if [ "$BB_OK" -ne 1 ]; then + echo "Trying Alpine busybox-static aarch64..." + $GET_O "$DOWNLOAD_DIR/bb.apk" "https://dl-cdn.alpinelinux.org/alpine/v3.19/main/aarch64/busybox-static-1.36.1-r11.apk" 2>/dev/null || true + if [ -f "$DOWNLOAD_DIR/bb.apk" ] && [ -s "$DOWNLOAD_DIR/bb.apk" ]; then + case "$(head -c 4 "$DOWNLOAD_DIR/bb.apk" | od -An -tx1 2>/dev/null | tr -d ' ')" in + 3c21444f|3c68746d) ;; # HTML response, skip extract + *) + (cd "$DOWNLOAD_DIR" && (tar xf bb.apk 2>/dev/null || tar xzf bb.apk 2>/dev/null) && [ -f data.tar.gz ] && tar xzf data.tar.gz 2>/dev/null) + ;; + esac + fi + if [ -f "$DOWNLOAD_DIR/bin/busybox" ] && [ -s "$DOWNLOAD_DIR/bin/busybox" ]; then + cp "$DOWNLOAD_DIR/bin/busybox" "$DOWNLOAD_DIR/busybox" + BB_OK=1 + fi + fi + if [ "$BB_OK" -ne 1 ]; then + echo "Failed to download busybox (x86 host cannot run arm64 container without emulation)." + echo "" + echo "Option A - Enable arm64 containers (one-time, needs sudo):" + echo " Fedora: sudo dnf install -y qemu-user-static" + echo " Then re-run this script (Podman will use QEMU to run the arm64 build)." + echo "" + echo "Option B - Build on a Raspberry Pi 4 (aarch64):" + echo " scp -r $(dirname "$SCRIPT_DIR") pi@:~/ && ssh pi@ 'cd ~/emmc-provisioning/network-boot-initramfs && sudo apt install -y busybox curl && ./build.sh'" + echo " Then scp pi@:~/emmc-provisioning/network-boot-initramfs/initrd.img ." + exit 1 + fi + chmod +x "$DOWNLOAD_DIR/busybox" + cp "$DOWNLOAD_DIR/busybox" "$BUILD_DIR/bin/busybox" + # Static curl aarch64 glibc (Raspberry Pi OS uses glibc) + $GET "https://github.com/stunnel/static-curl/releases/download/8.18.0/curl-linux-aarch64-glibc-8.18.0.tar.xz" -o "$DOWNLOAD_DIR/curl.tar.xz" || true + if [ ! -f "$DOWNLOAD_DIR/curl.tar.xz" ] || [ ! -s "$DOWNLOAD_DIR/curl.tar.xz" ]; then + echo "Failed to download static curl." + exit 1 + fi + (cd "$DOWNLOAD_DIR" && tar xf curl.tar.xz) + CURL_BIN=$(find "$DOWNLOAD_DIR" -maxdepth 3 -name "curl" -type f 2>/dev/null | head -1) + if [ -n "$CURL_BIN" ] && [ -x "$CURL_BIN" ]; then + cp "$CURL_BIN" "$BUILD_DIR/usr/bin/curl" + chmod +x "$BUILD_DIR/usr/bin/curl" + else + echo "Could not find curl binary in tarball." + exit 1 + fi + rm -rf "$DOWNLOAD_DIR" + trap "rm -rf $BUILD_DIR" EXIT + fi +fi + +# Verify we have busybox (container or fallback must have left it) +if [ ! -f "$BUILD_DIR/bin/busybox" ] || [ ! -s "$BUILD_DIR/bin/busybox" ]; then + echo "Error: busybox not found in $BUILD_DIR/bin. If the container ran, check Podman volume mount." + exit 1 +fi +chmod +x "$BUILD_DIR/bin/busybox" 2>/dev/null || true + +# Busybox applets we need (sh, mount, udhcpc, etc.) +cd "$BUILD_DIR/bin" +./busybox --list 2>/dev/null | while read applet; do + case "$applet" in + sh|ash|mount|umount|mkdir|cat|ip|udhcpc|sleep|echo|grep|cut|awk|hostname|dd) ln -sf busybox "$applet"; ;; + esac +done +[ -e sh ] || ln -sf busybox sh + +# Build cpio (gzip) +echo "Building cpio..." +( cd "$BUILD_DIR"; find . -print0 | cpio -o -H newc -0 2>/dev/null ) | gzip -9 > "$OUTPUT" +echo "Written: $OUTPUT ($(stat -c%s "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null) bytes)" +echo "" +echo "Next: copy initrd.img to TFTP root (e.g. /srv/tftpboot on LXC) and in config.txt add:" +echo " initramfs initrd.img followkernel" +echo "Then ensure the kernel line loads the initrd (followkernel does that)." +echo "Default provisioning server: http://10.20.50.1:5000 (override with kernel cmdline: provisioning_server=http://...)" \ No newline at end of file diff --git a/emmc-provisioning/network-boot-initramfs/config.txt.snippet b/emmc-provisioning/network-boot-initramfs/config.txt.snippet new file mode 100644 index 0000000..ac6a03d --- /dev/null +++ b/emmc-provisioning/network-boot-initramfs/config.txt.snippet @@ -0,0 +1,9 @@ +# Add this line to config.txt in the TFTP root (/srv/tftpboot) so the Pi loads +# the provisioning initramfs after the kernel. +# +# initramfs initrd.img followkernel +# +# Ensure initrd.img is in the same directory (TFTP root). The kernel will use +# it as the initial root and run /init, which starts the provisioning client. + +initramfs initrd.img followkernel diff --git a/emmc-provisioning/network-boot-initramfs/init b/emmc-provisioning/network-boot-initramfs/init new file mode 100644 index 0000000..96bccc4 --- /dev/null +++ b/emmc-provisioning/network-boot-initramfs/init @@ -0,0 +1,35 @@ +#!/bin/sh +# Init for provisioning initramfs: bring up minimal env and run provisioning-client.sh. +# PROVISIONING_SERVER can be set via kernel cmdline: provisioning_server=http://10.20.50.1:5000 + +set -e +export PATH=/bin:/usr/bin +export LD_LIBRARY_PATH=/lib + +echo "=== CM4 provisioning initramfs ===" + +# Minimal filesystem +mount -t proc none /proc +mount -t sysfs none /sys +mount -t devtmpfs none /dev +mkdir -p /dev/pts +mount -t devpts none /dev/pts + +# Kernel might have brought up eth0 via ip=dhcp; ensure we have an IP +if ! ip addr show | grep -q 'inet .* scope global'; then + echo "Getting DHCP lease..." + udhcpc -f -q -i eth0 -n 2>/dev/null || true +fi + +# Allow kernel cmdline to override: provisioning_server=http://10.20.50.1:5000 +for arg in $(cat /proc/cmdline); do + case "$arg" in + provisioning_server=*) export PROVISIONING_SERVER="${arg#*=}"; ;; + esac +done +PROVISIONING_SERVER="${PROVISIONING_SERVER:-http://10.20.50.1:5000}" +export PROVISIONING_SERVER + +echo "Provisioning server: $PROVISIONING_SERVER" +echo "Running provisioning client..." +exec /bin/sh /provisioning-client.sh diff --git a/emmc-provisioning/network-boot-initramfs/provisioning-client.sh b/emmc-provisioning/network-boot-initramfs/provisioning-client.sh new file mode 100644 index 0000000..d50e793 --- /dev/null +++ b/emmc-provisioning/network-boot-initramfs/provisioning-client.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# POSIX sh version for initramfs. Registers with dashboard and runs Deploy or Backup. +# PROVISIONING_SERVER must be set (e.g. http://10.20.50.1:5000). + +BASE_URL="${PROVISIONING_SERVER:-http://10.20.50.1:5000}" +BASE_URL="${BASE_URL%/}" +EMMC_DEV="${EMMC_DEV:-/dev/mmcblk0}" + +get_mac() { + cat /sys/class/net/eth0/address 2>/dev/null || \ + cat /sys/class/net/enp0s1f0/address 2>/dev/null || \ + echo "unknown" +} + +get_ip() { + hostname -I 2>/dev/null | awk '{print $1}' || echo "" +} + +MAC=$(get_mac) +IP=$(get_ip) + +echo "Registering with $BASE_URL (MAC=$MAC IP=$IP)..." +curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"mac\":\"$MAC\",\"ip\":\"$IP\"}" "$BASE_URL/api/register-device" || true + +echo "Polling for action (Backup or Deploy)..." +while true; do + resp=$(curl -s "$BASE_URL/api/device-action-poll?mac=$MAC" 2>/dev/null || echo '{"action":"wait"}') + action=$(echo "$resp" | grep -o '"action":"[^"]*"' | cut -d'"' -f4) + url=$(echo "$resp" | grep -o '"url":"[^"]*"' | cut -d'"' -f4) + upload_url=$(echo "$resp" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4) + + if [ "$action" = "deploy" ] && [ -n "$url" ]; then + echo "Deploy: downloading image and writing to $EMMC_DEV..." + if [ ! -b "$EMMC_DEV" ]; then + echo "Error: $EMMC_DEV not found" + sleep 10 + continue + fi + curl -sL "$url" | dd of="$EMMC_DEV" bs=4M status=progress conv=fsync + echo "Deploy done. Reboot to run from eMMC." + exit 0 + fi + + if [ "$action" = "backup" ] && [ -n "$upload_url" ]; then + echo "Backup: reading $EMMC_DEV and uploading..." + if [ ! -b "$EMMC_DEV" ]; then + echo "Error: $EMMC_DEV not found" + sleep 10 + continue + fi + dd if="$EMMC_DEV" bs=4M status=progress 2>/dev/null | curl -s -X POST -T - "$upload_url" + echo "Backup done." + exit 0 + fi + + sleep 5 +done