Subversion Repositories zfs_utils

Rev

Rev 53 | Go to most recent revision | 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) {
141
    print "TEST PASS\n";
142
    exit 0;
143
} else {
144
    print "TEST FAIL\n";
145
    exit 1;
146
}