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:
38
btrfs/README.md
Normal file
38
btrfs/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# btrfs in Arcline OS
|
||||||
|
|
||||||
|
Arcline OS is **btrfs-native**: the root filesystem layout is created at
|
||||||
|
install time and snapshots are first-class, not bolted on.
|
||||||
|
|
||||||
|
## Subvolume layout
|
||||||
|
|
||||||
|
| Subvolume | Mount point | Purpose |
|
||||||
|
|--------------|---------------|------------------------------------------------|
|
||||||
|
| `@` | `/` | system root — rolled back on failed upgrade |
|
||||||
|
| `@home` | `/home` | user data — not rolled back by default |
|
||||||
|
| `@log` | `/var/log` | logs survive rollbacks |
|
||||||
|
| `@snapshots` | `/.snapshots` | read-only snapshots live here |
|
||||||
|
|
||||||
|
The layout is created by `btrfs/init.sh <device>` (this is what the installer
|
||||||
|
runs). `/etc/fstab` templates live in each edition manifest and reference these
|
||||||
|
subvolumes with `subvol=` mount options.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
- `btrfs/init.sh <device>` — create the subvolume layout on a target device.
|
||||||
|
- `btrfs/snapshot.sh` — snapshot / list / prune. Installed in images as
|
||||||
|
`/usr/local/sbin/arcline-snapshot` and triggered automatically by the
|
||||||
|
`arcline-snapshot.timer` systemd unit (see `overlays/base/usr/lib/systemd/system/`).
|
||||||
|
- `btrfs/rollback.sh` — promote a snapshot back to `@`. Installed as
|
||||||
|
`/usr/local/sbin/arcline-rollback`.
|
||||||
|
|
||||||
|
## How a rollback works
|
||||||
|
|
||||||
|
Rollbacks are done from a rescue/live environment (never against the mounted
|
||||||
|
`@`). The tool moves the current `@` aside to `@.rollback-<ts>` and promotes
|
||||||
|
the chosen snapshot to `@`. Because btrfs renames are atomic, a crash mid-way
|
||||||
|
leaves a recoverable system on disk.
|
||||||
|
|
||||||
|
## Compression
|
||||||
|
|
||||||
|
All subvolumes mount with `compress=zstd:3` (see the edition `fstab`
|
||||||
|
templates). Transparent compression is on by default.
|
||||||
66
btrfs/init.sh
Executable file
66
btrfs/init.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Arcline OS — btrfs subvolume layout initialiser
|
||||||
|
#
|
||||||
|
# btrfs/init.sh <device> (e.g. /dev/sda2)
|
||||||
|
# btrfs/init.sh --list <device>
|
||||||
|
#
|
||||||
|
# Creates the Arcline subvolume layout on a fresh (or existing) btrfs device:
|
||||||
|
#
|
||||||
|
# @ → / system root (rolled back on failure)
|
||||||
|
# @home → /home user data
|
||||||
|
# @log → /var/log logs (not rolled back, survives)
|
||||||
|
# @snapshots → /.snapshots where arcline-snapshot keeps snapshots
|
||||||
|
#
|
||||||
|
# This is the step the installer runs; it is safe to re-run (existing
|
||||||
|
# subvolumes are kept, missing ones are created). Requires root.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEV="${1:?usage: btrfs/init.sh <device>}"
|
||||||
|
TOP="/run/arcline-btrfs-init"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
require_root "$0" "$@"
|
||||||
|
|
||||||
|
command -v btrfs >/dev/null || { echo "error: btrfs-progs not installed" >&2; exit 1; }
|
||||||
|
|
||||||
|
if ! blkid -p -O "$DEV" | grep -q btrfs 2>/dev/null; then
|
||||||
|
echo "error: $DEV is not a btrfs filesystem" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TOP"
|
||||||
|
mount -o subvolid=5 "$DEV" "$TOP"
|
||||||
|
trap 'umount "$TOP" 2>/dev/null || true; rmdir "$TOP" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
create_subvol() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -d "$TOP/$name" ]]; then
|
||||||
|
echo " keeping existing subvolume: $name"
|
||||||
|
else
|
||||||
|
echo " creating subvolume: $name"
|
||||||
|
btrfs subvolume create "$TOP/$name" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Arcline btrfs layout on $DEV:"
|
||||||
|
create_subvol "@"
|
||||||
|
create_subvol "@home"
|
||||||
|
create_subvol "@log"
|
||||||
|
create_subvol "@snapshots"
|
||||||
|
|
||||||
|
# mark @snapshots read-only friendly (snapper/arcline-snapshot manage it)
|
||||||
|
btrfs property set -ts "$TOP/@snapshots" ro false 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "done. The installer mounts:"
|
||||||
|
echo " @ → /"
|
||||||
|
echo " @home → /home"
|
||||||
|
echo " @log → /var/log"
|
||||||
|
echo " @snapshots → /.snapshots"
|
||||||
83
btrfs/rollback.sh
Executable file
83
btrfs/rollback.sh
Executable 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
|
||||||
89
btrfs/snapshot.sh
Executable file
89
btrfs/snapshot.sh
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/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
|
||||||
Reference in New Issue
Block a user