Merge branch 'master' into skip-parent

This commit is contained in:
Christoph Klaffl 2018-06-29 08:13:12 +02:00
commit 77e066b3d6
No known key found for this signature in database
GPG Key ID: FC1C525C2A47CC28
13 changed files with 579 additions and 22 deletions

View File

@ -6,9 +6,11 @@
More prosaically, you can use Sanoid to create, automatically thin, and monitor snapshots and pool health from a single eminently human-readable TOML config file at /etc/sanoid/sanoid.conf. (Sanoid also requires a "defaults" file located at /etc/sanoid/sanoid.defaults.conf, which is not user-editable.) A typical Sanoid system would have a single cron job: More prosaically, you can use Sanoid to create, automatically thin, and monitor snapshots and pool health from a single eminently human-readable TOML config file at /etc/sanoid/sanoid.conf. (Sanoid also requires a "defaults" file located at /etc/sanoid/sanoid.defaults.conf, which is not user-editable.) A typical Sanoid system would have a single cron job:
``` ```
* * * * * /usr/local/bin/sanoid --cron * * * * * TZ=UTC /usr/local/bin/sanoid --cron
``` ```
`Note`: Using UTC as timezone is recommend to prevent problems with daylight saving times
And its /etc/sanoid/sanoid.conf might look something like this: And its /etc/sanoid/sanoid.conf might look something like this:
``` ```
@ -62,6 +64,10 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da
This option is designed to be run by a Nagios monitoring system. It reports on the health of the zpool your filesystems are on. It only monitors filesystems that are configured in the sanoid.conf file. This option is designed to be run by a Nagios monitoring system. It reports on the health of the zpool your filesystems are on. It only monitors filesystems that are configured in the sanoid.conf file.
+ --monitor-capacity
This option is designed to be run by a Nagios monitoring system. It reports on the capacity of the zpool your filesystems are on. It only monitors pools that are configured in the sanoid.conf file.
+ --force-update + --force-update
This clears out sanoid's zfs snapshot listing cache. This is normally not needed. This clears out sanoid's zfs snapshot listing cache. This is normally not needed.
@ -154,6 +160,10 @@ As of 1.4.18, syncoid also automatically supports and enables resume of interrup
This argument tells syncoid to restrict itself to existing snapshots, instead of creating a semi-ephemeral syncoid snapshot at execution time. Especially useful in multi-target (A->B, A->C) replication schemes, where you might otherwise accumulate a large number of foreign syncoid snapshots. This argument tells syncoid to restrict itself to existing snapshots, instead of creating a semi-ephemeral syncoid snapshot at execution time. Especially useful in multi-target (A->B, A->C) replication schemes, where you might otherwise accumulate a large number of foreign syncoid snapshots.
+ --exclude=REGEX
The given regular expression will be matched against all datasets which would be synced by this run and excludes them. This argument can be specified multiple times.
+ --no-resume + --no-resume
This argument tells syncoid to not use resumeable zfs send/receive streams. This argument tells syncoid to not use resumeable zfs send/receive streams.

View File

@ -1,3 +1,18 @@
sanoid (1.4.18) unstable; urgency=medium
implemented special character handling and support of ZFS resume/receive tokens by default in syncoid,
thank you @phreaker0!
-- Jim Salter <github@jrs-s.net> Wed, 25 Apr 2018 16:24:00 -0400
sanoid (1.4.17) unstable; urgency=medium
changed die to warn when unexpectedly unable to remove a snapshot - this
allows sanoid to continue taking/removing other snapshots not affected by
whatever lock prevented the first from being taken or removed
-- Jim Salter <github@jrs-s.net> Wed, 8 Nov 2017 15:25:00 -0400
sanoid (1.4.16) unstable; urgency=medium sanoid (1.4.16) unstable; urgency=medium
* merged @hrast01's extended fix to support -o option1=val,option2=val passthrough to SSH. merged @JakobR's * merged @hrast01's extended fix to support -o option1=val,option2=val passthrough to SSH. merged @JakobR's

View File

@ -5,5 +5,6 @@ After=zfs.target
ConditionFileNotEmpty=/etc/sanoid/sanoid.conf ConditionFileNotEmpty=/etc/sanoid/sanoid.conf
[Service] [Service]
Environment=TZ=UTC
Type=oneshot Type=oneshot
ExecStart=/usr/sbin/sanoid --cron ExecStart=/usr/sbin/sanoid --cron

