Subversion Repositories zfs_utils

Rev

Rev 55 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 55 Rev 60
Line 25... Line 25...
25
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
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
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.
27
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 
28
 
29
# cleanSnaps - detect and remove old ZFS snapshots named like YYYY-MM-DD-<N><d|w|m>
29
# cleanSnaps - detect and remove old ZFS snapshots named like YYYY-MM-DD-<N><d|w|m>
30
# Usage: cleanSnaps [-f] [-v] [-t <shift>] [-h]
30
# Usage: cleanSnaps [-f] [-v] [-t <shift>] [-V] [-h]
31
#   -f  actually destroy snapshots (use with care)
31
#   -f  actually destroy snapshots (use with care)
32
#   -v  verbose
32
#   -v  verbose
33
#   -t  time shift, simulate running at a different time. Examples:
33
#   -t  time shift, simulate running at a different time. Examples:
34
#       -t 2m   (run as if 2 months ago)
34
#       -t 2m   (run as if 2 months ago)
35
#       -t -4w  (run as if 4 weeks earlier)
35
#       -t -4w  (run as if 4 weeks earlier)
36
#       -t +3d  (run as if 3 days in the future)
36
#       -t +3d  (run as if 3 days in the future)
-
 
37
#   -V  show version and exit
37
#   -h  show this help and exit
38
#   -h  show this help and exit
-
 
39
#
-
 
40
# Author: R. W. Rodlico <rodo@dailydata.net>
-
 
41
# Created: December 2025
-
 
42
#
-
 
43
# Revision History:
-
 
44
# Version: 1.0 RWR 2025-12-18
-
 
45
# Initial release with time-shift simulation and snapshot retention logic
-
 
46
#
-
 
47
# Version: 1.1 RWR 2025-12-18
-
 
48
# Added --unmatched/-u flag to clean snapshots that don't match the retention pattern regex
-
 
49
# When --unmatched is used, ONLY unmatched snapshots are removed (normal retention logic is skipped)
38
 
50
 
39
use strict;
51
use strict;
40
use warnings;
52
use warnings;
-
 
53
 
-
 
54
our $VERSION = '1.1';
-
 
55
 
41
use Getopt::Long qw(GetOptions);
56
use Getopt::Long qw(GetOptions);
42
use Time::Piece;
57
use Time::Piece;
43
 
58
 
44
# Allow short option bundling (e.g. -fv) and accept long names
59
# Allow short option bundling (e.g. -fv) and accept long names
45
Getopt::Long::Configure('bundling');
60
Getopt::Long::Configure('bundling');
Line 47... Line 62...
47
my %opts;
62
my %opts;
48
GetOptions(\%opts,
63
GetOptions(\%opts,
49
    'force|f',        # force (actually perform destroys)
64
    'force|f',        # force (actually perform destroys)
50
    'verbose|v',      # verbose
65
    'verbose|v',      # verbose
51
    'help|h',         # help
66
    'help|h',         # help
-
 
67
    'version|V',      # version
52
    'timeshift|t=s',  # time shift string (alias -t)
68
    'timeshift|t=s',  # time shift string (alias -t)
-
 
69
    'unmatched|u',    # clean snapshots that don't match the pattern
53
) or die "Error parsing command line options\n";
70
) or die "Error parsing command line options\n";
54
 
71
 
55
# Normalize options: default behaviour is dry-run unless --force is specified.
72
# Normalize options: default behaviour is dry-run unless --force is specified.
56
my $FORCE   = (defined $opts{'force'} || defined $opts{'f'}) ? 1 : 0;
73
my $FORCE     = (defined $opts{'force'} || defined $opts{'f'}) ? 1 : 0;
57
my $VERBOSE = (defined $opts{'verbose'} || defined $opts{'v'}) ? 1 : 0;
74
my $VERBOSE   = (defined $opts{'verbose'} || defined $opts{'v'}) ? 1 : 0;
-
 
75
my $UNMATCHED = (defined $opts{'unmatched'} || defined $opts{'u'}) ? 1 : 0;
58
my $timeshift = defined $opts{'timeshift'} ? $opts{'timeshift'} : $opts{'t'};
76
my $timeshift = defined $opts{'timeshift'} ? $opts{'timeshift'} : $opts{'t'};
59
 
77
 
60
my $ZFS_CMD = $ENV{ZFS_CMD} // 'zfs';
78
my $ZFS_CMD = $ENV{ZFS_CMD} // 'zfs';
61
 
79
 
62
sub logmsg { print @_, "\n" if $VERBOSE }
80
sub logmsg { print @_, "\n" if $VERBOSE }
63
 
81
 
-
 
82
# show version and exit
-
 
83
if ($opts{'version'} || $opts{'V'}) {
-
 
84
    print "cleanSnaps version $VERSION\n";
-
 
85
    exit 0;
-
 
86
}
-
 
