Subversion Repositories zfs_utils

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
47 rodolico 1
#!/usr/bin/env perl
2
 
48 rodolico 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
 
47 rodolico 29
# cleanSnaps - detect and remove old ZFS snapshots named like YYYY-MM-DD-<N><d|w|m>
48 rodolico 30
# Usage: cleanSnaps [-n] [-f] [-v] [-t <shift>] [-h]
47 rodolico 31
#   -n  dry-run (default)
32
#   -f  actually destroy snapshots
33
#   -v  verbose
48 rodolico 34
#   -t  time shift, simulate running at a different time. Examples:
35
#       -t 2m   (run as if 2 months ago)
36
#       -t -4w  (run as if 4 weeks earlier)
37
#       -t +3d  (run as if 3 days in the future)
38
#   -h  show this help and exit
47 rodolico 39
 
40
use strict;
41
use warnings;
42
use Getopt::Std;
43
use Time::Piece;
44
 
45
my %opts;
48 rodolico 46
getopts('nfvht:', \%opts);
47 rodolico 47
 
48
my $DRY_RUN = $opts{f} ? 0 : 1;    # default dry-run, -f disables dry-run
49
my $FORCE = $opts{f} ? 1 : 0;
50
my $VERBOSE = $opts{v} ? 1 : 0;
51
 
52
my $ZFS_CMD = $ENV{ZFS_CMD} // 'zfs';
53
 
54
sub logmsg { print @_, "\n" if $VERBOSE }
55
 
48 rodolico 56
# show help and exit
57
if ($opts{h}) {
58
    print "Usage: cleanSnaps [-n] [-f] [-v] [-t <shift>] [-h]\n";
59
    print "  -n  dry-run (default)\n";
60
    print "  -f  actually destroy snapshots\n";
61
    print "  -v  verbose\n";
62
    print "  -t  time shift, simulate running at a different time (examples: 2m, -4w, +3d)\n";
63
    print "  -h  show this help and exit\n";
64
    exit 0;
65
}
66
 
67
# compute simulated "now" if the user requested a time shift
47 rodolico 68
my $now = time();
48 rodolico 69
if (defined $opts{t} && $opts{t} =~ /^(?:([+-]?)(\d+)([smhdwy]))$/i) {
70
    my ($sign, $num, $unit) = ($1, $2, lc $3);
71
    # default behavior: no leading sign => treat as negative (simulate running in the past)
72
    $sign = '-' unless defined $sign && $sign ne '';
73
    my %unit_secs = (
74
        s => 1,
75
        m => 30 * 86400,   # months approximated as 30 days to match snapshot 'm' semantics
76
        h => 3600,
77
        d => 86400,
78
        w => 7 * 86400,
79
        y => 365 * 86400,
80
    );
81
    if (!exists $unit_secs{$unit}) {
82
        die "Invalid time unit '$unit' in -t option; use s,m,h,d,w,y\n";
83
    }
84
    my $shift_seconds = $num * $unit_secs{$unit};
85
    $shift_seconds = -$shift_seconds if $sign eq '-';
86
    $now += $shift_seconds;
87
    logmsg("Simulating current time with shift $opts{t}; adjusted now -> " . scalar(localtime($now)) );
88
}
47 rodolico 89
my @candidates;
90
 
91
open my $fh, '-|', "$ZFS_CMD list -H -o name -t snapshot" or die "Failed to run $ZFS_CMD: $!";
92
while (my $snap = <$fh>) {
93
    chomp $snap;
94
    next unless defined $snap && $snap =~ /\S/;
95
 
96
    unless ($snap =~ /@/) {
97
        logmsg("skipping invalid snapshot name: $snap");
98
        next;
99
    }
100
    my ($dataset, $snapname) = split /@/, $snap, 2;
101
 
49 rodolico 102
    # match date + retention tag (allow optional text between date and TTL).
103
    # Examples matched: 2025-12-01-3d, 2025-12-01-snap-3d, 2025-12-01_foo_bar_2w
104
    unless ($snapname =~ m/^(\d{4}-\d{2}-\d{2})(?:.*?)(\d+)([dwmy])$/) {
105
        logmsg("snapshot does not match YYYY-MM-DD ... <N><d|w|m|y>: $snap");
47 rodolico 106
        next;
107
    }
108
    my ($snap_date, $num, $unit) = ($1, $2, $3);
109
 
110
    # parse snapshot date using Time::Piece
111
    my $snap_epoch;
112
    eval {
113
        my $tp = Time::Piece->strptime($snap_date, '%Y-%m-%d');
114
        $snap_epoch = $tp->epoch;
115
        1;
116
    } or do {
117
        logmsg("failed to parse date $snap_date for $snap");
118
        next;
119
    };
120
 
121
    my $days;
122
    if ($unit eq 'd') { $days = $num }
123
    elsif ($unit eq 'w') { $days = $num * 7 }
124
    elsif ($unit eq 'm') { $days = $num * 30 }
125
    elsif ($unit eq 'y') { $days = $num * 365}
126
    else { logmsg("unknown unit $unit for $snap"); next }
127
 
128
    my $retention_seconds = $days * 86400;
129
    my $age = $now - $snap_epoch;
130
    if ($age > $retention_seconds) {
131
        push @candidates, $snap;
132
    } else {
133
        logmsg("keep: $snap (age " . int($age/86400) . "d <= ${days}d)");
134
    }
135
}
136
close $fh;
137
 
138
if (!@candidates) {
139
    print "No snapshots to remove.\n";
140
    exit 0;
141
}
142
 
143
printf "Snapshots to remove (%d):\n", scalar @candidates;
144
for my $s (@candidates) { printf "  %s\n", $s }
145
 
146
if ($DRY_RUN) {
147
    print "\nDry-run: no snapshots were destroyed. Use -f to actually remove them.\n";
148
    exit 0;
149
}
150
 
151
# actual removal
152
my $failed = 0;
153
for my $s (@candidates) {
154
    printf "Destroying: %s\n", $s;
155
    my $rc = system($ZFS_CMD, 'destroy', $s);
156
    if ($rc != 0) {
157
        printf STDERR "Failed to destroy %s\n", $s;
158
        $failed = 1;
159
    }
160
}
161
 
162
if ($failed) {
163
    print STDERR "One or more destroys failed.\n";
164
    exit 1;
165
}
166
 
167
print "Done.\n";