From 512aca533267799cbd0dfe6bdf4637c1524926f3 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 27 Sep 2017 20:23:08 +0200 Subject: [PATCH] btrbk: parse output of "btrfs subvolume delete" When doing a batch delete (multiple deletes with one call to "btrfs subvolume delete"), we want to know which subvolumes have failed. For this, we need parse the error output. On any parsing failure, we assume that nothing has been deleted, and warn accordingly (forward compatibility). --- btrbk | 123 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 22 deletions(-) diff --git a/btrbk b/btrbk index af9407b..2e445c0 100755 --- a/btrbk +++ b/btrbk @@ -1152,7 +1152,7 @@ sub btrfs_subvolume_delete($@) my $commit = $opts{commit}; die if($commit && ($commit ne "after") && ($commit ne "each")); $targets = [ $targets ] unless(ref($targets) eq "ARRAY"); - return 0 unless(scalar(@$targets)); + return () unless(scalar(@$targets)); # NOTE: rsh and backend command is taken from first target my $rsh_host_check = $targets->[0]->{HOST} || ""; @@ -1167,9 +1167,13 @@ sub btrfs_subvolume_delete($@) INFO "[delete] options: commit-$commit" if($commit && (not $target_type)); INFO "[delete] target: $_->{PRINT}" foreach(@$targets); start_transaction($opts{type} // "delete", + # NOTE: "target_url" from vinfo_prefixed_keys() is used for matching in end_transaction() below map( { { vinfo_prefixed_keys("target", $_) }; } @$targets) ); my $ret; + my @deleted; + my @unparsed_errors; + my %err_catch; if($target_type eq "raw") { my @cmd_target_paths; foreach(@$targets) { @@ -1183,8 +1187,26 @@ sub btrfs_subvolume_delete($@) push @cmd_target_paths, { unsafe => "$_->{PATH}.info" }; } } - $ret = run_cmd(cmd => ['rm', @cmd_target_paths ], + $ret = run_cmd(cmd => ['rm', '-f', @cmd_target_paths ], rsh => vinfo_rsh($targets->[0]), + catch_stderr => 1, # hack for shell-based run_cmd() + filter_stderr => sub { + # catch errors from "rm -f" + my @error_lines = split("\n", $_); + foreach (@error_lines) { + if(/^rm: cannot remove '($file_match)':/) { + my $catch = $1; # make sure $catch matches $vol->{PATH} + $catch =~ s/\.info$//; + $catch =~ s/\.split_[a-z][a-z]$//; + $err_catch{$catch} //= []; + push(@{$err_catch{$catch}}, $_); + } + else { + push @unparsed_errors, $_; + } + } + $_ = undef; # prevent "Command execution failed" error message + } ); } else { @@ -1193,11 +1215,66 @@ sub btrfs_subvolume_delete($@) @options = ("--commit-$commit") if($commit); $ret = run_cmd(cmd => vinfo_cmd($targets->[0], "btrfs subvolume delete", @options, @cmd_target_paths ), rsh => vinfo_rsh($targets->[0]), + catch_stderr => 1, # hack for shell-based run_cmd() + filter_stderr => sub { + # catch errors from btrfs command + my @error_lines = split("\n", $_); + foreach (@error_lines) { + next if(/^Delete subvolume/); # NOTE: stdout is also reflected here! + if(/^ERROR: cannot access subvolume ($file_match):/ || + /^ERROR: not a subvolume: ($file_match)/ || + /^ERROR: cannot find real path for '($file_match)':/ || + /^ERROR: cannot delete '($file_match)'/ || + /^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 + } ); } - end_transaction($opts{type} // "delete", defined($ret)); - ERROR "Failed to delete btrfs subvolumes: " . join(' ', map( { $_->{PRINT} } @$targets)) unless(defined($ret)); - return defined($ret) ? scalar(@$targets) : undef; + + if(defined($ret)) { + @deleted = @$targets; + } + else { + if(%err_catch) { + my $catch_count = 0; + foreach my $check_target (@$targets) { + my $err_ary = $err_catch{$check_target->{PATH}}; + if($err_ary) { + ERROR "Failed to delete subvolume \"$check_target->{PRINT}\": $_" foreach(@$err_ary); + $catch_count++; + } + else { + push @deleted, $check_target; + } + } + 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) { + @deleted = (); + ERROR "Failed to parse error messages, assuming nothing deleted"; + ERROR "[delete]: $_" foreach(@unparsed_errors); + ERROR "Failed to delete subvolume: $_" foreach(map( { $_->{PRINT} } @$targets)); + } + } + + end_transaction($opts{type} // "delete", sub { my $h = shift; return (grep { $_->{URL} eq $h->{target_url} } @deleted); }); + return @deleted; } @@ -1329,8 +1406,8 @@ sub btrfs_send_receive($$$$) # we need to do this by hand. # TODO: remove this as soon as btrfs-progs handle receive errors correctly. DEBUG "send/received failed, deleting (possibly present and garbled) received subvolume: $vol_received->{PRINT}"; - my $ret = btrfs_subvolume_delete($vol_received, commit => "after", type => "delete_garbled"); - if(defined($ret)) { + my @deleted = btrfs_subvolume_delete($vol_received, commit => "after", type => "delete_garbled"); + if(scalar(@deleted)) { WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}"; } else { @@ -3428,13 +3505,15 @@ sub macro_delete($$$$$;@) schedule => \@schedule, preserve_date_in_future => 1, ); - my $ret = btrfs_subvolume_delete($delete, %delete_options); - if(defined($ret)) { - $subvol_dir .= '/' if($subvol_dir ne ""); - INFO "Deleted $ret subvolumes in: $root_subvol->{PRINT}/$subvol_dir$subvol_basename.*"; - $result_vinfo->{SUBVOL_DELETED} //= []; - push @{$result_vinfo->{SUBVOL_DELETED}}, @$delete; - return $delete; + + my @delete_success = btrfs_subvolume_delete($delete, %delete_options); + $subvol_dir .= '/' if($subvol_dir ne ""); + INFO "Deleted " . scalar(@delete_success) . " subvolumes in: $root_subvol->{PRINT}/$subvol_dir$subvol_basename.*"; + $result_vinfo->{SUBVOL_DELETED} //= []; + push @{$result_vinfo->{SUBVOL_DELETED}}, @delete_success; + + if(scalar(@delete_success) == scalar(@$delete)) { + return 1; } else { ABORTED($result_vinfo, "Failed to delete subvolume"); @@ -5335,14 +5414,14 @@ MAIN: push(@delete, $target_vol); } } - my $ret = btrfs_subvolume_delete(\@delete, commit => config_key($droot, "btrfs_commit_delete"), type => "delete_garbled"); - if(defined($ret)) { - INFO "Deleted $ret incomplete backups in: $droot->{PRINT}/$snapshot_name.*"; - $droot->{SUBVOL_DELETED} //= []; - push @{$droot->{SUBVOL_DELETED}}, @delete; - push @out, map("--- $_->{PRINT}", @delete); - } - else { + + my @delete_success = btrfs_subvolume_delete(\@delete, commit => config_key($droot, "btrfs_commit_delete"), type => "delete_garbled"); + INFO "Deleted " . scalar(@delete_success) . " incomplete backups in: $droot->{PRINT}/$snapshot_name.*"; + $droot->{SUBVOL_DELETED} //= []; + push @{$droot->{SUBVOL_DELETED}}, @delete_success; + push @out, map("--- $_->{PRINT}", @delete_success); + + if(scalar(@delete_success) != scalar(@delete)) { ABORTED($droot, "Failed to delete incomplete target subvolume"); push @out, "!!! Target \"$droot->{PRINT}\" aborted: $abrt"; }