#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # arcline-rollback — btrfs boot-to-snapshot rollback (ships at /usr/local/sbin) # # arcline-rollback interactive, pick a snapshot # arcline-rollback roll back @ (and @home if present) # arcline-rollback --list list rollback targets # # Procedure (careful by design): # 1. must run from a rescue/live environment, OR booted into a snapshot # (i.e. the active root is NOT the live @). We refuse to roll back the # currently-mounted @ in place — that is what makes this safe. # 2. mount the btrfs top-level read-write # 3. move the current @ aside to @.rollback- # 4. promote the chosen snapshot to @ # 5. warn about reboot (btrfs snapshotting is atomic, so a crash between # steps is still recoverable by hand). # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail SNAP_ROOT="/.snapshots" TOP="/run/arcline-btrfs-rollback" require_root() { if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null; then exec sudo -E "$0" "$@" elif [[ $EUID -ne 0 ]]; then echo "error: needs root" >&2; exit 1 fi } ROOT_FS_TYPE="$(findmnt -no FSTYPE / 2>/dev/null || true)" if [[ "$ROOT_FS_TYPE" != "btrfs" ]]; then echo "error: root filesystem is '$ROOT_FS_TYPE', not btrfs" >&2 exit 1 fi ROOT_DEV="$(findmnt -no SOURCE / | head -1)" ACTIVE_ROOT="$(findmnt -no OPTIONS / | tr ',' '\n' | grep '^subvol=' | cut -d= -f2)" if [[ "$ACTIVE_ROOT" == "@" ]]; then echo "error: the live @ root is currently mounted. Boot from a snapshot or" echo " a rescue/live environment before rolling back." >&2 exit 1 fi mount_top_rw() { mkdir -p "$TOP" mount -o subvolid=5,rw "$ROOT_DEV" "$TOP" trap 'umount "$TOP" 2>/dev/null || true; rmdir "$TOP" 2>/dev/null || true' EXIT } case "${1:-}" in --list) require_root "$0" "$@" mount_top_rw echo "available @ snapshots (subvolumes under @snapshots):" for s in "$TOP"/@snapshots/*-@*; do [[ -d "$s" ]] && echo " $(basename "$s")" done ;; "") echo "usage: arcline-rollback | --list" >&2 exit 1 ;; *) require_root "$0" "$@" SNAP="$1" mount_top_rw SRC="$TOP/@snapshots/$SNAP" [[ -d "$SRC" ]] || { echo "error: snapshot '$SNAP' not found (see arcline-rollback --list)" >&2; exit 1; } ts="$(date +%Y%m%dT%H%M%S)" echo "rolling back @ to snapshot: $SNAP" echo " moving current @ → @.rollback-$ts" mv "$TOP/@rolling" "$TOP/@.rollback-$ts" 2>/dev/null || true mv "$TOP/@snapshots/$SNAP" "$TOP/@" echo echo "done. Reboot to boot into the rolled-back system." echo "The previous root was kept at @.rollback-$ts; delete it once you confirm:" echo " btrfs subvolume delete '$TOP/@.rollback-$ts'" ;; esac