This commit is contained in:
Felix Matouschek 2026-02-20 13:46:12 +01:00 committed by GitHub
commit 117706e2b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 151 additions and 7 deletions

34
syncoid
View File

@ -24,7 +24,7 @@ my %args = ('sshconfig' => '', 'sshkey' => '', 'sshport' => '', 'sshcipher' => '
GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsnaps", "recursive|r", "sendoptions=s", "recvoptions=s", GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsnaps", "recursive|r", "sendoptions=s", "recvoptions=s",
"source-bwlimit=s", "target-bwlimit=s", "sshconfig=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@", "source-bwlimit=s", "target-bwlimit=s", "sshconfig=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@",
"debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent", "identifier=s", "debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent", "identifier=s",
"no-clone-handling", "no-privilege-elevation", "force-delete", "no-rollback", "create-bookmark", "use-hold", "no-clone-handling", "no-privilege-elevation", "force-delete", "no-rollback", "create-bookmark", "no-bookmark", "use-hold",
"pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size, "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size,
"delete-target-snapshots", "insecure-direct-connection=s", "preserve-properties", "delete-target-snapshots", "insecure-direct-connection=s", "preserve-properties",
"include-snaps=s@", "exclude-snaps=s@", "exclude-datasets=s@") "include-snaps=s@", "exclude-snaps=s@", "exclude-datasets=s@")
@ -440,6 +440,7 @@ sub syncdataset {
my $newsyncsnap; my $newsyncsnap;
my $matchingsnap; my $matchingsnap;
my $usebookmark = 0;
# skip snapshot checking/creation in case of resumed receive # skip snapshot checking/creation in case of resumed receive
if (!defined($receivetoken)) { if (!defined($receivetoken)) {
@ -606,10 +607,10 @@ sub syncdataset {
my $targetsize = getzfsvalue($targethost,$targetfs,$targetisroot,'-p used'); my $targetsize = getzfsvalue($targethost,$targetfs,$targetisroot,'-p used');
my %bookmark = (); my %bookmark = ();
my $bookmarksnap = 0;
$matchingsnap = getmatchingsnapshot($sourcefs, $targetfs, \%snaps); if (! defined $args{'no-bookmark'}) {
if (! $matchingsnap) { # find most recent matching bookmark
# no matching snapshots, check for bookmarks as fallback
my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot); my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot);
# check for matching guid of source bookmark and target snapshot (newest first) # check for matching guid of source bookmark and target snapshot (newest first)
@ -619,11 +620,15 @@ sub syncdataset {
if (defined $bookmarks{$guid}) { if (defined $bookmarks{$guid}) {
# found a match # found a match
%bookmark = %{ $bookmarks{$guid} }; %bookmark = %{ $bookmarks{$guid} };
$matchingsnap = $snap; $bookmarksnap = $snap;
last; last;
} }
} }
}
$matchingsnap = getmatchingsnapshot($sourcefs, $targetfs, \%snaps);
if (! $matchingsnap) {
# no matching snapshots, check for bookmarks as fallback
if (! %bookmark) { if (! %bookmark) {
# force delete is not possible for the root dataset # force delete is not possible for the root dataset
if ($args{'force-delete'} && index($targetfs, '/') != -1) { if ($args{'force-delete'} && index($targetfs, '/') != -1) {
@ -678,6 +683,20 @@ sub syncdataset {
# return false now in case more child datasets need replication. # return false now in case more child datasets need replication.
return 0; return 0;
} else {
# use bookmark as fallback
$matchingsnap = $bookmarksnap;
$usebookmark = 1;
}
} elsif (%bookmark) {
my $comparisonkey = 'creation';
if (defined $snaps{'source'}{$matchingsnap}{'createtxg'} && defined $bookmark{'createtxg'}) {
$comparisonkey = 'createtxg';
}
if ($bookmark{$comparisonkey} > $snaps{'source'}{$matchingsnap}{$comparisonkey}) {
writelog('DEBUG', "using bookmark $bookmark{'name'} because it was created after latest matching snapshot $matchingsnap");
$matchingsnap = $bookmarksnap;
$usebookmark = 1;
} }
} }
@ -697,7 +716,7 @@ sub syncdataset {
my $nextsnapshot = 0; my $nextsnapshot = 0;
if (%bookmark) { if ($usebookmark) {
if (!defined $args{'no-stream'}) { if (!defined $args{'no-stream'}) {
# if intermediate snapshots are needed we need to find the next oldest snapshot, # if intermediate snapshots are needed we need to find the next oldest snapshot,
@ -758,7 +777,7 @@ sub syncdataset {
# do a normal replication if bookmarks aren't used or if previous # do a normal replication if bookmarks aren't used or if previous
# bookmark replication was only done to the next oldest snapshot # bookmark replication was only done to the next oldest snapshot
# edge case: skip replication if bookmark replication used the latest snapshot # edge case: skip replication if bookmark replication used the latest snapshot
if ((!%bookmark || $nextsnapshot) && !($matchingsnap eq $newsyncsnap)) { if ((!$usebookmark || $nextsnapshot) && !($matchingsnap eq $newsyncsnap)) {
($exit, $stdout) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $matchingsnap, $newsyncsnap, defined($args{'no-stream'})); ($exit, $stdout) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $matchingsnap, $newsyncsnap, defined($args{'no-stream'}));
@ -2422,6 +2441,7 @@ Options:
--no-sync-snap Does not create new snapshot, only transfers existing --no-sync-snap Does not create new snapshot, only transfers existing
--keep-sync-snap Don't destroy created sync snapshots --keep-sync-snap Don't destroy created sync snapshots
--create-bookmark Creates a zfs bookmark for the newest snapshot on the source after replication succeeds (only works with --no-sync-snap) --create-bookmark Creates a zfs bookmark for the newest snapshot on the source after replication succeeds (only works with --no-sync-snap)
--no-bookmark Do not use bookmarks when trying to find the latest common snapshot. This forces a rollback when the latest snapshot on the source was deleted but a common and older snapshot was found.
--use-hold Adds a hold to the newest snapshot on the source and target after replication succeeds and removes the hold after the next successful replication. The hold name includes the identifier if set. This allows for separate holds in case of multiple targets --use-hold Adds a hold to the newest snapshot on the source and target after replication succeeds and removes the hold after the next successful replication. The hold name includes the identifier if set. This allows for separate holds in case of multiple targets
--preserve-recordsize Preserves the recordsize on initial sends to the target --preserve-recordsize Preserves the recordsize on initial sends to the target
--preserve-properties Preserves locally set dataset properties similar to the zfs send -p flag but this one will also work for encrypted datasets in non raw sends --preserve-properties Preserves locally set dataset properties similar to the zfs send -p flag but this one will also work for encrypted datasets in non raw sends

View File

@ -0,0 +1,62 @@
#!/bin/bash
# test using bookmark created after last snapshot
set -x
set -e
. ../../common/lib.sh
POOL_IMAGE="/tmp/syncoid-test-015.zpool"
MOUNT_TARGET="/tmp/syncoid-test-015.mount"
POOL_SIZE="1000M"
POOL_NAME="syncoid-test-015"
TARGET_CHECKSUM="73d7271f58f0d79eea0dd69d5ee3f4fe3aeaa3cb8106f7fc88feded5be3ce04e -"
truncate -s "${POOL_SIZE}" "${POOL_IMAGE}"
zpool create -m "${MOUNT_TARGET}" -f "${POOL_NAME}" "${POOL_IMAGE}"
function cleanUp {
zpool export "${POOL_NAME}"
}
# export pool in any case
trap cleanUp EXIT
zfs create "${POOL_NAME}"/a
zfs snapshot "${POOL_NAME}"/a@s0
# This fully replicates a to b
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 1" > "${MOUNT_TARGET}"/a/file1
zfs snapshot "${POOL_NAME}"/a@s1
# This incrementally replicates from a@s0 to a@s1
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 2" > "${MOUNT_TARGET}"/a/file2
zfs snapshot "${POOL_NAME}"/a@s2
# Destroy latest common snap between a and b
zfs destroy "${POOL_NAME}"/a@s1
# This uses a#s1 as base snap although common but older snap a@s0 exists
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 3" > "${MOUNT_TARGET}"/a/file3
zfs snapshot "${POOL_NAME}"/a@s3
# This uses a@s2 as base snap again
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
# verify
output=$(zfs list -t snapshot -r -H -o name "${POOL_NAME}")
checksum=$(echo "${output}" | shasum -a 256)
if [ "${checksum}" != "${TARGET_CHECKSUM}" ]; then
exit 1
fi
exit 0

View File

@ -0,0 +1,62 @@
#!/bin/bash
# test that a rollback is required when not using bookmarks
set -x
set -e
. ../../common/lib.sh
POOL_IMAGE="/tmp/syncoid-test-016.zpool"
MOUNT_TARGET="/tmp/syncoid-test-016.mount"
POOL_SIZE="1000M"
POOL_NAME="syncoid-test-016"
TARGET_CHECKSUM="0ed2eed1488bbba5c35c2b24beee9e6e8a76f8de10aa1e3710dc737cf626635a -"
truncate -s "${POOL_SIZE}" "${POOL_IMAGE}"
zpool create -m "${MOUNT_TARGET}" -f "${POOL_NAME}" "${POOL_IMAGE}"
function cleanUp {
zpool export "${POOL_NAME}"
}
# export pool in any case
trap cleanUp EXIT
zfs create "${POOL_NAME}"/a
zfs snapshot "${POOL_NAME}"/a@s0
# This fully replicates a to b
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 1" > "${MOUNT_TARGET}"/a/file1
zfs snapshot "${POOL_NAME}"/a@s1
# This incrementally replicates from a@s0 to a@s1
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 2" > "${MOUNT_TARGET}"/a/file2
zfs snapshot "${POOL_NAME}"/a@s2
# Destroy latest common snap between a and b
zfs destroy "${POOL_NAME}"/a@s1
# This uses a@s0 and rolls b back to it although common and newer bookmark a#s1 exists
../../../syncoid --debug --no-sync-snap --no-bookmark --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
echo "Test 3" > "${MOUNT_TARGET}"/a/file3
zfs snapshot "${POOL_NAME}"/a@s3
# This uses a@s2 as base snap again
../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b
# verify
output=$(zfs list -t snapshot -r -H -o name "${POOL_NAME}")
checksum=$(echo "${output}" | shasum -a 256)
if [ "${checksum}" != "${TARGET_CHECKSUM}" ]; then
exit 1
fi
exit 0