Subversion Repositories zfs_utils

Rev

Rev 54 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
51 rodolico 1
#!/usr/bin/env perl
2
use strict;
3
use warnings;
4
use File::Temp qw(tempdir);
5
use File::Spec;
6
use Cwd qw(abs_path);
54 rodolico 7
use File::Basename qw(dirname);
8
use Time::Piece;
51 rodolico 9
 
10
my $td = tempdir(CLEANUP => 1);
11
 
54 rodolico 12
# location of the cleanSnaps script (this test lives in the same directory as the test file)
13
my $here = dirname(abs_path(__FILE__));
51 rodolico 14
my $clean = File::Spec->catfile($here, 'cleanSnaps');
15
unless (-e $clean) {
16
    die "Could not find cleanSnaps at $clean";
17
}
18
 
19
# If a snapshot list filename was provided on the command line, use that file as the
20
# source of snapshot names. Otherwise, create a fake zfs-list script with sample names.
21
my $snaplist_file = shift @ARGV;
22
my $fake;
23
if ($snaplist_file && -e $snaplist_file) {
24
    $fake = File::Spec->catfile($td, 'fake_zfs_list.sh');
25
    open my $fh, '>', $fake or die $!;
26
    print $fh "#!/bin/sh\ncat \"$snaplist_file\"\n";
27
    close $fh;
28
    chmod 0755, $fake;
29
} else {
30
    $fake = File::Spec->catfile($td, 'fake_zfs_list.sh');
31
    open my $fh, '>', $fake or die $!;
32
    print $fh <<'EOF';
33
#!/bin/sh
34
cat <<'SNAPS'
35
pool/fs@2025-01-01-3d
36
pool/fs@2025-12-10-3d
37
pool/fs@2025-12-14-2d
38
pool/fs@2025-11-01-snap-4w
39
pool/fs@2025-12-01_foo_1m
40
pool/fs@badname
41
pool/fs@2025-12-01T12:00:00_1h
42
pool/fs@prefix_2025-12-01 13:00:00_2h
43
SNAPS
44
EOF
45
    close $fh;
46
    chmod 0755, $fake;
47
}
48
 
49
# Run cleanSnaps in dry-run verbose mode with the fake ZFS command
50
local %ENV = %ENV;
51
$ENV{ZFS_CMD} = $fake;
52
 
53
# execute and capture output (use perl from the environment if available)
54
my $out;
55
if ($ENV{PERL}) {
54 rodolico 56
    $out = qx{$ENV{PERL} -I. "$clean" -v 2>&1};
51 rodolico 57
} else {
54 rodolico 58
    $out = qx{perl -I. "$clean" -v 2>&1};
51 rodolico 59
}
60
 
61
print "=== cleanSnaps output ===\n$out\n";
62
 
