btrbk: never delete multiple subvolumes at once

Deleting multiple subvolumes at once always caused the problem that we
need to parse stderr of "rm" and "btrfs subvolume delete" in order to
know which subvolume actually failed, which is problematic (version
dependent, language dependent). Also, we would need to restrict the
number of subvolumes based on the maximum allowed length for shell
commands, which is system-dependent (check `getconf ARG_MAX`).

Deleting subvolumes sequentially has slightly negative impact on
execution time (multiple rsh commands), with the benefit of being more
robust and reducing the codesize.
disable-ssh-password-prompt
Axel Burri 2022-07-27 20:35:58 +02:00
parent b7df717611
commit 6b465bf06b
1 changed files with 40 additions and 101 deletions

133
btrbk
View File

@ -97,7 +97,6 @@ my %config_options = (
target_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ] }, target_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ] },
archive_preserve => { default => undef, accept => [qw( no )], accept_preserve_matrix => 1, context => [qw( global )] }, archive_preserve => { default => undef, accept => [qw( no )], accept_preserve_matrix => 1, context => [qw( global )] },
archive_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ], context => [qw( global )] }, archive_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ], context => [qw( global )] },
btrfs_commit_delete => { default => undef, accept => [qw( after each no )] },
ssh_identity => { default => undef, accept_file => { absolute => 1 } }, ssh_identity => { default => undef, accept_file => { absolute => 1 } },
ssh_user => { default => "root", accept => [ qr/[a-z_][a-z0-9_-]*/ ] }, ssh_user => { default => "root", accept => [ qr/[a-z_][a-z0-9_-]*/ ] },
ssh_compression => { default => undef, accept => [qw( yes no )] }, ssh_compression => { default => undef, accept => [qw( yes no )] },
@ -145,6 +144,8 @@ my %config_options = (
compat_local => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 }, compat_local => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 },
compat_remote => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 }, compat_remote => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 },
safe_commands => { default => undef, accept => [qw( yes no )], context => [qw( global )] }, safe_commands => { default => undef, accept => [qw( yes no )], context => [qw( global )] },
btrfs_commit_delete => { default => undef, accept => [qw( yes no after each )],
deprecated => { MATCH => { regex => qr/^(?:after|each)$/, warn => 'Please use "btrfs_commit_delete yes|no"', replace_key => "btrfs_commit_delete", replace_value => "yes" } } },
snapshot_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global volume subvolume )] }, snapshot_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global volume subvolume )] },
target_qgroup_destroy => { default => undef, accept => [qw( yes no )] }, target_qgroup_destroy => { default => undef, accept => [qw( yes no )] },
@ -1413,98 +1414,37 @@ sub btrfs_subvolume_snapshot($$)
sub btrfs_subvolume_delete($@) sub btrfs_subvolume_delete($@)
{ {
my $targets = shift // die; my $vol = shift // die;
my %opts = @_; my %opts = @_;
my $commit = $opts{commit}; my $target_type = $vol->{node}{TARGET_TYPE} || "";
die if($commit && ($commit ne "after") && ($commit ne "each"));
$targets = [ $targets ] unless(ref($targets) eq "ARRAY");
return () unless(scalar(@$targets));
# NOTE: rsh and backend command is taken from first target
my $rsh_machine_check = $targets->[0]->{MACHINE_ID};
my $target_type = $targets->[0]->{node}{TARGET_TYPE} || "";
foreach (@$targets) {
# assert all targets share same MACHINE_ID
die if($rsh_machine_check ne $_->{MACHINE_ID});
# assert all targets share same target type
die if($target_type && ($_->{node}{TARGET_TYPE} ne $target_type));
}
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 $ret;
my @deleted; INFO "[delete] target: $vol->{PRINT}";
my %err_catch; start_transaction($opts{type} // "delete", vinfo_prefixed_keys("target", $vol));
if($target_type eq "raw") { if($target_type eq "raw") {
my @cmd_target_paths; $ret = run_cmd(cmd => [ 'rm', '-f',
foreach(@$targets) { { unsafe => $vol->{PATH}, postfix => ($vol->{node}{BTRBK_RAW}{split} && ".split_??") },
push @cmd_target_paths, { unsafe => $_->{PATH}, postfix => ($_->{node}{BTRBK_RAW}{split} && ".split_??") }; { unsafe => $vol->{PATH}, postfix => ".info" },
push @cmd_target_paths, { unsafe => "$_->{PATH}.info" }; ],
} rsh => vinfo_rsh($vol),
$ret = run_cmd(cmd => [ 'rm', '-f', @cmd_target_paths ],
rsh => vinfo_rsh($targets->[0]),
); );
unless(defined($ret)) {
foreach(@stderr) {
next unless(/^rm: cannot remove '(.*?)':/);
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 { else {
my @cmd_target_paths = map { { unsafe => $_->{PATH} } } @$targets;
my @options; my @options;
@options = ("--commit-$commit") if($commit); push @options, "--commit-each" if($opts{commit});
$ret = run_cmd(cmd => vinfo_cmd($targets->[0], "btrfs subvolume delete", @options, @cmd_target_paths ), $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume delete", @options, { unsafe => $vol->{PATH} } ),
rsh => vinfo_rsh($targets->[0]), rsh => vinfo_rsh($vol),
fatal_stderr => sub { m/^ERROR: /; }, # probably not needed, "btrfs sub delete" returns correct exit status fatal_stderr => sub { m/^ERROR: /; }, # probably not needed, "btrfs sub delete" returns correct exit status
filter_stderr => \&_btrfs_filter_stderr, filter_stderr => \&_btrfs_filter_stderr,
); );
}
end_transaction($opts{type} // "delete", defined($ret));
unless(defined($ret)) { unless(defined($ret)) {
foreach(@stderr) { ERROR "Failed to delete subvolume: $vol->{PRINT}", @stderr;
next unless(/'(\/.*?)'/ || /: (\/.*)$/ || /(\/.*?):/); return undef;
# NOTE: as of btrfs-progs-4.16, this does not catch anything
$err_catch{$1} //= [];
push(@{$err_catch{$1}}, $_);
}
}
} }
if(defined($ret)) { return $vol;
@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 map("Failed to delete subvolume \"$check_target->{PRINT}\": $_", @$err_ary);
$catch_count++;
}
else {
push @deleted, $check_target;
}
}
@deleted = () if($catch_count != (scalar keys %err_catch));
}
unless(scalar(@deleted)) {
ERROR "Failed to match error messages from delete command, assuming nothing deleted", @stderr;
ERROR map("Possibly not deleted subvolume: $_->{PRINT}", @$targets);
ERROR "Consider running 'btrbk prune -n'";
}
}
end_transaction($opts{type} // "delete", sub { my $h = shift; return (grep { $_->{URL} eq $h->{target_url} } @deleted); });
return @deleted;
} }
@ -1661,11 +1601,9 @@ sub btrfs_send_receive($$;$$$)
# NOTE: btrfs-progs does not delete incomplete received (garbled) subvolumes, # NOTE: btrfs-progs does not delete incomplete received (garbled) subvolumes,
# we need to do this by hand. # we need to do this by hand.
# TODO: remove this as soon as btrfs-progs handle receive errors correctly. # TODO: remove this as soon as btrfs-progs handle receive errors correctly.
my @deleted = btrfs_subvolume_delete($vol_received, commit => "after", type => "delete_garbled"); if(btrfs_subvolume_delete($vol_received, commit => "after", type => "delete_garbled")) {
if(scalar(@deleted)) {
WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}"; WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}";
} } else {
else {
WARN "Deletion of partially received (garbled) subvolume failed, assuming clean environment: $vol_received->{PRINT}"; WARN "Deletion of partially received (garbled) subvolume failed, assuming clean environment: $vol_received->{PRINT}";
} }
} }
@ -4561,23 +4499,23 @@ sub macro_delete($$$$;@)
preserve_date_in_future => 1, preserve_date_in_future => 1,
); );
if($delete_options{qgroup}->{destroy}) { my @delete_success;
foreach my $vol (@$delete) {
# NOTE: we do not abort on qgroup destroy errors # NOTE: we do not abort on qgroup destroy errors
btrfs_qgroup_destroy($_, %{$delete_options{qgroup}}) foreach(@$delete); btrfs_qgroup_destroy($vol, %{$delete_options{qgroup}}) if($delete_options{qgroup}->{destroy});
if(btrfs_subvolume_delete($vol, %delete_options)) {
push @delete_success, $vol;
}
} }
my @delete_success = btrfs_subvolume_delete($delete, %delete_options);
INFO "Deleted " . scalar(@delete_success) . " subvolumes in: $root_subvol->{PRINT}/$subvol_basename.*"; INFO "Deleted " . scalar(@delete_success) . " subvolumes in: $root_subvol->{PRINT}/$subvol_basename.*";
$result_vinfo->{SUBVOL_DELETED} //= []; $result_vinfo->{SUBVOL_DELETED} //= [];
push @{$result_vinfo->{SUBVOL_DELETED}}, @delete_success; push @{$result_vinfo->{SUBVOL_DELETED}}, @delete_success;
if(scalar(@delete_success) == scalar(@$delete)) { if(scalar(@delete_success) != scalar(@$delete)) {
return 1;
}
else {
ABORTED($result_vinfo, "Failed to delete subvolume"); ABORTED($result_vinfo, "Failed to delete subvolume");
return undef; return undef;
} }
return 1;
} }
@ -6834,17 +6772,18 @@ MAIN:
foreach my $droot (vinfo_subsection($svol, 'target')) { foreach my $droot (vinfo_subsection($svol, 'target')) {
INFO "Cleaning incomplete backups in: $droot->{PRINT}/$snapshot_name.*"; INFO "Cleaning incomplete backups in: $droot->{PRINT}/$snapshot_name.*";
push @out, "$droot->{PRINT}/$snapshot_name.*"; push @out, "$droot->{PRINT}/$snapshot_name.*";
my @delete;
foreach my $target_vol (@{vinfo_subvol_list($droot, btrbk_direct_leaf => $snapshot_name, sort => 'path')}) {
# incomplete received (garbled) subvolumes are not readonly and have no received_uuid (as of btrfs-progs v4.3.1). # incomplete received (garbled) subvolumes are not readonly and have no received_uuid (as of btrfs-progs v4.3.1).
# a subvolume in droot matching our naming is considered incomplete if received_uuid is not set! # a subvolume in droot matching our naming is considered incomplete if received_uuid is not set!
if($target_vol->{node}{received_uuid} eq '-') { my @delete = grep $_->{node}{received_uuid} eq '-', @{vinfo_subvol_list($droot, btrbk_direct_leaf => $snapshot_name, sort => 'path')};
my @delete_success;
foreach my $target_vol (@delete) {
DEBUG "Found incomplete target subvolume: $target_vol->{PRINT}"; DEBUG "Found incomplete target subvolume: $target_vol->{PRINT}";
push(@delete, $target_vol); if(btrfs_subvolume_delete($target_vol, commit => config_key($droot, "btrfs_commit_delete"), type => "delete_garbled")) {
push(@delete_success, $target_vol);
} }
} }
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.*"; INFO "Deleted " . scalar(@delete_success) . " incomplete backups in: $droot->{PRINT}/$snapshot_name.*";
$droot->{SUBVOL_DELETED} //= []; $droot->{SUBVOL_DELETED} //= [];
push @{$droot->{SUBVOL_DELETED}}, @delete_success; push @{$droot->{SUBVOL_DELETED}}, @delete_success;