<message>Update the bootstrap script to ensure hostname resolution by adding entries to /etc/hosts, preventing "sudo: unable to resolve host" errors. Modify user-data.bootstrap to include the same hostname resolution logic. Revise dashboard templates to reflect the new project name "GNSS Guard Provisioning" and improve user interface elements related to USB boot operations, including clearer instructions and status messages. These changes enhance the overall user experience and streamline the provisioning process.
286 lines
14 KiB
Bash
286 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
# Revision: 2
|
|
# Provision CM4 eMMC when reTerminal is connected in boot mode (eMMC disable jumper).
|
|
# On USB boot device (2b8e) detection: run rpiboot to expose eMMC, then wait for the user
|
|
# to choose Backup or Deploy in the portal — no auto-flash; action runs only after portal choice.
|
|
# Run this from udev or a systemd service. Requires: usbboot (rpiboot) built, golden image for deploy.
|
|
|
|
# Do NOT use set -e: rpiboot and intermediate commands may return non-zero without being fatal
|
|
|
|
# Redirect all output to log file (stdout + stderr) so nothing is lost to nohup buffering
|
|
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
|
exec >> "$LOG_FILE" 2>&1
|
|
|
|
# Load overrides from env file if present
|
|
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
|
|
|
# Configuration - adjust paths and size for your setup
|
|
RPIBOOT_DIR="${RPIBOOT_DIR:-/opt/usbboot}"
|
|
GOLDEN_IMAGE="${GOLDEN_IMAGE:-/var/lib/cm4-provisioning/golden.img}"
|
|
# Expected eMMC size in bytes. reTerminal DM (CM4) has 32 GB eMMC (~31268536320 bytes).
|
|
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-$(( 32 * 1024 * 1024 * 1024 ))}"
|
|
LOG_TAG="cm4-flash"
|
|
STATUS_FILE="${STATUS_FILE:-/var/lib/cm4-provisioning/status.json}"
|
|
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
|
LOCK_FILE="${LOCK_FILE:-/var/lib/cm4-provisioning/flash.lock}"
|
|
|
|
log() { echo "[$(date -Iseconds)] [$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
|
|
|
|
write_status() {
|
|
local phase="$1" message="$2" progress="${3:-null}" error="${4:-}"
|
|
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
message="${message//\"/\\\"}"; error="${error//\"/\\\"}"
|
|
if [[ -n "$error" ]]; then
|
|
printf '{"phase":"%s","message":"%s","progress":%s,"error":"%s","updated":"%s"}\n' \
|
|
"$phase" "$message" "$progress" "$error" "$ts" > "$STATUS_FILE" 2>/dev/null || true
|
|
else
|
|
printf '{"phase":"%s","message":"%s","progress":%s,"updated":"%s"}\n' \
|
|
"$phase" "$message" "$progress" "$ts" > "$STATUS_FILE" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Prevent concurrent runs (udev fires multiple times during rpiboot re-enumeration)
|
|
exec 9>"$LOCK_FILE"
|
|
if ! flock -n 9; then
|
|
echo "[$(date -Iseconds)] Another flash-emmc instance is already running; exiting."
|
|
exit 0
|
|
fi
|
|
trap 'rm -f "$LOCK_FILE" "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null; exec 9>&-' EXIT
|
|
|
|
# Optional: only run if this file exists (safety)
|
|
ENABLE_FILE="${ENABLE_FILE:-/etc/cm4-provisioning/enabled}"
|
|
if [[ -n "$ENABLE_FILE" && ! -f "$ENABLE_FILE" ]]; then
|
|
log "Skipping: $ENABLE_FILE not present"
|
|
write_status "idle" "Provisioning disabled (remove /etc/cm4-provisioning/enabled to enable)" "null" 2>/dev/null || true
|
|
exit 0
|
|
fi
|
|
|
|
# When a device is detected we ask the user (dashboard): Backup or Deploy?
|
|
# These files are used to wait for the user's choice (written by dashboard, read by this script).
|
|
BACKUPS_DIR="${BACKUPS_DIR:-/var/lib/cm4-provisioning/backups}"
|
|
# Optional: shrink backup with PiShrink (requires pishrink + parted, e2fsprogs). SHRINK_BACKUP=1 to enable.
|
|
SHRINK_BACKUP="${SHRINK_BACKUP:-0}"
|
|
# If SHRINK_BACKUP=1, optionally compress: PISHRINK_COMPRESS=gz or xz (uses parallel when available). Uncompressed .img can be dd'd directly for deploy.
|
|
PISHRINK_COMPRESS="${PISHRINK_COMPRESS:-}"
|
|
ACTION_REQUEST_FILE="${ACTION_REQUEST_FILE:-/var/lib/cm4-provisioning/action_request}"
|
|
CURRENT_DEVICE_FILE="${CURRENT_DEVICE_FILE:-/var/lib/cm4-provisioning/current_device}"
|
|
DEVICE_SOURCE_FILE="${DEVICE_SOURCE_FILE:-/var/lib/cm4-provisioning/device_source}"
|
|
WAIT_TIMEOUT="${WAIT_TIMEOUT:-600}"
|
|
# Golden image required only for Deploy; allow Backup without it
|
|
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
|
|
log "Golden image not found (Deploy will be unavailable): $GOLDEN_IMAGE"
|
|
fi
|
|
|
|
RPIBOOT_BIN="$RPIBOOT_DIR/rpiboot"
|
|
# Gadget dir for CM4 (64-bit); fallback to mass-storage-gadget
|
|
RPIBOOT_GADGET=""
|
|
for d in "$RPIBOOT_DIR/mass-storage-gadget64" "$RPIBOOT_DIR/mass-storage-gadget"; do
|
|
if [[ -d "$d" ]]; then
|
|
RPIBOOT_GADGET="$d"
|
|
break
|
|
fi
|
|
done
|
|
if [[ ! -x "$RPIBOOT_BIN" ]]; then
|
|
log "rpiboot not found: $RPIBOOT_BIN (build usbboot and set RPIBOOT_DIR)"
|
|
write_status "error" "rpiboot not installed" "null" "rpiboot not found. Run install-usbboot-on-host.sh on the host or build-and-deploy-usbboot-to-host.sh from your machine."
|
|
exit 1
|
|
fi
|
|
if [[ -z "$RPIBOOT_GADGET" ]]; then
|
|
log "rpiboot gadget dir not found under $RPIBOOT_DIR (need mass-storage-gadget64 or mass-storage-gadget)"
|
|
write_status "error" "rpiboot gadget missing" "null" "Copy mass-storage-gadget(64) to $RPIBOOT_DIR"
|
|
exit 1
|
|
fi
|
|
# rpiboot requires bootfiles.bin or one of bootcode*.bin in the gadget dir; empty dir causes "No 'bootcode' files found"
|
|
if [[ ! -f "$RPIBOOT_GADGET/bootfiles.bin" && ! -f "$RPIBOOT_GADGET/bootcode.bin" && ! -f "$RPIBOOT_GADGET/bootcode4.bin" && ! -f "$RPIBOOT_GADGET/bootcode5.bin" ]]; then
|
|
log "rpiboot gadget dir has no boot files: $RPIBOOT_GADGET (reinstall usbboot)"
|
|
write_status "error" "rpiboot gadget empty" "null" "No boot files in $RPIBOOT_GADGET. On the host run: fix-gadget-bootcode-on-host.sh (or from your machine: ssh root@HOST 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh). See docs troubleshooting."
|
|
exit 1
|
|
fi
|
|
|
|
# Ensure status dir exists and start with running state
|
|
mkdir -p "$(dirname "$STATUS_FILE")" "$BACKUPS_DIR" 2>/dev/null || true
|
|
write_status "rpiboot" "Connecting to CM4 in boot mode…" "0"
|
|
|
|
# Block devices before rpiboot (so we can detect new one after)
|
|
before_devs=$(lsblk -nd -o NAME 2>/dev/null | sort)
|
|
|
|
log "Starting rpiboot to expose CM4 eMMC as mass storage..."
|
|
# Run rpiboot with 90s timeout so we don't hang if it doesn't exit cleanly when device switches to mass storage
|
|
rpiboot_exit=0
|
|
timeout 90 "$RPIBOOT_BIN" -d "$RPIBOOT_GADGET" || rpiboot_exit=$?
|
|
# timeout returns 124 if killed by timeout; 0 or other if rpiboot exited on its own
|
|
if [[ "$rpiboot_exit" -eq 124 ]]; then
|
|
log "rpiboot timed out after 90s (device may have switched to mass storage)"
|
|
elif [[ "$rpiboot_exit" -ne 0 ]]; then
|
|
log "rpiboot exited with code $rpiboot_exit"
|
|
write_status "error" "rpiboot failed" "null" "rpiboot failed or no device connected. Check flash.log on host. Try unplug/replug USB."
|
|
exit 1
|
|
fi
|
|
|
|
echo "[$(date -Iseconds)] rpiboot finished (exit=$rpiboot_exit); starting device scan"
|
|
log "rpiboot completed; waiting for block device..."
|
|
write_status "rpiboot" "rpiboot done, waiting for block device…" "10"
|
|
|
|
# rpiboot exits when device switches to mass storage; udev may need several seconds to create /dev/sdX
|
|
# Poll for new block device for up to 30s (device switch can be slow)
|
|
target_dev=""
|
|
for wait_sec in $(seq 2 2 10) $(seq 12 2 30); do
|
|
sleep 2
|
|
for dev in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
|
|
[[ -b "$dev" ]] || continue
|
|
[[ "$dev" =~ [0-9]$ ]] && continue
|
|
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
|
|
if [[ -n "$size" ]]; then
|
|
if (( size >= EMMC_SIZE_BYTES * 95 / 100 && size <= EMMC_SIZE_BYTES * 105 / 100 )); then
|
|
target_dev=$dev
|
|
break 2
|
|
fi
|
|
if [[ -z "$target_dev" && "$before_devs" != *"${dev#/dev/}"* ]]; then
|
|
target_dev=$dev
|
|
fi
|
|
fi
|
|
done
|
|
[[ -n "$target_dev" ]] && break
|
|
log "Waiting for block device... ${wait_sec}s"
|
|
write_status "rpiboot" "Waiting for eMMC block device… (${wait_sec}s)" "10"
|
|
done
|
|
|
|
log "Device scan complete. before_devs=[$before_devs] target_dev=[$target_dev]"
|
|
if [[ -z "$target_dev" ]]; then
|
|
log "No suitable block device found after rpiboot (expected ~${EMMC_SIZE_BYTES} bytes)"
|
|
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot"
|
|
exit 1
|
|
fi
|
|
|
|
# Ask user (dashboard): Backup or Deploy?
|
|
write_status "waiting_choice" "Device connected (USB boot mode). Choose Backup, Deploy, or Update EEPROM in the dashboard." "null"
|
|
echo "usb" > "$DEVICE_SOURCE_FILE" 2>/dev/null || true
|
|
echo "$target_dev" > "$CURRENT_DEVICE_FILE" 2>/dev/null || true
|
|
log "Waiting for user choice (Backup, Deploy, or Update EEPROM) in dashboard; timeout ${WAIT_TIMEOUT}s..."
|
|
for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
|
sleep 2
|
|
if [[ -f "$ACTION_REQUEST_FILE" ]]; then
|
|
action=$(cat "$ACTION_REQUEST_FILE" 2>/dev/null | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
rm -f "$ACTION_REQUEST_FILE" 2>/dev/null || true
|
|
if [[ "$action" == "backup" ]]; then
|
|
backup_name="backup-$(date +%Y%m%d-%H%M%S).img"
|
|
backup_path="$BACKUPS_DIR/$backup_name"
|
|
write_status "backup" "Creating backup…" "null"
|
|
log "Backing up $target_dev to $backup_path..."
|
|
if dd if="$target_dev" of="$backup_path" bs=4M status=progress conv=fsync; then
|
|
final_name="$backup_name"
|
|
shrink_requested=false
|
|
[[ "$SHRINK_BACKUP" == "1" || "$SHRINK_BACKUP" == "true" ]] && shrink_requested=true
|
|
[[ -f "$(dirname "$STATUS_FILE")/shrink_next_backup" ]] && shrink_requested=true
|
|
rm -f "$(dirname "$STATUS_FILE")/shrink_next_backup" 2>/dev/null || true
|
|
if [[ "$shrink_requested" == "true" ]]; then
|
|
pishrink_cmd=""
|
|
for p in /usr/local/bin/pishrink.sh /usr/local/bin/pishrink; do
|
|
[[ -x "$p" ]] && pishrink_cmd="$p" && break
|
|
done
|
|
if [[ -n "$pishrink_cmd" ]]; then
|
|
write_status "backup" "Shrinking backup image…" "null"
|
|
log "Shrinking backup with PiShrink (timeout 30 min)..."
|
|
pishrink_opts="-n"
|
|
[[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]] && pishrink_opts="$pishrink_opts -z -a"
|
|
[[ "$PISHRINK_COMPRESS" == "xz" ]] && pishrink_opts="$pishrink_opts -Z -a"
|
|
PISHRINK_TIMEOUT="${PISHRINK_TIMEOUT:-1800}"
|
|
if timeout "$PISHRINK_TIMEOUT" $pishrink_cmd $pishrink_opts "$backup_path" 2>&1; then
|
|
if [[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]]; then
|
|
final_name="${backup_name}.gz"
|
|
elif [[ "$PISHRINK_COMPRESS" == "xz" ]]; then
|
|
final_name="${backup_name}.xz"
|
|
fi
|
|
log "Shrunk backup: $BACKUPS_DIR/$final_name"
|
|
else
|
|
rc=$?
|
|
if [[ "$rc" -eq 124 ]]; then
|
|
log "PiShrink timed out after ${PISHRINK_TIMEOUT}s; keeping full backup $backup_path"
|
|
else
|
|
log "PiShrink failed (exit $rc); keeping full backup $backup_path"
|
|
fi
|
|
fi
|
|
else
|
|
log "SHRINK_BACKUP=1 but pishrink not found; keeping full backup. Install with scripts/install-pishrink-on-host.sh"
|
|
fi
|
|
fi
|
|
log "Backup complete: $BACKUPS_DIR/$final_name"
|
|
write_status "done" "Backup complete: $final_name" "100"
|
|
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
|
else
|
|
write_status "error" "Backup failed" "null" "dd failed"
|
|
fi
|
|
elif [[ "$action" == "deploy" ]]; then
|
|
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
|
|
log "Golden image not found; cannot deploy."
|
|
write_status "error" "Deploy unavailable" "null" "Golden image not found. Add golden.img to /var/lib/cm4-provisioning/ for deploy."
|
|
exit 1
|
|
fi
|
|
write_status "flashing" "Writing golden image…" "null"
|
|
log "Flashing $GOLDEN_IMAGE to $target_dev..."
|
|
GOLDEN_RESOLVED="$(readlink -f "$GOLDEN_IMAGE" 2>/dev/null || echo "$GOLDEN_IMAGE")"
|
|
if [[ "$GOLDEN_RESOLVED" == *.img.xz ]]; then
|
|
decompress_cmd="xz -d -c"
|
|
elif [[ "$GOLDEN_RESOLVED" == *.img.gz ]]; then
|
|
decompress_cmd="gzip -c -d"
|
|
else
|
|
decompress_cmd="cat"
|
|
fi
|
|
if $decompress_cmd "$GOLDEN_IMAGE" 2>/dev/null | dd of="$target_dev" bs=4M status=progress conv=fsync; then
|
|
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
|
|
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
|
|
# Auto-reset status to idle after 90s so dashboard does not stay on this message (dashboard also auto-clears after 60s when open)
|
|
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
|
else
|
|
write_status "error" "Flash failed" "null" "dd failed"
|
|
fi
|
|
elif [[ "$action" == "eeprom_update" ]]; then
|
|
# Dashboard has written pieeprom.upd and pieeprom.sig to BASE_DIR; copy to eMMC boot partition
|
|
PROV_DIR="$(dirname "$STATUS_FILE")"
|
|
EEPROM_UPD="$PROV_DIR/pieeprom.upd"
|
|
EEPROM_SIG="$PROV_DIR/pieeprom.sig"
|
|
if [[ ! -f "$EEPROM_UPD" || ! -f "$EEPROM_SIG" ]]; then
|
|
log "EEPROM update files not found: $EEPROM_UPD / $EEPROM_SIG"
|
|
write_status "error" "EEPROM update failed" "null" "pieeprom.upd or pieeprom.sig not found in $PROV_DIR"
|
|
exit 1
|
|
fi
|
|
boot_part=""
|
|
for p in "${target_dev}1" "${target_dev}p1"; do
|
|
if [[ -b "$p" ]]; then
|
|
boot_part="$p"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "$boot_part" ]]; then
|
|
log "No boot partition found for $target_dev (tried ...1 and ...p1)"
|
|
write_status "error" "EEPROM update failed" "null" "Boot partition not found"
|
|
exit 1
|
|
fi
|
|
write_status "eeprom_update" "Writing EEPROM update to boot partition…" "null"
|
|
log "Mounting $boot_part and copying EEPROM update..."
|
|
mnt=$(mktemp -d)
|
|
if mount "$boot_part" "$mnt" 2>/dev/null; then
|
|
if cp "$EEPROM_UPD" "$mnt/pieeprom.upd" && cp "$EEPROM_SIG" "$mnt/pieeprom.sig"; then
|
|
sync
|
|
log "EEPROM update written. Remove eMMC disable jumper and power cycle to apply."
|
|
write_status "done" "EEPROM update written to boot partition. Remove eMMC disable jumper and power cycle the reTerminal to apply." "100"
|
|
rm -f "$EEPROM_UPD" "$EEPROM_SIG"
|
|
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
|
else
|
|
write_status "error" "EEPROM update failed" "null" "Failed to copy pieeprom.upd/sig"
|
|
fi
|
|
umount "$mnt" 2>/dev/null || true
|
|
else
|
|
write_status "error" "EEPROM update failed" "null" "Could not mount boot partition"
|
|
fi
|
|
rm -rf "$mnt"
|
|
else
|
|
write_status "error" "Unknown action" "null" "action_request must be 'backup', 'deploy', or 'eeprom_update'"
|
|
fi
|
|
exit 0
|
|
fi
|
|
done
|
|
log "Timeout waiting for user choice"
|
|
write_status "idle" "Timeout waiting for choice. Connect the device again to retry." "null"
|
|
exit 0
|