255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# eMMC provisioning for reTerminal DM4 (CM4)
|
||
|
||
This guide covers:
|
||
|
||
1. **USB boot mode**: When the reTerminal is in boot mode (eMMC disable jumper) and connected via USB, the host runs **rpiboot** to expose the eMMC, then the **dashboard** shows "Device connected (USB)". You choose **Backup** or **Deploy** in the portal — there is no auto-flash; the action runs only after your choice.
|
||
2. **Network boot**: If the device boots over the network and runs the **provisioning client** (see `network-client/`), it registers with the dashboard and appears as "Device (Network)"; you then choose Backup or Deploy. Deploy streams the golden image to the device; Backup uploads the device eMMC to the server.
|
||
3. **Cloud-init**: The golden image can include cloud-init so each device configures itself on first boot (hostname, network, packages, kiosk setup).
|
||
|
||
---
|
||
|
||
## Part 1: USB boot mode — detect device, choose Backup or Deploy in portal
|
||
|
||
### How it works
|
||
|
||
- reTerminal has an **eMMC disable** jumper (see reTerminal docs; often “J2” or “nRPIBOOT”). When the jumper is fitted, the CM4 boots in **USB device mode** and waits for `rpiboot` from the host.
|
||
- You connect the reTerminal’s **USB slave** port to a **provisioning PC** (Linux).
|
||
- **udev** detects the Raspberry Pi Foundation USB device (vendor `2b8e`) and runs a trigger script.
|
||
- The trigger starts the **provisioning script** that:
|
||
1. Runs **rpiboot** (from the `usbboot` project). The CM4 then exposes its eMMC as a USB mass-storage device.
|
||
2. Finds the new block device (eMMC) and writes status so the **dashboard** shows "Device connected (USB boot mode). Choose Backup or Deploy in the dashboard."
|
||
3. **Waits for your choice in the portal** — no automatic flash. When you click **Backup** or **Deploy** in the dashboard, the script runs that action (dd backup or dd deploy).
|
||
- You remove the jumper and power cycle; the reTerminal boots from eMMC and can run **cloud-init** on first boot.
|
||
|
||
### Provisioning host setup (Linux)
|
||
|
||
#### 1. Build and install usbboot (rpiboot)
|
||
|
||
```bash
|
||
sudo apt-get install -y libusb-1.0-0-dev
|
||
git clone --depth=1 https://github.com/raspberrypi/usbboot
|
||
cd usbboot
|
||
make
|
||
sudo mkdir -p /opt/usbboot
|
||
sudo cp rpiboot /opt/usbboot/
|
||
```
|
||
|
||
#### 2. Create golden image and config directory
|
||
|
||
- Build your golden image (see Part 2) and place it where the script will find it, e.g.:
|
||
|
||
```bash
|
||
sudo mkdir -p /var/lib/cm4-provisioning
|
||
sudo cp /path/to/your/golden-reterminal.img /var/lib/cm4-provisioning/golden.img
|
||
```
|
||
|
||
- Or use a different path and set `GOLDEN_IMAGE` when installing the script (see below). On Proxmox with LXC, you can store backup images on a host directory by setting `CM4_BACKUPS_HOST_PATH` at deploy so that folder is bind-mounted into the LXC — see **PROXMOX-LXC-DEPLOYMENT.md** § Store backup images on a host directory.
|
||
|
||
#### 3. Install the provisioning script and trigger
|
||
|
||
```bash
|
||
# From this repo (chromium-setup/emmc-provisioning/host/)
|
||
cd chromium-setup/emmc-provisioning/host
|
||
|
||
sudo mkdir -p /opt/cm4-provisioning
|
||
sudo cp flash-emmc-on-connect.sh /opt/cm4-provisioning/
|
||
sudo chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
||
|
||
# Optional: override paths via environment (create env file)
|
||
echo 'GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img' | sudo tee /opt/cm4-provisioning/env
|
||
echo 'RPIBOOT_DIR=/opt/usbboot' | sudo tee -a /opt/cm4-provisioning/env
|
||
echo 'EMMC_SIZE_BYTES=8589934592' | sudo tee -a /opt/cm4-provisioning/env # 8GB; use 17179869184 for 16GB
|
||
# Optional: shrink backups after dd (requires PiShrink; see "Shrinking backup and golden images" below)
|
||
# echo 'SHRINK_BACKUP=1' | sudo tee -a /opt/cm4-provisioning/env
|
||
# echo 'PISHRINK_COMPRESS=gz' | sudo tee -a /opt/cm4-provisioning/env # or xz
|
||
|
||
sudo cp cm4-flash-trigger.sh /usr/local/bin/
|
||
sudo chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
||
```
|
||
|
||
If your golden image path or rpiboot path is different, set `GOLDEN_IMAGE`, `RPIBOOT_DIR`, and optionally `EMMC_SIZE_BYTES` in `/opt/cm4-provisioning/env` and source it from the script, or pass them into the systemd-run call in the trigger (e.g. by making the trigger source the env file and export variables before `systemd-run`).
|
||
|
||
#### 4. Install udev rule
|
||
|
||
```bash
|
||
# From emmc-provisioning/host/
|
||
sudo cp 90-cm4-boot-mode.rules /etc/udev/rules.d/
|
||
sudo udevadm control --reload-rules
|
||
sudo udevadm trigger
|
||
```
|
||
|
||
#### 5. Enable provisioning (safety)
|
||
|
||
Provisioning runs only if the “enabled” file exists:
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/cm4-provisioning
|
||
sudo touch /etc/cm4-provisioning/enabled
|
||
```
|
||
|
||
To disable provisioning (no device detection), remove that file: `sudo rm /etc/cm4-provisioning/enabled`.
|
||
|
||
#### 6. Optional: pass environment into the provisioning job
|
||
|
||
If you use `/opt/cm4-provisioning/env`, update the trigger so the flash script sees those variables. For example change `/usr/local/bin/cm4-flash-trigger.sh` to:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -a
|
||
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
||
set +a
|
||
export GOLDEN_IMAGE RPIBOOT_DIR EMMC_SIZE_BYTES
|
||
FLASH_SCRIPT="${CM4_FLASH_SCRIPT:-/opt/cm4-provisioning/flash-emmc-on-connect.sh}"
|
||
exec systemd-run --no-block --unit=cm4-flash-once --property=Environment="GOLDEN_IMAGE=$GOLDEN_IMAGE" ...
|
||
```
|
||
|
||
Or keep it simple and edit the defaults inside `flash-emmc-on-connect.sh` (e.g. `GOLDEN_IMAGE`, `RPIBOOT_DIR`, `EMMC_SIZE_BYTES`).
|
||
|
||
### Usage
|
||
|
||
1. Fit the **eMMC disable** jumper on the reTerminal.
|
||
2. Connect the reTerminal **USB slave** port to the provisioning PC.
|
||
3. Power the reTerminal (or apply power after USB).
|
||
4. On the host, `rpiboot` will run automatically; when it exits, the script will `dd` the golden image to the eMMC. Watch logs: `journalctl -u cm4-flash-once -f` or `journalctl -t cm4-flash -f`.
|
||
5. When done, remove the jumper and power cycle the reTerminal. It will boot from eMMC; cloud-init will run on first boot.
|
||
|
||
---
|
||
|
||
## Part 2: Golden image with cloud-init
|
||
|
||
Raspberry Pi OS (recent versions) supports **cloud-init** using the **NoCloud** datasource: it reads `user-data`, `meta-data`, and optionally `network-config` from the **boot** (FAT32) partition.
|
||
|
||
### Steps to prepare a cloud-init image for Raspberry Pi OS
|
||
|
||
1. **Obtain Raspberry Pi OS**
|
||
- Download from [raspberrypi.com/software](https://www.raspberrypi.com/software/) (desktop or Lite), or use **Raspberry Pi Imager** with "Edit settings" for locale/SSH. Recent Raspberry Pi OS has cloud-init built in.
|
||
|
||
2. **Flash the image**
|
||
- Flash to a spare SD card, or to a loop file for building without physical media: `cp raspios.img golden-work.img`
|
||
|
||
3. **Mount the boot partition**
|
||
- **From an image file**: `sudo losetup -fP golden-work.img` then e.g. `sudo mount /dev/loop0p1 /mnt/boot`
|
||
- **From SD**: mount the first (FAT32) partition at `/mnt/boot`. On a running Pi, boot is often `/boot/firmware`.
|
||
|
||
4. **Add NoCloud files on the boot partition** (root of FAT32, same level as `config.txt`):
|
||
- `user-data`, `meta-data`, `network-config`
|
||
- From this repo: `cp emmc-provisioning/cloud-init/{user-data,meta-data,network-config} /mnt/boot/`
|
||
|
||
5. **Customise** `user-data` and `network-config`. Use the **remote bootstrap script** pattern (below) to avoid rebuilding the image when you change first-boot commands.
|
||
|
||
6. **Unmount and create golden image**: `sudo umount /mnt/boot`, then copy the image to your `GOLDEN_IMAGE` path (e.g. `/var/lib/cm4-provisioning/golden.img`).
|
||
|
||
### Remote bootstrap script (no image rebuild for script changes)
|
||
|
||
You can keep the golden image **fixed** and have cloud-init **download a script from a file server** and run it on first boot. When you change the script on the server, the next device gets the new commands without rebuilding the image.
|
||
|
||
1. **Host the script** on an HTTP/HTTPS file server (e.g. nginx, or `python3 -m http.server`) at a URL the Pi can reach, e.g. `http://192.168.1.10/provisioning/bootstrap.sh`.
|
||
|
||
2. **In `user-data`, use `runcmd` to download and run it** (runcmd runs after packages and network are up):
|
||
|
||
```yaml
|
||
#cloud-config
|
||
package_update: true
|
||
package_upgrade: false
|
||
packages:
|
||
- curl # or wget
|
||
|
||
runcmd:
|
||
- curl -fsSL "http://YOUR_FILE_SERVER/provisioning/bootstrap.sh" -o /tmp/bootstrap.sh
|
||
- chmod +x /tmp/bootstrap.sh
|
||
- /tmp/bootstrap.sh
|
||
```
|
||
|
||
With **wget**: `wget -q -O /tmp/bootstrap.sh "http://.../bootstrap.sh"` then `chmod +x /tmp/bootstrap.sh` and `/tmp/bootstrap.sh`.
|
||
|
||
3. **Bootstrap script**: use a normal shell script (e.g. `#!/bin/bash` and `set -e`). It can install packages, configure kiosk, set hostname, register with a dashboard, etc.
|
||
|
||
4. **Notes**: Ensure `network-config` (or DHCP) gives the Pi an IP before runcmd. For HTTPS, add `ca-certificates` to `packages` if needed. You can use one script for all devices or have the script call your server with serial/MAC for device-specific config.
|
||
|
||
### Creating the golden image
|
||
|
||
1. **Flash Raspberry Pi OS** (or your base image) to a spare SD card or a loop file.
|
||
2. **Mount the boot partition** (first partition, FAT32). On the image file it might be at an offset; use `losetup -P` or mount the SD’s partition.
|
||
3. **Add cloud-init NoCloud files** on the boot partition (same level as `config.txt`, not in a subfolder for default NoCloud):
|
||
- `user-data` – main config (packages, runcmd, etc.)
|
||
- `meta-data` – optional (instance-id, local-hostname)
|
||
- `network-config` – optional (network config in netplan format)
|
||
|
||
You can use the examples in this repo:
|
||
|
||
```bash
|
||
# After mounting boot partition at e.g. /mnt/boot
|
||
# (On Raspberry Pi OS, boot is often /boot/firmware on the running system, or the first FAT partition of the image)
|
||
cp emmc-provisioning/cloud-init/user-data /mnt/boot/
|
||
cp emmc-provisioning/cloud-init/meta-data /mnt/boot/
|
||
cp emmc-provisioning/cloud-init/network-config /mnt/boot/
|
||
```
|
||
|
||
4. **Customise** `user-data` and `network-config` (hostname, WiFi, packages, Chromium kiosk, or a single runcmd that downloads and runs a remote script—see "Remote bootstrap script" above).
|
||
5. **Copy your kiosk/Chromium scripts** into the image rootfs only if you are not using a remote script; otherwise the remote script can pull what it needs.
|
||
6. **Unmount**, then create a **golden image** from the SD or loop device (e.g. `dd` or copy of the whole block device). Use that as `golden.img` on the provisioning host. Optionally shrink it with PiShrink (see below) to save space and speed up deploy.
|
||
|
||
### Shrinking backup and golden images (PiShrink)
|
||
|
||
Raw full-disk backups and golden images are the full size of the eMMC (e.g. 32 GB). [PiShrink](https://github.com/Drewsif/PiShrink) shrinks the **last partition** (must be ext2/3/4) to its minimum size and truncates the image file; on first boot the rootfs can expand back to fill the device. This reduces backup/golden image size (often to a few GB) and improves transfer times.
|
||
|
||
**On the provisioning host:**
|
||
|
||
1. **Install PiShrink and dependencies** (run as root on the host, or via `ssh root@HOST 'bash -s' < scripts/install-pishrink-on-host.sh`):
|
||
|
||
```bash
|
||
# From this repo (on the host or via ssh)
|
||
bash chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh
|
||
```
|
||
|
||
This installs `parted`, `e2fsprogs`, `gzip`, `pigz`, `xz-utils` and downloads `pishrink.sh` to `/usr/local/bin/pishrink.sh`.
|
||
|
||
2. **Shrink backups automatically** after each Backup in the dashboard: add to `/opt/cm4-provisioning/env` and ensure the trigger sources it:
|
||
|
||
```bash
|
||
echo 'SHRINK_BACKUP=1' | sudo tee -a /opt/cm4-provisioning/env
|
||
# Optional: compress after shrinking (smaller file, but must decompress before using as golden image)
|
||
# echo 'PISHRINK_COMPRESS=gz' | sudo tee -a /opt/cm4-provisioning/env # or xz
|
||
```
|
||
|
||
With `SHRINK_BACKUP=1`, once a backup finishes, the script runs PiShrink on the `.img` file. Use `PISHRINK_COMPRESS=gz` or `xz` for maximum size reduction; the file becomes `.img.gz` or `.img.xz` and must be decompressed before deploy (e.g. `gunzip -c backup.img.gz > golden.img`).
|
||
|
||
3. **Shrink a golden image manually** (e.g. after building from Raspberry Pi OS):
|
||
|
||
```bash
|
||
sudo pishrink.sh -n /path/to/large.img /var/lib/cm4-provisioning/golden.img
|
||
```
|
||
|
||
`-n` disables update check. Omit the second argument to shrink in place. The shrunk image will expand the rootfs on first boot when deployed to eMMC.
|
||
|
||
**Notes:**
|
||
|
||
- PiShrink only shrinks the **last** partition; it must be ext2/3/4 (standard Raspberry Pi OS root is ext4).
|
||
- Compressed backups (`.img.gz` / `.img.xz`) are for archival; to use as golden image, decompress first (e.g. `gunzip -k backup.img.gz` then copy to `golden.img`).
|
||
|
||
### Cloud-init file locations on the Pi
|
||
|
||
- **NoCloud**: Boot partition root – `user-data`, `meta-data`, `network-config`.
|
||
- Some images expect them in a subfolder `cloud-init/` or on a separate vfat partition labeled `cidata`; check your OS docs. Standard Raspberry Pi OS NoCloud uses the boot partition root.
|
||
|
||
### Per-device config (optional)
|
||
|
||
NoCloud can also use a **seed** partition or **config drive**. For per-device hostname/settings you can:
|
||
|
||
- Use **meta-data** `instance-id` and `local-hostname` and generate different `meta-data` per device when imaging (e.g. script that writes `meta-data` before flashing), or
|
||
- Use a first-boot script that calls a provisioning server (e.g. by serial number) and applies device-specific config; cloud-init can launch that script from `runcmd`.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
| Step | Action |
|
||
|------|--------|
|
||
| 1 | Build `usbboot`, install `rpiboot` on provisioning host. |
|
||
| 2 | Create golden image with cloud-init `user-data`, `meta-data`, `network-config` on boot partition. |
|
||
| 3 | Install `flash-emmc-on-connect.sh`, `cm4-flash-trigger.sh`, and udev rule; set `GOLDEN_IMAGE` and enable file. |
|
||
| 4 | Put reTerminal in boot mode (jumper), connect USB to host; image is written automatically. |
|
||
| 5 | Remove jumper, power cycle; device boots from eMMC and cloud-init runs on first boot. |
|
||
|
||
This gives you automatic deployment of the golden image to eMMC when the reTerminal is in boot mode, plus first-boot configuration via cloud-init.
|