Skip to content

Instantly share code, notes, and snippets.

@Laeeth
Created October 3, 2017 10:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Laeeth/65ddf31fffc291c0203314c70c7633d6 to your computer and use it in GitHub Desktop.
Save Laeeth/65ddf31fffc291c0203314c70c7633d6 to your computer and use it in GitHub Desktop.
Port of syncoid from perl to dlang
#!/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