87
 
64
# show help and exit
88
# show help and exit
65
if ($opts{'help'} || $opts{'h'}) {
89
if ($opts{'help'} || $opts{'h'}) {
66
    print "Usage: cleanSnaps [--force|-f] [--verbose|-v] [--timeshift|-t <shift>] [--help|-h] [pool]\n";
90
    print "Usage: cleanSnaps [--force|-f] [--verbose|-v] [--timeshift|-t <shift>] [--unmatched|-u] [--version|-V] [--help|-h] [pool]\n";
67
    print "  --force, -f         actually destroy snapshots (default: dry-run)\n";
91
    print "  --force, -f         actually destroy snapshots (default: dry-run)\n";
68
    print "  --verbose, -v       verbose logging\n";
92
    print "  --verbose, -v       verbose logging\n";
69
    print "  --timeshift, -t     time shift, simulate running at a previous time (examples: 2m, -4w, +3d)\n";
93
    print "  --timeshift, -t     time shift, simulate running at a previous time (examples: 2m, -4w, +3d)\n";
-
 
94
    print "  --unmatched, -u     ONLY clean snapshots that don't match the retention pattern (skips normal retention)\n";
-
 
95
    print "  --version, -V       show version and exit\n";
70
    print "  --help, -h          show this help and exit\n";
96
    print "  --help, -h          show this help and exit\n";
71
    exit 0;
97
    exit 0;
72
}
98
}
73
 
99
 
74
# compute simulated "now" if the user requested a time shift
100
# compute simulated "now" if the user requested a time shift
Line 126... Line 152...
126
    #   pool@prefix_2025-12-01T12:30:00_foo_3h
152
    #   pool@prefix_2025-12-01T12:30:00_foo_3h
127
    #   pool@2025-12-01_foo_2w
153
    #   pool@2025-12-01_foo_2w
128
    # allow time separators of ':' or '.' and optional underscore or space before time
154
    # allow time separators of ':' or '.' and optional underscore or space before time
129
    unless ($snapname =~ m/.*?(\d{4}-\d{2}-\d{2}(?:[T _]\d{2}[:\.]\d{2}(?:[:\.]\d{2})?)?)(?:.*?)(\d+)([smhdwy])$/) {
155
    unless ($snapname =~ m/.*?(\d{4}-\d{2}-\d{2}(?:[T _]\d{2}[:\.]\d{2}(?:[:\.]\d{2})?)?)(?:.*?)(\d+)([smhdwy])$/) {
130
        logmsg("snapshot does not match ...<date/time>...<N><s|m|h|d|w|y>: $snap");
156
        logmsg("snapshot does not match ...<date/time>...<N><s|m|h|d|w|y>: $snap");
-
 
157
        # if --unmatched flag is set, mark this snapshot for removal
-
 
158
        if ($UNMATCHED) {
-
 
159
            push @candidates, $snap;
-
 
160
            logmsg("marked for removal (unmatched pattern): $snap") if $VERBOSE;
-
 
161
        }
-
 
162
        next;
-
 
163
    }
-
 
164
    
-
 
165
    # If --unmatched flag is set, skip normal retention processing for matched snapshots
-
 
166
    if ($UNMATCHED) {
-
 
167
        logmsg("skipping retention check (matched pattern): $snap") if $VERBOSE;
131
        next;
168
        next;
132
    }
169
    }
-
 
170
    
133
    my ($snap_date, $num, $unit) = ($1, $2, $3);
171
    my ($snap_date, $num, $unit) = ($1, $2, $3);
134
 
172
 
135
    # parse snapshot date/time using Time::Piece; accept several common formats
173
    # parse snapshot date/time using Time::Piece; accept several common formats
136
    my $snap_epoch;
174
    my $snap_epoch;
137
    my @fmts = (
175
    my @fmts = (
Line 154... Line 192...
154
        last if $parsed;
192
        last if $parsed;
155
    }
193
    }
156
    # didn't parse successfully, show message and skip
194
    # didn't parse successfully, show message and skip
157
    unless ($parsed) {
195
    unless ($parsed) {
158
        logmsg("failed to parse date/time '$snap_date' for $snap");
196
        logmsg("failed to parse date/time '$snap_date' for $snap");
-
 
197
        # if --unmatched flag is set, mark this snapshot for removal (pattern matched but date parsing failed)
-
 
198
        if ($UNMATCHED) {
-
 
199
            push @candidates, $snap;
-
 
200
            logmsg("marked for removal (date parse failed): $snap") if $VERBOSE;
-
 
201
        }
159
        next;
202
        next;
160
    }
203
    }
161
 
204
 
162
    # compute retention in seconds according to unit (s,m,h,d,w,m,y)
205
    # compute retention in seconds according to unit (s,m,h,d,w,m,y)
163
    my $retention_seconds;
206
    my $retention_seconds;