View File

@ -58,6 +58,7 @@ Requires=zfs.target
After=zfs.target After=zfs.target
[Service] [Service]
Environment=TZ=UTC
Type=oneshot Type=oneshot
ExecStart=%{_sbindir}/sanoid --cron ExecStart=%{_sbindir}/sanoid --cron
EOF EOF

262
sanoid
View File

@ -18,7 +18,8 @@ use Time::Local; # to parse dates in reverse
my %args = ("configdir" => "/etc/sanoid"); my %args = ("configdir" => "/etc/sanoid");
GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet",
"monitor-health", "force-update", "configdir=s", "monitor-health", "force-update", "configdir=s",
"monitor-snapshots", "take-snapshots", "prune-snapshots" "monitor-snapshots", "take-snapshots", "prune-snapshots",
"monitor-capacity"
) or pod2usage(2); ) or pod2usage(2);
# If only config directory (or nothing) has been specified, default to --cron --verbose # If only config directory (or nothing) has been specified, default to --cron --verbose
@ -39,8 +40,10 @@ my %config = init($conf_file,$default_conf_file);
# if we call getsnaps(%config,1) it will forcibly update the cache, TTL or no TTL # if we call getsnaps(%config,1) it will forcibly update the cache, TTL or no TTL
my $forcecacheupdate = 0; my $forcecacheupdate = 0;
my $cache = '/var/cache/sanoidsnapshots.txt';
my $cacheTTL = 900; # 15 minutes my $cacheTTL = 900; # 15 minutes
my %snaps = getsnaps( \%config, $cacheTTL, $forcecacheupdate ); my %snaps = getsnaps( \%config, $cacheTTL, $forcecacheupdate );
my %pruned;
my %snapsbytype = getsnapsbytype( \%config, \%snaps ); my %snapsbytype = getsnapsbytype( \%config, \%snaps );
@ -52,6 +55,7 @@ my @params = ( \%config, \%snaps, \%snapsbytype, \%snapsbypath );
if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); } if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); }
if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); } if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); }
if ($args{'monitor-health'}) { monitor_health(@params); } if ($args{'monitor-health'}) { monitor_health(@params); }
if ($args{'monitor-capacity'}) { monitor_capacity(@params); }
if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); } if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); }
if ($args{'cron'}) { if ($args{'cron'}) {
@ -174,6 +178,61 @@ sub monitor_snapshots {
exit $errorlevel; exit $errorlevel;
} }
####################################################################################
####################################################################################
####################################################################################
sub monitor_capacity {
my ($config, $snaps, $snapsbytype, $snapsbypath) = @_;
my %pools;
my @messages;
my $errlevel=0;
# build pool list with corresponding capacity limits
foreach my $section (keys %config) {
my @pool = split ('/',$section);
if (scalar @pool == 1 || !defined($pools{$pool[0]}) ) {
my %capacitylimits;
if (!check_capacity_limit($config{$section}{'capacity_warn'})) {
die "ERROR: invalid zpool capacity warning limit!\n";
}
if ($config{$section}{'capacity_warn'} != 0) {
$capacitylimits{'warn'} = $config{$section}{'capacity_warn'};
}
if (!check_capacity_limit($config{$section}{'capacity_crit'})) {
die "ERROR: invalid zpool capacity critical limit!\n";
}
if ($config{$section}{'capacity_crit'} != 0) {
$capacitylimits{'crit'} = $config{$section}{'capacity_crit'};
}
if (%capacitylimits) {
$pools{$pool[0]} = \%capacitylimits;
}
}
}
foreach my $pool (keys %pools) {
my $capacitylimitsref = $pools{$pool};
my ($exitcode, $msg) = check_zpool_capacity($pool,\%$capacitylimitsref);
if ($exitcode > $errlevel) { $errlevel = $exitcode; }
chomp $msg;
push (@messages, $msg);
}
my @warninglevels = ('','*** WARNING *** ','*** CRITICAL *** ');
my $message = $warninglevels[$errlevel] . join (', ',@messages);
print "$message\n";
exit $errlevel;
}
#################################################################################### ####################################################################################
#################################################################################### ####################################################################################
#################################################################################### ####################################################################################
@ -235,23 +294,30 @@ sub prune_snapshots {
foreach my $snap( @prunesnaps ){ foreach my $snap( @prunesnaps ){
if ($args{'verbose'}) { print "INFO: pruning $snap ... \n"; } if ($args{'verbose'}) { print "INFO: pruning $snap ... \n"; }
if (iszfsbusy($path)) { if (iszfsbusy($path)) {
print "INFO: deferring pruning of $snap - $path is currently in zfs send or receive.\n"; if ($args{'verbose'}) { print "INFO: deferring pruning of $snap - $path is currently in zfs send or receive.\n"; }
} else { } else {
if (! $args{'readonly'}) { system($zfs, "destroy",$snap) == 0 or warn "could not remove $snap : $?"; } if (! $args{'readonly'}) {
if (system($zfs, "destroy", $snap) == 0) {
$pruned{$snap} = 1;
} else {
warn "could not remove $snap : $?";
}
}
} }
} }
removelock('sanoid_pruning'); removelock('sanoid_pruning');
$forcecacheupdate = 1; removecachedsnapshots(0);
%snaps = getsnaps(%config,$cacheTTL,$forcecacheupdate);
} else { } else {
print "INFO: deferring snapshot pruning - valid pruning lock held by other sanoid process.\n"; if ($args{'verbose'}) { print "INFO: deferring snapshot pruning - valid pruning lock held by other sanoid process.\n"; }
} }
} }
} }
} }
} }
# if there were any deferred cache updates,
# do them now and wait if necessary
removecachedsnapshots(1);
} # end prune_snapshots } # end prune_snapshots
@ -268,6 +334,19 @@ sub take_snapshots {
my @newsnaps; my @newsnaps;
# get utc timestamp of the current day for DST check
my $daystartUtc = timelocal(0, 0, 0, $datestamp{'mday'}, ($datestamp{'mon'}-1), $datestamp{'year'});
my ($isdst) = (localtime($daystartUtc))[8];
my $dstOffset = 0;
if ($isdst ne $datestamp{'isdst'}) {
# current dst is different then at the beginning og the day
if ($isdst) {
# DST ended in the current day
$dstOffset = 60*60;
}
}
if ($args{'verbose'}) { print "INFO: taking snapshots...\n"; } if ($args{'verbose'}) { print "INFO: taking snapshots...\n"; }
foreach my $section (keys %config) { foreach my $section (keys %config) {
if ($section =~ /^template/) { next; } if ($section =~ /^template/) { next; }
@ -291,6 +370,9 @@ sub take_snapshots {
my @preferredtime; my @preferredtime;
my $lastpreferred; my $lastpreferred;
# to avoid duplicates with DST
my $dateSuffix = "";
if ($type eq 'hourly') { if ($type eq 'hourly') {
push @preferredtime,0; # try to hit 0 seconds push @preferredtime,0; # try to hit 0 seconds
push @preferredtime,$config{$section}{'hourly_min'}; push @preferredtime,$config{$section}{'hourly_min'};
@ -299,6 +381,13 @@ sub take_snapshots {
push @preferredtime,($datestamp{'mon'}-1); # january is month 0 push @preferredtime,($datestamp{'mon'}-1); # january is month 0
push @preferredtime,$datestamp{'year'}; push @preferredtime,$datestamp{'year'};
$lastpreferred = timelocal(@preferredtime); $lastpreferred = timelocal(@preferredtime);
if ($dstOffset ne 0) {
# timelocal doesn't take DST into account
$lastpreferred += $dstOffset;
# DST ended, avoid duplicates
$dateSuffix = "_y";
}
if ($lastpreferred > time()) { $lastpreferred -= 60*60; } # preferred time is later this hour - so look at last hour's if ($lastpreferred > time()) { $lastpreferred -= 60*60; } # preferred time is later this hour - so look at last hour's
} elsif ($type eq 'daily') { } elsif ($type eq 'daily') {
push @preferredtime,0; # try to hit 0 seconds push @preferredtime,0; # try to hit 0 seconds
@ -308,7 +397,29 @@ sub take_snapshots {
push @preferredtime,($datestamp{'mon'}-1); # january is month 0 push @preferredtime,($datestamp{'mon'}-1); # january is month 0
push @preferredtime,$datestamp{'year'}; push @preferredtime,$datestamp{'year'};
$lastpreferred = timelocal(@preferredtime); $lastpreferred = timelocal(@preferredtime);
if ($lastpreferred > time()) { $lastpreferred -= 60*60*24; } # preferred time is later today - so look at yesterday's
# timelocal doesn't take DST into account
$lastpreferred += $dstOffset;
# check if the planned time has different DST flag than the current
my ($isdst) = (localtime($lastpreferred))[8];
if ($isdst ne $datestamp{'isdst'}) {
if (!$isdst) {
# correct DST difference
$lastpreferred -= 60*60;
}
}
if ($lastpreferred > time()) {
$lastpreferred -= 60*60*24;
if ($dstOffset ne 0) {
# because we are going back one day
# the DST difference has to be accounted
# for in reverse now
$lastpreferred -= 2*$dstOffset;
}
} # preferred time is later today - so look at yesterday's
} elsif ($type eq 'monthly') { } elsif ($type eq 'monthly') {
push @preferredtime,0; # try to hit 0 seconds push @preferredtime,0; # try to hit 0 seconds
push @preferredtime,$config{$section}{'monthly_min'}; push @preferredtime,$config{$section}{'monthly_min'};
@ -336,7 +447,7 @@ sub take_snapshots {
# update to most current possible datestamp # update to most current possible datestamp
%datestamp = get_date(); %datestamp = get_date();
# print "we should have had a $type snapshot of $path $maxage seconds ago; most recent is $newestage seconds old.\n"; # print "we should have had a $type snapshot of $path $maxage seconds ago; most recent is $newestage seconds old.\n";
push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type"); push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}${dateSuffix}_$type");
} }
} }
} }
@ -484,7 +595,6 @@ sub getsnaps {
my ($config, $cacheTTL, $forcecacheupdate) = @_; my ($config, $cacheTTL, $forcecacheupdate) = @_;
my $cache = '/var/cache/sanoidsnapshots.txt';
my @rawsnaps; my @rawsnaps;
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($cache); my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($cache);
@ -521,7 +631,7 @@ sub getsnaps {
} }
foreach my $snap (@rawsnaps) { foreach my $snap (@rawsnaps) {
my ($fs,$snapname,$snapdate) = ($snap =~ m/(.*)\@(.*ly)\s*creation\s*(\d*)/); my ($fs,$snapname,$snapdate) = ($snap =~ m/(.*)\@(.*ly)\t*creation\t*(\d*)/);
# avoid pissing off use warnings # avoid pissing off use warnings
if (defined $snapname) { if (defined $snapname) {
@ -900,6 +1010,74 @@ sub check_zpool() {
return ($ERRORS{$state},$msg); return ($ERRORS{$state},$msg);
} # end check_zpool() } # end check_zpool()
sub check_capacity_limit() {
my $value = shift;
if (!defined($value) || $value !~ /^\d+\z/) {
return undef;
}
if ($value < 0 || $value > 100) {
return undef;
}
return 1
}
sub check_zpool_capacity() {
my %ERRORS=('DEPENDENT'=>4,'UNKNOWN'=>3,'OK'=>0,'WARNING'=>1,'CRITICAL'=>2);
my $state="UNKNOWN";
my $msg="FAILURE";
my $pool=shift;
my $capacitylimitsref=shift;
my %capacitylimits=%$capacitylimitsref;
my $statcommand="/sbin/zpool list -H -o cap $pool";
if (! open STAT, "$statcommand|") {
print ("$state '$statcommand' command returns no result!\n");
exit $ERRORS{$state};
}
my $line = <STAT>;
close(STAT);
chomp $line;
my @row = split(/ +/, $line);
my $cap=$row[0];
## check for valid capacity value
if ($cap !~ m/^[0-9]{1,3}%$/ ) {
$state = "CRITICAL";
$msg = sprintf "ZPOOL {%s} does not exist and/or is not responding!\n", $pool;
print $state, " ", $msg;
exit ($ERRORS{$state});
}
$state="OK";
# check capacity
my $capn = $cap;
$capn =~ s/\D//g;
if (defined($capacitylimits{"warn"})) {
if ($capn >= $capacitylimits{"warn"}) {
$state = "WARNING";
}
}
if (defined($capacitylimits{"crit"})) {
if ($capn >= $capacitylimits{"crit"}) {
$state = "CRITICAL";
}
}
$msg = sprintf "ZPOOL %s : %s\n", $pool, $cap;
$msg = "$state $msg";
return ($ERRORS{$state},$msg);
} # end check_zpool_capacity()
###################################################################################################### ######################################################################################################
###################################################################################################### ######################################################################################################
###################################################################################################### ######################################################################################################
@ -930,13 +1108,22 @@ sub checklock {
# no lockfile # no lockfile
return 1; return 1;
} }
# make sure lockfile contains something
if ( -z $lockfile) {
# zero size lockfile, something is wrong
die "ERROR: something is wrong! $lockfile is empty\n";
}
# lockfile exists. read pid and mutex from it. see if it's our pid. if not, see if # lockfile exists. read pid and mutex from it. see if it's our pid. if not, see if
# there's still a process running with that pid and with the same mutex. # there's still a process running with that pid and with the same mutex.
open FH, "< $lockfile"; open FH, "< $lockfile" or die "ERROR: unable to open $lockfile";
my @lock = <FH>; my @lock = <FH>;
close FH; close FH;
# if we didn't get exactly 2 items from the lock file there is a problem
if (scalar(@lock) != 2) {
die "ERROR: $lockfile is invalid.\n"
}
my $lockmutex = pop(@lock); my $lockmutex = pop(@lock);
my $lockpid = pop(@lock); my $lockpid = pop(@lock);
@ -948,7 +1135,6 @@ sub checklock {
# we own the lockfile. no need to check any further. # we own the lockfile. no need to check any further.
return 2; return 2;
} }
open PL, "$pscmd -p $lockpid -o args= |"; open PL, "$pscmd -p $lockpid -o args= |";
my @processlist = <PL>; my @processlist = <PL>;
close PL; close PL;
@ -1056,6 +1242,55 @@ sub getchilddatasets {
return @children; return @children;
} }
#######################################################################################################################3
#######################################################################################################################3
#######################################################################################################################3
sub removecachedsnapshots {
my $wait = shift;
if (not %pruned) {
return;
}
my $unlocked = checklock('sanoid_cacheupdate');
if ($wait != 1 && not $unlocked) {
if ($args{'verbose'}) { print "INFO: deferring cache update (snapshot removal) - valid cache update lock held by another sanoid process.\n"; }
return;
}
# wait until we can get a lock to do our cache changes
while (not $unlocked) {
if ($args{'verbose'}) { print "INFO: waiting for cache update lock held by another sanoid process.\n"; }
sleep(10);
$unlocked = checklock('sanoid_cacheupdate');
}
writelock('sanoid_cacheupdate');
if ($args{'verbose'}) {
print "INFO: removing destroyed snapshots from cache.\n";
}
open FH, "< $cache";
my @rawsnaps = <FH>;
close FH;
open FH, "> $cache" or die 'Could not write to $cache!\n';
foreach my $snapline ( @rawsnaps ) {
my @columns = split("\t", $snapline);
my $snap = $columns[0];
print FH $snapline unless ( exists($pruned{$snap}) );
}
close FH;
removelock('sanoid_cacheupdate');
%snaps = getsnaps(\%config,$cacheTTL,$forcecacheupdate);
# clear hash
undef %pruned;
}
__END__ __END__
=head1 NAME =head1 NAME
@ -1079,6 +1314,7 @@ Options:
--force-update Clears out sanoid's zfs snapshot cache --force-update Clears out sanoid's zfs snapshot cache
--monitor-health Reports on zpool "health", in a Nagios compatible format --monitor-health Reports on zpool "health", in a Nagios compatible format
--monitor-capacity Reports on zpool capacity, in a Nagios compatible format
--monitor-snapshots Reports on snapshot "health", in a Nagios compatible format --monitor-snapshots Reports on snapshot "health", in a Nagios compatible format
--take-snapshots Creates snapshots as specified in sanoid.conf --take-snapshots Creates snapshots as specified in sanoid.conf
--prune-snapshots Purges expired snapshots as specified in sanoid.conf --prune-snapshots Purges expired snapshots as specified in sanoid.conf

View File

@ -70,3 +70,7 @@ monthly_warn = 32
monthly_crit = 35 monthly_crit = 35
yearly_warn = 0 yearly_warn = 0
yearly_crit = 0 yearly_crit = 0
# default limits for capacity checks (if set to 0, limit will not be checked)
capacity_warn = 80
capacity_crit = 95

43
syncoid
View File

@ -19,7 +19,7 @@ use Sys::Hostname;
my %args = ('sshkey' => '', 'sshport' => '', 'sshcipher' => '', 'sshoption' => [], 'target-bwlimit' => '', 'source-bwlimit' => ''); my %args = ('sshkey' => '', 'sshport' => '', 'sshcipher' => '', 'sshoption' => [], 'target-bwlimit' => '', 'source-bwlimit' => '');
GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsnaps", "recursive|r", GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsnaps", "recursive|r",
"source-bwlimit=s", "target-bwlimit=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@", "source-bwlimit=s", "target-bwlimit=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@",
"debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "skip-parent") or pod2usage(2); "debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent") or pod2usage(2);
my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set
@ -146,6 +146,20 @@ sub getchilddatasets {
shift @children; shift @children;
} }
if (defined $args{'exclude'}) {
my $excludes = $args{'exclude'};
foreach (@$excludes) {
for my $i ( 0 .. $#children ) {
if ($children[$i] =~ /$_/) {
if ($debug) { print "DEBUG: excluded $children[$i] because of $_\n"; }
undef $children[$i]
}
}
@children = grep{ defined }@children;
}
}
return @children; return @children;
} }
@ -428,6 +442,18 @@ sub compressargset {
decomrawcmd => '/usr/bin/pigz', decomrawcmd => '/usr/bin/pigz',
decomargs => '-dc', decomargs => '-dc',
}, },
'zstd-fast' => {
rawcmd => '/usr/bin/zstd',
args => '-3',
decomrawcmd => '/usr/bin/zstd',
decomargs => '-dc',
},
'zstd-slow' => {
rawcmd => '/usr/bin/zstd',
args => '-19',
decomrawcmd => '/usr/bin/zstd',
decomargs => '-dc',
},
'lzo' => { 'lzo' => {
rawcmd => '/usr/bin/lzop', rawcmd => '/usr/bin/lzop',
args => '', args => '',
@ -438,7 +464,7 @@ sub compressargset {
if ($value eq 'default') { if ($value eq 'default') {
$value = $DEFAULT_COMPRESSION; $value = $DEFAULT_COMPRESSION;
} elsif (!(grep $value eq $_, ('gzip', 'pigz-fast', 'pigz-slow', 'lzo', 'default', 'none'))) { } elsif (!(grep $value eq $_, ('gzip', 'pigz-fast', 'pigz-slow', 'zstd-fast', 'zstd-slow', 'lzo', 'default', 'none'))) {
warn "Unrecognised compression value $value, defaulting to $DEFAULT_COMPRESSION"; warn "Unrecognised compression value $value, defaulting to $DEFAULT_COMPRESSION";
$value = $DEFAULT_COMPRESSION; $value = $DEFAULT_COMPRESSION;
} }
@ -669,7 +695,7 @@ sub getzfsvalue {
open FH, "$rhost $mysudocmd $zfscmd get -H $property $fsescaped |"; open FH, "$rhost $mysudocmd $zfscmd get -H $property $fsescaped |";
my $value = <FH>; my $value = <FH>;
close FH; close FH;
my @values = split(/\s/,$value); my @values = split(/\t/,$value);
$value = $values[2]; $value = $values[2];
return $value; return $value;
} }
@ -990,7 +1016,7 @@ sub getsnaps() {
if ($line =~ /\Q$fs\E\@.*guid/) { if ($line =~ /\Q$fs\E\@.*guid/) {
chomp $line; chomp $line;
my $guid = $line; my $guid = $line;
$guid =~ s/^.*\sguid\s*(\d*).*/$1/; $guid =~ s/^.*\tguid\t*(\d*).*/$1/;
my $snap = $line; my $snap = $line;
$snap =~ s/^.*\@(.*)\tguid.*$/$1/; $snap =~ s/^.*\@(.*)\tguid.*$/$1/;
$snaps{$type}{$snap}{'guid'}=$guid; $snaps{$type}{$snap}{'guid'}=$guid;
@ -1002,7 +1028,7 @@ sub getsnaps() {
if ($line =~ /\Q$fs\E\@.*creation/) { if ($line =~ /\Q$fs\E\@.*creation/) {
chomp $line; chomp $line;
my $creation = $line; my $creation = $line;
$creation =~ s/^.*\screation\s*(\d*).*/$1/; $creation =~ s/^.*\tcreation\t*(\d*).*/$1/;
my $snap = $line; my $snap = $line;
$snap =~ s/^.*\@(.*)\tcreation.*$/$1/; $snap =~ s/^.*\@(.*)\tcreation.*$/$1/;
$snaps{$type}{$snap}{'creation'}=$creation; $snaps{$type}{$snap}{'creation'}=$creation;
@ -1061,9 +1087,9 @@ sub getsendsize {
# the output format is different in case of # the output format is different in case of
# a resumed receive # a resumed receive
if (defined($receivetoken)) { if (defined($receivetoken)) {
$sendsize =~ s/.*\s([0-9]+)$/$1/; $sendsize =~ s/.*\t([0-9]+)$/$1/;
} else { } else {
$sendsize =~ s/^size\s*//; $sendsize =~ s/^size\t*//;
} }
chomp $sendsize; chomp $sendsize;
@ -1147,6 +1173,7 @@ Options:
--target-bwlimit=<limit k|m|g|t> Bandwidth limit on the target transfer --target-bwlimit=<limit k|m|g|t> Bandwidth limit on the target transfer
--no-stream Replicates using newest snapshot instead of intermediates --no-stream Replicates using newest snapshot instead of intermediates
--no-sync-snap Does not create new snapshot, only transfers existing --no-sync-snap Does not create new snapshot, only transfers existing
--exclude=REGEX Exclude specific datasets which match the given regular expression. Can be specified multiple times
--sshkey=FILE Specifies a ssh public key to use to connect --sshkey=FILE Specifies a ssh public key to use to connect
--sshport=PORT Connects to remote on a particular port --sshport=PORT Connects to remote on a particular port
@ -1154,7 +1181,7 @@ Options:
--sshoption|o=OPTION Passes OPTION to ssh for remote usage. Can be specified multiple times --sshoption|o=OPTION Passes OPTION to ssh for remote usage. Can be specified multiple times
--help Prints this helptext --help Prints this helptext
--verbose Prints the version number --version Prints the version number
--debug Prints out a lot of additional information during a syncoid run --debug Prints out a lot of additional information during a syncoid run
--monitor-version Currently does nothing --monitor-version Currently does nothing
--quiet Suppresses non-error output --quiet Suppresses non-error output

55
tests/1_one_year/run.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
set -x
# this test will take hourly, daily and monthly snapshots
# for the whole year of 2017 in the timezone Europe/Vienna
# sanoid is run hourly and no snapshots are pruned
. ../common/lib.sh
POOL_NAME="sanoid-test-1"
POOL_TARGET="" # root
RESULT="/tmp/sanoid_test_result"
RESULT_CHECKSUM="aa15e5595b0ed959313289ecb70323dad9903328ac46e881da5c4b0f871dd7cf"
# UTC timestamp of start and end
START="1483225200"
END="1514761199"
# prepare
setup
checkEnvironment
disableTimeSync
# set timezone
ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
timestamp=$START
mkdir -p "${POOL_TARGET}"
truncate -s 5120M "${POOL_TARGET}"/zpool.img
zpool create -f "${POOL_NAME}" "${POOL_TARGET}"/zpool.img
function cleanUp {
zpool export "${POOL_NAME}"
}
# export pool in any case
trap cleanUp EXIT
while [ $timestamp -le $END ]; do
date --utc --set @$timestamp; date; "${SANOID}" --cron --verbose
timestamp=$((timestamp+3600))
done
saveSnapshotList "${POOL_NAME}" "${RESULT}"
# hourly daily monthly
verifySnapshotList "${RESULT}" 8759 366 12 "${RESULT_CHECKSUM}"
# hourly count should be 8760 but one hour get's lost because of DST
# daily count should be 365 but one additional daily is taken
# because the DST change leads to a day with 25 hours
# which will trigger an additional daily snapshot

View File

@ -0,0 +1,10 @@
[sanoid-test-1]
use_template = production
[template_production]
hourly = 36
daily = 30
monthly = 3
yearly = 0
autosnap = yes
autoprune = no

54
tests/2_dst_handling/run.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/bash
set -x
# this test will check the behaviour arround a date where DST ends
# with hourly, daily and monthly snapshots checked in a 15 minute interval
# Daylight saving time 2017 in Europe/Vienna began at 02:00 on Sunday, 26 March
# and ended at 03:00 on Sunday, 29 October. All times are in
# Central European Time.
. ../common/lib.sh
POOL_NAME="sanoid-test-2"
POOL_TARGET="" # root
RESULT="/tmp/sanoid_test_result"
RESULT_CHECKSUM="a916d9cd46f4b80f285d069f3497d02671bbb1bfd12b43ef93531cbdaf89d55c"
# UTC timestamp of start and end
START="1509141600"
END="1509400800"
# prepare
setup
checkEnvironment
disableTimeSync
# set timezone
ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
timestamp=$START
mkdir -p "${POOL_TARGET}"
truncate -s 512M "${POOL_TARGET}"/zpool2.img
zpool create -f "${POOL_NAME}" "${POOL_TARGET}"/zpool2.img
function cleanUp {
zpool export "${POOL_NAME}"
}
# export pool in any case
trap cleanUp EXIT
while [ $timestamp -le $END ]; do
date --utc --set @$timestamp; date; "${SANOID}" --cron --verbose
timestamp=$((timestamp+900))
done
saveSnapshotList "${POOL_NAME}" "${RESULT}"
# hourly daily monthly
verifySnapshotList "${RESULT}" 73 3 1 "${RESULT_CHECKSUM}"
# one more hour because of DST

View File

@ -0,0 +1,10 @@
[sanoid-test-2]
use_template = production
[template_production]
hourly = 36
daily = 30
monthly = 3
yearly = 0
autosnap = yes
autoprune = no

107
tests/common/lib.sh Normal file
View File

@ -0,0 +1,107 @@
#!/bin/bash
function setup {
export LANG=C
export LANGUAGE=C
export LC_ALL=C
export SANOID="../../sanoid"
# make sure that there is no cache file
rm -f /var/cache/sanoidsnapshots.txt
# install needed sanoid configuration files
[ -f sanoid.conf ] && cp sanoid.conf /etc/sanoid/sanoid.conf
cp ../../sanoid.defaults.conf /etc/sanoid/sanoid.defaults.conf
}
function checkEnvironment {
ASK=1
which systemd-detect-virt > /dev/null
if [ $? -eq 0 ]; then
systemd-detect-virt --vm > /dev/null
if [ $? -eq 0 ]; then
# we are in a vm
ASK=0
fi
fi
if [ $ASK -eq 1 ]; then
set +x
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "you should be running this test in a"
echo "dedicated vm, as it will mess with your system!"
echo "Are you sure you wan't to continue? (y)"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
set -x
read -n 1 c
if [ "$c" != "y" ]; then
exit 1
fi
fi
}
function disableTimeSync {
# disable ntp sync
which timedatectl > /dev/null
if [ $? -eq 0 ]; then
timedatectl set-ntp 0
fi
}
function saveSnapshotList {
POOL_NAME="$1"
RESULT="$2"
zfs list -t snapshot -o name -Hr "${POOL_NAME}" | sort > "${RESULT}"
# clear the seconds for comparing
sed -i 's/\(autosnap_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]:[0-9][0-9]:\)[0-9][0-9]_/\100_/g' "${RESULT}"
}
function verifySnapshotList {
RESULT="$1"
HOURLY_COUNT=$2
DAILY_COUNT=$3
MONTHLY_COUNT=$4
CHECKSUM="$5"
failed=0
message=""
hourly_count=$(grep -c "autosnap_.*_hourly" < "${RESULT}")
daily_count=$(grep -c "autosnap_.*_daily" < "${RESULT}")
monthly_count=$(grep -c "autosnap_.*_monthly" < "${RESULT}")
if [ "${hourly_count}" -ne "${HOURLY_COUNT}" ]; then
failed=1
message="${message}hourly snapshot count is wrong: ${hourly_count}\n"
fi
if [ "${daily_count}" -ne "${DAILY_COUNT}" ]; then
failed=1
message="${message}daily snapshot count is wrong: ${daily_count}\n"
fi
if [ "${monthly_count}" -ne "${MONTHLY_COUNT}" ]; then
failed=1
message="${message}monthly snapshot count is wrong: ${monthly_count}\n"
fi
checksum=$(sha256sum "${RESULT}" | cut -d' ' -f1)
if [ "${checksum}" != "${CHECKSUM}" ]; then
failed=1
message="${message}result checksum mismatch\n"
fi
if [ "${failed}" -eq 0 ]; then
exit 0
fi
echo "TEST FAILED:" >&2
echo -n -e "${message}" >&2
exit 1
}

27
tests/run-tests.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
# run's all the available tests
for test in */; do
if [ ! -x "${test}/run.sh" ]; then
continue
fi
testName="${test%/}"
LOGFILE=/tmp/sanoid_test_run_"${testName}".log
pushd . > /dev/null
echo -n "Running test ${testName} ... "
cd "${test}"
echo | bash run.sh > "${LOGFILE}" 2>&1
if [ $? -eq 0 ]; then
echo "[PASS]"
else
echo "[FAILED] (see ${LOGFILE})"
fi
popd > /dev/null
done