Subversion Repositories zfs_utils

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
62 rodolico 1
#!/usr/bin/env perl
2
 
3
# Simplified BSD License (FreeBSD License)
4
#
5
# Copyright (c) 2025, Daily Data Inc.
6
# All rights reserved.
7
#
8
# Redistribution and use in source and binary forms, with or without
9
# modification, are permitted provided that the following conditions are met:
10
#
11
# 1. Redistributions of source code must retain the above copyright notice, this
12
#    list of conditions and the following disclaimer.
13
#
14
# 2. Redistributions in binary form must reproduce the above copyright notice,
15
#    this list of conditions and the following disclaimer in the documentation
16
#    and/or other materials provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 
29
# resetSnapshots.pl - Reset a dataset to a previous snapshot state
30
#
31
# Usage: resetSnapshots.pl [--force|-f] [--verbose|-v] [--help|-h] [--version|-V] <snapshot_file> <dataset>
32
#   --force, -f         Actually destroy snapshots (default: dry-run)
33
#   --verbose, -v       Verbose output
34
#   --help, -h          Show help and exit
35
#   --version, -V       Show version and exit
36
#   snapshot_file       File containing previous snapshot list (one per line)
37
#   dataset             ZFS dataset to check (e.g., pool/dataset)
38
#
39
# Purpose:
40
#   Reads a file containing a previous list of snapshots for a dataset,
41
#   compares it with current snapshots, and destroys any new snapshots
42
#   that have been added since the file was created. This is useful for
43
#   resetting a test dataset to a previous state.
44
#
45
# Usage Example:
46
# Save current snapshot state
47
# zfs list -t snapshot -r pool/testdata > snapshots_before.txt
48
# ... do some testing that creates snapshots ...
49
# Preview what would be removed (dry-run)
50
# ./resetSnapshots.pl -v snapshots_before.txt pool/testdata
51
# Actually remove new snapshots
52
# ./resetSnapshots.pl -f -v snapshots_before.txt pool/testdata
53
#
54
# Author: R. W. Rodolico <rodo@dailydata.net>
55
# Created: December 2025
56
#
57
# Revision History:
58
# Version: 1.0 RWR 2025-12-19
59
# Initial release
60
 
61
use strict;
62
use warnings;
63
use Getopt::Long qw(GetOptions);
64
 
65
our $VERSION = '1.0';
66
 
67
Getopt::Long::Configure('bundling');
68
 
69
my %opts;
70
GetOptions(\%opts,
71
    'force|f',      # force (actually perform destroys)
72
    'verbose|v',    # verbose
73
    'help|h',       # help
74
    'version|V',    # version
75
) or die "Error parsing command line options\n";
76
 
77
# Show version and exit
78
if ($opts{'version'} || $opts{'V'}) {
79
    print "resetSnapshots version $VERSION\n";
80
    exit 0;
81
}
82
 
83
# Show help and exit
84
if ($opts{'help'} || $opts{'h'}) {
85
    print "Usage: resetSnapshots.pl [--force|-f] [--verbose|-v] [--version|-V] [--help|-h] <snapshot_file> <dataset>\n";
86
    print "  --force, -f         actually destroy new snapshots (default: dry-run)\n";
87
    print "  --verbose, -v       verbose logging\n";
88
    print "  --version, -V       show version and exit\n";
89
    print "  --help, -h          show this help and exit\n";
90
    print "  snapshot_file       file containing previous snapshot list (one per line)\n";
91
    print "  dataset             ZFS dataset to reset (e.g., pool/dataset)\n";
92
    print "\n";
93
    print "Purpose: Destroy snapshots that have been added since the snapshot_file was created.\n";
94
    print "         Used to reset a dataset to a previous snapshot state.\n";
95
    exit 0;
96
}
97
 
98
my $FORCE   = $opts{'force'} || $opts{'f'} || 0;
99
my $VERBOSE = $opts{'verbose'} || $opts{'v'} || 0;
100
 
101
sub logmsg {
102
    print @_, "\n" if $VERBOSE;
103
}
104
 
105
# Get command line arguments
106
my $snapshot_file = shift @ARGV;
107
my $dataset = shift @ARGV;
108
 
109
# Validate arguments
110
unless ($snapshot_file && $dataset) {
111
    die "Error: Both snapshot_file and dataset are required.\n" .
112
        "Usage: resetSnapshots.pl [options] <snapshot_file> <dataset>\n" .
113
        "Use --help for more information.\n";
114
}
115
 
116
unless (-e $snapshot_file) {
117
    die "Error: Snapshot file '$snapshot_file' does not exist.\n";
118
}
119
 
120
unless (-r $snapshot_file) {
121
    die "Error: Cannot read snapshot file '$snapshot_file'.\n";
122
}
123
 
124
logmsg("Reading previous snapshot list from: $snapshot_file");
125
logmsg("Target dataset: $dataset");
126
 
127
# Read previous snapshot list from file
128
my %previous_snapshots;
129
open my $fh, '<', $snapshot_file or die "Cannot open $snapshot_file: $!\n";
130
while (my $line = <$fh>) {
131
    chomp $line;
132
    next unless $line =~ /\S/;  # Skip empty lines
133
 
134
    # Extract snapshot name from various formats:
135
    # - Full line from 'zfs list -t snapshot': "pool/dataset@snap   0B   -   123K   -"
136
    # - Just the snapshot name: "pool/dataset@snap"
137
    my $snap_name;
138
    if ($line =~ /^(\S+@\S+)/) {
139
        $snap_name = $1;
140
    } else {
141
        next;  # Skip lines that don't look like snapshots
142
    }
143
 
144
    # Only include snapshots for the specified dataset
145
    if ($snap_name =~ /^\Q$dataset\E@/ || $snap_name =~ /^\Q$dataset\E\//) {
146
        $previous_snapshots{$snap_name} = 1;
147
        logmsg("Previous: $snap_name");
148
    }
149
}
150
close $fh;
151
 
152
my $prev_count = scalar keys %previous_snapshots;
153
logmsg("Found $prev_count previous snapshots for dataset $dataset");
154
 
155
# Get current snapshots for the dataset
156
logmsg("Fetching current snapshots for $dataset...");
157
my @current_snapshots = `zfs list -H -t snapshot -r -o name $dataset 2>&1`;
158
my $zfs_exit = $?;
159
 
160
if ($zfs_exit != 0) {
161
    die "Error: Failed to list snapshots for dataset '$dataset'\n" .
162
        "Make sure the dataset exists and you have permissions.\n" .
163
        "ZFS output: @current_snapshots\n";
164
}
165
 
166
# Find snapshots that are new (in current but not in previous)
167
my @new_snapshots;
168
foreach my $snap (@current_snapshots) {
169
    chomp $snap;
170
    next unless $snap =~ /\S/;
171
    next unless $snap =~ /@/;  # Must contain @ to be a snapshot
172
 
173
    unless ($previous_snapshots{$snap}) {
174
        push @new_snapshots, $snap;
175
    }
176
}
177
 
178
my $new_count = scalar @new_snapshots;
179
my $current_count = scalar @current_snapshots;
180
 
181
print "Current snapshots: $current_count\n";
182
print "Previous snapshots: $prev_count\n";
183
print "New snapshots to remove: $new_count\n";
184
 
185
if ($new_count == 0) {
186
    print "No new snapshots found. Dataset is already in previous state.\n";
187
    exit 0;
188
}
189
 
190
print "\nNew snapshots that will be destroyed:\n";
191
foreach my $snap (@new_snapshots) {
192
    print "  $snap\n";
193
}
194
 
195
if ($FORCE) {
196
    print "\nDestroying new snapshots...\n";
197
    my $destroyed = 0;
198
    my $failed = 0;
199
 
200
    foreach my $snap (@new_snapshots) {
201
        logmsg("Destroying: $snap");
202
        my $output = `zfs destroy $snap 2>&1`;
203
        my $exit_code = $?;
204
 
205
        if ($exit_code == 0) {
206
            print "  ✓ Destroyed: $snap\n" if $VERBOSE;
207
            $destroyed++;
208
        } else {
209
            print "  ✗ Failed to destroy: $snap\n";
210
            print "    Error: $output\n" if $output;
211
            $failed++;
212
        }
213
    }
214
 
215
    print "\nSummary:\n";
216
    print "  Destroyed: $destroyed\n";
217
    print "  Failed: $failed\n";
218
 
219
    if ($failed > 0) {
220
        print "\nWarning: Some snapshots could not be destroyed.\n";
221
        exit 1;
222
    } else {
223
        print "\nDataset successfully reset to previous snapshot state.\n";
224
        exit 0;
225
    }
226
} else {
227
    print "\nDry-run mode: No snapshots were destroyed.\n";
228
    print "Use --force to actually destroy these snapshots.\n";
229
    exit 0;
230
}