feat: add btrfs subvolume, snapshot, and rollback tooling

- init.sh: creates the @ / @home / @log / @snapshots subvolume layout
  on a target device (the installer step).
- snapshot.sh: scheduled read-only snapshots with pruning, installed as
  /usr/local/sbin/arcline-snapshot.
- rollback.sh: safe boot-to-snapshot rollback that refuses to touch the
  live @ and promotes a snapshot atomically.
This commit is contained in:
Blake Ridgway
2026-08-21 13:15:43 -05:00
parent 94ab6043e7
commit 36d0c5b9d1
4 changed files with 276 additions and 0 deletions

83
btrfs/rollback.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# arcline-rollback — btrfs boot-to-snapshot rollback (ships at /usr/local/sbin)
#
# arcline-rollback interactive, pick a snapshot
# arcline-rollback <snapshot-name> 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-<ts>
# 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 <snapshot-name> | --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