| 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 |
}
|