mirror of https://github.com/digint/btrbk
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
parent
b7df717611
commit
6b465bf06b
133
btrbk
133
btrbk
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue