summaryrefslogtreecommitdiff
path: root/dotfiles/system/.local/bin/zfsrollback
blob: f1365e601cc42dd3ae4914fab500326bd024eee9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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