From c4e70280225f7b3ec03cc075daf218dde468412f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 27 Nov 2021 16:10:02 -0700 Subject: [PATCH 1/9] Refactor terminal output Replace `print` and `warn` statements with a logging function. --- syncoid | 299 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 159 insertions(+), 140 deletions(-) diff --git a/syncoid b/syncoid index ec6ae9d..fa6feff 100755 --- a/syncoid +++ b/syncoid @@ -34,7 +34,7 @@ my @sendoptions = (); if (length $args{'sendoptions'}) { @sendoptions = parsespecialoptions($args{'sendoptions'}); if (! defined($sendoptions[0])) { - warn "invalid send options!"; + writelog('WARN', "invalid send options!"); pod2usage(2); exit 127; } @@ -42,7 +42,7 @@ if (length $args{'sendoptions'}) { if (defined $args{'recursive'}) { foreach my $option(@sendoptions) { if ($option->{option} eq 'R') { - warn "invalid argument combination, zfs send -R and --recursive aren't compatible!"; + writelog('WARN', "invalid argument combination, zfs send -R and --recursive aren't compatible!"); pod2usage(2); exit 127; } @@ -54,7 +54,7 @@ my @recvoptions = (); if (length $args{'recvoptions'}) { @recvoptions = parsespecialoptions($args{'recvoptions'}); if (! defined($recvoptions[0])) { - warn "invalid receive options!"; + writelog('WARN', "invalid receive options!"); pod2usage(2); exit 127; } @@ -63,7 +63,7 @@ if (length $args{'recvoptions'}) { # TODO Expand to accept multiple sources? if (scalar(@ARGV) != 2) { - print("Source or target not found!\n"); + writelog('WARN', "Source or target not found!"); pod2usage(2); exit 127; } else { @@ -117,7 +117,7 @@ my $identifier = ""; if (length $args{'identifier'}) { if ($args{'identifier'} !~ /^[a-zA-Z0-9-_:.]+$/) { # invalid extra identifier - print("CRITICAL: extra identifier contains invalid chars!\n"); + writelog('WARN', "extra identifier contains invalid chars!"); pod2usage(2); exit 127; } @@ -126,7 +126,7 @@ if (length $args{'identifier'}) { # figure out if source and/or target are remote. $sshcmd = "$sshcmd $args{'sshcipher'} $sshoptions $args{'sshport'} $args{'sshkey'}"; -if ($debug) { print "DEBUG: SSHCMD: $sshcmd\n"; } +writelog('DEBUG', "SSHCMD: $sshcmd"); my ($sourcehost,$sourcefs,$sourceisroot) = getssh($rawsourcefs); my ($targethost,$targetfs,$targetisroot) = getssh($rawtargetfs); @@ -148,11 +148,11 @@ my $exitcode = 0; if (!defined $args{'recursive'}) { syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, undef); } else { - if ($debug) { print "DEBUG: recursive sync of $sourcefs.\n"; } + writelog('DEBUG', "recursive sync of $sourcefs."); my @datasets = getchilddatasets($sourcehost, $sourcefs, $sourceisroot); if (!@datasets) { - warn "CRITICAL ERROR: no datasets found"; + writelog('CRITICAL', "no datasets found"); @datasets = (); $exitcode = 2; } @@ -191,7 +191,6 @@ if (!defined $args{'recursive'}) { chomp $dataset; my $childsourcefs = $sourcefs . $dataset; my $childtargetfs = $targetfs . $dataset; - # print "syncdataset($sourcehost, $childsourcefs, $targethost, $childtargetfs); \n"; syncdataset($sourcehost, $childsourcefs, $targethost, $childtargetfs, $origin); } @@ -238,7 +237,7 @@ sub getchilddatasets { } my $getchildrencmd = "$rhost $mysudocmd $zfscmd list -o name,origin -t filesystem,volume -Hr $fsescaped |"; - if ($debug) { print "DEBUG: getting list of child datasets on $fs using $getchildrencmd...\n"; } + writelog('DEBUG', "getting list of child datasets on $fs using $getchildrencmd..."); if (! open FH, $getchildrencmd) { die "ERROR: list command failed!\n"; } @@ -261,7 +260,7 @@ sub getchilddatasets { my $excludes = $args{'exclude'}; foreach (@$excludes) { if ($dataset =~ /$_/) { - if ($debug) { print "DEBUG: excluded $dataset because of $_\n"; } + writelog('DEBUG', "excluded $dataset because of $_"); next DATASETS; } } @@ -291,19 +290,19 @@ sub syncdataset { # keep forcedrecv as a variable to allow us to disable it with an optional argument later if necessary my $forcedrecv = "-F"; - if ($debug) { print "DEBUG: syncing source $sourcefs to target $targetfs.\n"; } + writelog('DEBUG', "syncing source $sourcefs to target $targetfs."); my ($sync, $error) = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'syncoid:sync'); if (!defined $sync) { # zfs already printed the corresponding error if ($error =~ /\bdataset does not exist\b/) { - if (!$quiet) { print "WARN Skipping dataset (dataset no longer exists): $sourcefs...\n"; } + writelog('WARN', "Skipping dataset (dataset no longer exists): $sourcefs..."); return 0; } else { # print the error out and set exit code - print "ERROR: $error\n"; + writelog('CRITICAL', "$error"); if ($exitcode < 2) { $exitcode = 2 } } @@ -314,20 +313,20 @@ sub syncdataset { # empty is handled the same as unset (aka: '-') # definitely sync this dataset - if a host is called 'true' or '-', then you're special } elsif ($sync eq 'false') { - if (!$quiet) { print "INFO: Skipping dataset (syncoid:sync=false): $sourcefs...\n"; } + writelog('INFO', "Skipping dataset (syncoid:sync=false): $sourcefs..."); return 0; } else { my $hostid = hostname(); my @hosts = split(/,/,$sync); if (!(grep $hostid eq $_, @hosts)) { - if (!$quiet) { print "INFO: Skipping dataset (syncoid:sync doesn't include $hostid): $sourcefs...\n"; } + writelog('INFO', "Skipping dataset (syncoid:sync doesn't include $hostid): $sourcefs..."); return 0; } } # make sure target is not currently in receive. if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - warn "Cannot sync now: $targetfs is already target of a zfs receive process.\n"; + writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); if ($exitcode < 1) { $exitcode = 1; } return 0; } @@ -346,8 +345,8 @@ sub syncdataset { # check remote dataset for receive resume token (interrupted receive) $receivetoken = getreceivetoken($targethost,$targetfs,$targetisroot); - if ($debug && defined($receivetoken)) { - print "DEBUG: got receive resume token: $receivetoken: \n"; + if (defined($receivetoken)) { + writelog('DEBUG', "got receive resume token: $receivetoken: "); } } } @@ -367,9 +366,8 @@ sub syncdataset { } if (defined $args{'dumpsnaps'}) { - print "merged snapshot list of $targetfs: \n"; + writelog('INFO', "merged snapshot list of $targetfs: "); dumphash(\%snaps); - print "\n\n\n"; } if (!defined $args{'no-sync-snap'} && !defined $skipsnapshot) { @@ -383,7 +381,7 @@ sub syncdataset { # we don't want sync snapshots created, so use the newest snapshot we can find. $newsyncsnap = getnewestsnapshot($sourcehost,$sourcefs,$sourceisroot); if ($newsyncsnap eq 0) { - warn "CRITICAL: no snapshots exist on source $sourcefs, and you asked for --no-sync-snap.\n"; + writelog('WARN', "CRITICAL: no snapshots exist on source $sourcefs, and you asked for --no-sync-snap."); if ($exitcode < 1) { $exitcode = 1; } return 0; } @@ -407,12 +405,10 @@ sub syncdataset { if (! $targetexists) { # do an initial sync from the oldest source snapshot # THEN do an -I to the newest - if ($debug) { - if (!defined ($args{'no-stream'}) ) { - print "DEBUG: target $targetfs does not exist. Finding oldest available snapshot on source $sourcefs ...\n"; - } else { - print "DEBUG: target $targetfs does not exist, and --no-stream selected. Finding newest available snapshot on source $sourcefs ...\n"; - } + if (!defined ($args{'no-stream'}) ) { + writelog('DEBUG', "target $targetfs does not exist. Finding oldest available snapshot on source $sourcefs ..."); + } else { + writelog('DEBUG', "target $targetfs does not exist, and --no-stream selected. Finding newest available snapshot on source $sourcefs ..."); } my $oldestsnap = getoldestsnapshot(\%snaps); if (! $oldestsnap) { @@ -422,7 +418,7 @@ sub syncdataset { } # getoldestsnapshot() returned false, so use new sync snapshot - if ($debug) { print "DEBUG: getoldestsnapshot() returned false, so using $newsyncsnap.\n"; } + writelog('DEBUG', "getoldestsnapshot() returned false, so using $newsyncsnap."); $oldestsnap = $newsyncsnap; } @@ -462,32 +458,30 @@ sub syncdataset { my $disp_pvsize = readablebytes($pvsize); if ($pvsize == 0) { $disp_pvsize = 'UNKNOWN'; } my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - if (!$quiet) { - if (defined $origin) { - print "INFO: Clone is recreated on target $targetfs based on $origin\n"; - } - if (!defined ($args{'no-stream'}) ) { - print "INFO: Sending oldest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:\n"; - } else { - print "INFO: --no-stream selected; sending newest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:\n"; - } + if (defined $origin) { + writelog('INFO', "Clone is recreated on target $targetfs based on $origin"); } - if ($debug) { print "DEBUG: $synccmd\n"; } + if (!defined ($args{'no-stream'}) ) { + writelog('INFO', "Sending oldest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:"); + } else { + writelog('INFO', "--no-stream selected; sending newest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:"); + } + writelog('DEBUG', "$synccmd"); # make sure target is (still) not currently in receive. if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - warn "Cannot sync now: $targetfs is already target of a zfs receive process.\n"; + writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); if ($exitcode < 1) { $exitcode = 1; } return 0; } system($synccmd) == 0 or do { if (defined $origin) { - print "INFO: clone creation failed, trying ordinary replication as fallback\n"; + writelog('INFO', "clone creation failed, trying ordinary replication as fallback"); syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, undef, 1); return 0; } - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; }; @@ -512,23 +506,23 @@ sub syncdataset { # make sure target is (still) not currently in receive. if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - warn "Cannot sync now: $targetfs is already target of a zfs receive process.\n"; + writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); if ($exitcode < 1) { $exitcode = 1; } return 0; } - if (!$quiet) { print "INFO: Updating new target filesystem with incremental $sourcefs\@$oldestsnap ... $newsyncsnap (~ $disp_pvsize):\n"; } - if ($debug) { print "DEBUG: $synccmd\n"; } + writelog('INFO', "Updating new target filesystem with incremental $sourcefs\@$oldestsnap ... $newsyncsnap (~ $disp_pvsize):"); + writelog('DEBUG', "$synccmd"); if ($oldestsnap ne $newsyncsnap) { my $ret = system($synccmd); if ($ret != 0) { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 1) { $exitcode = 1; } return 0; } } else { - if (!$quiet) { print "INFO: no incremental sync needed; $oldestsnap is already the newest available snapshot.\n"; } + writelog('INFO', "no incremental sync needed; $oldestsnap is already the newest available snapshot."); } # restore original readonly value to target after sync complete @@ -549,8 +543,8 @@ sub syncdataset { if ($pvsize == 0) { $disp_pvsize = "UNKNOWN"; } my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - if (!$quiet) { print "Resuming interrupted zfs send/receive from $sourcefs to $targetfs (~ $disp_pvsize remaining):\n"; } - if ($debug) { print "DEBUG: $synccmd\n"; } + writelog('INFO', "Resuming interrupted zfs send/receive from $sourcefs to $targetfs (~ $disp_pvsize remaining):"); + writelog('DEBUG', "$synccmd"); if ($pvsize == 0) { # we need to capture the error of zfs send, this will render pv useless but in this case @@ -570,12 +564,12 @@ sub syncdataset { $stdout =~ /\Qused in the initial send no longer exists\E/ || $stdout =~ /incremental source [0-9xa-f]+ no longer exists/ ) { - if (!$quiet) { print "WARN: resetting partially receive state because the snapshot source no longer exists\n"; } + writelog('WARN', "resetting partially receive state because the snapshot source no longer exists"); resetreceivestate($targethost,$targetfs,$targetisroot); # do an normal sync cycle return syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, $origin); } else { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -620,7 +614,7 @@ sub syncdataset { if (! $bookmark) { if ($args{'force-delete'}) { - if (!$quiet) { print "Removing $targetfs because no matching snapshots were found\n"; } + writelog('INFO', "Removing $targetfs because no matching snapshots were found"); my $rcommand = ''; my $mysudocmd = ''; @@ -636,7 +630,7 @@ sub syncdataset { my $ret = system("$rcommand $prunecmd"); if ($ret != 0) { - warn "WARNING: $rcommand $prunecmd failed: $?"; + writelog('WARN', "$rcommand $prunecmd failed: $?"); } else { # redo sync and skip snapshot creation (already taken) return syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, undef, 1); @@ -646,19 +640,27 @@ sub syncdataset { # if we got this far, we failed to find a matching snapshot/bookmark. if ($exitcode < 2) { $exitcode = 2; } - print "\n"; - print "CRITICAL ERROR: Target $targetfs exists but has no snapshots matching with $sourcefs!\n"; - print " Replication to target would require destroying existing\n"; - print " target. Cowardly refusing to destroy your existing target.\n\n"; + my $msg = <<~"EOT"; + + Target $targetfs exists but has no snapshots matching with $sourcefs! + Replication to target would require destroying existing + target. Cowardly refusing to destroy your existing target. + + EOT + + writelog('CRITICAL', $msg); # experience tells me we need a mollyguard for people who try to # zfs create targetpool/targetsnap ; syncoid sourcepool/sourcesnap targetpool/targetsnap ... if ( $targetsize < (64*1024*1024) ) { - print " NOTE: Target $targetfs dataset is < 64MB used - did you mistakenly run\n"; - print " \`zfs create $args{'target'}\` on the target? ZFS initial\n"; - print " replication must be to a NON EXISTENT DATASET, which will\n"; - print " then be CREATED BY the initial replication process.\n\n"; + $msg = <<~"EOT"; + NOTE: Target $targetfs dataset is < 64MB used - did you mistakenly run + `zfs create $args{'target'}` on the target? ZFS initial + replication must be to a NON EXISTENT DATASET, which will + then be CREATED BY the initial replication process. + + EOT } # return false now in case more child datasets need replication. @@ -668,14 +670,14 @@ sub syncdataset { # make sure target is (still) not currently in receive. if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - warn "Cannot sync now: $targetfs is already target of a zfs receive process.\n"; + writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); if ($exitcode < 1) { $exitcode = 1; } return 0; } if ($matchingsnap eq $newsyncsnap) { # barf some text but don't touch the filesystem - if (!$quiet) { print "INFO: no snapshots on source newer than $newsyncsnap on target. Nothing to do, not syncing.\n"; } + writelog('INFO', "no snapshots on source newer than $newsyncsnap on target. Nothing to do, not syncing."); return 0; } else { my $matchingsnapescaped = escapeshellparam($matchingsnap); @@ -708,8 +710,8 @@ sub syncdataset { my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - if (!$quiet) { print "Sending incremental $sourcefs#$bookmarkescaped ... $nextsnapshot (~ $disp_pvsize):\n"; } - if ($debug) { print "DEBUG: $synccmd\n"; } + writelog('INFO', "Sending incremental $sourcefs#$bookmarkescaped ... $nextsnapshot (~ $disp_pvsize):"); + writelog('DEBUG', "$synccmd"); ($stdout, $exit) = tee_stdout { system("$synccmd") @@ -717,15 +719,15 @@ sub syncdataset { $exit == 0 or do { if (!$resume && $stdout =~ /\Qcontains partially-complete state\E/) { - if (!$quiet) { print "WARN: resetting partially receive state\n"; } + writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); system("$synccmd") == 0 or do { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -738,8 +740,8 @@ sub syncdataset { my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - if (!$quiet) { print "Sending incremental $sourcefs#$bookmarkescaped ... $newsyncsnap (~ $disp_pvsize):\n"; } - if ($debug) { print "DEBUG: $synccmd\n"; } + writelog('INFO', "Sending incremental $sourcefs#$bookmarkescaped ... $newsyncsnap (~ $disp_pvsize):"); + writelog('DEBUG', "$synccmd"); ($stdout, $exit) = tee_stdout { system("$synccmd") @@ -747,15 +749,15 @@ sub syncdataset { $exit == 0 or do { if (!$resume && $stdout =~ /\Qcontains partially-complete state\E/) { - if (!$quiet) { print "WARN: resetting partially receive state\n"; } + writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); system("$synccmd") == 0 or do { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -779,8 +781,8 @@ sub syncdataset { if ($pvsize == 0) { $disp_pvsize = "UNKNOWN"; } my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - if (!$quiet) { print "Sending incremental $sourcefs\@$matchingsnap ... $newsyncsnap (~ $disp_pvsize):\n"; } - if ($debug) { print "DEBUG: $synccmd\n"; } + writelog('INFO', "Sending incremental $sourcefs\@$matchingsnap ... $newsyncsnap (~ $disp_pvsize):"); + writelog('DEBUG', "$synccmd"); ($stdout, $exit) = tee_stdout { system("$synccmd") @@ -789,15 +791,15 @@ sub syncdataset { $exit == 0 or do { # FreeBSD reports "dataset is busy" instead of "contains partially-complete state" if (!$resume && ($stdout =~ /\Qcontains partially-complete state\E/ || $stdout =~ /\Qdataset is busy\E/)) { - if (!$quiet) { print "WARN: resetting partially receive state\n"; } + writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); system("$synccmd") == 0 or do { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - warn "CRITICAL ERROR: $synccmd failed: $?"; + writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -819,22 +821,22 @@ sub syncdataset { } else { $bookmarkcmd = "$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped"; } - if ($debug) { print "DEBUG: $bookmarkcmd\n"; } + writelog('DEBUG', "$bookmarkcmd"); system($bookmarkcmd) == 0 or do { # fallback: assume nameing conflict and try again with guid based suffix my $guid = $snaps{'source'}{$newsyncsnap}{'guid'}; $guid = substr($guid, 0, 6); - if (!$quiet) { print "INFO: bookmark creation failed, retrying with guid based suffix ($guid)...\n"; } + writelog('INFO', "bookmark creation failed, retrying with guid based suffix ($guid)..."); if ($sourcehost ne '') { $bookmarkcmd = "$sshcmd $sourcehost " . escapeshellparam("$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped$guid"); } else { $bookmarkcmd = "$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped$guid"; } - if ($debug) { print "DEBUG: $bookmarkcmd\n"; } + writelog('DEBUG', "$bookmarkcmd"); system($bookmarkcmd) == 0 or do { - warn "CRITICAL ERROR: $bookmarkcmd failed: $?"; + writelog('CRITICAL', "$bookmarkcmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -913,7 +915,7 @@ sub compressargset { if ($value eq 'default') { $value = $DEFAULT_COMPRESSION; } elsif (!(grep $value eq $_, ('gzip', 'pigz-fast', 'pigz-slow', 'zstd-fast', 'zstd-slow', 'lz4', 'xz', 'lzo', 'default', 'none'))) { - warn "Unrecognised compression value $value, defaulting to $DEFAULT_COMPRESSION"; + writelog('WARN', "Unrecognised compression value $value, defaulting to $DEFAULT_COMPRESSION"); $value = $DEFAULT_COMPRESSION; } @@ -934,7 +936,7 @@ sub checkcommands { # if --nocommandchecks then assume everything's available and return if ($args{'nocommandchecks'}) { - if ($debug) { print "DEBUG: not checking for command availability due to --nocommandchecks switch.\n"; } + writelog('DEBUG', "not checking for command availability due to --nocommandchecks switch."); $avail{'compress'} = 1; $avail{'localpv'} = 1; $avail{'localmbuffer'} = 1; @@ -954,13 +956,13 @@ sub checkcommands { # if raw compress command is null, we must have specified no compression. otherwise, # make sure that compression is available everywhere we need it if ($compressargs{'compress'} eq 'none') { - if ($debug) { print "DEBUG: compression forced off from command line arguments.\n"; } + writelog('DEBUG', "compression forced off from command line arguments."); } else { - if ($debug) { print "DEBUG: checking availability of $compressargs{'rawcmd'} on source...\n"; } + writelog('DEBUG', "checking availability of $compressargs{'rawcmd'} on source..."); $avail{'sourcecompress'} = `$sourcessh $checkcmd $compressargs{'rawcmd'} 2>/dev/null`; - if ($debug) { print "DEBUG: checking availability of $compressargs{'rawcmd'} on target...\n"; } + writelog('DEBUG', "checking availability of $compressargs{'rawcmd'} on target..."); $avail{'targetcompress'} = `$targetssh $checkcmd $compressargs{'rawcmd'} 2>/dev/null`; - if ($debug) { print "DEBUG: checking availability of $compressargs{'rawcmd'} on local machine...\n"; } + writelog('DEBUG', "checking availability of $compressargs{'rawcmd'} on local machine..."); $avail{'localcompress'} = `$checkcmd $compressargs{'rawcmd'} 2>/dev/null`; } @@ -989,13 +991,13 @@ sub checkcommands { if ($avail{'sourcecompress'} eq '') { if ($compressargs{'rawcmd'} ne '') { - print "WARN: $compressargs{'rawcmd'} not available on source $s- sync will continue without compression.\n"; + writelog('WARN', "$compressargs{'rawcmd'} not available on source $s- sync will continue without compression."); } $avail{'compress'} = 0; } if ($avail{'targetcompress'} eq '') { if ($compressargs{'rawcmd'} ne '') { - print "WARN: $compressargs{'rawcmd'} not available on target $t - sync will continue without compression.\n"; + writelog('WARN', "$compressargs{'rawcmd'} not available on target $t - sync will continue without compression."); } $avail{'compress'} = 0; } @@ -1008,24 +1010,24 @@ sub checkcommands { # corner case - if source AND target are BOTH remote, we have to check for local compress too if ($sourcehost ne '' && $targethost ne '' && $avail{'localcompress'} eq '') { if ($compressargs{'rawcmd'} ne '') { - print "WARN: $compressargs{'rawcmd'} not available on local machine - sync will continue without compression.\n"; + writelog('WARN', "$compressargs{'rawcmd'} not available on local machine - sync will continue without compression."); } $avail{'compress'} = 0; } - if ($debug) { print "DEBUG: checking availability of $mbuffercmd on source...\n"; } + writelog('DEBUG', "checking availability of $mbuffercmd on source..."); $avail{'sourcembuffer'} = `$sourcessh $checkcmd $mbuffercmd 2>/dev/null`; if ($avail{'sourcembuffer'} eq '') { - if (!$quiet) { print "WARN: $mbuffercmd not available on source $s - sync will continue without source buffering.\n"; } + writelog('WARN', "$mbuffercmd not available on source $s - sync will continue without source buffering."); $avail{'sourcembuffer'} = 0; } else { $avail{'sourcembuffer'} = 1; } - if ($debug) { print "DEBUG: checking availability of $mbuffercmd on target...\n"; } + writelog('DEBUG', "checking availability of $mbuffercmd on target..."); $avail{'targetmbuffer'} = `$targetssh $checkcmd $mbuffercmd 2>/dev/null`; if ($avail{'targetmbuffer'} eq '') { - if (!$quiet) { print "WARN: $mbuffercmd not available on target $t - sync will continue without target buffering.\n"; } + writelog('WARN', "$mbuffercmd not available on target $t - sync will continue without target buffering."); $avail{'targetmbuffer'} = 0; } else { $avail{'targetmbuffer'} = 1; @@ -1033,18 +1035,18 @@ sub checkcommands { # if we're doing remote source AND remote target, check for local mbuffer as well if ($sourcehost ne '' && $targethost ne '') { - if ($debug) { print "DEBUG: checking availability of $mbuffercmd on local machine...\n"; } + writelog('DEBUG', "checking availability of $mbuffercmd on local machine..."); $avail{'localmbuffer'} = `$checkcmd $mbuffercmd 2>/dev/null`; if ($avail{'localmbuffer'} eq '') { $avail{'localmbuffer'} = 0; - if (!$quiet) { print "WARN: $mbuffercmd not available on local machine - sync will continue without local buffering.\n"; } + writelog('WARN', "$mbuffercmd not available on local machine - sync will continue without local buffering."); } } - if ($debug) { print "DEBUG: checking availability of $pvcmd on local machine...\n"; } + writelog('DEBUG', "checking availability of $pvcmd on local machine..."); $avail{'localpv'} = `$checkcmd $pvcmd 2>/dev/null`; if ($avail{'localpv'} eq '') { - if (!$quiet) { print "WARN: $pvcmd not available on local machine - sync will continue without progress bar.\n"; } + writelog('WARN', "$pvcmd not available on local machine - sync will continue without progress bar."); $avail{'localpv'} = 0; } else { $avail{'localpv'} = 1; @@ -1072,11 +1074,11 @@ sub checkcommands { my $resumechkcmd = "$zpoolcmd get -o value -H feature\@extensible_dataset"; - if ($debug) { print "DEBUG: checking availability of zfs resume feature on source...\n"; } + writelog('DEBUG', "checking availability of zfs resume feature on source..."); $avail{'sourceresume'} = system("$sourcessh $sourcesudocmd $resumechkcmd $srcpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1"); $avail{'sourceresume'} = $avail{'sourceresume'} == 0 ? 1 : 0; - if ($debug) { print "DEBUG: checking availability of zfs resume feature on target...\n"; } + writelog('DEBUG', "checking availability of zfs resume feature on target..."); $avail{'targetresume'} = system("$targetssh $targetsudocmd $resumechkcmd $dstpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1"); $avail{'targetresume'} = $avail{'targetresume'} == 0 ? 1 : 0; @@ -1092,7 +1094,7 @@ sub checkcommands { push @hosts, 'target'; } my $affected = join(" and ", @hosts); - print "WARN: ZFS resume feature not available on $affected machine - sync will continue without resume support.\n"; + writelog('WARN', "ZFS resume feature not available on $affected machine - sync will continue without resume support."); } } else { $avail{'sourceresume'} = 0; @@ -1105,17 +1107,16 @@ sub checkcommands { sub iszfsbusy { my ($rhost,$fs,$isroot) = @_; if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } - if ($debug) { print "DEBUG: checking to see if $fs on $rhost is already in zfs receive using $rhost $pscmd -Ao args= ...\n"; } + writelog('DEBUG', "checking to see if $fs on $rhost is already in zfs receive using $rhost $pscmd -Ao args= ..."); open PL, "$rhost $pscmd -Ao args= |"; my @processes = ; close PL; foreach my $process (@processes) { - # if ($debug) { print "DEBUG: checking process $process...\n"; } if ($process =~ /zfs *(receive|recv).*\Q$fs\E\Z/) { # there's already a zfs receive process for our target filesystem - return true - if ($debug) { print "DEBUG: process $process matches target $fs!\n"; } + writelog('DEBUG', "process $process matches target $fs!"); return 1; } } @@ -1135,12 +1136,12 @@ sub setzfsvalue { $fsescaped = escapeshellparam($fsescaped); } - if ($debug) { print "DEBUG: setting $property to $value on $fs...\n"; } + writelog('DEBUG', "setting $property to $value on $fs..."); my $mysudocmd; if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } - if ($debug) { print "$rhost $mysudocmd $zfscmd set $property=$value $fsescaped\n"; } + writelog('DEBUG', "$rhost $mysudocmd $zfscmd set $property=$value $fsescaped"); system("$rhost $mysudocmd $zfscmd set $property=$value $fsescaped") == 0 - or warn "WARNING: $rhost $mysudocmd $zfscmd set $property=$value $fsescaped died: $?, proceeding anyway.\n"; + or writelog('WARN', "$rhost $mysudocmd $zfscmd set $property=$value $fsescaped died: $?, proceeding anyway."); return; } @@ -1155,10 +1156,10 @@ sub getzfsvalue { $fsescaped = escapeshellparam($fsescaped); } - if ($debug) { print "DEBUG: getting current value of $property on $fs...\n"; } + writelog('DEBUG', "getting current value of $property on $fs..."); my $mysudocmd; if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } - if ($debug) { print "$rhost $mysudocmd $zfscmd get -H $property $fsescaped\n"; } + writelog('DEBUG', "$rhost $mysudocmd $zfscmd get -H $property $fsescaped"); my ($value, $error, $exit) = capture { system("$rhost $mysudocmd $zfscmd get -H $property $fsescaped"); }; @@ -1171,7 +1172,7 @@ sub getzfsvalue { # If we are in scalar context and there is an error, print it out. # Otherwise we assume the caller will deal with it. if (!$wantarray and $error) { - print "ERROR getzfsvalue $fs $property: $error\n"; + writelog('CRITICAL', "getzfsvalue $fs $property: $error"); } return $wantarray ? ($value, $error) : $value; @@ -1200,7 +1201,7 @@ sub getoldestsnapshot { # must not have had any snapshots on source - luckily, we already made one, amirite? if (defined ($args{'no-sync-snap'}) ) { # well, actually we set --no-sync-snap, so no we *didn't* already make one. Whoops. - warn "CRIT: --no-sync-snap is set, and getoldestsnapshot() could not find any snapshots on source!\n"; + writelog('CRITICAL', "--no-sync-snap is set, and getoldestsnapshot() could not find any snapshots on source!"); } return 0; } @@ -1209,7 +1210,7 @@ sub getnewestsnapshot { my $snaps = shift; foreach my $snap ( sort { $snaps{'source'}{$b}{'creation'}<=>$snaps{'source'}{$a}{'creation'} } keys %{ $snaps{'source'} }) { # return on first snap found - it's the newest - if (!$quiet) { print "NEWEST SNAPSHOT: $snap\n"; } + writelog('INFO', "NEWEST SNAPSHOT: $snap"); return $snap; } # must not have had any snapshots on source - looks like we'd better create one! @@ -1221,7 +1222,7 @@ sub getnewestsnapshot { # fixme: we need to output WHAT the current dataset IS if we encounter this WARN condition. # we also probably need an argument to mute this WARN, for people who deliberately exclude # datasets from recursive replication this way. - warn "WARN: --no-sync-snap is set, and getnewestsnapshot() could not find any snapshots on source for current dataset. Continuing.\n"; + writelog('WARN', "--no-sync-snap is set, and getnewestsnapshot() could not find any snapshots on source for current dataset. Continuing."); if ($exitcode < 2) { $exitcode = 2; } } return 0; @@ -1340,13 +1341,13 @@ sub pruneoldsyncsnaps { $prunecmd .= "$mysudocmd $zfscmd destroy $fsescaped\@$snap; "; if ($counter > $maxsnapspercmd) { $prunecmd =~ s/\; $//; - if ($debug) { print "DEBUG: pruning up to $maxsnapspercmd obsolete sync snapshots...\n"; } - if ($debug) { print "DEBUG: $rhost $prunecmd\n"; } + writelog('DEBUG', "pruning up to $maxsnapspercmd obsolete sync snapshots..."); + writelog('DEBUG', "$rhost $prunecmd"); if ($rhost ne '') { $prunecmd = escapeshellparam($prunecmd); } system("$rhost $prunecmd") == 0 - or warn "WARNING: $rhost $prunecmd failed: $?"; + or writelog('WARN', "$rhost $prunecmd failed: $?"); $prunecmd = ''; $counter = 0; } @@ -1355,13 +1356,13 @@ sub pruneoldsyncsnaps { # the loop, commit 'em now if ($counter) { $prunecmd =~ s/\; $//; - if ($debug) { print "DEBUG: pruning up to $maxsnapspercmd obsolete sync snapshots...\n"; } - if ($debug) { print "DEBUG: $rhost $prunecmd\n"; } + writelog('DEBUG', "pruning up to $maxsnapspercmd obsolete sync snapshots..."); + writelog('DEBUG', "$rhost $prunecmd"); if ($rhost ne '') { $prunecmd = escapeshellparam($prunecmd); } system("$rhost $prunecmd") == 0 - or warn "WARNING: $rhost $prunecmd failed: $?"; + or writelog('WARN', "$rhost $prunecmd failed: $?"); } return; } @@ -1393,9 +1394,9 @@ sub newsyncsnap { my %date = getdate(); my $snapname = "syncoid\_$identifier$hostid\_$date{'stamp'}"; my $snapcmd = "$rhost $mysudocmd $zfscmd snapshot $fsescaped\@$snapname\n"; - if ($debug) { print "DEBUG: creating sync snapshot using \"$snapcmd\"...\n"; } + writelog('DEBUG', "creating sync snapshot using \"$snapcmd\"..."); system($snapcmd) == 0 or do { - warn "CRITICAL ERROR: $snapcmd failed: $?"; + writelog('CRITICAL', "$snapcmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; }; @@ -1414,7 +1415,7 @@ sub targetexists { my $mysudocmd; if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } my $checktargetcmd = "$rhost $mysudocmd $zfscmd get -H name $fsescaped"; - if ($debug) { print "DEBUG: checking to see if target filesystem exists using \"$checktargetcmd 2>&1 |\"...\n"; } + writelog('DEBUG', "checking to see if target filesystem exists using \"$checktargetcmd 2>&1 |\"..."); open FH, "$checktargetcmd 2>&1 |"; my $targetexists = ; close FH; @@ -1451,7 +1452,7 @@ sub getssh { }; $rhost = $fs; if ($exit != 0) { - warn "Unable to enumerate pools (is zfs available?)"; + writelog('WARN', "Unable to enumerate pools (is zfs available?)"); } else { foreach (split(/\n/,$pools)) { if ($_ eq $pool) { @@ -1478,7 +1479,7 @@ sub getssh { system("$sshcmd -S $socket $rhost echo -n") == 0 or do { my $code = $? >> 8; - warn "CRITICAL ERROR: ssh connection echo test failed for $rhost with exit code $code"; + writelog('CRITICAL', "ssh connection echo test failed for $rhost with exit code $code"); exit(2); }; @@ -1487,14 +1488,13 @@ sub getssh { my $localuid = $<; if ($localuid == 0 || $args{'no-privilege-elevation'}) { $isroot = 1; } else { $isroot = 0; } } - # if ($isroot) { print "this user is root.\n"; } else { print "this user is not root.\n"; } return ($rhost,$fs,$isroot); } sub dumphash() { my $hash = shift; $Data::Dumper::Sortkeys = 1; - print Dumper($hash); + writelog('INFO', Dumper($hash)); } sub getsnaps() { @@ -1512,7 +1512,7 @@ sub getsnaps() { my $getsnapcmd = "$rhost $mysudocmd $zfscmd get -Hpd 1 -t snapshot guid,creation $fsescaped"; if ($debug) { $getsnapcmd = "$getsnapcmd |"; - print "DEBUG: getting list of snapshots on $fs using $getsnapcmd...\n"; + writelog('DEBUG', "getting list of snapshots on $fs using $getsnapcmd..."); } else { $getsnapcmd = "$getsnapcmd 2>/dev/null |"; } @@ -1586,8 +1586,8 @@ sub getsnapsfallback() { } my $getsnapcmd = "$rhost $mysudocmd $zfscmd get -Hpd 1 type,guid,creation $fsescaped |"; - warn "snapshot listing failed, trying fallback command"; - if ($debug) { print "DEBUG: FALLBACK, getting list of snapshots on $fs using $getsnapcmd...\n"; } + writelog('WARN', "snapshot listing failed, trying fallback command"); + writelog('DEBUG', "FALLBACK, getting list of snapshots on $fs using $getsnapcmd..."); open FH, $getsnapcmd; my @rawsnaps = ; close FH or die "CRITICAL ERROR: snapshots couldn't be listed for $fs (exit code $?)"; @@ -1669,7 +1669,7 @@ sub getbookmarks() { my $error = 0; my $getbookmarkcmd = "$rhost $mysudocmd $zfscmd get -Hpd 1 -t bookmark guid,creation $fsescaped 2>&1 |"; - if ($debug) { print "DEBUG: getting list of bookmarks on $fs using $getbookmarkcmd...\n"; } + writelog('DEBUG', "getting list of bookmarks on $fs using $getbookmarkcmd..."); open FH, $getbookmarkcmd; my @rawbookmarks = ; close FH or $error = 1; @@ -1750,7 +1750,7 @@ sub getsendsize { $sendoptions = getoptionsline(\@sendoptions, ('D','L','R','c','e','h','p','w')); } my $getsendsizecmd = "$sourcessh $mysudocmd $zfscmd send $sendoptions -nvP $snaps"; - if ($debug) { print "DEBUG: getting estimated transfer size from source $sourcehost using \"$getsendsizecmd 2>&1 |\"...\n"; } + writelog('DEBUG', "getting estimated transfer size from source $sourcehost using \"$getsendsizecmd 2>&1 |\"..."); open FH, "$getsendsizecmd 2>&1 |"; my @rawsize = ; @@ -1777,7 +1777,7 @@ sub getsendsize { # to avoid confusion with a zero size pv, give sendsize # a minimum 4K value - or if empty, make sure it reads UNKNOWN - if ($debug) { print "DEBUG: sendsize = $sendsize\n"; } + writelog('DEBUG', "sendsize = $sendsize"); if ($sendsize eq '' || $exit != 0) { $sendsize = '0'; } elsif ($sendsize < 4096) { @@ -1836,9 +1836,7 @@ sub getreceivetoken() { return $token; } - if ($debug) { - print "DEBUG: no receive token found \n"; - } + writelog('DEBUG', "no receive token found"); return } @@ -1905,8 +1903,7 @@ sub getoptionsline { return $line; } -sub resetreceivestate { - my ($rhost,$fs,$isroot) = @_; +sub resetreceivestate { my ($rhost,$fs,$isroot) = @_; my $fsescaped = escapeshellparam($fs); @@ -1916,15 +1913,37 @@ sub resetreceivestate { $fsescaped = escapeshellparam($fsescaped); } - if ($debug) { print "DEBUG: reset partial receive state of $fs...\n"; } + writelog('DEBUG', "reset partial receive state of $fs..."); my $mysudocmd; if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } my $resetcmd = "$rhost $mysudocmd $zfscmd receive -A $fsescaped"; - if ($debug) { print "$resetcmd\n"; } + writelog('DEBUG', "$resetcmd"); system("$resetcmd") == 0 or die "CRITICAL ERROR: $resetcmd failed: $?"; } +# $loglevel can be one of: +# - CRITICAL +# - WARN +# - INFO +# - DEBUG +sub writelog { + my ($loglevel, $msg) = @_; + + my $header; + chomp($msg); + + if ($loglevel eq 'CRITICAL') { + warn("CRITICAL ERROR: $msg\n"); + } elsif ($loglevel eq 'WARN') { + if (!$quiet) { warn("WARNING: $msg\n"); } + } elsif ($loglevel eq 'INFO') { + if (!$quiet) { print("INFO: $msg\n"); } + } elsif ($loglevel eq 'DEBUG') { + if ($debug) { print("DEBUG: $msg\n"); } + } +} + __END__ =head1 NAME From 09b42d6ade2843171be810db7df3b189954b7992 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 28 Nov 2021 20:50:46 -0700 Subject: [PATCH 2/9] Refactor system calls Build the zfs send and receive commands in a new subroutine, and implement other subroutines that can be called instead of building a zfs command and running it with system(); --- syncoid | 341 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 172 insertions(+), 169 deletions(-) diff --git a/syncoid b/syncoid index fa6feff..1ceed0c 100755 --- a/syncoid +++ b/syncoid @@ -284,12 +284,6 @@ sub syncdataset { my $stdout; my $exit; - my $sourcefsescaped = escapeshellparam($sourcefs); - my $targetfsescaped = escapeshellparam($targetfs); - - # keep forcedrecv as a variable to allow us to disable it with an optional argument later if necessary - my $forcedrecv = "-F"; - writelog('DEBUG', "syncing source $sourcefs to target $targetfs."); my ($sync, $error) = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'syncoid:sync'); @@ -334,13 +328,9 @@ sub syncdataset { # does the target filesystem exist yet? my $targetexists = targetexists($targethost,$targetfs,$targetisroot); - my $receiveextraargs = ""; my $receivetoken; if ($resume) { - # save state of interrupted receive stream - $receiveextraargs = "-s"; - if ($targetexists) { # check remote dataset for receive resume token (interrupted receive) $receivetoken = getreceivetoken($targethost,$targetfs,$targetisroot); @@ -398,9 +388,6 @@ sub syncdataset { # with ZFS on Linux (possibly OpenZFS in general) when setting/unsetting readonly. #my $originaltargetreadonly; - my $sendoptions = getoptionsline(\@sendoptions, ('D','L','P','R','c','e','h','p','v','w')); - my $recvoptions = getoptionsline(\@recvoptions, ('h','o','x','u','v')); - # sync 'em up. if (! $targetexists) { # do an initial sync from the oldest source snapshot @@ -430,61 +417,30 @@ sub syncdataset { $oldestsnap = $newsyncsnap; } } - my $oldestsnapescaped = escapeshellparam($oldestsnap); - if (defined $args{'preserve-recordsize'}) { - my $type = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'type'); - if ($type eq "filesystem") { - my $recordsize = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'recordsize'); - $recvoptions .= "-o recordsize=$recordsize" - } - } - - my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions $sourcefsescaped\@$oldestsnapescaped"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped"; - - my $pvsize; - if (defined $origin) { - my $originescaped = escapeshellparam($origin); - $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions -i $originescaped $sourcefsescaped\@$oldestsnapescaped"; - my $streamargBackup = $args{'streamarg'}; - $args{'streamarg'} = "-i"; - $pvsize = getsendsize($sourcehost,$origin,"$sourcefs\@$oldestsnap",$sourceisroot); - $args{'streamarg'} = $streamargBackup; - } else { - $pvsize = getsendsize($sourcehost,"$sourcefs\@$oldestsnap",0,$sourceisroot); - } - - my $disp_pvsize = readablebytes($pvsize); - if ($pvsize == 0) { $disp_pvsize = 'UNKNOWN'; } - my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); + my $ret; if (defined $origin) { writelog('INFO', "Clone is recreated on target $targetfs based on $origin"); - } - if (!defined ($args{'no-stream'}) ) { - writelog('INFO', "Sending oldest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:"); - } else { - writelog('INFO', "--no-stream selected; sending newest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:"); - } - writelog('DEBUG', "$synccmd"); - - # make sure target is (still) not currently in receive. - if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); - if ($exitcode < 1) { $exitcode = 1; } - return 0; - } - system($synccmd) == 0 or do { - if (defined $origin) { + ($ret, $stdout) = syncclone($sourcehost, $sourcefs, $origin, $targethost, $targetfs, $oldestsnap); + if ($ret) { writelog('INFO', "clone creation failed, trying ordinary replication as fallback"); syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, undef, 1); return 0; } + } else { + if (!defined ($args{'no-stream'}) ) { + writelog('INFO', "Sending oldest full snapshot $sourcefs\@$oldestsnap to new target filesystem:"); + } else { + writelog('INFO', "--no-stream selected; sending newest full snapshot $sourcefs\@$oldestsnap to new target filesystem:"); + } - writelog('CRITICAL', "$synccmd failed: $?"); + ($ret, $stdout) = syncfull($sourcehost, $sourcefs, $targethost, $targetfs, $oldestsnap); + } + + if ($ret) { if ($exitcode < 2) { $exitcode = 2; } return 0; - }; + } # now do an -I to the new sync snapshot, assuming there were any snapshots # other than the new sync snapshot to begin with, of course - and that we @@ -498,33 +454,15 @@ sub syncdataset { # $originaltargetreadonly = getzfsvalue($targethost,$targetfs,$targetisroot,'readonly'); # setzfsvalue($targethost,$targetfs,$targetisroot,'readonly','on'); - $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions $args{'streamarg'} $sourcefsescaped\@$oldestsnapescaped $sourcefsescaped\@$newsyncsnapescaped"; - $pvsize = getsendsize($sourcehost,"$sourcefs\@$oldestsnap","$sourcefs\@$newsyncsnap",$sourceisroot); - $disp_pvsize = readablebytes($pvsize); - if ($pvsize == 0) { $disp_pvsize = "UNKNOWN"; } - $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); + writelog('INFO', "Updating new target filesystem with incremental $sourcefs\@$oldestsnap ... $newsyncsnap:"); - # make sure target is (still) not currently in receive. - if (iszfsbusy($targethost,$targetfs,$targetisroot)) { - writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); + (my $ret, $stdout) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $oldestsnap, $newsyncsnap, 0); + + if ($ret != 0) { if ($exitcode < 1) { $exitcode = 1; } return 0; } - writelog('INFO', "Updating new target filesystem with incremental $sourcefs\@$oldestsnap ... $newsyncsnap (~ $disp_pvsize):"); - writelog('DEBUG', "$synccmd"); - - if ($oldestsnap ne $newsyncsnap) { - my $ret = system($synccmd); - if ($ret != 0) { - writelog('CRITICAL', "$synccmd failed: $?"); - if ($exitcode < 1) { $exitcode = 1; } - return 0; - } - } else { - writelog('INFO', "no incremental sync needed; $oldestsnap is already the newest available snapshot."); - } - # restore original readonly value to target after sync complete # dyking this functionality out for the time being due to buggy mount/unmount behavior # with ZFS on Linux (possibly OpenZFS in general) when setting/unsetting readonly. @@ -535,29 +473,7 @@ sub syncdataset { # and because this will ony resume the receive to the next # snapshot, do a normal sync after that if (defined($receivetoken)) { - $sendoptions = getoptionsline(\@sendoptions, ('P','e','v','w')); - my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions -t $receivetoken"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; - my $pvsize = getsendsize($sourcehost,"","",$sourceisroot,$receivetoken); - my $disp_pvsize = readablebytes($pvsize); - if ($pvsize == 0) { $disp_pvsize = "UNKNOWN"; } - my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - - writelog('INFO', "Resuming interrupted zfs send/receive from $sourcefs to $targetfs (~ $disp_pvsize remaining):"); - writelog('DEBUG', "$synccmd"); - - if ($pvsize == 0) { - # we need to capture the error of zfs send, this will render pv useless but in this case - # it doesn't matter because we don't know the estimated send size (probably because - # the initial snapshot used for resumed send doesn't exist anymore) - ($stdout, $exit) = tee_stderr { - system("$synccmd") - }; - } else { - ($stdout, $exit) = tee_stdout { - system("$synccmd") - }; - } + ($exit, $stdout) = syncresume($sourcehost, $sourcefs, $targethost, $targetfs, $receivetoken); $exit == 0 or do { if ( @@ -569,7 +485,6 @@ sub syncdataset { # do an normal sync cycle return syncdataset($sourcehost, $sourcefs, $targethost, $targetfs, $origin); } else { - writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -699,35 +614,19 @@ sub syncdataset { } } - # bookmark stream size can't be determined - my $pvsize = 0; - my $disp_pvsize = "UNKNOWN"; - - $sendoptions = getoptionsline(\@sendoptions, ('L','c','e','w')); if ($nextsnapshot) { - my $nextsnapshotescaped = escapeshellparam($nextsnapshot); - my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions -i $sourcefsescaped#$bookmarkescaped $sourcefsescaped\@$nextsnapshotescaped"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; - my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - - writelog('INFO', "Sending incremental $sourcefs#$bookmarkescaped ... $nextsnapshot (~ $disp_pvsize):"); - writelog('DEBUG', "$synccmd"); - - ($stdout, $exit) = tee_stdout { - system("$synccmd") - }; + ($exit, $stdout) = syncbookmark($sourcehost, $sourcefs, $targethost, $targetfs, $bookmark, $nextsnapshot); $exit == 0 or do { if (!$resume && $stdout =~ /\Qcontains partially-complete state\E/) { writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); - system("$synccmd") == 0 or do { - writelog('CRITICAL', "$synccmd failed: $?"); + (my $ret) = syncbookmark($sourcehost, $sourcefs, $targethost, $targetfs, $bookmark, $nextsnapshot); + $ret == 0 or do { if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -736,28 +635,18 @@ sub syncdataset { $matchingsnap = $nextsnapshot; $matchingsnapescaped = escapeshellparam($matchingsnap); } else { - my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions -i $sourcefsescaped#$bookmarkescaped $sourcefsescaped\@$newsyncsnapescaped"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; - my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - - writelog('INFO', "Sending incremental $sourcefs#$bookmarkescaped ... $newsyncsnap (~ $disp_pvsize):"); - writelog('DEBUG', "$synccmd"); - - ($stdout, $exit) = tee_stdout { - system("$synccmd") - }; + ($exit, $stdout) = syncbookmark($sourcehost, $sourcefs, $targethost, $targetfs, $bookmark, $newsyncsnap); $exit == 0 or do { if (!$resume && $stdout =~ /\Qcontains partially-complete state\E/) { writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); - system("$synccmd") == 0 or do { - writelog('CRITICAL', "$synccmd failed: $?"); + (my $ret) = syncbookmark($sourcehost, $sourcefs, $targethost, $targetfs, $bookmark, $newsyncsnap); + $ret == 0 or do { if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -773,33 +662,19 @@ sub syncdataset { return 0; } - $sendoptions = getoptionsline(\@sendoptions, ('D','L','P','R','c','e','h','p','v','w')); - my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions $args{'streamarg'} $sourcefsescaped\@$matchingsnapescaped $sourcefsescaped\@$newsyncsnapescaped"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $receiveextraargs $forcedrecv $targetfsescaped 2>&1"; - my $pvsize = getsendsize($sourcehost,"$sourcefs\@$matchingsnap","$sourcefs\@$newsyncsnap",$sourceisroot); - my $disp_pvsize = readablebytes($pvsize); - if ($pvsize == 0) { $disp_pvsize = "UNKNOWN"; } - my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); - - writelog('INFO', "Sending incremental $sourcefs\@$matchingsnap ... $newsyncsnap (~ $disp_pvsize):"); - writelog('DEBUG', "$synccmd"); - - ($stdout, $exit) = tee_stdout { - system("$synccmd") - }; + ($exit, $stdout) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $matchingsnap, $newsyncsnap, defined($args{'no-stream'})); $exit == 0 or do { # FreeBSD reports "dataset is busy" instead of "contains partially-complete state" if (!$resume && ($stdout =~ /\Qcontains partially-complete state\E/ || $stdout =~ /\Qdataset is busy\E/)) { writelog('WARN', "resetting partially receive state"); resetreceivestate($targethost,$targetfs,$targetisroot); - system("$synccmd") == 0 or do { - writelog('CRITICAL', "$synccmd failed: $?"); + (my $ret) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $matchingsnap, $newsyncsnap, defined($args{'no-stream'})); + $ret == 0 or do { if ($exitcode < 2) { $exitcode = 2; } return 0; } } else { - writelog('CRITICAL', "$synccmd failed: $?"); if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -815,28 +690,16 @@ sub syncdataset { if (defined $args{'no-sync-snap'}) { if (defined $args{'create-bookmark'}) { - my $bookmarkcmd; - if ($sourcehost ne '') { - $bookmarkcmd = "$sshcmd $sourcehost " . escapeshellparam("$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped"); - } else { - $bookmarkcmd = "$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped"; - } - writelog('DEBUG', "$bookmarkcmd"); - system($bookmarkcmd) == 0 or do { + my $ret = createbookmark($sourcehost, $sourcefs, $newsyncsnap, $newsyncsnap); + $ret == 0 or do { # fallback: assume nameing conflict and try again with guid based suffix my $guid = $snaps{'source'}{$newsyncsnap}{'guid'}; $guid = substr($guid, 0, 6); writelog('INFO', "bookmark creation failed, retrying with guid based suffix ($guid)..."); - if ($sourcehost ne '') { - $bookmarkcmd = "$sshcmd $sourcehost " . escapeshellparam("$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped$guid"); - } else { - $bookmarkcmd = "$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$newsyncsnapescaped $sourcefsescaped\#$newsyncsnapescaped$guid"; - } - writelog('DEBUG', "$bookmarkcmd"); - system($bookmarkcmd) == 0 or do { - writelog('CRITICAL', "$bookmarkcmd failed: $?"); + my $ret = createbookmark($sourcehost, $sourcefs, $newsyncsnap, "$newsyncsnap$guid"); + $ret == 0 or do { if ($exitcode < 2) { $exitcode = 2; } return 0; } @@ -852,6 +715,146 @@ sub syncdataset { } # end syncdataset() +# Return codes: +# 0 - ZFS send/receive completed without errors +# 1 - ZFS target is currently in receive +# 2 - Critical error encountered when running the ZFS send/receive command +sub runsynccmd { + my ($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, $pvsize) = @_; + + my $sourcefsescaped = escapeshellparam($sourcefs); + my $targetfsescaped = escapeshellparam($targetfs); + + my $disp_pvsize = $pvsize == 0 ? 'UNKNOWN' : readablebytes($pvsize); + my $sendoptions; + if ($sendsource =~ / -t /) { + writelog('INFO', "Resuming interrupted zfs send/receive from $sourcefs to $targetfs (~ $disp_pvsize remaining):"); + $sendoptions = getoptionsline(\@sendoptions, ('P','e','v','w')); + } elsif ($sendsource =~ /#/) { + $sendoptions = getoptionsline(\@sendoptions, ('L','c','e','w')); + } else { + $sendoptions = getoptionsline(\@sendoptions, ('D','L','P','R','c','e','h','p','v','w')); + } + + my $recvoptions = getoptionsline(\@recvoptions, ('h','o','x','u','v')); + + # save state of interrupted receive stream + if ($resume) { $recvoptions .= ' -s'; } + # if no rollbacks are allowed, disable forced receive + if (!defined $args{'no-rollback'}) { $recvoptions .= ' -F'; } + + if (defined $args{'preserve-recordsize'}) { + my $type = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'type'); + if ($type eq "filesystem") { + my $recordsize = getzfsvalue($sourcehost,$sourcefs,$sourceisroot,'recordsize'); + $recvoptions .= " -o recordsize=$recordsize" + } + } + + my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions $sendsource"; + my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $targetfsescaped"; + + my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); + writelog('INFO', "Sync size: ~$disp_pvsize"); + writelog('DEBUG', "$synccmd"); + + # make sure target is (still) not currently in receive. + if (iszfsbusy($targethost,$targetfs,$targetisroot)) { + writelog('WARN', "Cannot sync now: $targetfs is already target of a zfs receive process."); + return (1, ''); + } + + my $stdout; + my $ret; + if ($pvsize == 0) { + ($stdout, $ret) = tee_stderr { + system("$synccmd"); + }; + } else { + ($stdout, $ret) = tee_stdout { + system("$synccmd"); + }; + } + + if ($ret != 0) { + writelog('CRITICAL', "$synccmd failed: $?"); + return (2, $stdout); + } else { + return 0; + } +} # end runsendcmd() + +sub syncfull { + my ($sourcehost, $sourcefs, $targethost, $targetfs, $snapname) = @_; + + my $sourcefsescaped = escapeshellparam($sourcefs); + my $snapescaped = escapeshellparam($snapname); + my $sendsource = "$sourcefsescaped\@$snapescaped"; + my $pvsize = getsendsize($sourcehost,"$sourcefs\@$snapname",0,$sourceisroot); + + return runsynccmd($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, $pvsize); +} # end syncfull() + +sub syncincremental { + my ($sourcehost, $sourcefs, $targethost, $targetfs, $fromsnap, $tosnap, $skipintermediate) = @_; + + my $streamarg = ($skipintermediate == 1 ? '-i' : '-I'); + my $sourcefsescaped = escapeshellparam($sourcefs); + my $fromsnapescaped = escapeshellparam($fromsnap); + my $tosnapescaped = escapeshellparam($tosnap); + my $sendsource = "$streamarg $sourcefsescaped\@$fromsnapescaped $sourcefsescaped\@$tosnapescaped"; + my $pvsize = getsendsize($sourcehost,"$sourcefs\@$fromsnap","$sourcefs\@$tosnap",$sourceisroot); + + return runsynccmd($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, $pvsize); +} # end syncincremental() + +sub syncclone { + my ($sourcehost, $sourcefs, $origin, $targethost, $targetfs, $tosnap) = @_; + + my $sourcefsescaped = escapeshellparam($sourcefs); + my $originescaped = escapeshellparam($origin); + my $tosnapescaped = escapeshellparam($tosnap); + my $sendsource = "-i $originescaped $sourcefsescaped\@$tosnapescaped"; + my $pvsize = getsendsize($sourcehost,$origin,"$sourcefs\@$tosnap",$sourceisroot); + + return runsynccmd($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, $pvsize); +} # end syncclone() + +sub syncresume { + my ($sourcehost, $sourcefs, $targethost, $targetfs, $receivetoken) = @_; + + my $sendsource = "-t $receivetoken"; + my $pvsize = getsendsize($sourcehost,"","",$sourceisroot,$receivetoken); + + return runsynccmd($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, $pvsize); +} # end syncresume() + +sub syncbookmark { + my ($sourcehost, $sourcefs, $targethost, $targetfs, $bookmark, $tosnap) = @_; + + my $sourcefsescaped = escapeshellparam($sourcefs); + my $bookmarkescaped = escapeshellparam($bookmark); + my $tosnapescaped = escapeshellparam($tosnap); + my $sendsource = "-i $sourcefsescaped#$bookmarkescaped $sourcefsescaped\@$tosnapescaped"; + + return runsynccmd($sourcehost, $sourcefs, $sendsource, $targethost, $targetfs, 0); +} # end syncbookmark + +sub createbookmark { + my ($sourcehost, $sourcefs, $snapname, $bookmark) = @_; + + my $sourcefsescaped = escapeshellparam($sourcefs); + my $bookmarkescaped = escapeshellparam($bookmark); + my $snapnameescaped = escapeshellparam($snapname); + my $cmd = "$sourcesudocmd $zfscmd bookmark $sourcefsescaped\@$snapname $sourcefsescaped\#$bookmark"; + if ($sourcehost ne '') { + $cmd = "$sshcmd $sourcehost " . escapeshellparam($cmd); + } + + writelog('DEBUG', "$cmd"); + return system($cmd); +} # end createbookmark() + sub compressargset { my ($value) = @_; my $DEFAULT_COMPRESSION = 'lzo'; From 603c286b50128a3ceb061f0e2c03310a00220977 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 20 Nov 2021 13:09:10 -0700 Subject: [PATCH 3/9] Don't iterate over snaps twice Process snapshots in one pass rather than looping separately for both guid and create time. --- syncoid | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/syncoid b/syncoid index 1ceed0c..d0afe10 100755 --- a/syncoid +++ b/syncoid @@ -1541,11 +1541,8 @@ sub getsnaps() { $snap =~ s/^.*\@(.*)\tguid.*$/$1/; $snaps{$type}{$snap}{'guid'}=$guid; } - } - - foreach my $line (@rawsnaps) { # only import snap creations from the specified filesystem - if ($line =~ /\Q$fs\E\@.*creation/) { + elsif ($line =~ /\Q$fs\E\@.*creation/) { chomp $line; my $creation = $line; $creation =~ s/^.*\tcreation\t*(\d*).*/$1/; From 9a067729a9b1c71f3656129f177c371bd5c6b43d Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 20 Nov 2021 20:16:21 -0700 Subject: [PATCH 4/9] Implement include-snaps and exclude-snaps Add --include-snaps and --exclude-snaps options to filter the snapshots that syncoid uses. --- syncoid | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/syncoid b/syncoid index d0afe10..8f8db31 100755 --- a/syncoid +++ b/syncoid @@ -25,8 +25,8 @@ GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsn "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", "exclude=s@", "skip-parent", "identifier=s", "no-clone-handling", "no-privilege-elevation", "force-delete", "create-bookmark", - "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size) - or pod2usage(2); + "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size, + "include-snaps=s@", "exclude-snaps=s@") or pod2usage(2); my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set @@ -367,6 +367,16 @@ sub syncdataset { # we already whined about the error return 0; } + # Don't send the sync snap if it's filtered out by --exclude-snaps or + # --include-snaps + if (!snapisincluded($newsyncsnap)) { + $newsyncsnap = getnewestsnapshot($sourcehost,$sourcefs,$sourceisroot); + if ($newsyncsnap eq 0) { + writelog('WARN', "CRITICAL: no snapshots exist on source $sourcefs, and you asked for --no-sync-snap."); + if ($exitcode < 1) { $exitcode = 1; } + return 0; + } + } } else { # we don't want sync snapshots created, so use the newest snapshot we can find. $newsyncsnap = getnewestsnapshot($sourcehost,$sourcefs,$sourceisroot); @@ -798,7 +808,49 @@ sub syncfull { sub syncincremental { my ($sourcehost, $sourcefs, $targethost, $targetfs, $fromsnap, $tosnap, $skipintermediate) = @_; - my $streamarg = ($skipintermediate == 1 ? '-i' : '-I'); + my $streamarg = '-I'; + + if ($skipintermediate) { + $streamarg = '-i'; + } + + # If this is an -I sync but we're filtering snaps, then we should do a series + # of -i syncs instead. + if (!$skipintermediate) { + if (defined($args{'exclude-snaps'}) || defined($args{'include-snaps'})) { + writelog('INFO', '--no-stream is omitted but snaps are filtered. Simulating -I with filtered snaps'); + + # Get the snap names between $fromsnap and $tosnap + my @intsnaps = (); + my $inrange = 0; + foreach my $testsnap (sort { $snaps{'source'}{$a}{'creation'}<=>$snaps{'source'}{$b}{'creation'} } keys %{ $snaps{'source'} }) { + if ($testsnap eq $fromsnap) { $inrange = 1; } + + if ($inrange) { push(@intsnaps, $testsnap); } + + if ($testsnap eq $tosnap) { last; } + } + + # If we created a new sync snap, it won't be in @intsnaps yet + if ($intsnaps[-1] ne $tosnap) { + # Make sure that the sync snap isn't filtered out by --include-snaps or --exclude-snaps + if (snapisincluded($tosnap)) { + push(@intsnaps, $tosnap); + } + } + + foreach my $i (0..(scalar(@intsnaps) - 2)) { + my $snapa = $intsnaps[$i]; + my $snapb = $intsnaps[$i + 1]; + writelog('INFO', "Performing an incremental sync between '$snapa' and '$snapb'"); + syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $snapa, $snapb, 1) == 0 or return $?; + } + + # Return after finishing the -i syncs so that we don't try to do another -I + return 0; + } + } + my $sourcefsescaped = escapeshellparam($sourcefs); my $fromsnapescaped = escapeshellparam($fromsnap); my $tosnapescaped = escapeshellparam($tosnap); @@ -1532,6 +1584,11 @@ sub getsnaps() { my %creationtimes=(); foreach my $line (@rawsnaps) { + $line =~ /\Q$fs\E\@(\S*)/; + my $snapname = $1; + + if (!snapisincluded($snapname)) { next; } + # only import snap guids from the specified filesystem if ($line =~ /\Q$fs\E\@.*guid/) { chomp $line; @@ -1944,6 +2001,37 @@ sub writelog { } } +sub snapisincluded { + my ($snapname) = @_; + + # Return false if the snapshot matches an exclude-snaps pattern + if (defined $args{'exclude-snaps'}) { + my $excludes = $args{'exclude-snaps'}; + foreach (@$excludes) { + if ($snapname =~ /$_/) { + writelog('DEBUG', "excluded $snapname because of exclude pattern /$_/"); + return 0; + } + } + } + + # Return true if the snapshot matches an include-snaps pattern + if (defined $args{'include-snaps'}) { + my $includes = $args{'include-snaps'}; + foreach (@$includes) { + if ($snapname =~ /$_/) { + writelog('DEBUG', "included $snapname because of include pattern /$_/"); + return 1; + } + } + + # Return false if the snapshot didn't match any inclusion patterns + return 0; + } + + return 1; +} + __END__ =head1 NAME @@ -1976,6 +2064,8 @@ Options: --create-bookmark Creates a zfs bookmark for the newest snapshot on the source after replication succeeds (only works with --no-sync-snap) --preserve-recordsize Preserves the recordsize on initial sends to the target --exclude=REGEX Exclude specific datasets which match the given regular expression. Can be specified multiple times + --exclude-snaps=REGEX Exclude specific snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + --include-snaps=REGEX Only include snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. --sendoptions=OPTIONS Use advanced options for zfs send (the arguments are filtered as needed), e.g. syncoid --sendoptions="Lc e" sets zfs send -L -c -e ... --recvoptions=OPTIONS Use advanced options for zfs receive (the arguments are filtered as needed), e.g. syncoid --recvoptions="ux recordsize o compression=lz4" sets zfs receive -u -x recordsize -o compression=lz4 ... --sshkey=FILE Specifies a ssh key to use to connect From 3a1b1b006ffbe44afdb45c77172493fabcf8235c Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Tue, 30 Nov 2021 19:46:41 -0700 Subject: [PATCH 5/9] Add new syncoid options to the README Update the README with the new --include-snaps and --exclude-snaps syncoid options. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 21432ac..385f66f 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,14 @@ As of 1.4.18, syncoid also automatically supports and enables resume of interrup 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. ++ --exclude-snaps=REGEX + + Exclude specific snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + ++ --include-snaps=REGEX + + Only include snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + + --no-resume This argument tells syncoid to not use resumeable zfs send/receive streams. From 8e867c6f142bda4bcb0b7151246c2b549f2e3ab8 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 29 Nov 2021 20:48:12 -0700 Subject: [PATCH 6/9] Add new syncoid tests Test the new --include-snaps and --exclude-snaps options for syncoid. --- tests/syncoid/8_filter_snaps/run.sh | 142 ++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100755 tests/syncoid/8_filter_snaps/run.sh diff --git a/tests/syncoid/8_filter_snaps/run.sh b/tests/syncoid/8_filter_snaps/run.sh new file mode 100755 index 0000000..1b91ff4 --- /dev/null +++ b/tests/syncoid/8_filter_snaps/run.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# test filtering snapshot names using --include-snaps and --exclude-snaps + +set -x +set -e + +. ../../common/lib.sh + +POOL_IMAGE="/tmp/syncoid-test-8.zpool" +MOUNT_TARGET="/tmp/syncoid-test-8.mount" +POOL_SIZE="100M" +POOL_NAME="syncoid-test-8" + +truncate -s "${POOL_SIZE}" "${POOL_IMAGE}" + +zpool create -m none -f "${POOL_NAME}" "${POOL_IMAGE}" + +##### +# Create source snapshots and destroy the destination snaps and dataset. +##### +function setup_snaps { + # create intermediate snapshots + # sleep is needed so creation time can be used for proper sorting + sleep 1 + zfs snapshot "${POOL_NAME}"/src@monthly1 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@daily1 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@daily2 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@hourly1 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@hourly2 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@daily3 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@hourly3 + sleep 1 + zfs snapshot "${POOL_NAME}"/src@hourly4 +} + +##### +# Remove the destination snapshots and dataset so that each test starts with a +# blank slate. +##### +function clean_snaps { + zfs destroy "${POOL_NAME}"/dst@% + zfs destroy "${POOL_NAME}"/dst +} + +##### +# Verify that the correct set of snapshots is present on the destination. +##### +function verify_checksum { + zfs list -r -t snap "${POOL_NAME}" + + checksum=$(zfs list -t snap -r -H -o name "${POOL_NAME}" | sed 's/@syncoid_.*/@syncoid_/' | shasum -a 256) + + echo "Expected checksum: $1" + echo "Actual checksum: $checksum" + return $( [[ "$checksum" == "$1" ]] ) +} + +function cleanUp { + zpool export "${POOL_NAME}" +} + +# export pool in any case +trap cleanUp EXIT + +zfs create "${POOL_NAME}"/src +setup_snaps + +##### +# TEST 1 +# +# --exclude-snaps is provided and --no-stream is omitted. Hourly snaps should +# be missing from the destination, and all other intermediate snaps should be +# present. +##### + +../../../syncoid --debug --compress=none --no-sync-snap --exclude-snaps='hourly' "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum 'fb408c21b8540b3c1bd04781b6091d77ff9432defef3303c1a34321b45e8b6a9 -' +clean_snaps + +##### +# TEST 2 +# +# --exclude-snaps and --no-stream are provided. Only the daily3 snap should be +# present on the destination. +##### + +../../../syncoid --debug --compress=none --no-sync-snap --exclude-snaps='hourly' --no-stream "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum 'c9ad1d3e07156847f957509fcd4805edc7d4c91fe955c605ac4335076367d19a -' +clean_snaps + +##### +# TEST 3 +# +# --include-snaps is provided and --no-stream is omitted. Hourly snaps should +# be present on the destination, and all other snaps should be missing +##### + +../../../syncoid --debug --compress=none --no-sync-snap --include-snaps='hourly' "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum 'f2fb62a2b475bec85796dbf4f6c02af5b4ccaca01f9995ef3d0909787213cbde -' +clean_snaps + +##### +# TEST 4 +# +# --include-snaps and --no-stream are provided. Only the hourly4 snap should +# be present on the destination. +##### + +../../../syncoid --debug --compress=none --no-sync-snap --include-snaps='hourly' --no-stream "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum '194e60e9d635783f7c7d64e2b0d9f0897c926e69a86ffa2858cf0ca874ffeeb4 -' +clean_snaps + +##### +# TEST 5 +# +# --include-snaps='hourly' and --exclude-snaps='3' are both provided. The +# hourly snaps should be present on the destination except for hourly3; daily +# and monthly snaps should be missing. +##### + +../../../syncoid --debug --compress=none --no-sync-snap --include-snaps='hourly' --exclude-snaps='3' "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum '55267405e346e64d6f7eed29d62bc9bb9ea0e15c9515103a92ee47a7439a99a2 -' +clean_snaps + +##### +# TEST 6 +# +# --exclude-snaps='syncoid' and --no-stream are provided, and --no-sync-snap is +# omitted. The sync snap should be created on the source but not sent to the +# destination; only hourly4 should be sent. +##### + +../../../syncoid --debug --compress=none --no-stream --exclude-snaps='syncoid' "${POOL_NAME}"/src "${POOL_NAME}"/dst +verify_checksum '47380e1711d08c46fb1691fa4bd65e5551084fd5b961baa2de7f91feff2cb4b8 -' +clean_snaps From 14ed85163a82c743d67c2b25fac1882f302ab0ea Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Wed, 1 Dec 2021 20:40:03 -0700 Subject: [PATCH 7/9] Filter snapshots in getsnapsfallback() --- syncoid | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncoid b/syncoid index 8f8db31..33e4694 100755 --- a/syncoid +++ b/syncoid @@ -1674,6 +1674,7 @@ sub getsnapsfallback() { $guid =~ s/^.*\tguid\t*(\d*).*/$1/; my $snap = $line; $snap =~ s/^.*\@(.*)\tguid.*$/$1/; + if (!snapisincluded($snap)) { next; } $snaps{$type}{$snap}{'guid'}=$guid; } elsif ($state eq 2) { if ($line !~ /\Q$fs\E\@.*creation/) { @@ -1685,6 +1686,7 @@ sub getsnapsfallback() { $creation =~ s/^.*\tcreation\t*(\d*).*/$1/; my $snap = $line; $snap =~ s/^.*\@(.*)\tcreation.*$/$1/; + if (!snapisincluded($snap)) { next; } # the accuracy of the creation timestamp is only for a second, but # snapshots in the same second are highly likely. The list command From 0c577fc73541e4ca9d971aa18b6ee076830bb34f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Thu, 2 Dec 2021 21:36:52 -0700 Subject: [PATCH 8/9] Deprecate the --exclude option Add a new option, --exclude-datasets, to replace --exclude. This makes the naming more consistent now that there are options to filter both snapshots and datasets. Also add more information to the README about the distinction between --exclude-datasets and --(in|ex)clude-snaps. --- README.md | 12 +++++++++--- syncoid | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 385f66f..81d70d5 100644 --- a/README.md +++ b/README.md @@ -330,15 +330,21 @@ As of 1.4.18, syncoid also automatically supports and enables resume of interrup + --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. + __DEPRECATION NOTICE:__ `--exclude` has been deprecated and will be removed in a future release. Please use `--exclude-datasets` instead. + + 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. The provided regex pattern is matched against the dataset name only; this option does not affect which snapshots are synchronized. If both `--exclude` and `--exclude-datasets` are provided, then `--exclude` is ignored. + ++ --exclude-datasets=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. The provided regex pattern is matched against the dataset name only; this option does not affect which snapshots are synchronized. + --exclude-snaps=REGEX - Exclude specific snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + Exclude specific snapshots that match the given regular expression. The provided regex pattern is matched against the snapshot name only. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + --include-snaps=REGEX - Only include snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + Only include snapshots that match the given regular expression. The provided regex pattern is matched against the snapshot name only. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. + --no-resume diff --git a/syncoid b/syncoid index 33e4694..6e37af2 100755 --- a/syncoid +++ b/syncoid @@ -26,10 +26,20 @@ GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsn "debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent", "identifier=s", "no-clone-handling", "no-privilege-elevation", "force-delete", "create-bookmark", "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size, - "include-snaps=s@", "exclude-snaps=s@") or pod2usage(2); + "include-snaps=s@", "exclude-snaps=s@", "exclude-datasets=s@") or pod2usage(2); my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set +if (defined($args{'exclude'})) { + writelog('WARN', 'The --exclude option is deprecated, please use --exclude-datasets instead'); + + # If both --exclude and --exclude-datasets are provided, then ignore + # --exclude + if (!defined($args{'exclude-datasets'})) { + $args{'exclude-datasets'} = $args{'exclude'}; + } +} + my @sendoptions = (); if (length $args{'sendoptions'}) { @sendoptions = parsespecialoptions($args{'sendoptions'}); @@ -256,8 +266,8 @@ sub getchilddatasets { my ($dataset, $origin) = /^([^\t]+)\t([^\t]+)/; - if (defined $args{'exclude'}) { - my $excludes = $args{'exclude'}; + if (defined $args{'exclude-datasets'}) { + my $excludes = $args{'exclude-datasets'}; foreach (@$excludes) { if ($dataset =~ /$_/) { writelog('DEBUG', "excluded $dataset because of $_"); @@ -2065,7 +2075,8 @@ Options: --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) --preserve-recordsize Preserves the recordsize on initial sends to the target - --exclude=REGEX Exclude specific datasets which match the given regular expression. Can be specified multiple times + --exclude=REGEX DEPRECATED. Equivalent to --exclude-datasets, but will be removed in a future release. Ignored if --exclude-datasets is also provided. + --exclude-datasets=REGEX Exclude specific datasets which match the given regular expression. Can be specified multiple times --exclude-snaps=REGEX Exclude specific snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. --include-snaps=REGEX Only include snapshots that match the given regular expression. Can be specified multiple times. If a snapshot matches both the exclude-snaps and include-snaps patterns, then it will be excluded. --sendoptions=OPTIONS Use advanced options for zfs send (the arguments are filtered as needed), e.g. syncoid --sendoptions="Lc e" sets zfs send -L -c -e ... From 8ce1ea4dc8b6ec0ec6c0bed8ae61656d67bfed7f Mon Sep 17 00:00:00 2001 From: Christoph Klaffl Date: Sat, 13 Jan 2024 19:49:20 +0100 Subject: [PATCH 9/9] fixed refactoring regression --- syncoid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncoid b/syncoid index 4bf7072..b30abce 100755 --- a/syncoid +++ b/syncoid @@ -928,7 +928,7 @@ sub runsynccmd { } my $sendcmd = "$sourcesudocmd $zfscmd send $sendoptions $sendsource"; - my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $targetfsescaped"; + my $recvcmd = "$targetsudocmd $zfscmd receive $recvoptions $targetfsescaped 2>&1"; my $synccmd = buildsynccmd($sendcmd,$recvcmd,$pvsize,$sourceisroot,$targetisroot); writelog('INFO', "Sync size: ~$disp_pvsize");