From 17e26fab9763ea764628e2da29a3f73a4df4c7a2 Mon Sep 17 00:00:00 2001 From: Roland Date: Sun, 24 May 2026 21:21:16 -0600 Subject: [PATCH] =?UTF-8?q?Adding=20scripts/wrolpi-usb.sh=20=E2=80=94=20up?= =?UTF-8?q?grade=20subcommand.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wrolpi-usb.sh upgrade ` replaces the boot partition of an existing WROLPi USB with a new ISO while preserving the persistence partition. Flow: 1. Locate the persistence partition by label. 2. Read its sector range from sfdisk -d. 3. Confirm the new ISO fits in the headroom (i.e. its byte count <= persistence_start_sector * 512). If not, bail with instructions telling the user to back up /media/wrolpi, re-flash from scratch, and restore. This is the case for USBs flashed before the 8 GiB- headroom commit. 4. Save the partition table to /tmp/ for emergency recovery. 5. Prompt for "yes". 6. dd the new ISO onto the device. The persistence partition's sectors are past the ISO's end, so its data is untouched. 7. sfdisk --append to re-add the persistence partition entry at the saved sector range. The data on disk is still there; we are just fixing up the partition table the new ISO wrote. 8. partx -u to refresh the kernel view; verify the label is reachable. `install` subcommand is wired up to a die() for now; will land in a follow-up commit. The script refuses to operate on non-removable disks (foot-gun guard). Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/wrolpi-usb.sh | 224 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100755 scripts/wrolpi-usb.sh diff --git a/scripts/wrolpi-usb.sh b/scripts/wrolpi-usb.sh new file mode 100755 index 00000000..d265bb18 --- /dev/null +++ b/scripts/wrolpi-usb.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# WROLPi USB upgrade + install tool. +# +# Run from any modern Linux host. Plug the target USB drive in, then: +# +# sudo ./wrolpi-usb.sh upgrade +# Replace the boot partition of an existing WROLPi USB with a new +# ISO. The persistence partition (your library + database + +# configuration) is preserved. +# +# sudo ./wrolpi-usb.sh install +# [not yet implemented] +# Convert an existing ext4 drive into a WROLPi USB by shrinking the +# existing partition from the end, adding WROLPi boot partitions in +# the freed space, and relabelling the existing partition as the +# persistence partition. +# +# Both commands save the current partition table to +# /tmp/wrolpi-usb--.sfdisk before any destructive +# operation, and refuse to run on non-removable disks. + +set -euo pipefail + +HEADROOM_BYTES=$((8 * 1024 * 1024 * 1024)) # 8 GiB; mirrors wrolpi-bootstrap.sh + +# --------------------------------------------------------------------------- + +die() { echo "ERROR: $*" >&2; exit 1; } +warn() { echo "WARN: $*" >&2; } +info() { echo "$*"; } + +usage() { + sed -n '2,/^$/p' "$0" | sed 's|^# \{0,1\}||' + exit 0 +} + +require_root() { + [ "$(id -u)" -eq 0 ] || die "this script must run as root (try sudo)" +} + +require_tool() { + command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1" +} + +confirm() { + echo + echo "$1" + read -r -p "Type 'yes' to continue, anything else to abort: " ans + [ "$ans" = "yes" ] || die "aborted by user" +} + +backup_partition_table() { + local dev=$1 + local stamp; stamp=$(date +%Y%m%d-%H%M%S) + local out=/tmp/wrolpi-usb-$(basename "$dev")-${stamp}.sfdisk + sfdisk -d "$dev" > "$out" + info "Saved current partition table to $out" + info " (restore with: sudo sfdisk $dev < $out)" +} + +sanity_check_device() { + local dev=$1 + [ -b "$dev" ] || die "$dev is not a block device" + + # Refuse non-removable disks unless explicitly forced. Avoids the + # common foot-gun of typing the OS disk by mistake. + local removable + removable=$(lsblk -dno RM "$dev" 2>/dev/null || echo 0) + if [ "$removable" != "1" ]; then + die "$dev is not a removable disk. If this really is the target, " \ + "use a Linux box where it shows as removable." + fi + + # Anything currently mounted? We need it idle. + local mounted + mounted=$(lsblk -no MOUNTPOINTS "$dev" 2>/dev/null | grep -v '^$' || true) + if [ -n "$mounted" ]; then + info "Mounted children of $dev:" + echo "$mounted" | sed 's/^/ /' + confirm "These mounts will be unmounted before we proceed." + for child in $(lsblk -lnpo NAME "$dev" | tail -n +2); do + umount "$child" 2>/dev/null || true + done + fi +} + +partition_sector_range() { + # Print start and size (in 512-byte sectors) of the partition. + # Args: + # + # sfdisk -d output pads values with whitespace ("start= 64,") + # which confuses naive awk field-splitting; use sed to pull both numbers + # out at once. + local dev=$1 part=$2 + sfdisk -d "$dev" \ + | sed -n "s|^${part} *: *start= *\([0-9]\+\), *size= *\([0-9]\+\).*|\1 \2|p" +} + +# --------------------------------------------------------------------------- + +cmd_upgrade() { + local iso=$1 + local dev=$2 + [ -f "$iso" ] || die "ISO not found: $iso" + sanity_check_device "$dev" + + require_tool dd + require_tool sfdisk + require_tool partx + require_tool blkid + require_tool lsblk + require_tool udevadm + + # Locate the persistence partition on the target device. + local persist_part="" + for part in $(lsblk -lnpo NAME "$dev" | tail -n +2); do + if [ "$(lsblk -no LABEL "$part" 2>/dev/null)" = "persistence" ]; then + persist_part=$part + break + fi + done + [ -n "$persist_part" ] || die "no partition labelled 'persistence' found on $dev" + + local sector_info + sector_info=$(partition_sector_range "$dev" "$persist_part") + [ -n "$sector_info" ] || die "could not read sector range for $persist_part" + local persist_start persist_size + read -r persist_start persist_size <<<"$sector_info" + + local iso_size; iso_size=$(stat -c %s "$iso") + local headroom_bytes=$((persist_start * 512 - iso_size)) + + info "ISO: $iso ($(numfmt --to=iec --suffix=B $iso_size))" + info "Target device: $dev ($(lsblk -no MODEL,SIZE "$dev" | head -1))" + info "Persistence: $persist_part starts at sector $persist_start ($(numfmt --to=iec --suffix=B $((persist_start * 512))))" + info "Headroom for ISO: $(numfmt --to=iec --suffix=B $((persist_start * 512)))" + info "Headroom vs ISO: $(numfmt --to=iec --suffix=B $headroom_bytes) free" + + if [ "$headroom_bytes" -lt 0 ]; then + cat >&2 </dev/null || true + udevadm settle || true + + # Verify the persistence partition is reachable + still labelled. + sleep 1 + local found_label + found_label=$(blkid -L persistence 2>/dev/null || true) + if [ -z "$found_label" ]; then + warn "could not find a 'persistence' label after re-adding the partition." + warn "data should still be present at sector $persist_start; you may need" + warn "to re-label with: sudo e2label persistence" + else + info "Persistence label resolved to: $found_label" + fi + + info "Done. Eject the USB and boot it to verify." +} + +cmd_install() { + die "install is not yet implemented (planned: see scripts/wrolpi-usb.sh comments)" +} + +# --------------------------------------------------------------------------- + +main() { + case "${1:-}" in + upgrade) + shift + [ $# -eq 2 ] || { usage; exit 1; } + require_root + cmd_upgrade "$@" + ;; + install) + shift + [ $# -eq 2 ] || { usage; exit 1; } + require_root + cmd_install "$@" + ;; + -h|--help|help|"") + usage + ;; + *) + echo "Unknown command: $1" >&2 + usage + exit 1 + ;; + esac +} + +main "$@"