btrbk: use arrays as arguments for run_cmd(), making it compatible with the adaptions in the open3 branch

pull/44/head
Axel Burri 2015-08-07 15:31:05 +02:00
parent a802674d11
commit fd94bc25fc
1 changed files with 131 additions and 71 deletions

202
btrbk
View File

@ -47,7 +47,7 @@ use Date::Calc qw(Today Delta_Days Day_of_Week);
use Getopt::Std; use Getopt::Std;
use Data::Dumper; use Data::Dumper;
our $VERSION = "0.19.3"; our $VERSION = "0.20.0-dev";
our $AUTHOR = 'Axel Burri <axel@tty0.ch>'; our $AUTHOR = 'Axel Burri <axel@tty0.ch>';
our $PROJECT_HOME = '<http://www.digint.ch/btrbk/>'; our $PROJECT_HOME = '<http://www.digint.ch/btrbk/>';
@ -156,47 +156,57 @@ sub WARN { my $t = shift; print STDERR "WARNING: $t\n" if($loglevel >= 1); }
sub ERROR { my $t = shift; print STDERR "ERROR: $t\n"; } sub ERROR { my $t = shift; print STDERR "ERROR: $t\n"; }
sub run_cmd($;@) sub run_cmd(@)
{ {
my $cmd = shift || die; my @commands = (ref($_[0]) eq "HASH") ? @_ : { @_ };
my %opts = @_;
my $ret = "";
$cmd =~ s/^\s+//;
$cmd =~ s/\s+$//;
$cmd .= ' 2>&1' if($opts{catch_stderr});
$err = ""; $err = "";
if($opts{non_destructive} || (not $dryrun)) {
DEBUG "### $cmd";
$ret = `$cmd`;
chomp($ret);
TRACE "Command output:\n$ret";
if($?) {
my $exitcode= $? >> 8;
my $signal = $? & 127;
DEBUG "Command execution failed (exitcode=$exitcode" . ($signal ? ", signal=$signal" : "") . "): \"$cmd\"";
if($opts{catch_stderr}) { my $cmd = "";
if($ret =~ /ssh command rejected/) { my $name = "";
# catch errors from ssh_filter_btrbk.sh my $destructive = 0;
$err = "ssh command rejected (please fix ssh_filter_btrbk.sh)"; my $pipe = "";
} my $catch_stderr = 0;
elsif($ret =~ /^ERROR: (.*)/) { my $filter_stderr = undef;
# catch errors from btrfs command foreach (@commands) {
$err = $1; $_->{rsh} //= [];
} $_->{cmd} = [ @{$_->{rsh}}, @{$_->{cmd}} ];
else { $_->{cmd_text} = join(' ', map { s/\n/\\n/g; "'$_'" } @{$_->{cmd}}); # ugly escape of \n, do we need to escape others?
DEBUG "Unparseable error: $ret"; $name = $_->{name} // $_->{cmd_text};
$err = "unparseable error"; $_->{_buf} = '';
} $cmd .= $pipe . $_->{cmd_text};
} $pipe = ' | ';
return undef; if($_->{catch_stderr}) {
$cmd .= ' 2>&1';
$catch_stderr = 1;
$filter_stderr = $_->{filter_stderr};
} }
else { $destructive = 1 unless($_->{non_destructive});
DEBUG "Command execution successful"; }
if($dryrun && $destructive) {
DEBUG "### (dryrun) $cmd";
return "";
}
DEBUG "### $cmd";
my $ret = "";
$ret = `$cmd`;
chomp($ret);
TRACE "Command output:\n$ret";
if($?) {
my $exitcode= $? >> 8;
my $signal = $? & 127;
DEBUG "Command execution failed (exitcode=$exitcode" . ($signal ? ", signal=$signal" : "") . "): \"$cmd\"";
if($catch_stderr) {
$_ = $ret;
&{$filter_stderr} ($cmd) if($filter_stderr);
ERROR "[$cmd] $_" if($_);
} }
return undef;
} }
else { else {
DEBUG "### (dryrun) $cmd"; DEBUG "Command execution successful";
} }
return $ret; return $ret;
} }
@ -218,9 +228,9 @@ sub vinfo($$)
my ($host, $path) = ($1, $2); my ($host, $path) = ($1, $2);
my $ssh_user = config_key($config, "ssh_user"); my $ssh_user = config_key($config, "ssh_user");
my $ssh_identity = config_key($config, "ssh_identity"); my $ssh_identity = config_key($config, "ssh_identity");
my $ssh_options = ""; my @ssh_options;
if($ssh_identity) { if($ssh_identity) {
$ssh_options .= "-i $ssh_identity "; @ssh_options = ('-i', $ssh_identity);
} }
else { else {
WARN "No SSH identity provided (option ssh_identity is not set) for: $url"; WARN "No SSH identity provided (option ssh_identity is not set) for: $url";
@ -233,7 +243,7 @@ sub vinfo($$)
RSH_TYPE => "ssh", RSH_TYPE => "ssh",
SSH_USER => $ssh_user, SSH_USER => $ssh_user,
SSH_IDENTITY => $ssh_identity, SSH_IDENTITY => $ssh_identity,
RSH => "/usr/bin/ssh $ssh_options" . $ssh_user . '@' . $host, RSH => ['/usr/bin/ssh', @ssh_options, $ssh_user . '@' . $host ],
); );
} }
elsif(($url =~ /^\//) && ($url =~ /^$file_match$/)) { elsif(($url =~ /^\//) && ($url =~ /^$file_match$/)) {
@ -586,7 +596,9 @@ sub parse_config(@)
sub btrfs_filesystem_show_all_local() sub btrfs_filesystem_show_all_local()
{ {
return run_cmd("btrfs filesystem show", non_destructive => 1); return run_cmd( cmd => [ qw(btrfs filesystem show) ],
non_destructive => 1
);
} }
@ -594,8 +606,10 @@ sub btrfs_filesystem_show($)
{ {
my $vol = shift || die; my $vol = shift || die;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || ""; return run_cmd( cmd => [ qw(btrfs filesystem show), $path ],
return run_cmd("$rsh btrfs filesystem show '$path'", non_destructive => 1); rsh => $vol->{RSH},
non_destructive => 1
);
} }
@ -603,8 +617,10 @@ sub btrfs_filesystem_df($)
{ {
my $vol = shift || die; my $vol = shift || die;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || ""; return run_cmd( cmd => [qw(btrfs filesystem df), $path],
return run_cmd("$rsh btrfs filesystem df '$path'", non_destructive => 1); rsh => $vol->{RSH},
non_destructive => 1
);
} }
@ -612,8 +628,10 @@ sub btrfs_filesystem_usage($)
{ {
my $vol = shift || die; my $vol = shift || die;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || ""; return run_cmd( cmd => [ qw(btrfs filesystem usage), $path ],
return run_cmd("$rsh btrfs filesystem usage '$path'", non_destructive => 1); rsh => $vol->{RSH},
non_destructive => 1
);
} }
@ -621,8 +639,28 @@ sub btrfs_subvolume_detail($)
{ {
my $vol = shift || die; my $vol = shift || die;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || ""; my $ret = run_cmd(cmd => [ qw(btrfs subvolume show), $path],
my $ret = run_cmd("$rsh btrfs subvolume show '$path'", non_destructive => 1, catch_stderr => 1); rsh => $vol->{RSH},
non_destructive => 1,
catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub {
if(/ssh command rejected/) {
# catch errors from ssh_filter_btrbk.sh
$err = "ssh command rejected (please fix ssh_filter_btrbk.sh)";
}
elsif(/^ERROR: (.*)/) {
# catch errors from btrfs command
$err = $1;
}
else {
DEBUG "Unparsed error: $_";
$err = $_;
}
# consume stderr line, as $err will be displayed as a user-friendly WARNING
$_ = undef;
}
);
return undef unless(defined($ret)); return undef unless(defined($ret));
# workaround for btrfs-progs < 3.17.3 (returns exit status 0 on errors) # workaround for btrfs-progs < 3.17.3 (returns exit status 0 on errors)
@ -643,7 +681,7 @@ sub btrfs_subvolume_detail($)
} }
my %detail = ( REAL_PATH => $real_path ); my %detail = ( REAL_PATH => $real_path );
if($ret eq "$real_path is btrfs root") { if($ret =~ /^\Q$real_path\E is btrfs root/) {
DEBUG "found btrfs root: $vol->{PRINT}"; DEBUG "found btrfs root: $vol->{PRINT}";
$detail{id} = 5; $detail{id} = 5;
$detail{is_root} = 1; $detail{is_root} = 1;
@ -690,13 +728,15 @@ sub btrfs_subvolume_list($;@)
my $vol = shift || die; my $vol = shift || die;
my %opts = @_; my %opts = @_;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || "";
my $btrfs_progs_compat = $vol->{BTRFS_PROGS_COMPAT} || $opts{btrfs_progs_compat}; my $btrfs_progs_compat = $vol->{BTRFS_PROGS_COMPAT} || $opts{btrfs_progs_compat};
my $filter_option = "-a"; my @filter_options = ('-a');
$filter_option = "-o" if($opts{subvol_only}); push(@filter_options, '-o') if($opts{subvol_only});
my $display_options = "-c -u -q"; my @display_options = ('-c', '-u', '-q');
$display_options .= " -R" unless($btrfs_progs_compat); push(@display_options, '-R') unless($btrfs_progs_compat);
my $ret = run_cmd("$rsh btrfs subvolume list $filter_option $display_options '$path'", non_destructive => 1); my $ret = run_cmd(cmd => [ qw(btrfs subvolume list), @filter_options, @display_options, $path ],
rsh => $vol->{RSH},
non_destructive => 1,
);
return undef unless(defined($ret)); return undef unless(defined($ret));
my @nodes; my @nodes;
@ -754,9 +794,11 @@ sub btrfs_subvolume_find_new($$;$)
{ {
my $vol = shift || die; my $vol = shift || die;
my $path = $vol->{PATH} // die; my $path = $vol->{PATH} // die;
my $rsh = $vol->{RSH} || "";
my $lastgen = shift // die; my $lastgen = shift // die;
my $ret = run_cmd("$rsh btrfs subvolume find-new '$path' $lastgen", non_destructive => 1); my $ret = run_cmd(cmd => [ qw(btrfs subvolume find-new), $path, $lastgen ],
rsh => $vol->{RSH},
non_destructive => 1,
);
unless(defined($ret)) { unless(defined($ret)) {
ERROR "Failed to fetch modified files for: $vol->{PRINT}"; ERROR "Failed to fetch modified files for: $vol->{PRINT}";
return undef; return undef;
@ -813,13 +855,14 @@ sub btrfs_subvolume_snapshot($$)
my $svol = shift || die; my $svol = shift || die;
my $target_path = shift // die; my $target_path = shift // die;
my $src_path = $svol->{PATH} // die; my $src_path = $svol->{PATH} // die;
my $rsh = $svol->{RSH} || "";
DEBUG "[btrfs] snapshot (ro):"; DEBUG "[btrfs] snapshot (ro):";
DEBUG "[btrfs] host : $svol->{HOST}" if($svol->{HOST}); DEBUG "[btrfs] host : $svol->{HOST}" if($svol->{HOST});
DEBUG "[btrfs] source: $src_path"; DEBUG "[btrfs] source: $src_path";
DEBUG "[btrfs] target: $target_path"; DEBUG "[btrfs] target: $target_path";
INFO ">>> " . ($svol->{HOST} ? "{$svol->{HOST}}" : "") . $target_path; INFO ">>> " . ($svol->{HOST} ? "{$svol->{HOST}}" : "") . $target_path;
my $ret = run_cmd("$rsh btrfs subvolume snapshot -r '$src_path' '$target_path'"); my $ret = run_cmd(cmd => [ qw(btrfs subvolume snapshot), '-r', $src_path, $target_path ],
rsh => $svol->{RSH},
);
ERROR "Failed to create btrfs subvolume snapshot: $svol->{PRINT} -> $target_path" unless(defined($ret)); ERROR "Failed to create btrfs subvolume snapshot: $svol->{PRINT} -> $target_path" unless(defined($ret));
return defined($ret) ? $target_path : undef; return defined($ret) ? $target_path : undef;
} }
@ -833,17 +876,21 @@ sub btrfs_subvolume_delete($@)
die if($commit && ($commit ne "after") && ($commit ne "each")); die if($commit && ($commit ne "after") && ($commit ne "each"));
$targets = [ $targets ] unless(ref($targets) eq "ARRAY"); $targets = [ $targets ] unless(ref($targets) eq "ARRAY");
return 0 unless(scalar(@$targets)); return 0 unless(scalar(@$targets));
my $rsh = $targets->[0]->{RSH} || ""; my $rsh = $targets->[0]->{RSH};
my $rsh_host_check = $targets->[0]->{HOST} || "";
foreach (@$targets) { foreach (@$targets) {
# make sure all targets share same RSH # make sure all targets share same HOST
my $rsh_check = $_->{RSH} || ""; my $host = $_->{HOST} || "";
die if($rsh ne $rsh_check); die if($rsh_host_check ne $host);
} }
DEBUG "[btrfs] delete" . ($commit ? " (commit-$commit):" : ":"); DEBUG "[btrfs] delete" . ($commit ? " (commit-$commit):" : ":");
DEBUG "[btrfs] subvolume: $_->{PRINT}" foreach(@$targets); DEBUG "[btrfs] subvolume: $_->{PRINT}" foreach(@$targets);
my $options = ""; my @options;
$options = "--commit-$commit " if($commit); @options = ("--commit-$commit") if($commit);
my $ret = run_cmd("$rsh btrfs subvolume delete $options" . join(' ', map( { "'$_->{PATH}'" } @$targets))); my @target_paths = map( { $_->{PATH} } @$targets);
my $ret = run_cmd(cmd => [ qw(btrfs subvolume delete), @options, @target_paths ],
rsh => $rsh,
);
ERROR "Failed to delete btrfs subvolumes: " . join(' ', map( { $_->{PRINT} } @$targets)) unless(defined($ret)); ERROR "Failed to delete btrfs subvolumes: " . join(' ', map( { $_->{PRINT} } @$targets)) unless(defined($ret));
return defined($ret) ? scalar(@$targets) : undef; return defined($ret) ? scalar(@$targets) : undef;
} }
@ -855,9 +902,9 @@ sub btrfs_send_receive($$$)
my $target = shift || die; my $target = shift || die;
my $parent = shift; my $parent = shift;
my $snapshot_path = $snapshot->{PATH} // die; my $snapshot_path = $snapshot->{PATH} // die;
my $snapshot_rsh = $snapshot->{RSH} || ""; my $snapshot_rsh = $snapshot->{RSH};
my $target_path = $target->{PATH} // die; my $target_path = $target->{PATH} // die;
my $target_rsh = $target->{RSH} || ""; my $target_rsh = $target->{RSH};
my $parent_path = $parent ? $parent->{PATH} : undef; my $parent_path = $parent ? $parent->{PATH} : undef;
my $snapshot_name = $snapshot_path; my $snapshot_name = $snapshot_path;
@ -869,11 +916,24 @@ sub btrfs_send_receive($$$)
DEBUG "[btrfs] parent: $parent->{PRINT}" if($parent); DEBUG "[btrfs] parent: $parent->{PRINT}" if($parent);
DEBUG "[btrfs] target: $target->{PRINT}"; DEBUG "[btrfs] target: $target->{PRINT}";
my $parent_option = $parent_path ? "-p '$parent_path'" : ""; my @send_options;
my $receive_option = ""; my @receive_options;
$receive_option = "-v" if($loglevel >= 3); push(@send_options, '-p', $parent_path) if($parent_path);
push(@send_options, '-v') if($loglevel >= 3);
push(@receive_options, '-v') if($loglevel >= 3);
my $ret = run_cmd("$snapshot_rsh btrfs send $parent_option '$snapshot_path' | $target_rsh btrfs receive $receive_option '$target_path/'"); my $ret = run_cmd(
{
cmd => [ qw(btrfs send), @send_options, $snapshot_path ],
rsh => $snapshot_rsh,
name => "btrfs send",
},
{
cmd => [ qw(btrfs receive), @receive_options, $target_path . '/' ],
rsh => $target_rsh,
name => "btrfs receive",
},
);
unless(defined($ret)) { unless(defined($ret)) {
ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}"; ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}";
return undef; return undef;