| 47 |
rodolico |
1 |
#!/usr/bin/env perl
|
|
|
2 |
|
|
|
3 |
# cleanSnaps - detect and remove old ZFS snapshots named like YYYY-MM-DD-<N><d|w|m>
|
|
|
4 |
# Usage: cleanSnaps [-n] [-f] [-v]
|
|
|
5 |
# -n dry-run (default)
|
|
|
6 |
# -f actually destroy snapshots
|
|
|
7 |
# -v verbose
|
|
|
8 |
|
|
|
9 |
use strict;
|
|
|
10 |
use warnings;
|
|
|
11 |
use Getopt::Std;
|
|
|
12 |
use Time::Piece;
|
|
|
13 |
|
|
|
14 |
my %opts;
|
|
|
15 |
getopts('nfv', \%opts);
|
|
|
16 |
|
|
|
17 |
my $DRY_RUN = $opts{f} ? 0 : 1; # default dry-run, -f disables dry-run
|
|
|
18 |
my $FORCE = $opts{f} ? 1 : 0;
|
|
|
19 |
my $VERBOSE = $opts{v} ? 1 : 0;
|
|
|
20 |
|
|
|
21 |
my $ZFS_CMD = $ENV{ZFS_CMD} // 'zfs';
|
|
|
22 |
|
|
|
23 |
sub logmsg { print @_, "\n" if $VERBOSE }
|
|
|
24 |
|
|
|
25 |
# gather snapshots
|
|
|
26 |
my $now = time();
|
|
|
27 |
my @candidates;
|
|
|
28 |
|
|
|
29 |
open my $fh, '-|', "$ZFS_CMD list -H -o name -t snapshot" or die "Failed to run $ZFS_CMD: $!";
|
|
|
30 |
while (my $snap = <$fh>) {
|
|
|
31 |
chomp $snap;
|
|
|
32 |
next unless defined $snap && $snap =~ /\S/;
|
|
|
33 |
|
|
|
34 |
unless ($snap =~ /@/) {
|
|
|
35 |
logmsg("skipping invalid snapshot name: $snap");
|
|
|
36 |
next;
|
|
|
37 |
}
|
|
|
38 |
my ($dataset, $snapname) = split /@/, $snap, 2;
|
|
|
39 |
|
|
|
40 |
# match date + retention tag
|
|
|
41 |
unless ($snapname =~ m/^(\d{4}-\d{2}-\d{2}).*(\d+)([dwm])$/) {
|
|
|
42 |
logmsg("snapshot does not match YYYY-MM-DD-<N><d|w|m>: $snap");
|
|
|
43 |
next;
|
|
|
44 |
}
|
|
|
45 |
my ($snap_date, $num, $unit) = ($1, $2, $3);
|
|
|
46 |
|
|
|
47 |
# parse snapshot date using Time::Piece
|
|
|
48 |
my $snap_epoch;
|
|
|
49 |
eval {
|
|
|
50 |
my $tp = Time::Piece->strptime($snap_date, '%Y-%m-%d');
|
|
|
51 |
$snap_epoch = $tp->epoch;
|
|
|
52 |
1;
|
|
|
53 |
} or do {
|
|
|
54 |
logmsg("failed to parse date $snap_date for $snap");
|
|
|
55 |
next;
|
|
|
56 |
};
|
|
|
57 |
|
|
|
58 |
my $days;
|
|
|
59 |
if ($unit eq 'd') { $days = $num }
|
|
|
60 |
elsif ($unit eq 'w') { $days = $num * 7 }
|
|
|
61 |
elsif ($unit eq 'm') { $days = $num * 30 }
|
|
|
62 |
elsif ($unit eq 'y') { $days = $num * 365}
|
|
|
63 |
else { logmsg("unknown unit $unit for $snap"); next }
|
|
|
64 |
|
|
|
65 |
my $retention_seconds = $days * 86400;
|
|
|
66 |
my $age = $now - $snap_epoch;
|
|
|
67 |
if ($age > $retention_seconds) {
|
|
|
68 |
push @candidates, $snap;
|
|
|
69 |
} else {
|
|
|
70 |
logmsg("keep: $snap (age " . int($age/86400) . "d <= ${days}d)");
|
|
|
71 |
}
|
|
|
72 |
}
|
|
|
73 |
close $fh;
|
|
|
74 |
|
|
|
75 |
if (!@candidates) {
|
|
|
76 |
print "No snapshots to remove.\n";
|
|
|
77 |
exit 0;
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
printf "Snapshots to remove (%d):\n", scalar @candidates;
|
|
|
81 |
for my $s (@candidates) { printf " %s\n", $s }
|
|
|
82 |
|
|
|
83 |
if ($DRY_RUN) {
|
|
|
84 |
print "\nDry-run: no snapshots were destroyed. Use -f to actually remove them.\n";
|
|
|
85 |
exit 0;
|
|
|
86 |
}
|
|
|
87 |
|
|
|
88 |
# actual removal
|
|
|
89 |
my $failed = 0;
|
|
|
90 |
for my $s (@candidates) {
|
|
|
91 |
printf "Destroying: %s\n", $s;
|
|
|
92 |
my $rc = system($ZFS_CMD, 'destroy', $s);
|
|
|
93 |
if ($rc != 0) {
|
|
|
94 |
printf STDERR "Failed to destroy %s\n", $s;
|
|
|
95 |
$failed = 1;
|
|
|
96 |
}
|
|
|
97 |
}
|
|
|
98 |
|
|
|
99 |
if ($failed) {
|
|
|
100 |
print STDERR "One or more destroys failed.\n";
|
|
|
101 |
exit 1;
|
|
|
102 |
}
|
|
|
103 |
|
|
|
104 |
print "Done.\n";
|