Files
os-build/btrfs/snapshot.sh
Blake Ridgway 36d0c5b9d1 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.
2026-08-21 13:15:43 -05:00

90 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# arcline-snapshot — btrfs snapshot manager (ships at /usr/local/sbin)
#
# arcline-snapshot snapshot take a snapshot of @ and @home
# arcline-snapshot list list stored snapshots
# arcline-snapshot prune [--keep N] keep N newest, delete the rest
#
# Snapshots are read-only copies under /.snapshots, so they survive a failed
# upgrade. Run as root (or via sudo). Configured through the
# arcline-snapshot.timer unit (see overlays/base/usr/lib/systemd/system/).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
ACTION="${1:-list}"
SNAP_ROOT="/.snapshots"
KEEP="${ARCLINE_SNAPSHOT_KEEP:-5}"
TOP="/run/arcline-btrfs"
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 "info: root filesystem is '$ROOT_FS_TYPE', not btrfs — snapshots disabled"
exit 0
fi
ROOT_DEV="$(findmnt -no SOURCE / | head -1)"
mount_top() {
mkdir -p "$TOP"
mount -o subvolid=5,ro "$ROOT_DEV" "$TOP"
trap 'umount "$TOP" 2>/dev/null || true; rmdir "$TOP" 2>/dev/null || true' EXIT
}
stamp() { date +%Y%m%dT%H%M%S; }
case "$ACTION" in
snapshot)
require_root "$0" "$@"
mount_top
ts="$(stamp)"
for sub in "@" "@home"; do
[[ -d "$TOP/$sub" ]] || { echo "warn: subvolume $sub not found, skipping"; continue; }
dst="$TOP/@snapshots/$ts-$sub"
echo "snapshotting $sub$dst"
btrfs subvolume snapshot -r "$TOP/$sub" "$dst" >/dev/null
done
echo "snapshot taken: $ts"
;;
list)
require_root "$0" "$@"
mount_top
if ! compgen -G "$TOP/@snapshots/*-@*" >/dev/null; then
echo "no snapshots yet"
exit 0
fi
for snap in "$TOP"/@snapshots/*-@*; do
[[ -d "$snap" ]] || continue
echo "$(basename "$snap")"
done | sort -r
;;
prune)
require_root "$0" "$@"
mount_top
if [[ "${2:-}" == "--keep" ]]; then KEEP="${3:-$KEEP}"; fi
mapfile -t snaps < <(for s in "$TOP"/@snapshots/*-@*; do [[ -d "$s" ]] && echo "$s"; done | sort -r)
if [[ ${#snaps[@]} -le $KEEP ]]; then
echo "nothing to prune (${#snaps[@]} ≤ keep $KEEP)"
exit 0
fi
for old in "${snaps[@]:$KEEP}"; do
echo "deleting $(basename "$old")"
btrfs subvolume delete "$old" >/dev/null
done
;;
*)
echo "usage: arcline-snapshot {snapshot|list|prune [--keep N]}" >&2
exit 1
;;
esac