aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.org3
-rwxr-xr-xbuild.sh11
-rwxr-xr-xcustom/zfsrollback184
-rwxr-xr-xcustom/zfssnapshot110
4 files changed, 307 insertions, 1 deletions
diff --git a/TODO.org b/TODO.org
index 72d31d5..1d5d4f6 100644
--- a/TODO.org
+++ b/TODO.org
@@ -143,7 +143,8 @@ fi
- arch-wiki-lite: ~200MB (text only, smaller)
- Could include both for ~600MB total
-** TODO [#B] Add zfsrollback and zfssnapshot scripts to ISO
+** DONE [#B] Add zfsrollback and zfssnapshot scripts to ISO
+CLOSED: [2026-01-19 Sun]
Include dedicated ZFS snapshot management scripts in the archzfs ISO rather than archsetup.
These tools belong here since they're useful for rescue scenarios and post-install management.
diff --git a/build.sh b/build.sh
index 59dea31..e6d7111 100755
--- a/build.sh
+++ b/build.sh
@@ -266,6 +266,11 @@ cp "$CUSTOM_DIR/zz-grub-zfs-snap.hook" "$PROFILE_DIR/airootfs/usr/local/share/gr
info "Copying zfs-snap-prune..."
cp "$CUSTOM_DIR/zfs-snap-prune" "$PROFILE_DIR/airootfs/usr/local/bin/"
+# Copy zfssnapshot and zfsrollback for ZFS management
+info "Copying zfssnapshot and zfsrollback..."
+cp "$CUSTOM_DIR/zfssnapshot" "$PROFILE_DIR/airootfs/usr/local/bin/"
+cp "$CUSTOM_DIR/zfsrollback" "$PROFILE_DIR/airootfs/usr/local/bin/"
+
# Copy example config for unattended installs
mkdir -p "$PROFILE_DIR/airootfs/root"
cp "$CUSTOM_DIR/install-archzfs.conf.example" "$PROFILE_DIR/airootfs/root/"
@@ -293,6 +298,12 @@ if grep -q "file_permissions=" "$PROFILE_DIR/profiledef.sh"; then
/)/ i\ ["/usr/local/bin/zfs-snap-prune"]="0:0:755"
}' "$PROFILE_DIR/profiledef.sh"
sed -i '/^file_permissions=(/,/)/ {
+ /)/ i\ ["/usr/local/bin/zfssnapshot"]="0:0:755"
+ }' "$PROFILE_DIR/profiledef.sh"
+ sed -i '/^file_permissions=(/,/)/ {
+ /)/ i\ ["/usr/local/bin/zfsrollback"]="0:0:755"
+ }' "$PROFILE_DIR/profiledef.sh"
+ sed -i '/^file_permissions=(/,/)/ {
/)/ i\ ["/etc/shadow"]="0:0:400"
}' "$PROFILE_DIR/profiledef.sh"
fi
diff --git a/custom/zfsrollback b/custom/zfsrollback
new file mode 100755
index 0000000..6e727e2
--- /dev/null
+++ b/custom/zfsrollback
@@ -0,0 +1,184 @@
+#!/bin/env bash
+# Craig Jennings <c@cjennings.net>
+# Roll back ZFS datasets to a selected snapshot using fzf.
+
+set -euo pipefail
+
+# Usage info
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [-h] [-s]
+Roll back ZFS datasets to a selected snapshot.
+
+ -h display this help and exit
+ -s single dataset mode (roll back only the selected dataset,
+ not all datasets with matching snapshot name)
+
+By default, rolling back a snapshot will roll back ALL datasets that share
+that snapshot name. Use -s for single dataset rollback.
+
+WARNING: Rolling back destroys all data and snapshots newer than the target.
+ This operation cannot be undone!
+
+Requires: fzf, zfs
+EOF
+}
+
+# Check dependencies
+for cmd in zfs fzf; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "Error: $cmd command not found"
+ exit 1
+ fi
+done
+
+# Check for root/sudo
+if [ "$EUID" -ne 0 ]; then
+ echo "Error: This script must be run as root (use sudo)"
+ exit 1
+fi
+
+# Parse arguments
+single_mode=false
+while getopts ":hs" opt; do
+ case ${opt} in
+ h)
+ show_help
+ exit 0
+ ;;
+ s)
+ single_mode=true
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+# Get all snapshots
+snapshots=$(zfs list -t snapshot -H -o name 2>/dev/null)
+
+if [ -z "$snapshots" ]; then
+ echo "No ZFS snapshots found"
+ exit 0
+fi
+
+if $single_mode; then
+ # Single mode: show full dataset@snapshot names (sorted newest first)
+ selected=$(zfs list -t snapshot -H -o name -S creation | fzf --height=40% --reverse \
+ --header="Select snapshot to roll back (ESC to cancel)" \
+ --preview="zfs list -t snapshot -o name,creation,used,refer -r {1}" \
+ --preview-window=down:3)
+
+ if [ -z "$selected" ]; then
+ echo "No snapshot selected, exiting"
+ exit 0
+ fi
+
+ dataset="${selected%@*}"
+ snap_name="${selected#*@}"
+ targets=("$selected")
+else
+ # Multi mode: show unique snapshot names, roll back all matching datasets
+ # Sort reverse so newest (latest date) appears at top
+ unique_snaps=$(echo "$snapshots" | sed 's/.*@//' | sort -ru)
+
+ snap_name=$(echo "$unique_snaps" | fzf --height=40% --reverse \
+ --header="Select snapshot name to roll back ALL matching datasets (ESC to cancel)" \
+ --preview="zfs list -t snapshot -o name,creation,used -H | grep '@{}$' | column -t" \
+ --preview-window=down:10)
+
+ if [ -z "$snap_name" ]; then
+ echo "No snapshot selected, exiting"
+ exit 0
+ fi
+
+ # Find all datasets with this snapshot
+ mapfile -t targets < <(echo "$snapshots" | grep "@${snap_name}$")
+
+ if [ ${#targets[@]} -eq 0 ]; then
+ echo "Error: No datasets found with snapshot @${snap_name}"
+ exit 1
+ fi
+fi
+
+# Display what will happen
+echo ""
+echo "═══════════════════════════════════════════════════════════════════"
+echo " ⚠️ WARNING ⚠️"
+echo "═══════════════════════════════════════════════════════════════════"
+echo ""
+
+# Special warning for genesis rollback
+if [[ "$snap_name" == "genesis" ]]; then
+ echo " 🚨 GENESIS ROLLBACK DETECTED 🚨"
+ echo ""
+ echo " Rolling back to genesis will destroy ALL changes since installation!"
+ echo " This includes all packages installed, configurations, and user data."
+ echo ""
+fi
+
+echo "You are about to roll back to snapshot: @${snap_name}"
+echo ""
+echo "The following datasets will be rolled back:"
+for target in "${targets[@]}"; do
+ dataset="${target%@*}"
+ echo " • $dataset"
+
+ # Show newer snapshots that will be destroyed
+ newer=$(zfs list -t snapshot -H -o name -S creation "$dataset" 2>/dev/null | \
+ awk -v snap="$target" 'found {print " ✗ " $0 " (will be DESTROYED)"} $0 == snap {found=1}')
+ if [ -n "$newer" ]; then
+ echo "$newer"
+ fi
+done
+
+echo ""
+echo "═══════════════════════════════════════════════════════════════════"
+echo " THIS OPERATION CANNOT BE UNDONE!"
+echo " All data written after the snapshot will be permanently lost."
+echo " All snapshots newer than the target will be destroyed."
+echo "═══════════════════════════════════════════════════════════════════"
+echo ""
+
+# Require explicit confirmation
+read -r -p "Type 'yes' to confirm rollback: " confirmation
+
+if [ "$confirmation" != "yes" ]; then
+ echo "Rollback cancelled"
+ exit 0
+fi
+
+echo ""
+echo "Rolling back..."
+
+# Perform rollbacks
+failed=0
+for target in "${targets[@]}"; do
+ dataset="${target%@*}"
+ echo -n " Rolling back $dataset... "
+ if zfs rollback -r "$target" 2>&1; then
+ echo "✓"
+ else
+ echo "✗ FAILED"
+ ((failed++))
+ fi
+done
+
+echo ""
+if [ $failed -eq 0 ]; then
+ echo "Rollback complete."
+
+ # Update GRUB boot menu if grub-zfs-snap is available
+ # (destroyed snapshots need to be removed from menu)
+ if command -v grub-zfs-snap &> /dev/null; then
+ echo ""
+ echo "Updating GRUB boot menu..."
+ grub-zfs-snap
+ fi
+else
+ echo "Rollback completed with $failed failure(s)"
+ exit 1
+fi
diff --git a/custom/zfssnapshot b/custom/zfssnapshot
new file mode 100755
index 0000000..1fa7e3b
--- /dev/null
+++ b/custom/zfssnapshot
@@ -0,0 +1,110 @@
+#!/bin/env bash
+# Craig Jennings <c@cjennings.net>
+# Create a ZFS snapshot across all datasets with a dated, descriptive name.
+
+set -euo pipefail
+
+# Usage info
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [-h] [DESCRIPTION]
+Create a ZFS snapshot across all datasets.
+
+ -h display this help and exit
+ DESCRIPTION short description for the snapshot (optional, will prompt if omitted)
+
+Snapshot names are formatted as: YYYY-MM-DD_HH-MM-SS_description
+Only alphanumeric characters, hyphens, and underscores are allowed in descriptions.
+Spaces are converted to underscores automatically.
+
+Examples:
+ ${0##*/} before-upgrade
+ ${0##*/} "pre system update"
+ ${0##*/} # prompts for description
+EOF
+}
+
+# Check for ZFS
+if ! command -v zfs &> /dev/null; then
+ echo "Error: zfs command not found. Is ZFS installed?"
+ exit 1
+fi
+
+# Check for root/sudo
+if [ "$EUID" -ne 0 ]; then
+ echo "Error: This script must be run as root (use sudo)"
+ exit 1
+fi
+
+# Parse arguments
+while getopts ":h" opt; do
+ case ${opt} in
+ h)
+ show_help
+ exit 0
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ show_help
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+# Get description from argument or prompt
+if [ $# -ge 1 ]; then
+ description="$*"
+else
+ read -r -p "Enter snapshot description: " description
+ if [ -z "$description" ]; then
+ echo "Error: Description cannot be empty"
+ exit 1
+ fi
+fi
+
+# Sanitize description: convert spaces to underscores, lowercase
+description=$(echo "$description" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
+
+# Validate description: only allow alphanumeric, hyphens, underscores
+if [[ ! "$description" =~ ^[a-z0-9_-]+$ ]]; then
+ echo "Error: Description contains invalid characters"
+ echo "Only letters, numbers, hyphens, and underscores are allowed"
+ echo "Sanitized input was: $description"
+ exit 1
+fi
+
+# Create snapshot name with timestamp prefix (matches pre-pacman format)
+timestamp=$(date +%Y-%m-%d_%H-%M-%S)
+snapshot_name="${timestamp}_${description}"
+
+# Get all pools
+pools=$(zpool list -H -o name)
+
+if [ -z "$pools" ]; then
+ echo "Error: No ZFS pools found"
+ exit 1
+fi
+
+echo "Creating snapshots with name: @${snapshot_name}"
+echo ""
+
+# Create recursive snapshots on each pool
+for pool in $pools; do
+ echo "Snapshotting pool: $pool"
+ if zfs snapshot -r "${pool}@${snapshot_name}"; then
+ echo " ✓ Created ${pool}@${snapshot_name} (recursive)"
+ else
+ echo " ✗ Failed to snapshot $pool"
+ fi
+done
+
+echo ""
+echo "Snapshot complete. Verify with: zfs list -t snapshot | grep $snapshot_name"
+
+# Update GRUB boot menu if grub-zfs-snap is available
+if command -v grub-zfs-snap &> /dev/null; then
+ echo ""
+ echo "Updating GRUB boot menu..."
+ grub-zfs-snap
+fi