Blame | Last modification | View Log | Download | RSS feed
#!/usr/bin/env perl
# Simplified BSD License (FreeBSD License)
#
# Copyright (c) 2025, Daily Data Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# resetSnapshots.pl - Reset a dataset to a previous snapshot state
#
# Usage: resetSnapshots.pl [--force|-f] [--verbose|-v] [--help|-h] [--version|-V] <snapshot_file> <dataset>
# --force, -f Actually destroy snapshots (default: dry-run)
# --verbose, -v Verbose output
# --help, -h Show help and exit
# --version, -V Show version and exit
# snapshot_file File containing previous snapshot list (one per line)
# dataset ZFS dataset to check (e.g., pool/dataset)
#
# Purpose:
# Reads a file containing a previous list of snapshots for a dataset,
# compares it with current snapshots, and destroys any new snapshots
# that have been added since the file was created. This is useful for
# resetting a test dataset to a previous state.
#
# Usage Example:
# Save current snapshot state
# zfs list -t snapshot -r pool/testdata > snapshots_before.txt
# ... do some testing that creates snapshots ...
# Preview what would be removed (dry-run)
# ./resetSnapshots.pl -v snapshots_before.txt pool/testdata
# Actually remove new snapshots
# ./resetSnapshots.pl -f -v snapshots_before.txt pool/testdata
#
# Author: R. W. Rodolico <rodo@dailydata.net>
# Created: December 2025
#
# Revision History:
# Version: 1.0 RWR 2025-12-19
# Initial release
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
our $VERSION = '1.0';
Getopt::Long::Configure('bundling');
my %opts;
GetOptions(\%opts,
'force|f', # force (actually perform destroys)
'verbose|v', # verbose
'help|h', # help
'version|V', # version
) or die "Error parsing command line options\n";
# Show version and exit
if ($opts{'version'} || $opts{'V'}) {
print "resetSnapshots version $VERSION\n";
exit 0;
}
# Show help and exit
if ($opts{'help'} || $opts{'h'}) {
print "Usage: resetSnapshots.pl [--force|-f] [--verbose|-v] [--version|-V] [--help|-h] <snapshot_file> <dataset>\n";
print " --force, -f actually destroy new snapshots (default: dry-run)\n";
print " --verbose, -v verbose logging\n";
print " --version, -V show version and exit\n";
print " --help, -h show this help and exit\n";
print " snapshot_file file containing previous snapshot list (one per line)\n";
print " dataset ZFS dataset to reset (e.g., pool/dataset)\n";
print "\n";
print "Purpose: Destroy snapshots that have been added since the snapshot_file was created.\n";
print " Used to reset a dataset to a previous snapshot state.\n";
exit 0;
}
my $FORCE = $opts{'force'} || $opts{'f'} || 0;
my $VERBOSE = $opts{'verbose'} || $opts{'v'} || 0;
sub logmsg {
print @_, "\n" if $VERBOSE;
}
# Get command line arguments
my $snapshot_file = shift @ARGV;
my $dataset = shift @ARGV;
# Validate arguments
unless ($snapshot_file && $dataset) {
die "Error: Both snapshot_file and dataset are required.\n" .
"Usage: resetSnapshots.pl [options] <snapshot_file> <dataset>\n" .
"Use --help for more information.\n";
}
unless (-e $snapshot_file) {
die "Error: Snapshot file '$snapshot_file' does not exist.\n";
}
unless (-r $snapshot_file) {
die "Error: Cannot read snapshot file '$snapshot_file'.\n";
}
logmsg("Reading previous snapshot list from: $snapshot_file");
logmsg("Target dataset: $dataset");
# Read previous snapshot list from file
my %previous_snapshots;
open my $fh, '<', $snapshot_file or die "Cannot open $snapshot_file: $!\n";
while (my $line = <$fh>) {
chomp $line;
next unless $line =~ /\S/; # Skip empty lines
# Extract snapshot name from various formats:
# - Full line from 'zfs list -t snapshot': "pool/dataset@snap 0B - 123K -"
# - Just the snapshot name: "pool/dataset@snap"
my $snap_name;
if ($line =~ /^(\S+@\S+)/) {
$snap_name = $1;
} else {
next; # Skip lines that don't look like snapshots
}
# Only include snapshots for the specified dataset
if ($snap_name =~ /^\Q$dataset\E@/ || $snap_name =~ /^\Q$dataset\E\//) {
$previous_snapshots{$snap_name} = 1;
logmsg("Previous: $snap_name");
}
}
close $fh;
my $prev_count = scalar keys %previous_snapshots;
logmsg("Found $prev_count previous snapshots for dataset $dataset");
# Get current snapshots for the dataset
logmsg("Fetching current snapshots for $dataset...");
my @current_snapshots = `zfs list -H -t snapshot -r -o name $dataset 2>&1`;
my $zfs_exit = $?;
if ($zfs_exit != 0) {
die "Error: Failed to list snapshots for dataset '$dataset'\n" .
"Make sure the dataset exists and you have permissions.\n" .
"ZFS output: @current_snapshots\n";
}
# Find snapshots that are new (in current but not in previous)
my @new_snapshots;
foreach my $snap (@current_snapshots) {
chomp $snap;
next unless $snap =~ /\S/;
next unless $snap =~ /@/; # Must contain @ to be a snapshot
unless ($previous_snapshots{$snap}) {
push @new_snapshots, $snap;
}
}
my $new_count = scalar @new_snapshots;
my $current_count = scalar @current_snapshots;
print "Current snapshots: $current_count\n";
print "Previous snapshots: $prev_count\n";
print "New snapshots to remove: $new_count\n";
if ($new_count == 0) {
print "No new snapshots found. Dataset is already in previous state.\n";
exit 0;
}
print "\nNew snapshots that will be destroyed:\n";
foreach my $snap (@new_snapshots) {
print " $snap\n";
}
if ($FORCE) {
print "\nDestroying new snapshots...\n";
my $destroyed = 0;
my $failed = 0;
foreach my $snap (@new_snapshots) {
logmsg("Destroying: $snap");
my $output = `zfs destroy $snap 2>&1`;
my $exit_code = $?;
if ($exit_code == 0) {
print " ✓ Destroyed: $snap\n" if $VERBOSE;
$destroyed++;
} else {
print " ✗ Failed to destroy: $snap\n";
print " Error: $output\n" if $output;
$failed++;
}
}
print "\nSummary:\n";
print " Destroyed: $destroyed\n";
print " Failed: $failed\n";
if ($failed > 0) {
print "\nWarning: Some snapshots could not be destroyed.\n";
exit 1;
} else {
print "\nDataset successfully reset to previous snapshot state.\n";
exit 0;
}
} else {
print "\nDry-run mode: No snapshots were destroyed.\n";
print "Use --force to actually destroy these snapshots.\n";
exit 0;
}