diff --git a/README.md b/README.md index 5ce4e8a..bc8ed83 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ More prosaically, you can use Sanoid to create, automatically thin, and monitor * * * * * TZ=UTC /usr/local/bin/sanoid --cron ``` -**`IMPORTANT NOTE`**: using a local timezone will result in a single hourly snapshot to be **skipped** during `daylight->nodaylight` transition. To avoid that, using UTC as timezone is recommend whenever possible. +`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/sanoid b/sanoid index 5a842bb..a17b91d 100755 --- a/sanoid +++ b/sanoid @@ -15,6 +15,7 @@ use File::Path; # for rmtree command in use_prune use Getopt::Long qw(:config auto_version auto_help); use Pod::Usage; # pod2usage use Time::Local; # to parse dates in reverse +use Capture::Tiny ':all'; my %args = ("configdir" => "/etc/sanoid"); GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet", @@ -357,6 +358,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; } @@ -380,6 +394,9 @@ sub take_snapshots { my @preferredtime; my $lastpreferred; + # to avoid duplicates with DST + my $handleDst = 0; + if ($type eq 'frequently') { my $frequentslice = int($datestamp{'min'} / $config{$section}{'frequent_period'}); @@ -399,6 +416,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 + $handleDst = 1; + } 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 @@ -408,10 +432,29 @@ sub take_snapshots { push @preferredtime,($datestamp{'mon'}-1); # january is month 0 push @preferredtime,$datestamp{'year'}; $lastpreferred = timelocal(@preferredtime); - if ($lastpreferred > time()) { - $preferredtime[3] -= 1; # preferred time is later today - so look at yesterday's - $lastpreferred = timelocal(@preferredtime); + + # 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 'weekly') { # calculate offset in seconds for the desired weekday my $offset = 0; @@ -461,9 +504,17 @@ sub take_snapshots { %datestamp = get_date(); # print "we should have had a $type snapshot of $path $maxage seconds ago; most recent is $newestage seconds old.\n"; + my $flags = ""; # use zfs (atomic) recursion if specified in config if ($config{$section}{'zfs_recursion'}) { - push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type\@"); + $flags .= "r"; + } + if ($handleDst) { + $flags .= "d"; + } + + if ($flags ne "") { + push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type\@$flags"); } else { push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type"); } @@ -477,9 +528,18 @@ sub take_snapshots { my $extraMessage = ""; my @split = split '@', $snap, -1; my $recursiveFlag = 0; + my $dstHandling = 0; if (scalar(@split) == 3) { - $recursiveFlag = 1; - $extraMessage = " (zfs recursive)"; + my $flags = $split[2]; + if (index($flags, "r") != -1) { + $recursiveFlag = 1; + $extraMessage = " (zfs recursive)"; + chop $snap; + } + if (index($flags, "d") != -1) { + $dstHandling = 1; + chop $snap; + } chop $snap; } my $dataset = $split[0]; @@ -506,13 +566,40 @@ sub take_snapshots { } if ($args{'verbose'}) { print "taking snapshot $snap$extraMessage\n"; } if (!$args{'readonly'}) { - if ($recursiveFlag) { - system($zfs, "snapshot", "-r", "$snap") == 0 - or warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?"; - } else { - system($zfs, "snapshot", "$snap") == 0 - or warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?"; - } + my $stderr; + my $exit; + ($stderr, $exit) = tee_stderr { + if ($recursiveFlag) { + system($zfs, "snapshot", "-r", "$snap"); + } else { + system($zfs, "snapshot", "$snap"); + } + }; + + $exit == 0 or do { + if ($dstHandling) { + if ($stderr =~ /already exists/) { + $exit = 0; + $snap =~ s/_([a-z]+)$/dst_$1/g; + if ($args{'verbose'}) { print "taking dst snapshot $snap$extraMessage\n"; } + if ($recursiveFlag) { + system($zfs, "snapshot", "-r", "$snap") == 0 + or warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?"; + } else { + system($zfs, "snapshot", "$snap") == 0 + or warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?"; + } + } + } + }; + + $exit == 0 or do { + if ($recursiveFlag) { + warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?"; + } else { + warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?"; + } + }; } if ($config{$dataset}{'post_snapshot_script'}) { if (!$presnapshotfailure or $config{$dataset}{'force_post_snapshot_script'}) { diff --git a/tests/1_one_year/run.sh b/tests/1_one_year/run.sh index 88100da..fe76946 100755 --- a/tests/1_one_year/run.sh +++ b/tests/1_one_year/run.sh @@ -10,7 +10,7 @@ set -x POOL_NAME="sanoid-test-1" POOL_TARGET="" # root RESULT="/tmp/sanoid_test_result" -RESULT_CHECKSUM="68c67161a59d0e248094a66061972f53613067c9db52ad981030f36bc081fed7" +RESULT_CHECKSUM="92f2c7afba94b59e8a6f6681705f0aa3f1c61e4aededaa38281e0b7653856935" # UTC timestamp of start and end START="1483225200" diff --git a/tests/2_dst_handling/run.sh b/tests/2_dst_handling/run.sh index e86ca6e..3231631 100755 --- a/tests/2_dst_handling/run.sh +++ b/tests/2_dst_handling/run.sh @@ -13,7 +13,7 @@ set -x POOL_NAME="sanoid-test-2" POOL_TARGET="" # root RESULT="/tmp/sanoid_test_result" -RESULT_CHECKSUM="0a6336ccdc948c69563cb56994d190aebbc9b21588aef17bb97e51ae074f879a" +RESULT_CHECKSUM="846372ef238f2182b382c77a73ecddf99aa82f28cc9995bcc95592cc78305463" # UTC timestamp of start and end START="1509141600" @@ -49,6 +49,6 @@ done saveSnapshotList "${POOL_NAME}" "${RESULT}" # hourly daily monthly -verifySnapshotList "${RESULT}" 72 3 1 "${RESULT_CHECKSUM}" +verifySnapshotList "${RESULT}" 73 3 1 "${RESULT_CHECKSUM}" # one more hour because of DST diff --git a/tests/common/lib.sh b/tests/common/lib.sh index b070da2..904c98f 100644 --- a/tests/common/lib.sh +++ b/tests/common/lib.sh @@ -61,9 +61,9 @@ function saveSnapshotList { # clear the seconds for comparing if [ "$unamestr" == 'FreeBSD' ]; then - 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}" + 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}" else - 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}" + 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}" fi }