63
my @lines = split /\n/, $out;
64
my @removed = map { s/^\s+//; $_ } grep { /^\s+\S+@/ } @lines;
65
 
66
my %removed = map { $_ => 1 } @removed;
67
 
54 rodolico 68
# Compute expected removals using the same rules as cleanSnaps so the test is
69
# robust against clock/time differences. Read the sample snapshot list by
70
# executing the fake zfs list script and parse each snapshot name.
71
my $snap_output = qx{$fake};
72
my @snap_lines = split /\n/, $snap_output;
51 rodolico 73
 
54 rodolico 74
my @fmts = (
75
    '%Y-%m-%d %H:%M:%S',
76
    '%Y-%m-%dT%H:%M:%S',
77
    '%Y-%m-%d %H:%M',
78
    '%Y-%m-%d_%H.%M.%S',
79
    '%Y-%m-%d_%H.%M',
80
    '%Y-%m-%d',
51 rodolico 81
);
82
 
54 rodolico 83
sub parse_and_should_remove {
84
    my ($snap, $now) = @_;
85
    # expect full snapshot name like pool/fs@name
86
    return 0 unless $snap =~ /@/;
87
    my (undef, $snapname) = split /@/, $snap, 2;
88
    return 0 unless $snapname =~ m/.*?(\d{4}-\d{2}-\d{2}(?:[T _]\d{2}[:\.]\d{2}(?:[:\.]\d{2})?)?)(?:.*?)(\d+)([smhdwy])$/;
89
    my ($snap_date, $num, $unit) = ($1, $2, $3);
90
    # parse date/time
91
    my $snap_epoch;
92
    my $parsed = 0;
93
    for my $fmt (@fmts) {
94
        eval {
95
            my $tp = Time::Piece->strptime($snap_date, $fmt);
96
            $snap_epoch = $tp->epoch;
97
            $parsed = 1;
98
            1;
99
        };
100
        last if $parsed;
101
    }
102
    return 0 unless $parsed;
103
    my $retention_seconds;
104
    if ($unit eq 's') { $retention_seconds = $num }
105
    elsif ($unit eq 'm') { $retention_seconds = $num * 30 * 86400 }
106
    elsif ($unit eq 'h') { $retention_seconds = $num * 3600 }
107
    elsif ($unit eq 'd') { $retention_seconds = $num * 86400 }
108
    elsif ($unit eq 'w') { $retention_seconds = $num * 7 * 86400 }
109
    elsif ($unit eq 'y') { $retention_seconds = $num * 365 * 86400 }
110
    else { return 0 }
111
    my $age = $now - $snap_epoch;
112
    return $age > $retention_seconds ? 1 : 0;
113
}
114
 
115
my $now = time();
116
my %expected_removed;
117
for my $line (@snap_lines) {
118
    next unless $line =~ /@/;
119
    my $should = parse_and_should_remove($line, $now);
120
    $expected_removed{$line} = $should if $should;
121
}
122
 
51 rodolico 123
my $ok = 1;
54 rodolico 124
for my $snap (keys %expected_removed) {
125
    unless ($removed{$snap}) {
126
        print "FAIL: expected $snap to be marked for removal\n";
51 rodolico 127
        $ok = 0;
128
    }
129
}
54 rodolico 130
for my $line (@snap_lines) {
131
    next unless $line =~ /@/;
132
    unless ($expected_removed{$line}) {
133
        if ($removed{$line}) {
134
            print "FAIL: expected $line to be kept but it was marked for removal\n";
135
            $ok = 0;
136
        }
51 rodolico 137
    }
138
}
139
 
140
if ($ok) {
60 rodolico 141
    print "TEST 1 (retention logic) PASS\n";
51 rodolico 142
} else {
60 rodolico 143
    print "TEST 1 (retention logic) FAIL\n";
51 rodolico 144
    exit 1;
145
}
60 rodolico 146
 
147
# TEST 2: Test the --unmatched flag
148
print "\n=== Testing --unmatched flag ===\n";
149
 
150
# Run cleanSnaps with --unmatched flag to remove snapshots that don't match the pattern
151
my $out_unmatched;
152
if ($ENV{PERL}) {
153
    $out_unmatched = qx{$ENV{PERL} -I. "$clean" -v -u 2>&1};
154
} else {
155
    $out_unmatched = qx{perl -I. "$clean" -v -u 2>&1};
156
}
157
 
158
print "=== cleanSnaps --unmatched output ===\n$out_unmatched\n";
159
 
160
my @lines_unmatched = split /\n/, $out_unmatched;
161
my @removed_unmatched = map { s/^\s+//; $_ } grep { /^\s+\S+@/ } @lines_unmatched;
162
 
163
my %removed_unmatched = map { $_ => 1 } @removed_unmatched;
164
 
165
# Expected: ONLY pool/fs@badname should be marked for removal (doesn't match pattern)
166
# All other snapshots that match the pattern should be SKIPPED (not removed)
167
my $ok2 = 1;
168
if ($removed_unmatched{'pool/fs@badname'}) {
169
    print "PASS: pool/fs\@badname marked for removal with --unmatched flag\n";
170
} else {
171
    print "FAIL: pool/fs\@badname should be marked for removal with --unmatched flag\n";
172
    $ok2 = 0;
173
}
174
 
175
# Verify that matched snapshots are NOT removed when --unmatched is used
176
my @matched_snaps = grep { $_ ne 'pool/fs@badname' } @snap_lines;
177
for my $snap (@matched_snaps) {
178
    next unless $snap =~ /@/;
179
    if ($removed_unmatched{$snap}) {
180
        print "FAIL: $snap should NOT be removed with --unmatched flag (only unmatched should be removed)\n";
181
        $ok2 = 0;
182
    }
183
}
184
 
185
# Count should be exactly 1 (only the unmatched snapshot)
186
my $unmatched_count = scalar(@removed_unmatched);
187
if ($unmatched_count != 1) {
188
    print "FAIL: Expected exactly 1 snapshot to be removed with --unmatched flag, got $unmatched_count\n";
189
    $ok2 = 0;
190
} else {
191
    print "PASS: Exactly 1 unmatched snapshot marked for removal (normal retention skipped)\n";
192
}
193
 
194
if ($ok2) {
195
    print "TEST 2 (--unmatched flag) PASS\n";
196
} else {
197
    print "TEST 2 (--unmatched flag) FAIL\n";
198
    exit 1;
199
}
200
 
201
# TEST 3: Test --version flag
202
print "\n=== Testing --version flag ===\n";
203
my $version_out;
204
if ($ENV{PERL}) {
205
    $version_out = qx{$ENV{PERL} -I. "$clean" --version 2>&1};
206
} else {
207
    $version_out = qx{perl -I. "$clean" --version 2>&1};
208
}
209
 
210
print "=== cleanSnaps --version output ===\n$version_out\n";
211
 
212
if ($version_out =~ /cleanSnaps version \d+\.\d+/) {
213
    print "TEST 3 (--version flag) PASS\n";
214
} else {
215
    print "TEST 3 (--version flag) FAIL\n";
216
    exit 1;
217
}
218
 
219
print "\n=== All tests PASSED ===\n";
220
exit 0;