From 4a3e93372c3502a7a4d9e2202d147ab00fd23559 Mon Sep 17 00:00:00 2001 From: Jason Lewis Date: Mon, 20 Nov 2017 15:16:43 +1100 Subject: [PATCH 01/28] check for emtpy lockfile --- sanoid | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sanoid b/sanoid index d6e58ce..889696f 100755 --- a/sanoid +++ b/sanoid @@ -930,13 +930,22 @@ sub checklock { # no lockfile 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 # 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 = ; 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 $lockpid = pop(@lock); @@ -948,7 +957,6 @@ sub checklock { # we own the lockfile. no need to check any further. return 2; } - open PL, "$pscmd -p $lockpid -o args= |"; my @processlist = ; close PL; From 31da53140fda7c5bfc79d063e3c6da8d8f49300c Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 19:00:44 +0100 Subject: [PATCH 02/28] implemented a simple test which will take snapshots over a whole year and checks the resulting snapshot list --- tests/1_one_year/run.sh | 55 ++++++++++++++++++ tests/1_one_year/sanoid.conf | 10 ++++ tests/common/lib.sh | 106 +++++++++++++++++++++++++++++++++++ tests/run-tests.sh | 27 +++++++++ 4 files changed, 198 insertions(+) create mode 100755 tests/1_one_year/run.sh create mode 100644 tests/1_one_year/sanoid.conf create mode 100644 tests/common/lib.sh create mode 100755 tests/run-tests.sh diff --git a/tests/1_one_year/run.sh b/tests/1_one_year/run.sh new file mode 100755 index 0000000..7cec813 --- /dev/null +++ b/tests/1_one_year/run.sh @@ -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 diff --git a/tests/1_one_year/sanoid.conf b/tests/1_one_year/sanoid.conf new file mode 100644 index 0000000..f5692f0 --- /dev/null +++ b/tests/1_one_year/sanoid.conf @@ -0,0 +1,10 @@ +[sanoid-test-1] + use_template = production + +[template_production] + hourly = 36 + daily = 30 + monthly = 3 + yearly = 0 + autosnap = yes + autoprune = no diff --git a/tests/common/lib.sh b/tests/common/lib.sh new file mode 100644 index 0000000..2c15e9b --- /dev/null +++ b/tests/common/lib.sh @@ -0,0 +1,106 @@ +#!/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 + +} diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..a8469e9 --- /dev/null +++ b/tests/run-tests.sh @@ -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 From 9a6cdb85438eb730348a35e23fd4fdf7b41b60f3 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 20:15:29 +0100 Subject: [PATCH 03/28] handle DST (daylight saving times) properly for hourly and daily snapshots, fixes #155 --- sanoid | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/sanoid b/sanoid index d6e58ce..030fea2 100755 --- a/sanoid +++ b/sanoid @@ -268,6 +268,19 @@ sub take_snapshots { 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"; } foreach my $section (keys %config) { if ($section =~ /^template/) { next; } @@ -291,6 +304,9 @@ sub take_snapshots { my @preferredtime; my $lastpreferred; + # to avoid duplicates with DST + my $dateSuffix = ""; + if ($type eq 'hourly') { push @preferredtime,0; # try to hit 0 seconds push @preferredtime,$config{$section}{'hourly_min'}; @@ -299,6 +315,13 @@ sub take_snapshots { push @preferredtime,($datestamp{'mon'}-1); # january is month 0 push @preferredtime,$datestamp{'year'}; $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 } elsif ($type eq 'daily') { push @preferredtime,0; # try to hit 0 seconds @@ -308,7 +331,29 @@ sub take_snapshots { push @preferredtime,($datestamp{'mon'}-1); # january is month 0 push @preferredtime,$datestamp{'year'}; $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') { push @preferredtime,0; # try to hit 0 seconds push @preferredtime,$config{$section}{'monthly_min'}; @@ -336,7 +381,7 @@ sub take_snapshots { # update to most current possible datestamp %datestamp = get_date(); # 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"); } } } From c1f7cd4241cefcafc1951b1e005b4c0a566ac062 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 20:23:52 +0100 Subject: [PATCH 04/28] fixed snapshot suffix --- sanoid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanoid b/sanoid index 030fea2..1c470c3 100755 --- a/sanoid +++ b/sanoid @@ -381,7 +381,7 @@ sub take_snapshots { # update to most current possible datestamp %datestamp = get_date(); # 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'}_${dateSuffix}_$type"); + push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}${dateSuffix}_$type"); } } } From e61ccf1c9dcccb1e156ed684413e0c0a5671664a Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 20:24:54 +0100 Subject: [PATCH 05/28] added test for checking for correct DST handling behaviour --- tests/2_dst_handling/run.sh | 54 ++++++++++++++++++++++++++++++++ tests/2_dst_handling/sanoid.conf | 10 ++++++ 2 files changed, 64 insertions(+) create mode 100755 tests/2_dst_handling/run.sh create mode 100644 tests/2_dst_handling/sanoid.conf diff --git a/tests/2_dst_handling/run.sh b/tests/2_dst_handling/run.sh new file mode 100755 index 0000000..eba21ed --- /dev/null +++ b/tests/2_dst_handling/run.sh @@ -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 diff --git a/tests/2_dst_handling/sanoid.conf b/tests/2_dst_handling/sanoid.conf new file mode 100644 index 0000000..7ded3f8 --- /dev/null +++ b/tests/2_dst_handling/sanoid.conf @@ -0,0 +1,10 @@ +[sanoid-test-2] + use_template = production + +[template_production] + hourly = 36 + daily = 30 + monthly = 3 + yearly = 0 + autosnap = yes + autoprune = no From 8d4484a2d1641789c1e667c03e69773e96cde6ea Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 23:24:34 +0100 Subject: [PATCH 06/28] exit with error code upon failure --- debian/changelog | 4 ++++ tests/common/lib.sh | 1 + 2 files changed, 5 insertions(+) diff --git a/debian/changelog b/debian/changelog index ab530b0..beb6584 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,7 @@ +sanoid (1.4.17-SNAPSHOT) unstable; urgency=medium + +-- Jim Salter Wed, 9 Aug 2017 12:28:49 -0400 + 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 diff --git a/tests/common/lib.sh b/tests/common/lib.sh index 2c15e9b..78f128b 100644 --- a/tests/common/lib.sh +++ b/tests/common/lib.sh @@ -103,4 +103,5 @@ function verifySnapshotList { echo "TEST FAILED:" >&2 echo -n -e "${message}" >&2 + exit 1 } From 53894b2855016e6398609995ee383c173fc2ff99 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 23:36:13 +0100 Subject: [PATCH 07/28] indentation fix --- sanoid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanoid b/sanoid index 1c470c3..12d746d 100755 --- a/sanoid +++ b/sanoid @@ -317,7 +317,7 @@ sub take_snapshots { $lastpreferred = timelocal(@preferredtime); if ($dstOffset ne 0) { - # timelocal doesn't take DST into account + # timelocal doesn't take DST into account $lastpreferred += $dstOffset; # DST ended, avoid duplicates $dateSuffix = "_y"; From ceb1397ef084941b92bc2348d85b39020cac49a7 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 23:40:23 +0100 Subject: [PATCH 08/28] Revert "exit with error code upon failure" This reverts commit 8d4484a2d1641789c1e667c03e69773e96cde6ea. --- debian/changelog | 4 ---- tests/common/lib.sh | 1 - 2 files changed, 5 deletions(-) diff --git a/debian/changelog b/debian/changelog index beb6584..ab530b0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,3 @@ -sanoid (1.4.17-SNAPSHOT) unstable; urgency=medium - --- Jim Salter Wed, 9 Aug 2017 12:28:49 -0400 - 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 diff --git a/tests/common/lib.sh b/tests/common/lib.sh index 78f128b..2c15e9b 100644 --- a/tests/common/lib.sh +++ b/tests/common/lib.sh @@ -103,5 +103,4 @@ function verifySnapshotList { echo "TEST FAILED:" >&2 echo -n -e "${message}" >&2 - exit 1 } From 371f8ff318ad749bcb9374b8acc723ac77da841d Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 6 Dec 2017 23:41:15 +0100 Subject: [PATCH 09/28] exit with error code upon failure --- tests/common/lib.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/common/lib.sh b/tests/common/lib.sh index 2c15e9b..78f128b 100644 --- a/tests/common/lib.sh +++ b/tests/common/lib.sh @@ -103,4 +103,5 @@ function verifySnapshotList { echo "TEST FAILED:" >&2 echo -n -e "${message}" >&2 + exit 1 } From 742db32e686520dc22ea11a8f831ed15618fdf9c Mon Sep 17 00:00:00 2001 From: Martin van Wingerden Date: Fri, 29 Dec 2017 20:02:01 +0100 Subject: [PATCH 10/28] Made two INFO prints mutable Added a quiet check for two non-controllable INFO prints Fixes: #182 Signed-off-by: Martin van Wingerden --- sanoid | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanoid b/sanoid index d6e58ce..b995924 100755 --- a/sanoid +++ b/sanoid @@ -235,7 +235,7 @@ sub prune_snapshots { foreach my $snap( @prunesnaps ){ if ($args{'verbose'}) { print "INFO: pruning $snap ... \n"; } 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 { if (! $args{'readonly'}) { system($zfs, "destroy",$snap) == 0 or warn "could not remove $snap : $?"; } } @@ -244,7 +244,7 @@ sub prune_snapshots { $forcecacheupdate = 1; %snaps = getsnaps(%config,$cacheTTL,$forcecacheupdate); } 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"; } } } } From 1f64c9c35aac5d45c433833af56bba4126a0bcbd Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 20 Feb 2018 18:16:35 +0100 Subject: [PATCH 11/28] let monitor-health check the capacity too --- sanoid | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sanoid b/sanoid index d6e58ce..6b1257e 100755 --- a/sanoid +++ b/sanoid @@ -798,6 +798,17 @@ sub check_zpool() { ## determine health of zpool and subsequent error status if ($health eq "ONLINE" ) { $state = "OK"; + + # check capacity + my $capn = $cap; + $capn =~ s/\D//g; + + if ($capn >= 80) { + $state = "WARNING"; + } + if ($capn >= 95) { + $state = "CRITICAL"; + } } else { if ($health eq "DEGRADED") { $state = "WARNING"; From 06d029db684f49a577a4d912f82c4ea0462e137e Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Thu, 22 Feb 2018 16:53:30 +0100 Subject: [PATCH 12/28] remove destroyed snapshots from cache file instead of regenerating the whole thing (which can take very long on systems with many snapshots and/or datasets) --- sanoid | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/sanoid b/sanoid index d6e58ce..238f955 100755 --- a/sanoid +++ b/sanoid @@ -39,6 +39,7 @@ my %config = init($conf_file,$default_conf_file); # if we call getsnaps(%config,1) it will forcibly update the cache, TTL or no TTL my $forcecacheupdate = 0; +my $cache = '/var/cache/sanoidsnapshots.txt'; my $cacheTTL = 900; # 15 minutes my %snaps = getsnaps( \%config, $cacheTTL, $forcecacheupdate ); @@ -232,17 +233,23 @@ sub prune_snapshots { # print "found some snaps to prune!\n" if (checklock('sanoid_pruning')) { writelock('sanoid_pruning'); + my @pruned; foreach my $snap( @prunesnaps ){ if ($args{'verbose'}) { print "INFO: pruning $snap ... \n"; } if (iszfsbusy($path)) { print "INFO: deferring pruning of $snap - $path is currently in zfs send or receive.\n"; } else { - if (! $args{'readonly'}) { system($zfs, "destroy",$snap) == 0 or warn "could not remove $snap : $?"; } + if (! $args{'readonly'}) { + if (system($zfs, "destroy", $snap) == 0) { + push(@pruned, $snap); + } else { + warn "could not remove $snap : $?"; + } + } } } removelock('sanoid_pruning'); - $forcecacheupdate = 1; - %snaps = getsnaps(%config,$cacheTTL,$forcecacheupdate); + removecachedsnapshots(@pruned); } else { print "INFO: deferring snapshot pruning - valid pruning lock held by other sanoid process.\n"; } @@ -484,7 +491,6 @@ sub getsnaps { my ($config, $cacheTTL, $forcecacheupdate) = @_; - my $cache = '/var/cache/sanoidsnapshots.txt'; my @rawsnaps; my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($cache); @@ -1056,6 +1062,39 @@ sub getchilddatasets { return @children; } +#######################################################################################################################3 +#######################################################################################################################3 +#######################################################################################################################3 + +sub removecachedsnapshots { + my @prunedlist = shift; + my %pruned = map { $_ => 1 } @prunedlist; + + if (checklock('sanoid_cacheupdate')) { + writelock('sanoid_cacheupdate'); + + if ($args{'verbose'}) { + print "INFO: removing destroyed snapshots from cache.\n"; + } + open FH, "< $cache"; + my @rawsnaps = ; + 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); + } else { + if ($args{'verbose'}) { print "WARN: skipping cache update (snapshot removal) - valid cache update lock held by another sanoid process.\n"; } + } +} + __END__ =head1 NAME From d0f1445784c4e54470b72588d9d8954d0e2bf258 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Thu, 22 Feb 2018 17:29:58 +0100 Subject: [PATCH 13/28] defer cache updates after snapshot pruning and do them after all pruning is done (waiting for the cache update lock if necessary) --- sanoid | 74 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/sanoid b/sanoid index 238f955..e73b4a4 100755 --- a/sanoid +++ b/sanoid @@ -42,6 +42,7 @@ my $forcecacheupdate = 0; my $cache = '/var/cache/sanoidsnapshots.txt'; my $cacheTTL = 900; # 15 minutes my %snaps = getsnaps( \%config, $cacheTTL, $forcecacheupdate ); +my %pruned; my %snapsbytype = getsnapsbytype( \%config, \%snaps ); @@ -233,7 +234,6 @@ sub prune_snapshots { # print "found some snaps to prune!\n" if (checklock('sanoid_pruning')) { writelock('sanoid_pruning'); - my @pruned; foreach my $snap( @prunesnaps ){ if ($args{'verbose'}) { print "INFO: pruning $snap ... \n"; } if (iszfsbusy($path)) { @@ -241,7 +241,7 @@ sub prune_snapshots { } else { if (! $args{'readonly'}) { if (system($zfs, "destroy", $snap) == 0) { - push(@pruned, $snap); + $pruned{$snap} = 1; } else { warn "could not remove $snap : $?"; } @@ -249,7 +249,7 @@ sub prune_snapshots { } } removelock('sanoid_pruning'); - removecachedsnapshots(@pruned); + removecachedsnapshots(0); } else { print "INFO: deferring snapshot pruning - valid pruning lock held by other sanoid process.\n"; } @@ -258,7 +258,9 @@ sub prune_snapshots { } } - + # if there were any deferred cache updates, + # do them now and wait if necessary + removecachedsnapshots(1); } # end prune_snapshots @@ -1067,32 +1069,48 @@ sub getchilddatasets { #######################################################################################################################3 sub removecachedsnapshots { - my @prunedlist = shift; - my %pruned = map { $_ => 1 } @prunedlist; + my $wait = shift; - if (checklock('sanoid_cacheupdate')) { - writelock('sanoid_cacheupdate'); - - if ($args{'verbose'}) { - print "INFO: removing destroyed snapshots from cache.\n"; - } - open FH, "< $cache"; - my @rawsnaps = ; - 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); - } else { - if ($args{'verbose'}) { print "WARN: skipping cache update (snapshot removal) - valid cache update lock held by another sanoid process.\n"; } + 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 = ; + 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__ From 6c695f1a86274d8c7763833076b3b17e3a415b1b Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Mon, 26 Feb 2018 18:16:06 +0100 Subject: [PATCH 14/28] allow monitor-health to optionally check zpool capacity too by providing limits along the flag --- README.md | 2 +- sanoid | 89 ++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9c4e6ba..6239b79 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da + --monitor-health - 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. Optionally it can check capacity limits too by appending them like '=80,95" (>= 80% warning, >=95% critical). + --force-update diff --git a/sanoid b/sanoid index 6b1257e..9dd6198 100755 --- a/sanoid +++ b/sanoid @@ -17,7 +17,7 @@ use Time::Local; # to parse dates in reverse my %args = ("configdir" => "/etc/sanoid"); GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", - "monitor-health", "force-update", "configdir=s", + "monitor-health:s", "force-update", "configdir=s", "monitor-snapshots", "take-snapshots", "prune-snapshots" ) or pod2usage(2); @@ -51,7 +51,7 @@ my @params = ( \%config, \%snaps, \%snapsbytype, \%snapsbypath ); if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); } if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); } -if ($args{'monitor-health'}) { monitor_health(@params); } +if (defined($args{'monitor-health'})) { monitor_health(@params); } if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); } if ($args{'cron'}) { @@ -76,13 +76,32 @@ sub monitor_health { my @messages; my $errlevel=0; + my %capacitylimits; + + # if provided, parse capacity limits + if ($args{'monitor-health'} ne "") { + my @values = split(',', $args{'monitor-health'}); + + if (!check_capacity_limit($values[0])) { + die "ERROR: invalid zpool capacity warning limit!\n"; + } + $capacitylimits{"warn"} = $values[0]; + + if (scalar @values > 1) { + if (!check_capacity_limit($values[1])) { + die "ERROR: invalid zpool capacity critical limit!\n"; + } + $capacitylimits{"crit"} = $values[1]; + } + } + foreach my $path (keys %{ $snapsbypath}) { my @pool = split ('/',$path); $pools{$pool[0]}=1; } foreach my $pool (keys %pools) { - my ($exitcode, $msg) = check_zpool($pool,2); + my ($exitcode, $msg) = check_zpool($pool,2,\%capacitylimits); if ($exitcode > $errlevel) { $errlevel = $exitcode; } chomp $msg; push (@messages, $msg); @@ -748,6 +767,8 @@ sub check_zpool() { my $pool=shift; my $verbose=shift; + my $capacitylimitsref=shift; + my %capacitylimits=%$capacitylimitsref; my $size=""; my $used=""; @@ -799,15 +820,21 @@ sub check_zpool() { if ($health eq "ONLINE" ) { $state = "OK"; - # check capacity - my $capn = $cap; - $capn =~ s/\D//g; + if (%capacitylimits) { + # check capacity + my $capn = $cap; + $capn =~ s/\D//g; - if ($capn >= 80) { - $state = "WARNING"; - } - if ($capn >= 95) { - $state = "CRITICAL"; + if ($capacitylimits{"warn"}) { + if ($capn >= $capacitylimits{"warn"}) { + $state = "WARNING"; + } + } + if ($capacitylimits{"crit"}) { + if ($capn >= $capacitylimits{"crit"}) { + $state = "CRITICAL"; + } + } } } else { if ($health eq "DEGRADED") { @@ -911,6 +938,20 @@ sub check_zpool() { return ($ERRORS{$state},$msg); } # end check_zpool() +sub check_capacity_limit() { + my $value = shift; + + if ($value !~ /^\d+\z/) { + return undef; + } + + if ($value < 1 || $value > 100) { + return undef; + } + + return 1 +} + ###################################################################################################### ###################################################################################################### ###################################################################################################### @@ -1081,19 +1122,19 @@ Assumes --cron --verbose if no other arguments (other than configdir) are specif Options: - --configdir=DIR Specify a directory to find config file sanoid.conf + --configdir=DIR Specify a directory to find config file sanoid.conf - --cron Creates snapshots and purges expired snapshots - --verbose Prints out additional information during a sanoid run - --readonly Simulates creation/deletion of snapshots - --quiet Suppresses non-error output - --force-update Clears out sanoid's zfs snapshot cache + --cron Creates snapshots and purges expired snapshots + --verbose Prints out additional information during a sanoid run + --readonly Simulates creation/deletion of snapshots + --quiet Suppresses non-error output + --force-update Clears out sanoid's zfs snapshot cache - --monitor-health Reports on zpool "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 - --prune-snapshots Purges expired snapshots as specified in sanoid.conf + --monitor-health[=wlimit[,climit]] Reports on zpool "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 + --prune-snapshots Purges expired snapshots as specified in sanoid.conf - --help Prints this helptext - --version Prints the version number - --debug Prints out a lot of additional information during a sanoid run + --help Prints this helptext + --version Prints the version number + --debug Prints out a lot of additional information during a sanoid run From 6f29bed441aa0c4db015271ceb3b24931286cc3b Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 27 Feb 2018 17:53:04 +0100 Subject: [PATCH 15/28] Revert "allow monitor-health to optionally check zpool capacity too by providing limits along the flag" This reverts commit 6c695f1a86274d8c7763833076b3b17e3a415b1b. --- README.md | 2 +- sanoid | 89 +++++++++++++++---------------------------------------- 2 files changed, 25 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 6239b79..9c4e6ba 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da + --monitor-health - 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. Optionally it can check capacity limits too by appending them like '=80,95" (>= 80% warning, >=95% critical). + 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. + --force-update diff --git a/sanoid b/sanoid index 9dd6198..6b1257e 100755 --- a/sanoid +++ b/sanoid @@ -17,7 +17,7 @@ use Time::Local; # to parse dates in reverse my %args = ("configdir" => "/etc/sanoid"); GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", - "monitor-health:s", "force-update", "configdir=s", + "monitor-health", "force-update", "configdir=s", "monitor-snapshots", "take-snapshots", "prune-snapshots" ) or pod2usage(2); @@ -51,7 +51,7 @@ my @params = ( \%config, \%snaps, \%snapsbytype, \%snapsbypath ); if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); } if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); } -if (defined($args{'monitor-health'})) { monitor_health(@params); } +if ($args{'monitor-health'}) { monitor_health(@params); } if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); } if ($args{'cron'}) { @@ -76,32 +76,13 @@ sub monitor_health { my @messages; my $errlevel=0; - my %capacitylimits; - - # if provided, parse capacity limits - if ($args{'monitor-health'} ne "") { - my @values = split(',', $args{'monitor-health'}); - - if (!check_capacity_limit($values[0])) { - die "ERROR: invalid zpool capacity warning limit!\n"; - } - $capacitylimits{"warn"} = $values[0]; - - if (scalar @values > 1) { - if (!check_capacity_limit($values[1])) { - die "ERROR: invalid zpool capacity critical limit!\n"; - } - $capacitylimits{"crit"} = $values[1]; - } - } - foreach my $path (keys %{ $snapsbypath}) { my @pool = split ('/',$path); $pools{$pool[0]}=1; } foreach my $pool (keys %pools) { - my ($exitcode, $msg) = check_zpool($pool,2,\%capacitylimits); + my ($exitcode, $msg) = check_zpool($pool,2); if ($exitcode > $errlevel) { $errlevel = $exitcode; } chomp $msg; push (@messages, $msg); @@ -767,8 +748,6 @@ sub check_zpool() { my $pool=shift; my $verbose=shift; - my $capacitylimitsref=shift; - my %capacitylimits=%$capacitylimitsref; my $size=""; my $used=""; @@ -820,21 +799,15 @@ sub check_zpool() { if ($health eq "ONLINE" ) { $state = "OK"; - if (%capacitylimits) { - # check capacity - my $capn = $cap; - $capn =~ s/\D//g; + # check capacity + my $capn = $cap; + $capn =~ s/\D//g; - if ($capacitylimits{"warn"}) { - if ($capn >= $capacitylimits{"warn"}) { - $state = "WARNING"; - } - } - if ($capacitylimits{"crit"}) { - if ($capn >= $capacitylimits{"crit"}) { - $state = "CRITICAL"; - } - } + if ($capn >= 80) { + $state = "WARNING"; + } + if ($capn >= 95) { + $state = "CRITICAL"; } } else { if ($health eq "DEGRADED") { @@ -938,20 +911,6 @@ sub check_zpool() { return ($ERRORS{$state},$msg); } # end check_zpool() -sub check_capacity_limit() { - my $value = shift; - - if ($value !~ /^\d+\z/) { - return undef; - } - - if ($value < 1 || $value > 100) { - return undef; - } - - return 1 -} - ###################################################################################################### ###################################################################################################### ###################################################################################################### @@ -1122,19 +1081,19 @@ Assumes --cron --verbose if no other arguments (other than configdir) are specif Options: - --configdir=DIR Specify a directory to find config file sanoid.conf + --configdir=DIR Specify a directory to find config file sanoid.conf - --cron Creates snapshots and purges expired snapshots - --verbose Prints out additional information during a sanoid run - --readonly Simulates creation/deletion of snapshots - --quiet Suppresses non-error output - --force-update Clears out sanoid's zfs snapshot cache + --cron Creates snapshots and purges expired snapshots + --verbose Prints out additional information during a sanoid run + --readonly Simulates creation/deletion of snapshots + --quiet Suppresses non-error output + --force-update Clears out sanoid's zfs snapshot cache - --monitor-health[=wlimit[,climit]] Reports on zpool "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 - --prune-snapshots Purges expired snapshots as specified in sanoid.conf + --monitor-health Reports on zpool "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 + --prune-snapshots Purges expired snapshots as specified in sanoid.conf - --help Prints this helptext - --version Prints the version number - --debug Prints out a lot of additional information during a sanoid run + --help Prints this helptext + --version Prints the version number + --debug Prints out a lot of additional information during a sanoid run From 01398789f60f31f67002666daacce715c6e626a5 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 27 Feb 2018 17:53:32 +0100 Subject: [PATCH 16/28] Revert "let monitor-health check the capacity too" This reverts commit 1f64c9c35aac5d45c433833af56bba4126a0bcbd. --- sanoid | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/sanoid b/sanoid index 6b1257e..d6e58ce 100755 --- a/sanoid +++ b/sanoid @@ -798,17 +798,6 @@ sub check_zpool() { ## determine health of zpool and subsequent error status if ($health eq "ONLINE" ) { $state = "OK"; - - # check capacity - my $capn = $cap; - $capn =~ s/\D//g; - - if ($capn >= 80) { - $state = "WARNING"; - } - if ($capn >= 95) { - $state = "CRITICAL"; - } } else { if ($health eq "DEGRADED") { $state = "WARNING"; From f961a9f447344b0b5558b3c8382aec3c2b74632e Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 27 Feb 2018 17:58:51 +0100 Subject: [PATCH 17/28] implemented monitor-capacity flag for checking zpool capacity limits in the nagios monitoring format --- README.md | 5 ++ sanoid | 146 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 137 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9c4e6ba..1b3b37f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ 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. ++ --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. The default limits are 80% for the warning and 95% for the critical state. Those can be overridden by providing them + along like '=80,95". + + --force-update This clears out sanoid's zfs snapshot listing cache. This is normally not needed. diff --git a/sanoid b/sanoid index d6e58ce..660dfb2 100755 --- a/sanoid +++ b/sanoid @@ -18,7 +18,8 @@ use Time::Local; # to parse dates in reverse my %args = ("configdir" => "/etc/sanoid"); GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", "monitor-health", "force-update", "configdir=s", - "monitor-snapshots", "take-snapshots", "prune-snapshots" + "monitor-snapshots", "take-snapshots", "prune-snapshots", + "monitor-capacity:s" ) or pod2usage(2); # If only config directory (or nothing) has been specified, default to --cron --verbose @@ -52,6 +53,7 @@ my @params = ( \%config, \%snaps, \%snapsbytype, \%snapsbypath ); if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); } if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); } if ($args{'monitor-health'}) { monitor_health(@params); } +if (defined($args{'monitor-capacity'})) { monitor_capacity(@params); } if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); } if ($args{'cron'}) { @@ -174,6 +176,57 @@ sub monitor_snapshots { exit $errorlevel; } + +#################################################################################### +#################################################################################### +#################################################################################### + +sub monitor_capacity { + my ($config, $snaps, $snapsbytype, $snapsbypath) = @_; + my %pools; + my @messages; + my $errlevel=0; + + my %capacitylimits = ( + "warn" => 80, + "crit" => 95 + ); + + # if provided, parse capacity limits + if ($args{'monitor-capacity'} ne "") { + my @values = split(',', $args{'monitor-capacity'}); + + if (!check_capacity_limit($values[0])) { + die "ERROR: invalid zpool capacity warning limit!\n"; + } + $capacitylimits{"warn"} = $values[0]; + + if (scalar @values > 1) { + if (!check_capacity_limit($values[1])) { + die "ERROR: invalid zpool capacity critical limit!\n"; + } + $capacitylimits{"crit"} = $values[1]; + } + } + + foreach my $path (keys %{ $snapsbypath}) { + my @pool = split ('/',$path); + $pools{$pool[0]}=1; + } + + foreach my $pool (keys %pools) { + my ($exitcode, $msg) = check_zpool_capacity($pool,\%capacitylimits); + 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; +} + #################################################################################### #################################################################################### #################################################################################### @@ -900,6 +953,70 @@ sub check_zpool() { return ($ERRORS{$state},$msg); } # end check_zpool() +sub check_capacity_limit() { + my $value = shift; + + if ($value !~ /^\d+\z/) { + return undef; + } + + if ($value < 1 || $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 = ; + 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 ($capn >= $capacitylimits{"warn"}) { + $state = "WARNING"; + } + + 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() + ###################################################################################################### ###################################################################################################### ###################################################################################################### @@ -1070,19 +1187,20 @@ Assumes --cron --verbose if no other arguments (other than configdir) are specif Options: - --configdir=DIR Specify a directory to find config file sanoid.conf + --configdir=DIR Specify a directory to find config file sanoid.conf - --cron Creates snapshots and purges expired snapshots - --verbose Prints out additional information during a sanoid run - --readonly Simulates creation/deletion of snapshots - --quiet Suppresses non-error output - --force-update Clears out sanoid's zfs snapshot cache + --cron Creates snapshots and purges expired snapshots + --verbose Prints out additional information during a sanoid run + --readonly Simulates creation/deletion of snapshots + --quiet Suppresses non-error output + --force-update Clears out sanoid's zfs snapshot cache - --monitor-health Reports on zpool "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 - --prune-snapshots Purges expired snapshots as specified in sanoid.conf + --monitor-health Reports on zpool "health", in a Nagios compatible format + --monitor-capacity[=wlimit[,climit]] Reports on zpool capacity, 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 + --prune-snapshots Purges expired snapshots as specified in sanoid.conf - --help Prints this helptext - --version Prints the version number - --debug Prints out a lot of additional information during a sanoid run + --help Prints this helptext + --version Prints the version number + --debug Prints out a lot of additional information during a sanoid run From b405b589801dd05ea12be500766ef333638b3e80 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Thu, 1 Mar 2018 09:22:22 +0100 Subject: [PATCH 18/28] let monitor-capacity parse the limits from the configuration file --- README.md | 3 +- sanoid | 92 ++++++++++++++++++++++++-------------------- sanoid.defaults.conf | 4 ++ 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 1b3b37f..324fd30 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,7 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da + --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. The default limits are 80% for the warning and 95% for the critical state. Those can be overridden by providing them - along like '=80,95". + 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 diff --git a/sanoid b/sanoid index 660dfb2..a4a256a 100755 --- a/sanoid +++ b/sanoid @@ -19,7 +19,7 @@ my %args = ("configdir" => "/etc/sanoid"); GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", "monitor-health", "force-update", "configdir=s", "monitor-snapshots", "take-snapshots", "prune-snapshots", - "monitor-capacity:s" + "monitor-capacity" ) or pod2usage(2); # If only config directory (or nothing) has been specified, default to --cron --verbose @@ -53,7 +53,7 @@ my @params = ( \%config, \%snaps, \%snapsbytype, \%snapsbypath ); if ($args{'debug'}) { $args{'verbose'}=1; blabber (@params); } if ($args{'monitor-snapshots'}) { monitor_snapshots(@params); } if ($args{'monitor-health'}) { monitor_health(@params); } -if (defined($args{'monitor-capacity'})) { monitor_capacity(@params); } +if ($args{'monitor-capacity'}) { monitor_capacity(@params); } if ($args{'force-update'}) { my $snaps = getsnaps( \%config, $cacheTTL, 1 ); } if ($args{'cron'}) { @@ -187,35 +187,39 @@ sub monitor_capacity { my @messages; my $errlevel=0; - my %capacitylimits = ( - "warn" => 80, - "crit" => 95 - ); + # build pool list with corresponding capacity limits + foreach my $section (keys %config) { + my @pool = split ('/',$section); - # if provided, parse capacity limits - if ($args{'monitor-capacity'} ne "") { - my @values = split(',', $args{'monitor-capacity'}); + if (scalar @pool == 1 || !defined($pools{$pool[0]}) ) { + my %capacitylimits; - if (!check_capacity_limit($values[0])) { - die "ERROR: invalid zpool capacity warning limit!\n"; - } - $capacitylimits{"warn"} = $values[0]; + if (!check_capacity_limit($config{$section}{'capacity_warn'})) { + die "ERROR: invalid zpool capacity warning limit!\n"; + } - if (scalar @values > 1) { - if (!check_capacity_limit($values[1])) { + 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"; } - $capacitylimits{"crit"} = $values[1]; - } - } - foreach my $path (keys %{ $snapsbypath}) { - my @pool = split ('/',$path); - $pools{$pool[0]}=1; + if ($config{$section}{'capacity_crit'} != 0) { + $capacitylimits{'crit'} = $config{$section}{'capacity_crit'}; + } + + if (%capacitylimits) { + $pools{$pool[0]} = \%capacitylimits; + } + } } foreach my $pool (keys %pools) { - my ($exitcode, $msg) = check_zpool_capacity($pool,\%capacitylimits); + my $capacitylimitsref = $pools{$pool}; + + my ($exitcode, $msg) = check_zpool_capacity($pool,\%$capacitylimitsref); if ($exitcode > $errlevel) { $errlevel = $exitcode; } chomp $msg; push (@messages, $msg); @@ -956,11 +960,11 @@ sub check_zpool() { sub check_capacity_limit() { my $value = shift; - if ($value !~ /^\d+\z/) { + if (!defined($value) || $value !~ /^\d+\z/) { return undef; } - if ($value < 1 || $value > 100) { + if ($value < 0 || $value > 100) { return undef; } @@ -1004,12 +1008,16 @@ sub check_zpool_capacity() { my $capn = $cap; $capn =~ s/\D//g; - if ($capn >= $capacitylimits{"warn"}) { - $state = "WARNING"; + if (defined($capacitylimits{"warn"})) { + if ($capn >= $capacitylimits{"warn"}) { + $state = "WARNING"; + } } - if ($capn >= $capacitylimits{"crit"}) { - $state = "CRITICAL"; + if (defined($capacitylimits{"crit"})) { + if ($capn >= $capacitylimits{"crit"}) { + $state = "CRITICAL"; + } } $msg = sprintf "ZPOOL %s : %s\n", $pool, $cap; @@ -1187,20 +1195,20 @@ Assumes --cron --verbose if no other arguments (other than configdir) are specif Options: - --configdir=DIR Specify a directory to find config file sanoid.conf + --configdir=DIR Specify a directory to find config file sanoid.conf - --cron Creates snapshots and purges expired snapshots - --verbose Prints out additional information during a sanoid run - --readonly Simulates creation/deletion of snapshots - --quiet Suppresses non-error output - --force-update Clears out sanoid's zfs snapshot cache + --cron Creates snapshots and purges expired snapshots + --verbose Prints out additional information during a sanoid run + --readonly Simulates creation/deletion of snapshots + --quiet Suppresses non-error output + --force-update Clears out sanoid's zfs snapshot cache - --monitor-health Reports on zpool "health", in a Nagios compatible format - --monitor-capacity[=wlimit[,climit]] Reports on zpool capacity, 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 - --prune-snapshots Purges expired snapshots as specified in sanoid.conf + --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 + --take-snapshots Creates snapshots as specified in sanoid.conf + --prune-snapshots Purges expired snapshots as specified in sanoid.conf - --help Prints this helptext - --version Prints the version number - --debug Prints out a lot of additional information during a sanoid run + --help Prints this helptext + --version Prints the version number + --debug Prints out a lot of additional information during a sanoid run diff --git a/sanoid.defaults.conf b/sanoid.defaults.conf index 35c804d..d86cc47 100644 --- a/sanoid.defaults.conf +++ b/sanoid.defaults.conf @@ -70,3 +70,7 @@ monthly_warn = 32 monthly_crit = 35 yearly_warn = 0 yearly_crit = 0 + +# default limits for capacity checks (if set to 0, limit will not be checked) +capacity_warn = 80 +capacity_crit = 95 From 8dfdd1a7169b97a8b6d2a6a6c84b6ac3826caa49 Mon Sep 17 00:00:00 2001 From: Janne Savikko Date: Thu, 15 Mar 2018 13:50:12 +0200 Subject: [PATCH 19/28] syncoid: fix --version in options list of helptext syncoid does not have verbose option. Replace verbose with version in options list of helptext. --- syncoid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncoid b/syncoid index 7337f5b..29f0176 100755 --- a/syncoid +++ b/syncoid @@ -956,7 +956,7 @@ Options: --sshoption|o=OPTION Passes OPTION to ssh for remote usage. Can be specified multiple times --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 --monitor-version Currently does nothing --quiet Suppresses non-error output From ecf2a852b5e0fa54131c878597c9b24cffb8a663 Mon Sep 17 00:00:00 2001 From: danielewood Date: Thu, 12 Apr 2018 17:43:08 -0700 Subject: [PATCH 20/28] Added support for ZStandard compression. Available in all major distros with a simple yum/apt-get/pkg. References: ZSTD Compression by Allan Jude - https://www.youtube.com/watch?v=hWnWEitDPlM Zstandard - https://facebook.github.io/zstd/ --- syncoid | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/syncoid b/syncoid index 7337f5b..baf5053 100755 --- a/syncoid +++ b/syncoid @@ -368,6 +368,18 @@ sub compressargset { decomrawcmd => '/usr/bin/pigz', 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' => { rawcmd => '/usr/bin/lzop', args => '', @@ -378,7 +390,7 @@ sub compressargset { if ($value eq 'default') { $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"; $value = $DEFAULT_COMPRESSION; } From b3b69c598c6350cedd7db938692f591935417997 Mon Sep 17 00:00:00 2001 From: dcrdev Date: Sat, 28 Apr 2018 17:38:19 +0100 Subject: [PATCH 21/28] Restructure dist packages & bump rpm spec to 1.4.18 --- {debian => packages/debian}/changelog | 0 {debian => packages/debian}/compat | 0 {debian => packages/debian}/control | 0 {debian => packages/debian}/copyright | 0 {debian => packages/debian}/rules | 0 .../debian}/sanoid.README.Debian | 0 {debian => packages/debian}/sanoid.docs | 0 {debian => packages/debian}/sanoid.service | 0 {debian => packages/debian}/sanoid.timer | 0 {debian => packages/debian}/source/format | 0 packages/rhel/sanoid-1.4.18.tar.gz | Bin 0 -> 45685 bytes sanoid.spec => packages/rhel/sanoid.spec | 9 +++++---- packages/rhel/sources | 1 + 13 files changed, 6 insertions(+), 4 deletions(-) rename {debian => packages/debian}/changelog (100%) rename {debian => packages/debian}/compat (100%) rename {debian => packages/debian}/control (100%) rename {debian => packages/debian}/copyright (100%) rename {debian => packages/debian}/rules (100%) rename {debian => packages/debian}/sanoid.README.Debian (100%) rename {debian => packages/debian}/sanoid.docs (100%) rename {debian => packages/debian}/sanoid.service (100%) rename {debian => packages/debian}/sanoid.timer (100%) rename {debian => packages/debian}/source/format (100%) create mode 100644 packages/rhel/sanoid-1.4.18.tar.gz rename sanoid.spec => packages/rhel/sanoid.spec (92%) create mode 100644 packages/rhel/sources diff --git a/debian/changelog b/packages/debian/changelog similarity index 100% rename from debian/changelog rename to packages/debian/changelog diff --git a/debian/compat b/packages/debian/compat similarity index 100% rename from debian/compat rename to packages/debian/compat diff --git a/debian/control b/packages/debian/control similarity index 100% rename from debian/control rename to packages/debian/control diff --git a/debian/copyright b/packages/debian/copyright similarity index 100% rename from debian/copyright rename to packages/debian/copyright diff --git a/debian/rules b/packages/debian/rules similarity index 100% rename from debian/rules rename to packages/debian/rules diff --git a/debian/sanoid.README.Debian b/packages/debian/sanoid.README.Debian similarity index 100% rename from debian/sanoid.README.Debian rename to packages/debian/sanoid.README.Debian diff --git a/debian/sanoid.docs b/packages/debian/sanoid.docs similarity index 100% rename from debian/sanoid.docs rename to packages/debian/sanoid.docs diff --git a/debian/sanoid.service b/packages/debian/sanoid.service similarity index 100% rename from debian/sanoid.service rename to packages/debian/sanoid.service diff --git a/debian/sanoid.timer b/packages/debian/sanoid.timer similarity index 100% rename from debian/sanoid.timer rename to packages/debian/sanoid.timer diff --git a/debian/source/format b/packages/debian/source/format similarity index 100% rename from debian/source/format rename to packages/debian/source/format diff --git a/packages/rhel/sanoid-1.4.18.tar.gz b/packages/rhel/sanoid-1.4.18.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..df137cd5d635538c965b675d9e48d0f1abab742a GIT binary patch literal 45685 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 `eY~bgi^vmTxo4Des{JG?Scyn;d2qyT3%f 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 apCd18cKY=HGgSBf765;sEdZJW0s0Tr??sRR literal 0 HcmV?d00001 diff --git a/sanoid.spec b/packages/rhel/sanoid.spec similarity index 92% rename from sanoid.spec rename to packages/rhel/sanoid.spec index c0f33ed..ab299a5 100644 --- a/sanoid.spec +++ b/packages/rhel/sanoid.spec @@ -1,4 +1,4 @@ -%global version 1.4.14 +%global version 1.4.18 %global git_tag v%{version} # Enable with systemctl "enable sanoid.timer" @@ -6,13 +6,13 @@ Name: sanoid Version: %{version} -Release: 2%{?dist} +Release: 1%{?dist} BuildArch: noarch Summary: A policy-driven snapshot management tool for ZFS file systems Group: Applications/System License: GPLv3 URL: https://github.com/jimsalterjrs/sanoid -Source0: https://github.com/jimsalterjrs/%{name}/archive/%{git_tag}/%{name}-%{version}.tar.gz +Source0: https://github.com/jimsalterjrs/%{name}/archive/%{git_tag}/%{name}-%{version}.tar.gz Requires: perl, mbuffer, lzop, pv %if 0%{?_with_systemd} @@ -110,6 +110,8 @@ echo "* * * * * root %{_sbindir}/sanoid --cron" > %{buildroot}%{_docdir}/%{name} %endif %changelog +* Sat Apr 28 2018 Dominic Robinson - 1.4.18-1 +- Bump to 1.4.18 * Thu Aug 31 2017 Dominic Robinson - 1.4.14-2 - Add systemd timers * Wed Aug 30 2017 Dominic Robinson - 1.4.14-1 @@ -121,6 +123,5 @@ echo "* * * * * root %{_sbindir}/sanoid --cron" > %{buildroot}%{_docdir}/%{name} - Version bump - Clean up variables and macros - Compatible with both Fedora and Red Hat - * Sat Feb 13 2016 Thomas M. Lapp - 1.4.4-1 - Initial RPM Package diff --git a/packages/rhel/sources b/packages/rhel/sources new file mode 100644 index 0000000..d6068d4 --- /dev/null +++ b/packages/rhel/sources @@ -0,0 +1 @@ +cf0ec23c310d2f9416ebabe48f5edb73 sanoid-1.4.18.tar.gz From faf825003fbb7448be5a566fe1c238711a031298 Mon Sep 17 00:00:00 2001 From: Eric Coutu Date: Sun, 29 Apr 2018 15:06:31 -0400 Subject: [PATCH 22/28] Document compatibility with (t)csh shells Also, recommend installing mbuffer on FreeBSD systems when remote user is using bourne compatible shell. --- FREEBSD.readme | 11 +++++++++++ INSTALL | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/FREEBSD.readme b/FREEBSD.readme index 9960201..732940d 100644 --- a/FREEBSD.readme +++ b/FREEBSD.readme @@ -11,3 +11,14 @@ If you don't want to have to change the shebangs, your other option is to drop a root@bsd:~# ln -s /usr/local/bin/perl /usr/bin/perl After putting this symlink in place, ANY perl script shebanged for Linux will work on your system too. + +Syncoid assumes a bourne style shell on remote hosts. Using (t)csh (the default for root under FreeBSD) +has some known issues: + +* If mbuffer is present, syncoid will fail with an "Ambiguous output redirect." error. So if you: + root@bsd:~# ln -s /usr/local/bin/mbuffer /usr/bin/mbuffer + make sure the remote user is using an sh compatible shell. + +To change to a compatible shell, use the chsh command: + +root@bsd:~# chsh -s /bin/sh diff --git a/INSTALL b/INSTALL index 1492546..33b510d 100644 --- a/INSTALL +++ b/INSTALL @@ -9,7 +9,7 @@ is not available on either end of the transport. On Ubuntu: apt install pv lzop mbuffer On CentOS: yum install lzo pv mbuffer lzop -On FreeBSD: pkg install pv lzop +On FreeBSD: pkg install pv mbuffer lzop FreeBSD notes: FreeBSD may place pv and lzop in somewhere other than /usr/bin ; syncoid currently does not check path. @@ -19,6 +19,8 @@ FreeBSD notes: FreeBSD may place pv and lzop in somewhere other than or similar, as appropriate, to create links in /usr/bin to wherever the utilities actually are on your system. + See note about mbuffer in FREEBSD.readme + SANOID ------ From 63979973b2ed5da4c7c1884c43f428041cdcde84 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Wed, 2 May 2018 00:15:14 +0200 Subject: [PATCH 23/28] if syncoid is instructed to skip snapshot creation it shouldn't attempt to prune existing ones --- syncoid | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/syncoid b/syncoid index e63b68c..999ee79 100755 --- a/syncoid +++ b/syncoid @@ -387,9 +387,11 @@ sub syncdataset { } } - # prune obsolete sync snaps on source and target. - pruneoldsyncsnaps($sourcehost,$sourcefs,$newsyncsnap,$sourceisroot,keys %{ $snaps{'source'}}); - pruneoldsyncsnaps($targethost,$targetfs,$newsyncsnap,$targetisroot,keys %{ $snaps{'target'}}); + if (!defined $args{'no-sync-snap'}) { + # prune obsolete sync snaps on source and target (only if this run created ones). + pruneoldsyncsnaps($sourcehost,$sourcefs,$newsyncsnap,$sourceisroot,keys %{ $snaps{'source'}}); + pruneoldsyncsnaps($targethost,$targetfs,$newsyncsnap,$targetisroot,keys %{ $snaps{'target'}}); + } } # end syncdataset() From bb654c1cf6d59f06727be8b93c66bbdd2a42b9d1 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 5 Jun 2018 11:41:48 +0200 Subject: [PATCH 24/28] use utc for timestamps as default --- README.md | 4 +++- packages/debian/sanoid.service | 1 + packages/rhel/sanoid.spec | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc75bdf..be6195d 100644 --- a/README.md +++ b/README.md @@ -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: ``` -* * * * * /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: ``` diff --git a/packages/debian/sanoid.service b/packages/debian/sanoid.service index b54c586..2d01bbf 100644 --- a/packages/debian/sanoid.service +++ b/packages/debian/sanoid.service @@ -5,5 +5,6 @@ After=zfs.target ConditionFileNotEmpty=/etc/sanoid/sanoid.conf [Service] +Environment=TZ=UTC Type=oneshot ExecStart=/usr/sbin/sanoid --cron diff --git a/packages/rhel/sanoid.spec b/packages/rhel/sanoid.spec index ab299a5..3a9412f 100644 --- a/packages/rhel/sanoid.spec +++ b/packages/rhel/sanoid.spec @@ -58,6 +58,7 @@ Requires=zfs.target After=zfs.target [Service] +Environment=TZ=UTC Type=oneshot ExecStart=%{_sbindir}/sanoid --cron EOF From 52661afb4125ba350ce0ec54e3928a4ab2140f33 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Tue, 5 Jun 2018 12:45:13 +0200 Subject: [PATCH 25/28] updated debian changelog --- packages/debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/debian/changelog b/packages/debian/changelog index ab530b0..2bcf423 100644 --- a/packages/debian/changelog +++ b/packages/debian/changelog @@ -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 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 Wed, 8 Nov 2017 15:25:00 -0400 + 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 From e65879d1f8c4a1304ee0d16d4393645b5e07de1e Mon Sep 17 00:00:00 2001 From: Lucas Salibian Date: Fri, 8 Jun 2018 11:52:52 -0400 Subject: [PATCH 26/28] Fix --help typo --- syncoid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncoid b/syncoid index 999ee79..a15f872 100755 --- a/syncoid +++ b/syncoid @@ -1148,7 +1148,7 @@ Options: --sshoption|o=OPTION Passes OPTION to ssh for remote usage. Can be specified multiple times --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 --monitor-version Currently does nothing --quiet Suppresses non-error output From 34b942ea45cbc686c1e0fe16c8dd6c4515115826 Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Thu, 21 Jun 2018 18:18:28 +0200 Subject: [PATCH 27/28] correctly parse zfs column output (space can be included in the values) --- sanoid | 2 +- syncoid | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sanoid b/sanoid index b6dc9fe..c07e090 100755 --- a/sanoid +++ b/sanoid @@ -521,7 +521,7 @@ sub getsnaps { } 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 if (defined $snapname) { diff --git a/syncoid b/syncoid index 999ee79..ca7328f 100755 --- a/syncoid +++ b/syncoid @@ -664,7 +664,7 @@ sub getzfsvalue { open FH, "$rhost $mysudocmd $zfscmd get -H $property $fsescaped |"; my $value = ; close FH; - my @values = split(/\s/,$value); + my @values = split(/\t/,$value); $value = $values[2]; return $value; } @@ -985,7 +985,7 @@ sub getsnaps() { if ($line =~ /\Q$fs\E\@.*guid/) { chomp $line; my $guid = $line; - $guid =~ s/^.*\sguid\s*(\d*).*/$1/; + $guid =~ s/^.*\tguid\t*(\d*).*/$1/; my $snap = $line; $snap =~ s/^.*\@(.*)\tguid.*$/$1/; $snaps{$type}{$snap}{'guid'}=$guid; @@ -997,7 +997,7 @@ sub getsnaps() { if ($line =~ /\Q$fs\E\@.*creation/) { chomp $line; my $creation = $line; - $creation =~ s/^.*\screation\s*(\d*).*/$1/; + $creation =~ s/^.*\tcreation\t*(\d*).*/$1/; my $snap = $line; $snap =~ s/^.*\@(.*)\tcreation.*$/$1/; $snaps{$type}{$snap}{'creation'}=$creation; @@ -1056,9 +1056,9 @@ sub getsendsize { # the output format is different in case of # a resumed receive if (defined($receivetoken)) { - $sendsize =~ s/.*\s([0-9]+)$/$1/; + $sendsize =~ s/.*\t([0-9]+)$/$1/; } else { - $sendsize =~ s/^size\s*//; + $sendsize =~ s/^size\t*//; } chomp $sendsize; From 1f885801993eec2ce259d3ba4beb7704c6eb496d Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Thu, 28 Jun 2018 17:45:18 +0200 Subject: [PATCH 28/28] implemented support for excluding datasets from replication with a regular expression --- README.md | 4 ++++ syncoid | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc75bdf..e98763b 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,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. ++ --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 This argument tells syncoid to not use resumeable zfs send/receive streams. diff --git a/syncoid b/syncoid index 999ee79..5cd925c 100755 --- a/syncoid +++ b/syncoid @@ -19,7 +19,7 @@ use Sys::Hostname; my %args = ('sshkey' => '', 'sshport' => '', 'sshcipher' => '', 'sshoption' => [], 'target-bwlimit' => '', 'source-bwlimit' => ''); 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@", - "debug", "quiet", "no-stream", "no-sync-snap", "no-resume") or pod2usage(2); + "debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@") or pod2usage(2); my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set @@ -141,6 +141,20 @@ sub getchilddatasets { my @children = ; close FH; + 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; } @@ -1141,6 +1155,7 @@ Options: --target-bwlimit= Bandwidth limit on the target transfer --no-stream Replicates using newest snapshot instead of intermediates --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 --sshport=PORT Connects to remote on a particular port