btrbk: refactor filter_stderr; more flexible error handling in run_cmd; simplify btrfs_subvolume_delete parsing

pull/299/head
Axel Burri 2019-08-16 01:22:49 +02:00
parent 8434c6881c
commit df471c3692
1 changed files with 84 additions and 85 deletions

169
btrbk
View File

@ -690,6 +690,7 @@ sub run_cmd(@)
my $destructive = 0; my $destructive = 0;
my $catch_stderr = 0; my $catch_stderr = 0;
my $exitcode_loglevel = "debug";
my $filter_stderr = undef; my $filter_stderr = undef;
my @cmd_pipe; my @cmd_pipe;
my @unsafe_cmd; my @unsafe_cmd;
@ -706,6 +707,7 @@ sub run_cmd(@)
$catch_stderr = 1 if($href->{catch_stderr}); $catch_stderr = 1 if($href->{catch_stderr});
$filter_stderr = $href->{filter_stderr} if($href->{filter_stderr}); # NOTE: last filter wins! $filter_stderr = $href->{filter_stderr} if($href->{filter_stderr}); # NOTE: last filter wins!
$destructive = 1 unless($href->{non_destructive}); $destructive = 1 unless($href->{non_destructive});
$exitcode_loglevel = $href->{exitcode_loglevel} if($href->{exitcode_loglevel});
if($href->{check_unsafe}) { if($href->{check_unsafe}) {
_safe_cmd($href->{check_unsafe}, \@unsafe_cmd); _safe_cmd($href->{check_unsafe}, \@unsafe_cmd);
@ -819,12 +821,15 @@ sub run_cmd(@)
no warnings qw(exec); no warnings qw(exec);
# execute command and parse output # execute command and parse output
my @exitcode_msg;
my $stderr_fatal;
my $ret = `$cmd`; my $ret = `$cmd`;
if(defined($ret)) { if(defined($ret)) {
chomp($ret); chomp($ret);
TRACE "Command output:\n$ret" if($loglevel >= 4); TRACE "Command output:\n$ret" if($loglevel >= 4);
} }
# fatal errors
if($? == -1) { if($? == -1) {
ERROR "Command execution failed ($!): `$cmd_print`"; ERROR "Command execution failed ($!): `$cmd_print`";
return undef; return undef;
@ -834,18 +839,29 @@ sub run_cmd(@)
ERROR "Command execution failed (child died with signal $signal): `$cmd_print`"; ERROR "Command execution failed (child died with signal $signal): `$cmd_print`";
return undef; return undef;
} }
elsif($?) { my $exitcode = $? >> 8;
my $exitcode= $? >> 8;
DEBUG "Command execution failed (exitcode=$exitcode): `$cmd_print`";
if($catch_stderr) { # handle filter_stderr option
$_ = $ret; if(defined($ret) && $filter_stderr) {
&{$filter_stderr} ($cmd) if($filter_stderr); foreach(split("\n", $ret)) {
if($_) { next if($_ eq "");
# no filter, or uncaught by filter if(my $href = &{$filter_stderr} ($exitcode)) {
ERROR "Command execution failed (exitcode=$exitcode): `$cmd_print`: $_"; $stderr_fatal //= $href->{fatal};
push @exitcode_msg, $href;
$exitcode_loglevel = "error";
} }
} }
}
if($exitcode || $stderr_fatal) {
my $log_text = "Command execution failed (exitcode=$exitcode): `$cmd_print`";
if($exitcode_loglevel eq "error") { ERROR $log_text; }
elsif($exitcode_loglevel eq "warn") { WARN $log_text; }
else { DEBUG $log_text; }
foreach(@exitcode_msg) {
ERROR "... $_->{error}" if(defined($_->{error}));
WARN "... $_->{warn}" if(defined($_->{warn}));
}
return undef; return undef;
} }
else { else {
@ -958,21 +974,19 @@ sub btrfs_subvolume_show($;@)
non_destructive => 1, non_destructive => 1,
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub { filter_stderr => sub {
if(/ssh command rejected/) { return undef unless($_[0]); # do nothing if exitcode=0
# catch errors from ssh_filter_btrbk.sh if(s/^ERROR: //) { # catch errors from btrfs-progs as well as 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 = $_; $err = $_;
return { error => $_, fatal => 1 } if(/^ssh_filter_btrbk.sh/); # "fatal" causes run_cmd to return undef
} elsif(/(unrecognized|invalid) option/) {
WARN_ONCE "$_ (maybe using unsupported btrfs-progs < v$BTRFS_PROGS_MIN " . ($vol->{HOST} ? "on host=$vol->{HOST} " : "") . "?)";
} else {
DEBUG "Unparsed error: $_";
$err ||= $_;
} }
# consume stderr line, as $err will be displayed as a user-friendly WARNING # soft fail: $err can be displayed as a user-friendly WARNING
$_ = undef; return undef;
} },
); );
return undef unless(defined($ret)); return undef unless(defined($ret));
@ -1300,7 +1314,6 @@ sub btrfs_subvolume_delete($@)
); );
my $ret; my $ret;
my @deleted; my @deleted;
my @unparsed_errors;
my %err_catch; my %err_catch;
if($target_type eq "raw") { if($target_type eq "raw") {
my @cmd_target_paths; my @cmd_target_paths;
@ -1320,57 +1333,42 @@ sub btrfs_subvolume_delete($@)
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub { filter_stderr => sub {
# catch errors from "rm -f" # catch errors from "rm -f"
my @error_lines = split("\n", $_); if(/^rm: cannot remove '($file_match)':/) {
foreach (@error_lines) { my $catch = $1; # make sure $catch matches $vol->{PATH}
if(/^rm: cannot remove '($file_match)':/) { $catch =~ s/\.info$//;
my $catch = $1; # make sure $catch matches $vol->{PATH} $catch =~ s/\.split_[a-z][a-z]$//;
$catch =~ s/\.info$//; $err_catch{$catch} //= [];
$catch =~ s/\.split_[a-z][a-z]$//; push(@{$err_catch{$catch}}, $_);
$err_catch{$catch} //= []; return undef; # no error messages in run_cmd
push(@{$err_catch{$catch}}, $_);
}
else {
push @unparsed_errors, $_;
}
} }
$_ = undef; # prevent "Command execution failed" error message else {
} # show errors in run_cmd; force "Command execution failed" error message
return { error => $_, fatal => 1 };
}
},
); );
} }
else { else {
my @cmd_target_paths = map { { unsafe => $_->{PATH} } } @$targets; my @cmd_target_paths = map { { unsafe => $_->{PATH} } } @$targets;
my $unparsed_errors;
my @options; my @options;
@options = ("--commit-$commit") if($commit); @options = ("--commit-$commit") if($commit);
$ret = run_cmd(cmd => vinfo_cmd($targets->[0], "btrfs subvolume delete", @options, @cmd_target_paths ), $ret = run_cmd(cmd => vinfo_cmd($targets->[0], "btrfs subvolume delete", @options, @cmd_target_paths ),
rsh => vinfo_rsh($targets->[0]), rsh => vinfo_rsh($targets->[0]),
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub { filter_stderr => sub {
# catch errors from btrfs command return undef unless(s/^ERROR: //); # strip ERROR prefix
# NOTE: this is NOT RELIABLE ANY MORE (path is not printed in btrfs-progs >= 4.16), e.g.: if(/'($file_match)'/ || /: ($file_match)$/ || /($file_match):/) {
# "ERROR: Could not statfs: No such file or directory" # NOTE: as of btrfs-progs-4.16, this does not catch anything
my @error_lines = split("\n", $_); $err_catch{$1} //= [];
foreach (@error_lines) { push(@{$err_catch{$1}}, $_);
next if(/^Delete subvolume/); # NOTE: stdout is also reflected here! } else {
if(/^ERROR: cannot access subvolume ($file_match):/ || # btrfs-progs < 4.16 $unparsed_errors = 1;
/^ERROR: not a subvolume: ($file_match)/ || # btrfs-progs < 4.16 }
/^ERROR: cannot find real path for '($file_match)':/ || return undef; # no error messages in run_cmd
/^ERROR: cannot delete '($file_match)'/ || # btrfs-progs < 4.16 },
/^ERROR: cannot access subvolume '($file_match)'$/ || # btrfs-progs < 4.4
/^ERROR: error accessing '($file_match)'/ || # btrfs-progs < 4.4
/^ERROR: '($file_match)' is not a subvolume/ || # btrfs-progs < 4.4
/^ERROR: finding real path for '($file_match)'/ || # btrfs-progs < 4.4
/^ERROR: can't access '($file_match)'/ ) # btrfs-progs < 4.4
{
$err_catch{$1} //= [];
push(@{$err_catch{$1}}, $_);
}
else {
push @unparsed_errors, $_;
}
}
$_ = undef; # prevent "Command execution failed" error message
}
); );
$ret = undef if($unparsed_errors);
} }
if(defined($ret)) { if(defined($ret)) {
@ -1389,17 +1387,12 @@ sub btrfs_subvolume_delete($@)
push @deleted, $check_target; push @deleted, $check_target;
} }
} }
if($catch_count != (scalar keys %err_catch)) { @deleted = () if($catch_count != (scalar keys %err_catch));
@deleted = ();
ERROR "Failed to assign error messages, assuming nothing deleted";
ERROR "Failed to delete subvolume: $_" foreach(map( { $_->{PRINT} } @$targets));
}
} }
if(@unparsed_errors) { unless(scalar(@deleted)) {
@deleted = (); ERROR "Failed to match error messages from delete command, assuming nothing deleted:";
ERROR "Failed to delete subvolume, assuming nothing deleted"; ERROR "... possibly not deleted subvolume: $_" foreach(map( { $_->{PRINT} } @$targets));
ERROR "[delete]: $_" foreach(@unparsed_errors); ERROR "... consider running 'btrbk prune -n'";
ERROR "Failed to delete subvolume: $_" foreach(map( { $_->{PRINT} } @$targets));
} }
} }
@ -1471,6 +1464,7 @@ sub btrfs_send_receive($$;$$$)
rsh => vinfo_rsh($snapshot, disable_compression => $stream_options->{stream_compress}), rsh => vinfo_rsh($snapshot, disable_compression => $stream_options->{stream_compress}),
name => "btrfs send", name => "btrfs send",
stream_options => $stream_options, stream_options => $stream_options,
exitcode_loglevel => "error", # print error message if exitcode != 0
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
}; };
@ -1479,7 +1473,12 @@ sub btrfs_send_receive($$;$$$)
rsh => vinfo_rsh($target, disable_compression => $stream_options->{stream_compress}), rsh => vinfo_rsh($target, disable_compression => $stream_options->{stream_compress}),
name => "btrfs receive", name => "btrfs receive",
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub { $err = $_; $_ = undef } filter_stderr => sub {
# NOTE: btrfs-progs < 4.11: if "btrfs send" fails, "btrfs receive" returns 0!
return { error => $_, fatal => 1 } if(s/^ERROR: //); # "fatal" causes run_cmd to return undef
return { warn => $_ } if(s/^WARNING: //);
return undef;
},
}; };
my $send_receive_error = 0; my $send_receive_error = 0;
@ -1491,17 +1490,6 @@ sub btrfs_send_receive($$;$$$)
my $ret = run_cmd(@cmd_pipe); my $ret = run_cmd(@cmd_pipe);
unless(defined($ret)) { unless(defined($ret)) {
$send_receive_error = 1; $send_receive_error = 1;
$ret = $err; # print the errors below
}
if(defined($ret)) {
# NOTE: btrfs-progs < 4.11: if "btrfs send" fails, "btrfs receive" returns 0! so we need to parse the output for "ERROR:" lines:
my @ret_lines = split("\n", $ret);
$send_receive_error = 1 if(grep /^ERROR: /, @ret_lines);
foreach(@ret_lines) {
if(/^WARNING: / || $send_receive_error) {
WARN "[send/receive] (send=$snapshot_path, receive=$target_path) $_";
}
}
} }
# Read in target subvolume metadata (btrfs subvolume show): # Read in target subvolume metadata (btrfs subvolume show):
@ -1549,6 +1537,7 @@ sub btrfs_send_receive($$;$$$)
$is_garbled = ((not $detail->{readonly}) && defined($detail->{received_uuid}) && ($detail->{received_uuid} eq '-')); $is_garbled = ((not $detail->{readonly}) && defined($detail->{received_uuid}) && ($detail->{received_uuid} eq '-'));
} }
else { else {
$err = "" if($send_receive_error); # ignore $err if send/receive failed
$send_receive_error = 1; $send_receive_error = 1;
} }
} }
@ -1556,7 +1545,8 @@ sub btrfs_send_receive($$;$$$)
end_transaction("send-receive", not $send_receive_error); end_transaction("send-receive", not $send_receive_error);
if($send_receive_error) { if($send_receive_error) {
ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}"; ERROR "Failed to send/receive subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $vol_received->{PRINT}";
ERROR "... $err" if($err);
} }
if($is_garbled) { if($is_garbled) {
@ -1621,6 +1611,13 @@ sub btrfs_send_to_file($$$;$$)
rsh => vinfo_rsh($source, disable_compression => $stream_options->{stream_compress}), rsh => vinfo_rsh($source, disable_compression => $stream_options->{stream_compress}),
name => "btrfs send", name => "btrfs send",
stream_options => $stream_options, stream_options => $stream_options,
exitcode_loglevel => "error", # print error message if exitcode != 0
catch_stderr => 1, # hack for shell-based run_cmd()
filter_stderr => sub {
return { error => $_, fatal => 1 } if(s/^ERROR: //); # "fatal" causes run_cmd to return undef
return { warn => $_ } if(s/^WARNING: //);
return undef;
},
}; };
if($compress) { if($compress) {
@ -1836,6 +1833,7 @@ sub system_list_mountinfo($)
my $ret = run_cmd(cmd => [ qw(cat), $file ], my $ret = run_cmd(cmd => [ qw(cat), $file ],
rsh => vinfo_rsh($vol), rsh => vinfo_rsh($vol),
non_destructive => 1, non_destructive => 1,
exitcode_loglevel => "error", # print error message if exitcode != 0
catch_stderr => 1, # hack for shell-based run_cmd() catch_stderr => 1, # hack for shell-based run_cmd()
); );
return undef unless(defined($ret)); return undef unless(defined($ret));
@ -4091,6 +4089,7 @@ sub macro_send_receive(@)
if($source->{node}{uuid} =~ /^$fake_uuid_prefix/) { if($source->{node}{uuid} =~ /^$fake_uuid_prefix/) {
DEBUG "Fetching uuid of new subvolume: $source->{PRINT}"; DEBUG "Fetching uuid of new subvolume: $source->{PRINT}";
my $detail = btrfs_subvolume_show($source); my $detail = btrfs_subvolume_show($source);
return undef unless($detail);
die unless($detail->{uuid}); die unless($detail->{uuid});
$source->{node}{uuid} = $detail->{uuid}; $source->{node}{uuid} = $detail->{uuid};
$uuid_cache{$detail->{uuid}} = $source->{node}; $uuid_cache{$detail->{uuid}} = $source->{node};