Created
October 3, 2017 10:03
-
-
Save Laeeth/65ddf31fffc291c0203314c70c7633d6 to your computer and use it in GitHub Desktop.
Port of syncoid from perl to dlang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/rdmd | |
module kaleidic.apps.cli.syncoid; | |
import std.stdio; | |
import std.datetime; | |
import std.socket: hostName; | |
import std.getopt; | |
enum SanoidVersion = "1.4.16"; | |
struct Options | |
{ | |
bool debugMode; | |
short sshPort = 22; | |
string sshOption; | |
string pvCommand = "/usr/bin/pv"; | |
string mbufferCommand = "/usr/bin/mbuffer"; | |
string mbufferOptions = "-q -s 128k -m 16M 2>/dev/null"; | |
string sudoCommand = "/usr/bin/sudo"; | |
// currently using ls to check for file existence | |
string lsCommand = "/bin/ls"; | |
string sshCipher; | |
} | |
int main(string[] args) | |
{ | |
if ($args{'version'}) { | |
writef("Syncoid version: $version\n"; | |
exit 0; | |
} | |
if (!(defined $args{'source'} && defined $args{'target'})) { | |
writef('usage: syncoid [src_user@src_host:]src_pool/src_dataset [dst_user@dst_host:]dst_pool/dst_dataset'."\n"; | |
exit 127; | |
} | |
my $rawsourcefs = $args{'source'}; | |
my $rawtargetfs = $args{'target'}; | |
my $debug = $args{'debug'}; | |
my $quiet = $args{'quiet'}; | |
my $zfscmd = '/sbin/zfs'; | |
my $sshcmd = '/usr/bin/ssh'; | |
my $pscmd = '/bin/ps'; | |
my $sshcipher; | |
if (defined $args{'c'}) { | |
$sshcipher = "-c $args{'c'}"; | |
} else { | |
$sshcipher = '-c chacha20-poly1305@openssh.com,arcfour'; | |
} | |
my $sshport = '-p 22'; | |
my $sshoption; | |
if (defined $args{'o'}) { | |
my @options = split(',', $args{'o'}); | |
foreach my $option (@options) { | |
$sshoption .= " -o $option"; | |
if ($option eq "NoneSwitch=yes") { | |
$sshcipher = ""; | |
} | |
} | |
} else { | |
$sshoption = ""; | |
} | |
if ( $args{'sshport'} ) { | |
$sshport = "-p $args{'sshport'}"; | |
} | |
# figure out if source and/or target are remote. | |
if ( $args{'sshkey'} ) { | |
$sshcmd = "$sshcmd $sshoption $sshcipher $sshport -i $args{'sshkey'}"; | |
} | |
else { | |
$sshcmd = "$sshcmd $sshoption $sshcipher $sshport"; | |
} | |
my ($sourcehost,$sourcefs,$sourceisroot) = getssh($rawsourcefs); | |
my ($targethost,$targetfs,$targetisroot) = getssh($rawtargetfs); | |
my $sourcesudocmd; | |
my $targetsudocmd; | |
if ($sourceisroot) { $sourcesudocmd = ''; } else { $sourcesudocmd = $sudocmd; } | |
if ($targetisroot) { $targetsudocmd = ''; } else { $targetsudocmd = $sudocmd; } | |
# figure out whether compression, mbuffering, pv | |
# are available on source, target, local machines. | |
# warn user of anything missing, then continue with sync. | |
my %avail = checkcommands(); | |
my %snaps; | |
## break here to call replication individually so that we ## | |
## can loop across children separately, for recursive ## | |
## replication ## | |
if (! $args{'recursive'}) { | |
syncdataset($sourcehost, $sourcefs, $targethost, $targetfs); | |
} else { | |
infof("DEBUG: recursive sync of $sourcefs.\n"); | |
my @datasets = getchilddatasets($sourcehost, $sourcefs, $sourceisroot); | |
foreach my $dataset(@datasets) { | |
$dataset =~ s/$sourcefs//; | |
chomp $dataset; | |
my $childsourcefs = $sourcefs . $dataset; | |
my $childtargetfs = $targetfs . $dataset; | |
# writef("syncdataset($sourcehost, $childsourcefs, $targethost, $childtargetfs); \n"; | |
syncdataset($sourcehost, $childsourcefs, $targethost, $childtargetfs); | |
} | |
} | |
# close SSH sockets for master connections as applicable | |
if ($sourcehost ne '') { | |
open FH, "$sshcmd $sourcehost -O exit 2>&1 |"; | |
close FH; | |
} | |
if ($targethost ne '') { | |
open FH, "$sshcmd $targethost -O exit 2>&1 |"; | |
close FH; | |
} | |
exit 0; | |
sub getChildDataSets(string remoteHost, string fileSystem, bool isRoot, string[] snaps) | |
{ | |
my ($rhost,$fs,$isroot,%snaps) = @_; | |
auto mySudoCommand = isRoot ? "" : sudoCommand; | |
if (remoteHost.length>0) | |
remoteHost = sshCommand ~ " " ~ remoteHost; | |
my $getchildrencmd = "$rhost $mysudocmd $zfscmd list -o name -t filesystem,volume -Hr $fs |"; | |
infof("DEBUG: getting list of child datasets on $fs using $getchildrencmd...\n"); | |
open FH, $getchildrencmd; | |
my @children = <FH>; | |
close FH; | |
return @children; | |
} | |
auto syncDataSet(string sourceHost, string sourceFileSystem, string targetHost, string targetFileSystem) | |
{ | |
if ($debug) { stderr.writeln("DEBUG: syncing source $sourcefs to target $targetfs.\n"); } | |
// 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"); | |
return 0; | |
} | |
// does the target filesystem exist yet? | |
my $targetexists = targetexists($targethost,$targetfs,$targetisroot); | |
// build hashes of the snaps on the source and target filesystems. | |
%snaps = getsnaps('source',$sourcehost,$sourcefs,$sourceisroot); | |
if ($targetexists) | |
{ | |
my %targetsnaps = getsnaps('target',$targethost,$targetfs,$targetisroot); | |
my %sourcesnaps = %snaps; | |
%snaps = (%sourcesnaps, %targetsnaps); | |
} | |
if ($args{'dumpsnaps'}) { writef("merged snapshot list of $targetfs: \n"; dumphash(\%snaps); writef("\n\n\n"); | |
// create a new syncoid snapshot on the source filesystem. | |
my $newsyncsnap; | |
if (!defined ($args{'no-sync-snap'}) ) { | |
$newsyncsnap = newsyncsnap($sourcehost,$sourcefs,$sourceisroot); | |
} else { | |
# 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"; | |
return 0; | |
} | |
} | |
// there is currently (2014-09-01) a bug in ZFS on Linux | |
// that causes readonly to always show on if it's EVER | |
// been turned on... even when it's off... unless and | |
// until the filesystem is zfs umounted and zfs remounted. | |
// we're going to do the right thing anyway. | |
// 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. | |
// my $originaltargetreadonly; | |
// sync 'em up. | |
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'}) ) | |
{ | |
writef("DEBUG: target $targetfs does not exist. Finding oldest available snapshot on source $sourcefs ...\n"; | |
} else | |
{ | |
writef("DEBUG: target $targetfs does not exist, and --no-stream selected. Finding newest available snapshot on source $sourcefs ...\n"; | |
} | |
} | |
my $oldestsnap = getoldestsnapshot(\%snaps); | |
if (! $oldestsnap) | |
{ | |
// getoldestsnapshot() returned false, so use new sync snapshot | |
infof("DEBUG: getoldestsnapshot() returned false, so using $newsyncsnap.\n"); | |
$oldestsnap = $newsyncsnap; | |
} | |
// if --no-stream is specified, our full needs to be the newest snapshot, not the oldest. | |
if (defined $args{'no-stream'}) { $oldestsnap = getnewestsnapshot(\%snaps); } | |
auto sendCommand = "$sourcesudocmd $zfscmd send $sourcefs\@$oldestsnap"; | |
auto receiveCommand = "$targetsudocmd $zfscmd receive -F $targetfs"; | |
auto pvSize = getsendsize($sourcehost,"$sourcefs\@$oldestsnap",0,$sourceisroot); | |
my $disp_pvsize = readablebytes(pvSize); | |
if (pvSize == 0) { $disp_pvsize = 'UNKNOWN'; } | |
auto syncCommand = buildsynccmd(sendCommand,receiveCommand,pvSize,$sourceisroot,$targetisroot); | |
if (!$quiet) { | |
if (!defined ($args{'no-stream'}) ) { | |
writef("INFO: Sending oldest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:\n"; | |
} else { | |
writef("INFO: --no-stream selected; sending newest full snapshot $sourcefs\@$oldestsnap (~ $disp_pvsize) to new target filesystem:\n"; | |
} | |
} | |
infof("DEBUG: syncCommand\n"); | |
// 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"; | |
return 0; | |
} | |
system(syncCommand) == 0 | |
or die "CRITICAL ERROR: syncCommand failed: $?"; | |
// 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 | |
// aren't invoked with --no-stream, in which case a full of the newest snap available was all we needed to do | |
if (!defined ($args{'no-stream'}) && ($oldestsnap ne $newsyncsnap) ) | |
{ | |
# get current readonly status of target, then set it to on during sync | |
# 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. | |
# $originaltargetreadonly = getZFSValue($targethost,$targetfs,$targetisroot,'readonly'); | |
# setzfsvalue($targethost,$targetfs,$targetisroot,'readonly','on'); | |
sendCommand = "$sourcesudocmd $zfscmd send $args{'streamarg'} $sourcefs\@$oldestsnap $sourcefs\@$newsyncsnap"; | |
pvSize = getsendsize($sourcehost,"$sourcefs\@$oldestsnap","$sourcefs\@$newsyncsnap",$sourceisroot); | |
$disp_pvsize = readablebytes(pvSize); | |
if (pvSize == 0) { $disp_pvsize = "UNKNOWN"; } | |
syncCommand = buildsynccmd(sendCommand,receiveCommand,pvSize,$sourceisroot,$targetisroot); | |
# 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"; | |
return 0; | |
} | |
if (!$quiet) { writef("INFO: Updating new target filesystem with incremental $sourcefs\@$oldestsnap ... $newsyncsnap (~ $disp_pvsize):\n"); | |
infof("DEBUG: syncCommand\n"); | |
if ($oldestsnap ne $newsyncsnap) { | |
system(syncCommand) == 0 | |
or warn "CRITICAL ERROR: syncCommand failed: $?"; | |
return 0; | |
} else { | |
if (!$quiet) { writef("INFO: no incremental sync needed; $oldestsnap is already the newest available snapshot.\n"); | |
} | |
# 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. | |
# setzfsvalue($targethost,$targetfs,$targetisroot,'readonly',$originaltargetreadonly); | |
} | |
} else | |
{ | |
// find most recent matching snapshot and do an -I to the new snapshot get current readonly status of target, then set it to on during sync | |
// 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. | |
// $originaltargetreadonly = getZFSValue($targethost,$targetfs,$targetisroot,'readonly'); | |
// setzfsvalue($targethost,$targetfs,$targetisroot,'readonly','on'); | |
auto targetSize = getZFSValue($targethost,$targetfs,$targetisroot,'-p used'); | |
my matchingSnap = getMatchingSnapshot($sourcefs, $targetfs, targetSize, \%snaps); | |
if (! matchingSnap) | |
{ | |
// no matching snapshot; we whined piteously already, but let's go ahead and return false now in case more child datasets need replication. | |
return 0; | |
} | |
// 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"; | |
return 0; | |
} | |
if (matchingSnap eq $newsyncsnap) | |
{ | |
// barf some text but don't touch the filesystem | |
if (!$quiet) { writef("INFO: no snapshots on source newer than $newsyncsnap on target. Nothing to do, not syncing.\n"); | |
} else | |
{ | |
// rollback target to matchingsnap | |
infof("DEBUG: rolling back target to $targetfs\@matchingSnap...\n"); | |
if ($targethost ne '') | |
{ | |
infof("$sshcmd $targethost $targetsudocmd $zfscmd rollback -R $targetfs\@matchingSnap\n"); | |
system ("$sshcmd $targethost $targetsudocmd $zfscmd rollback -R $targetfs\@matchingSnap"); | |
} else { | |
infof("$targetsudocmd $zfscmd rollback -R $targetfs\@matchingSnap\n"); | |
system ("$targetsudocmd $zfscmd rollback -R $targetfs\@matchingSnap"); | |
} | |
my sendCommand = "$sourcesudocmd $zfscmd send $args{'streamarg'} $sourcefs\@matchingSnap $sourcefs\@$newsyncsnap"; | |
my receiveCommand = "$targetsudocmd $zfscmd receive -F $targetfs"; | |
my pvSize = getsendsize($sourcehost,"$sourcefs\@matchingSnap","$sourcefs\@$newsyncsnap",$sourceisroot); | |
my $disp_pvsize = readablebytes(pvSize); | |
if (pvSize == 0) { $disp_pvsize = "UNKNOWN"; } | |
my syncCommand = buildsynccmd(sendCommand,receiveCommand,pvSize,$sourceisroot,$targetisroot); | |
if (!$quiet) { writef("Sending incremental $sourcefs\@matchingSnap ... $newsyncsnap (~ $disp_pvsize):\n"); | |
infof("DEBUG: syncCommand\n"); | |
system("syncCommand") == 0 | |
or die "CRITICAL ERROR: syncCommand failed: $?"; | |
# 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. | |
#setzfsvalue($targethost,$targetfs,$targetisroot,'readonly',$originaltargetreadonly); | |
} | |
} | |
// prune obsolete sync snaps on source and target. | |
pruneOldSyncSnaps($sourcehost,$sourcefs,$newsyncsnap,$sourceisroot,keys %{ $snaps{'source'}}); | |
pruneOldSyncSnaps($targethost,$targetfs,$newsyncsnap,$targetisroot,keys %{ $snaps{'target'}}); | |
} # end syncdataset() | |
auto getArgs() | |
{ | |
my @args = @_; | |
my %args; | |
my %novaluearg; | |
my %validarg; | |
push my @validargs, ('debug','nocommandchecks','version','monitor-version','compress','c','o','source-bwlimit','target-bwlimit','dumpsnaps','recursive','r','sshkey','sshport','quiet','no-stream','no-sync-snap'); | |
foreach my $item (@validargs) { $validarg{$item} = 1; } | |
push my @novalueargs, ('debug','nocommandchecks','version','monitor-version','dumpsnaps','recursive','r','quiet','no-stream','no-sync-snap'); | |
foreach my $item (@novalueargs) { $novaluearg{$item} = 1; } | |
while (my $rawarg = shift(@args)) { | |
my $arg = $rawarg; | |
my $argvalue = ''; | |
if ($rawarg =~ /=/) { | |
# user specified the value for a CLI argument with = | |
# instead of with blank space. separate appropriately. | |
$argvalue = $arg; | |
$arg =~ s/=.*$//; | |
$argvalue =~ s/^.*=//; | |
} | |
if ($rawarg =~ /^--/) { | |
# doubledash arg | |
$arg =~ s/^--//; | |
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"); | |
if ($novaluearg{$arg}) { | |
$args{$arg} = 1; | |
} else { | |
# if this CLI arg takes a user-specified value and | |
# we don't already have it, then the user must have | |
# specified with a space, so pull in the next value | |
# from the array as this value rather than as the | |
# next argument. | |
if ($argvalue eq '') { $argvalue = shift(@args); } | |
$args{$arg} = $argvalue; | |
} | |
} elsif ($arg =~ /^-/) { | |
# singledash arg | |
$arg =~ s/^-//; | |
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"); | |
if ($novaluearg{$arg}) { | |
$args{$arg} = 1; | |
} else { | |
# if this CLI arg takes a user-specified value and | |
# we don't already have it, then the user must have | |
# specified with a space, so pull in the next value | |
# from the array as this value rather than as the | |
# next argument. | |
if ($argvalue eq '') { $argvalue = shift(@args); } | |
$args{$arg} = $argvalue; | |
} | |
} else { | |
# bare arg | |
if (defined $args{'source'}) { | |
if (! defined $args{'target'}) { | |
$args{'target'} = $arg; | |
} else { | |
die "ERROR: don't know what to do with third bare argument $rawarg.\n"; | |
} | |
} else { | |
$args{'source'} = $arg; | |
} | |
} | |
} | |
if (defined $args{'source-bwlimit'}) { $args{'source-bwlimit'} = "-R $args{'source-bwlimit'}"; } else { $args{'source-bwlimit'} = ''; } | |
if (defined $args{'target-bwlimit'}) { $args{'target-bwlimit'} = "-r $args{'target-bwlimit'}"; } else { $args{'target-bwlimit'} = ''; } | |
if (defined $args{'no-stream'}) { $args{'streamarg'} = '-i'; } else { $args{'streamarg'} = '-I'; } | |
if ($args{'r'}) { $args{'recursive'} = $args{'r'}; } | |
if (!defined $args{'compress'}) { $args{'compress'} = 'default'; } | |
if ($args{'compress'} eq 'gzip') { | |
$args{'rawcompresscmd'} = '/bin/gzip'; | |
$args{'compressargs'} = '-3'; | |
$args{'rawdecompresscmd'} = '/bin/zcat'; | |
$args{'decompressargs'} = ''; | |
} elsif ( ($args{'compress'} eq 'pigz-fast')) { | |
$args{'rawcompresscmd'} = '/usr/bin/pigz'; | |
$args{'compressargs'} = '-3'; | |
$args{'rawdecompresscmd'} = '/usr/bin/pigz'; | |
$args{'decompressargs'} = '-dc'; | |
} elsif ( ($args{'compress'} eq 'pigz-slow')) { | |
$args{'rawcompresscmd'} = '/usr/bin/pigz'; | |
$args{'compressargs'} = '-9'; | |
$args{'rawdecompresscmd'} = '/usr/bin/pigz'; | |
$args{'decompressargs'} = '-dc'; | |
} elsif ( ($args{'compress'} eq 'lzo') || ($args{'compress'} eq 'default') ) { | |
$args{'rawcompresscmd'} = '/usr/bin/lzop'; | |
$args{'compressargs'} = ''; | |
$args{'rawdecompresscmd'} = '/usr/bin/lzop'; | |
$args{'decompressargs'} = '-dfc'; | |
} else { | |
$args{'rawcompresscmd'} = ''; | |
$args{'compressargs'} = ''; | |
$args{'rawdecompresscmd'} = ''; | |
$args{'decompressargs'} = ''; | |
} | |
$args{'compresscmd'} = "$args{'rawcompresscmd'} $args{'compressargs'}"; | |
$args{'decompresscmd'} = "$args{'rawdecompresscmd'} $args{'decompressargs'}"; | |
return %args; | |
} | |
/// make sure compression, mbuffer, and pv are available on source, target, and local hosts as appropriate. | |
auto checkCommands() | |
{ | |
my %avail; | |
my $sourcessh; | |
my $targetssh; | |
# if --nocommandchecks then assume everything's available and return | |
if ($args{'nocommandchecks'}) { | |
infof("DEBUG: not checking for command availability due to --nocommandchecks switch.\n"); | |
$avail{'compress'} = 1; | |
$avail{'localpv'} = 1; | |
$avail{'localmbuffer'} = 1; | |
$avail{'sourcembuffer'} = 1; | |
$avail{'targetmbuffer'} = 1; | |
return %avail; | |
} | |
if (!defined $sourcehost) { $sourcehost = ''; } | |
if (!defined $targethost) { $targethost = ''; } | |
if ($sourcehost ne '') { $sourcessh = "$sshcmd $sourcehost"; } else { $sourcessh = ''; } | |
if ($targethost ne '') { $targetssh = "$sshcmd $targethost"; } else { $targetssh = ''; } | |
# if raw compress command is null, we must have specified no compression. otherwise, | |
# make sure that compression is available everywhere we need it | |
if ($args{'rawcompresscmd'} eq '') { | |
$avail{'sourcecompress'} = 0; | |
$avail{'sourcecompress'} = 0; | |
$avail{'localcompress'} = 0; | |
if ($args{'compress'} eq 'none' || | |
$args{'compress'} eq 'no' || | |
$args{'compress'} eq '0') { | |
infof("DEBUG: compression forced off from command line arguments.\n"); | |
} else { | |
writef("WARN: value $args{'compress'} for argument --compress not understood, proceeding without compression.\n"; | |
} | |
} else { | |
infof("DEBUG: checking availability of $args{'rawcompresscmd'} on source...\n"); | |
$avail{'sourcecompress'} = `$sourcessh $lscmd $args{'rawcompresscmd'} 2>/dev/null`; | |
infof("DEBUG: checking availability of $args{'rawcompresscmd'} on target...\n"); | |
$avail{'targetcompress'} = `$targetssh $lscmd $args{'rawcompresscmd'} 2>/dev/null`; | |
infof("DEBUG: checking availability of $args{'rawcompresscmd'} on local machine...\n"); | |
$avail{'localcompress'} = `$lscmd $args{'rawcompresscmd'} 2>/dev/null`; | |
} | |
my ($s,$t); | |
if ($sourcehost eq '') { | |
$s = '[local machine]' | |
} else { | |
$s = $sourcehost; | |
$s =~ s/^\S*\@//; | |
$s = "ssh:$s"; | |
} | |
if ($targethost eq '') { | |
$t = '[local machine]' | |
} else { | |
$t = $targethost; | |
$t =~ s/^\S*\@//; | |
$t = "ssh:$t"; | |
} | |
if (!defined $avail{'sourcecompress'}) { $avail{'sourcecompress'} = ''; } | |
if (!defined $avail{'targetcompress'}) { $avail{'targetcompress'} = ''; } | |
if (!defined $avail{'sourcembuffer'}) { $avail{'sourcembuffer'} = ''; } | |
if (!defined $avail{'targetmbuffer'}) { $avail{'targetmbuffer'} = ''; } | |
if ($avail{'sourcecompress'} eq '') { | |
if ($args{'rawcompresscmd'} ne '') { | |
writef("WARN: $args{'compresscmd'} not available on source $s- sync will continue without compression.\n"; | |
} | |
$avail{'compress'} = 0; | |
} | |
if ($avail{'targetcompress'} eq '') { | |
if ($args{'rawcompresscmd'} ne '') { | |
writef("WARN: $args{'compresscmd'} not available on target $t - sync will continue without compression.\n"; | |
} | |
$avail{'compress'} = 0; | |
} | |
if ($avail{'targetcompress'} ne '' && $avail{'sourcecompress'} ne '') { | |
# compression available - unless source and target are both remote, which we'll check | |
# for in the next block and respond to accordingly. | |
$avail{'compress'} = 1; | |
} | |
# 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 ($args{'rawcompresscmd'} ne '') { | |
writef("WARN: $args{'compresscmd'} not available on local machine - sync will continue without compression.\n"; | |
} | |
$avail{'compress'} = 0; | |
} | |
infof("DEBUG: checking availability of $mbuffercmd on source...\n"); | |
$avail{'sourcembuffer'} = `$sourcessh $lscmd $mbuffercmd 2>/dev/null`; | |
if ($avail{'sourcembuffer'} eq '') { | |
writef("WARN: $mbuffercmd not available on source $s - sync will continue without source buffering.\n"; | |
$avail{'sourcembuffer'} = 0; | |
} else { | |
$avail{'sourcembuffer'} = 1; | |
} | |
infof("DEBUG: checking availability of $mbuffercmd on target...\n"); | |
$avail{'targetmbuffer'} = `$targetssh $lscmd $mbuffercmd 2>/dev/null`; | |
if ($avail{'targetmbuffer'} eq '') { | |
writef("WARN: $mbuffercmd not available on target $t - sync will continue without target buffering.\n"; | |
$avail{'targetmbuffer'} = 0; | |
} else { | |
$avail{'targetmbuffer'} = 1; | |
} | |
# if we're doing remote source AND remote target, check for local mbuffer as well | |
if ($sourcehost ne '' && $targethost ne '') { | |
infof("DEBUG: checking availability of $mbuffercmd on local machine...\n"); | |
$avail{'localmbuffer'} = `$lscmd $mbuffercmd 2>/dev/null`; | |
if ($avail{'localmbuffer'} eq '') { | |
$avail{'localmbuffer'} = 0; | |
writef("WARN: $mbuffercmd not available on local machine - sync will continue without local buffering.\n"; | |
} | |
} | |
infof("DEBUG: checking availability of $pvcmd on local machine...\n"); | |
$avail{'localpv'} = `$lscmd $pvcmd 2>/dev/null`; | |
if ($avail{'localpv'} eq '') { | |
writef("WARN: $pvcmd not available on local machine - sync will continue without progress bar.\n"; | |
$avail{'localpv'} = 0; | |
} else { | |
$avail{'localpv'} = 1; | |
} | |
return %avail; | |
} | |
bool isZFSBusy() | |
{ | |
my ($rhost,$fs,$isroot) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
infof("DEBUG: checking to see if $fs on $rhost is already in zfs receive using $rhost $pscmd -Ao args= ...\n"); | |
open PL, "$rhost $pscmd -Ao args= |"; | |
my @processes = <PL>; | |
close PL; | |
foreach my $process (@processes) { | |
# infof("DEBUG: checking process $process...\n"); | |
if ($process =~ /zfs *(receive|recv).*$fs/) { | |
# there's already a zfs receive process for our target filesystem - return true | |
infof("DEBUG: process $process matches target $fs!\n"); | |
return 1; | |
} | |
} | |
# no zfs receive processes for our target filesystem found - return false | |
return 0; | |
} | |
auto setZFSValue() | |
{ | |
my ($rhost,$fs,$isroot,$property,$value) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
infof("DEBUG: setting $property to $value on $fs...\n"); | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
infof("$rhost $mysudocmd $zfscmd set $property=$value $fs\n"); | |
system("$rhost $mysudocmd $zfscmd set $property=$value $fs") == 0 | |
or warn "WARNING: $rhost $mysudocmd $zfscmd set $property=$value $fs died: $?, proceeding anyway.\n"; | |
return; | |
} | |
auto getZFSValue() | |
{ | |
my ($rhost,$fs,$isroot,$property) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
infof("DEBUG: getting current value of $property on $fs...\n"); | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
infof("$rhost $mysudocmd $zfscmd get -H $property $fs\n"); | |
open FH, "$rhost $mysudocmd $zfscmd get -H $property $fs |"; | |
my $value = <FH>; | |
close FH; | |
my @values = split(/\s/,$value); | |
$value = $values[2]; | |
return $value; | |
} | |
auto readableBytes() | |
{ | |
my $bytes = shift; | |
my $disp; | |
if ($bytes > 1024*1024*1024) { | |
$disp = sprintf("%.1f",$bytes/1024/1024/1024) . ' GB'; | |
} elsif ($bytes > 1024*1024) { | |
$disp = sprintf("%.1f",$bytes/1024/1024) . ' MB'; | |
} else { | |
$disp = sprintf("%d",$bytes/1024) . ' KB'; | |
} | |
return $disp; | |
} | |
auto getOldestSnapshot() | |
{ | |
my $snaps = shift; | |
foreach my $snap ( sort { $snaps{'source'}{$a}{'creation'}<=>$snaps{'source'}{$b}{'creation'} } keys %{ $snaps{'source'} }) { | |
# return on first snap found - it's the oldest | |
return $snap; | |
} | |
# 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. | |
die "CRIT: --no-sync-snap is set, and getoldestsnapshot() could not find any snapshots on source!\n"; | |
} | |
return 0; | |
} | |
sub getNewstSnapshot() | |
{ | |
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 | |
writef("NEWEST SNAPSHOT: $snap\n"; | |
return $snap; | |
} | |
# must not have had any snapshots on source - looks like we'd better create one! | |
if (defined ($args{'no-sync-snap'}) ) { | |
if (!defined ($args{'recursive'}) ) { | |
# well, actually we set --no-sync-snap and we're not recursive, so no we *can't* make one. Whoops. | |
die "CRIT: --no-sync-snap is set, and getnewestsnapshot() could not find any snapshots on source!\n"; | |
} | |
# 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"; | |
} | |
return 0; | |
} | |
auto buildSyncCommand() | |
{ | |
my (sendCommand,receiveCommand,pvSize,$sourceisroot,$targetisroot) = @_; | |
# here's where it gets fun: figuring out when to compress and decompress. | |
# to make this work for all possible combinations, you may have to decompress | |
# AND recompress across the pipe viewer. FUN. | |
my syncCommand; | |
if ($sourcehost eq '' && $targethost eq '') { | |
# both sides local. don't compress. do mbuffer, once, on the source side. | |
# syncCommand = "sendCommand | $mbuffercmd | $pvcmd | receiveCommand"; | |
syncCommand = "sendCommand |"; | |
# avoid confusion - accept either source-bwlimit or target-bwlimit as the bandwidth limiting option here | |
my $bwlimit = ''; | |
if (defined $args{'source-bwlimit'}) { | |
$bwlimit = $args{'source-bwlimit'}; | |
} elsif (defined $args{'target-bwlimit'}) { | |
$bwlimit = $args{'target-bwlimit'}; | |
} | |
if ($avail{'sourcembuffer'}) { syncCommand .= " $mbuffercmd $bwlimit $mbufferoptions |"; } | |
if ($avail{'localpv'} && !$quiet) { syncCommand .= " $pvcmd -s pvSize |"; } | |
syncCommand .= " receiveCommand"; | |
} elsif ($sourcehost eq '') { | |
# local source, remote target. | |
#syncCommand = "sendCommand | $pvcmd | $args{'compresscmd'} | $mbuffercmd | $sshcmd $targethost '$args{'decompresscmd'} | $mbuffercmd | receiveCommand'"; | |
syncCommand = "sendCommand |"; | |
if ($avail{'localpv'} && !$quiet) { syncCommand .= " $pvcmd -s pvSize |"; } | |
if ($avail{'compress'}) { syncCommand .= " $args{'compresscmd'} |"; } | |
if ($avail{'sourcembuffer'}) { syncCommand .= " $mbuffercmd $args{'source-bwlimit'} $mbufferoptions |"; } | |
syncCommand .= " $sshcmd $targethost '"; | |
if ($avail{'targetmbuffer'}) { syncCommand .= " $mbuffercmd $args{'target-bwlimit'} $mbufferoptions |"; } | |
if ($avail{'compress'}) { syncCommand .= " $args{'decompresscmd'} |"; } | |
syncCommand .= " receiveCommand'"; | |
} elsif ($targethost eq '') { | |
# remote source, local target. | |
#syncCommand = "$sshcmd $sourcehost 'sendCommand | $args{'compresscmd'} | $mbuffercmd' | $args{'decompresscmd'} | $mbuffercmd | $pvcmd | receiveCommand"; | |
syncCommand = "$sshcmd $sourcehost 'sendCommand"; | |
if ($avail{'compress'}) { syncCommand .= " | $args{'compresscmd'}"; } | |
if ($avail{'sourcembuffer'}) { syncCommand .= " | $mbuffercmd $args{'source-bwlimit'} $mbufferoptions"; } | |
syncCommand .= "' | "; | |
if ($avail{'targetmbuffer'}) { syncCommand .= "$mbuffercmd $args{'target-bwlimit'} $mbufferoptions | "; } | |
if ($avail{'compress'}) { syncCommand .= "$args{'decompresscmd'} | "; } | |
if ($avail{'localpv'} && !$quiet) { syncCommand .= "$pvcmd -s pvSize | "; } | |
syncCommand .= "receiveCommand"; | |
} else { | |
#remote source, remote target... weird, but whatever, I'm not here to judge you. | |
#syncCommand = "$sshcmd $sourcehost 'sendCommand | $args{'compresscmd'} | $mbuffercmd' | $args{'decompresscmd'} | $pvcmd | $args{'compresscmd'} | $mbuffercmd | $sshcmd $targethost '$args{'decompresscmd'} | $mbuffercmd | receiveCommand'"; | |
syncCommand = "$sshcmd $sourcehost 'sendCommand"; | |
if ($avail{'compress'}) { syncCommand .= " | $args{'compresscmd'}"; } | |
if ($avail{'sourcembuffer'}) { syncCommand .= " | $mbuffercmd $args{'source-bwlimit'} $mbufferoptions"; } | |
syncCommand .= "' | "; | |
if ($avail{'compress'}) { syncCommand .= "$args{'decompresscmd'} | "; } | |
if ($avail{'localpv'} && !$quiet) { syncCommand .= "$pvcmd -s pvSize | "; } | |
if ($avail{'compress'}) { syncCommand .= "$args{'compresscmd'} | "; } | |
if ($avail{'localmbuffer'}) { syncCommand .= "$mbuffercmd $mbufferoptions | "; } | |
syncCommand .= "$sshcmd $targethost '"; | |
if ($avail{'targetmbuffer'}) { syncCommand .= "$mbuffercmd $args{'target-bwlimit'} $mbufferoptions | "; } | |
if ($avail{'compress'}) { syncCommand .= "$args{'decompresscmd'} | "; } | |
syncCommand .= "receiveCommand'"; | |
} | |
return syncCommand; | |
} | |
auto pruneOldSyncSnaps() | |
{ | |
my ($rhost,$fs,$newsyncsnap,$isroot,@snaps) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
my $hostid = hostname(); | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd=''; } else { $mysudocmd = $sudocmd; } | |
my @prunesnaps; | |
# only prune snaps beginning with syncoid and our own hostname | |
foreach my $snap(@snaps) { | |
if ($snap =~ /^syncoid_$hostid/) { | |
# no matter what, we categorically refuse to | |
# prune the new sync snap we created for this run | |
if ($snap ne $newsyncsnap) { | |
push (@prunesnaps,$snap); | |
} | |
} | |
} | |
# concatenate pruning commands to ten per line, to cut down | |
# auth times for any remote hosts that must be operated via SSH | |
my $counter; | |
my $maxsnapspercmd = 10; | |
my $prunecmd; | |
foreach my $snap(@prunesnaps) { | |
$counter ++; | |
$prunecmd .= "$mysudocmd $zfscmd destroy $fs\@$snap; "; | |
if ($counter > $maxsnapspercmd) { | |
$prunecmd =~ s/\; $//; | |
if ($rhost ne '') { $prunecmd = '"' . $prunecmd . '"'; } | |
infof("DEBUG: pruning up to $maxsnapspercmd obsolete sync snapshots...\n"); | |
infof("DEBUG: $rhost $prunecmd\n"); | |
system("$rhost $prunecmd") == 0 | |
or warn "CRITICAL ERROR: $rhost $prunecmd failed: $?"; | |
$prunecmd = ''; | |
$counter = 0; | |
} | |
} | |
# if we still have some prune commands stacked up after finishing | |
# the loop, commit 'em now | |
if ($counter) { | |
$prunecmd =~ s/\; $//; | |
if ($rhost ne '') { $prunecmd = '"' . $prunecmd . '"'; } | |
infof("DEBUG: pruning up to $maxsnapspercmd obsolete sync snapshots...\n"); | |
infof("DEBUG: $rhost $prunecmd\n"); | |
system("$rhost $prunecmd") == 0 | |
or warn "WARNING: $rhost $prunecmd failed: $?"; | |
} | |
return; | |
} | |
auto getMatchingSnapshot() | |
{ | |
my ($sourcefs, $targetfs, targetSize, $snaps) = @_; | |
foreach my $snap ( sort { $snaps{'source'}{$b}{'creation'}<=>$snaps{'source'}{$a}{'creation'} } keys %{ $snaps{'source'} }) { | |
if (defined $snaps{'target'}{$snap}{'guid'}) { | |
if ($snaps{'source'}{$snap}{'guid'} == $snaps{'target'}{$snap}{'guid'}) { | |
return $snap; | |
} | |
} | |
} | |
// if we got this far, we failed to find a matching snapshot. | |
writef("\n"); | |
writef("CRITICAL ERROR: Target $targetfs exists but has no snapshots matching with $sourcefs!\n"); | |
writef(" Replication to target would require destroying existing\n"); | |
writef(" target. Cowardly refusing to destroy your existing target.\n\n"); | |
# 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) ) { | |
writef(" NOTE: Target $targetfs dataset is < 64MB used - did you mistakenly run\n"); | |
writef(" \`zfs create $args{'target'}\` on the target? ZFS initial\n"); | |
writef(" replication must be to a NON EXISTENT DATASET, which will\n"); | |
writef(" then be CREATED BY the initial replication process.\n\n"); | |
} | |
return 0; | |
} | |
auto newSyncSnap() | |
{ | |
my ($rhost,$fs,$isroot) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
my $hostid = hostname(); | |
my %date = getdate(); | |
my $snapname = "syncoid\_$hostid\_$date{'stamp'}"; | |
my $snapcmd = "$rhost $mysudocmd $zfscmd snapshot $fs\@$snapname\n"; | |
system($snapcmd) == 0 | |
or die "CRITICAL ERROR: $snapcmd failed: $?"; | |
return $snapname; | |
} | |
auto targetExists() | |
{ | |
my ($rhost,$fs,$isroot) = @_; | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
my $checktargetcmd = "$rhost $mysudocmd $zfscmd get -H name $fs"; | |
infof("DEBUG: checking to see if target filesystem exists using \"$checktargetcmd 2>&1 |\"...\n"); | |
open FH, "$checktargetcmd 2>&1 |"; | |
my $targetexists = <FH>; | |
close FH; | |
my $exit = $?; | |
$targetexists = ( $targetexists =~ /^$fs/ && $exit == 0 ); | |
return $targetexists; | |
} | |
auto getSSH() | |
{ | |
my $fs = shift; | |
my $rhost; | |
my $isroot; | |
my $socket; | |
# if we got passed something with an @ in it, we assume it's an ssh connection, eg root@myotherbox | |
if ($fs =~ /\@/) { | |
$rhost = $fs; | |
$fs =~ s/^\S*\@\S*://; | |
$rhost =~ s/:$fs$//; | |
my $remoteuser = $rhost; | |
$remoteuser =~ s/\@.*$//; | |
if ($remoteuser eq 'root') { $isroot = 1; } else { $isroot = 0; } | |
# now we need to establish a persistent master SSH connection | |
$socket = "/tmp/syncoid-$remoteuser-$rhost-" . time(); | |
open FH, "$sshcmd -M -S $socket -o ControlPersist=1m $sshport $rhost exit |"; | |
close FH; | |
$rhost = "-S $socket $rhost"; | |
} else { | |
my $localuid = $<; | |
if ($localuid == 0) { $isroot = 1; } else { $isroot = 0; } | |
} | |
# if ($isroot) { writef("this user is root.\n"); else { writef("this user is not root.\n"); | |
return ($rhost,$fs,$isroot); | |
} | |
auto dumpHash() | |
{ | |
my $hash = shift; | |
$Data::Dumper::Sortkeys = 1; | |
writef(Dumper($hash); | |
} | |
auto getSnaps() { | |
my ($type,$rhost,$fs,$isroot,%snaps) = @_; | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
if ($rhost ne '') { $rhost = "$sshcmd $rhost"; } | |
my $getsnapcmd = "$rhost $mysudocmd $zfscmd get -Hpd 1 -t snapshot guid,creation $fs |"; | |
infof("DEBUG: getting list of snapshots on $fs using $getsnapcmd...\n"); | |
open FH, $getsnapcmd; | |
my @rawsnaps = <FH>; | |
close FH; | |
# this is a little obnoxious. get guid,creation returns guid,creation on two separate lines | |
# as though each were an entirely separate get command. | |
foreach my $line (@rawsnaps) { | |
# only import snap guids from the specified filesystem | |
if ($line =~ /$fs\@.*guid/) { | |
chomp $line; | |
my $guid = $line; | |
$guid =~ s/^.*\sguid\s*(\d*).*/$1/; | |
my $snap = $line; | |
$snap =~ s/^\S*\@(\S*)\s*guid.*$/$1/; | |
$snaps{$type}{$snap}{'guid'}=$guid; | |
} | |
} | |
foreach my $line (@rawsnaps) { | |
# only import snap creations from the specified filesystem | |
if ($line =~ /$fs\@.*creation/) { | |
chomp $line; | |
my $creation = $line; | |
$creation =~ s/^.*\screation\s*(\d*).*/$1/; | |
my $snap = $line; | |
$snap =~ s/^\S*\@(\S*)\s*creation.*$/$1/; | |
$snaps{$type}{$snap}{'creation'}=$creation; | |
} | |
} | |
return %snaps; | |
} | |
auto getSendSize() | |
{ | |
my ($sourcehost,$snap1,$snap2,$isroot) = @_; | |
my $mysudocmd; | |
if ($isroot) { $mysudocmd = ''; } else { $mysudocmd = $sudocmd; } | |
my $snaps; | |
if ($snap2) { | |
# if we got a $snap2 argument, we want an incremental send estimate from $snap1 to $snap2. | |
$snaps = "$args{'streamarg'} $snap1 $snap2"; | |
} else { | |
# if we didn't get a $snap2 arg, we want a full send estimate for $snap1. | |
$snaps = "$snap1"; | |
} | |
my $sourcessh; | |
if ($sourcehost ne '') { $sourcessh = "$sshcmd $sourcehost"; } else { $sourcessh = ''; } | |
my $getsendsizecmd = "$sourcessh $mysudocmd $zfscmd send -nP $snaps"; | |
infof("DEBUG: getting estimated transfer size from source $sourcehost using \"$getsendsizecmd 2>&1 |\"...\n"); | |
open FH, "$getsendsizecmd 2>&1 |"; | |
my @rawsize = <FH>; | |
close FH; | |
my $exit = $?; | |
// process sendsize: last line of multi-line output is size of proposed xfer in bytes, but we need to remove human-readable crap from it | |
my $sendsize = pop(@rawsize); | |
$sendsize =~ s/^size\s*//; | |
chomp $sendsize; | |
// o avoid confusion with a zero size pv, give sendsize a minimum 4K value - or if empty, make sure it reads UNKNOWN | |
infof("DEBUG: sendsize = $sendsize\n"); | |
if ($sendsize eq '' || $exit != 0) { | |
$sendsize = '0'; | |
} else if ($sendsize < 4096) { | |
$sendsize = 4096; | |
} | |
return $sendsize; | |
} | |
auto getDate() | |
{ | |
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); | |
$year += 1900; | |
my %date; | |
$date{'unix'} = (((((((($year - 1971) * 365) + $yday) * 24) + $hour) * 60) + $min) * 60) + $sec; | |
$date{'year'} = $year; | |
$date{'sec'} = sprintf ("%02u", $sec); | |
$date{'min'} = sprintf ("%02u", $min); | |
$date{'hour'} = sprintf ("%02u", $hour); | |
$date{'mday'} = sprintf ("%02u", $mday); | |
$date{'mon'} = sprintf ("%02u", ($mon + 1)); | |
$date{'stamp'} = "$date{'year'}-$date{'mon'}-$date{'mday'}:$date{'hour'}:$date{'min'}:$date{'sec'}"; | |
return %date; | |
} | |
/** | |
this software is licensed for use under the Free Software Foundation's GPL v3.0 license, as retrieved | |
from http://www.gnu.org/licenses/gpl-3.0.html on 2014-11-17. A copy should also be available in this | |
project's Git repository at https://github.com/jimsalterjrs/sanoid/blob/master/LICENSE. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment