summaryrefslogtreecommitdiff
path: root/dotfiles
diff options
context:
space:
mode:
Diffstat (limited to 'dotfiles')
-rwxr-xr-xdotfiles/system/.local/bin/zfsrollback165
-rwxr-xr-xdotfiles/system/.local/bin/zfssnapshot103
2 files changed, 268 insertions, 0 deletions
diff --git a/dotfiles/system/.local/bin/zfsrollback b/dotfiles/system/.local/bin/zfsrollback
new file mode 100755
index 0000000..f1365e6
--- /dev/null
+++ b/dotfiles/system/.local/bin/zfsrollback
@@ -0,0 +1,165 @@
+#!/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
+ selected=$(echo "$snapshots" | 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
+ unique_snaps=$(echo "$snapshots" | sed 's/.*@//' | sort -u)
+
+ 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 ""
+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."
+else
+ echo "Rollback completed with $failed failure(s)"
+ exit 1
+fi
diff --git a/dotfiles/system/.local/bin/zfssnapshot b/dotfiles/system/.local/bin/zfssnapshot
new file mode 100755
index 0000000..b715722
--- /dev/null
+++ b/dotfiles/system/.local/bin/zfssnapshot
@@ -0,0 +1,103 @@
+#!/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-description
+Only alphanumeric characters, hyphens, and underscores are allowed in descriptions.
+Spaces are converted to hyphens 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 hyphens, 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 date prefix
+date_prefix=$(date +%Y-%m-%d)
+snapshot_name="${date_prefix}-${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"