Subversion Repositories zfs_utils

Rev

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