- 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.
90 lines
2.9 KiB
Bash
Executable File
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
|