mirror of https://github.com/digint/btrbk
Merge branch 'action_archive' into devel
commit
59b3cde303
|
@ -15,6 +15,7 @@ btrbk-current
|
||||||
* Allow wildcards in subvolume section (close: #71).
|
* Allow wildcards in subvolume section (close: #71).
|
||||||
* Propagate targets defined in "volume" or "root" context to all
|
* Propagate targets defined in "volume" or "root" context to all
|
||||||
"subvolume" sections (close: #78).
|
"subvolume" sections (close: #78).
|
||||||
|
* Added "archive" command (close: #79).
|
||||||
* Added configuration option "rate_limit" (close: #72).
|
* Added configuration option "rate_limit" (close: #72).
|
||||||
* Added "--print-schedule" command line option.
|
* Added "--print-schedule" command line option.
|
||||||
* Detect interrupted transfers of raw targets (close: #75).
|
* Detect interrupted transfers of raw targets (close: #75).
|
||||||
|
|
411
btrbk
411
btrbk
|
@ -86,6 +86,8 @@ my %config_options = (
|
||||||
snapshot_preserve_min => { default => "1d", accept => [ "all", "latest" ], accept_regexp => qr/^[1-9][0-9]*[hdwmy]$/, context => [ "root", "volume", "subvolume" ], },
|
snapshot_preserve_min => { default => "1d", accept => [ "all", "latest" ], accept_regexp => qr/^[1-9][0-9]*[hdwmy]$/, context => [ "root", "volume", "subvolume" ], },
|
||||||
target_preserve => { default => undef, accept => [ "no" ], accept_preserve_matrix => 1 },
|
target_preserve => { default => undef, accept => [ "no" ], accept_preserve_matrix => 1 },
|
||||||
target_preserve_min => { default => undef, accept => [ "all", "latest", "no" ], accept_regexp => qr/^[0-9]+[hdwmy]$/ },
|
target_preserve_min => { default => undef, accept => [ "all", "latest", "no" ], accept_regexp => qr/^[0-9]+[hdwmy]$/ },
|
||||||
|
archive_preserve => { default => undef, accept => [ "no" ], accept_preserve_matrix => 1 },
|
||||||
|
archive_preserve_min => { default => "all", accept => [ "all", "latest", "no" ], accept_regexp => qr/^[0-9]+[hdwmy]$/ },
|
||||||
btrfs_commit_delete => { default => undef, accept => [ "after", "each", "no" ] },
|
btrfs_commit_delete => { default => undef, accept => [ "after", "each", "no" ] },
|
||||||
ssh_identity => { default => undef, accept_file => { absolute => 1 } },
|
ssh_identity => { default => undef, accept_file => { absolute => 1 } },
|
||||||
ssh_user => { default => "root", accept_regexp => qr/^[a-z_][a-z0-9_-]*$/ },
|
ssh_user => { default => "root", accept_regexp => qr/^[a-z_][a-z0-9_-]*$/ },
|
||||||
|
@ -243,7 +245,7 @@ sub HELP_MESSAGE
|
||||||
print STDERR " --progress show progress bar on send-receive operation\n";
|
print STDERR " --progress show progress bar on send-receive operation\n";
|
||||||
print STDERR "\n";
|
print STDERR "\n";
|
||||||
print STDERR "commands:\n";
|
print STDERR "commands:\n";
|
||||||
print STDERR " run perform backup operations as defined in the config file\n";
|
print STDERR " run perform backup operations as defined in the config\n";
|
||||||
print STDERR " dryrun don't run btrfs commands; show what would be executed\n";
|
print STDERR " dryrun don't run btrfs commands; show what would be executed\n";
|
||||||
print STDERR " stats print snapshot/backup statistics\n";
|
print STDERR " stats print snapshot/backup statistics\n";
|
||||||
print STDERR " list <subcommand> available subcommands are:\n";
|
print STDERR " list <subcommand> available subcommands are:\n";
|
||||||
|
@ -255,9 +257,10 @@ sub HELP_MESSAGE
|
||||||
print STDERR " volume configured volume sections\n";
|
print STDERR " volume configured volume sections\n";
|
||||||
print STDERR " target configured targets\n";
|
print STDERR " target configured targets\n";
|
||||||
print STDERR " clean delete incomplete (garbled) backups\n";
|
print STDERR " clean delete incomplete (garbled) backups\n";
|
||||||
|
print STDERR " archive <src> <dst> recursively copy all subvolumes (experimental)\n";
|
||||||
print STDERR " usage print filesystem usage\n";
|
print STDERR " usage print filesystem usage\n";
|
||||||
print STDERR " origin <subvol> print origin information for subvolume\n";
|
print STDERR " origin <subvol> print origin information for subvolume\n";
|
||||||
print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\n";
|
print STDERR " diff <from> <to> shows new files between related subvolumes\n";
|
||||||
print STDERR "\n";
|
print STDERR "\n";
|
||||||
print STDERR "For additional information, see $PROJECT_HOME\n";
|
print STDERR "For additional information, see $PROJECT_HOME\n";
|
||||||
}
|
}
|
||||||
|
@ -1151,6 +1154,23 @@ sub system_realpath($)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub system_mkdir($)
|
||||||
|
{
|
||||||
|
my $vol = shift // die;
|
||||||
|
my $path = $vol->{PATH} // die;;
|
||||||
|
INFO "Creating directory: $vol->{PRINT}/";
|
||||||
|
my $ret = run_cmd(cmd => [ qw(mkdir), '-p', $path ],
|
||||||
|
rsh => $vol->{RSH},
|
||||||
|
);
|
||||||
|
action("mkdir",
|
||||||
|
vinfo_prefixed_keys("target", $vol),
|
||||||
|
status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")),
|
||||||
|
);
|
||||||
|
return undef unless(defined($ret));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btrfs_mountpoint($)
|
sub btrfs_mountpoint($)
|
||||||
{
|
{
|
||||||
my $vol = shift // die;
|
my $vol = shift // die;
|
||||||
|
@ -1815,28 +1835,24 @@ sub get_receive_targets($$;@)
|
||||||
my $src_vol = shift || die;
|
my $src_vol = shift || die;
|
||||||
my %opts = @_;
|
my %opts = @_;
|
||||||
my $droot_subvols = vinfo_subvol_list($droot);
|
my $droot_subvols = vinfo_subvol_list($droot);
|
||||||
my @ret_receive_targets;
|
my @ret;
|
||||||
my @all_receive_targets;
|
|
||||||
my $unexpected_count = 0;
|
my $unexpected_count = 0;
|
||||||
|
|
||||||
if($src_vol->{node}{is_root}) {
|
if($src_vol->{node}{is_root}) {
|
||||||
DEBUG "Skip search for targets: source subvolume is btrfs root: $src_vol->{PRINT}";
|
DEBUG "Skip search for targets: source subvolume is btrfs root: $src_vol->{PRINT}";
|
||||||
return @ret_receive_targets;
|
return @ret;
|
||||||
}
|
}
|
||||||
unless($src_vol->{node}{readonly}) {
|
unless($src_vol->{node}{readonly}) {
|
||||||
DEBUG "Skip search for targets: source subvolume is not read-only: $src_vol->{PRINT}";
|
DEBUG "Skip search for targets: source subvolume is not read-only: $src_vol->{PRINT}";
|
||||||
return @ret_receive_targets;
|
return @ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
# find matches by comparing uuid / received_uuid
|
# find matches by comparing uuid / received_uuid
|
||||||
my $uuid = $src_vol->{node}{uuid};
|
my $uuid = $src_vol->{node}{uuid};
|
||||||
my $received_uuid;
|
my $received_uuid = $src_vol->{node}{received_uuid};
|
||||||
if($src_vol->{node}{received_uuid} ne '-') {
|
$received_uuid = undef if($received_uuid eq '-');
|
||||||
TRACE "get_receive_targets: source subvolume has received_uuid";
|
TRACE "get_receive_targets: src_vol=\"$src_vol->{PRINT}\", droot=\"$droot->{PRINT}\"";
|
||||||
$received_uuid = $src_vol->{node}{received_uuid};
|
|
||||||
}
|
|
||||||
|
|
||||||
die("subvolume info not present: $uuid") unless($uuid_cache{$uuid});
|
|
||||||
foreach (@$droot_subvols) {
|
foreach (@$droot_subvols) {
|
||||||
next unless($_->{node}{readonly});
|
next unless($_->{node}{readonly});
|
||||||
my $matched = undef;
|
my $matched = undef;
|
||||||
|
@ -1846,28 +1862,53 @@ sub get_receive_targets($$;@)
|
||||||
elsif(defined($received_uuid) && ($_->{node}{received_uuid} eq $received_uuid)) {
|
elsif(defined($received_uuid) && ($_->{node}{received_uuid} eq $received_uuid)) {
|
||||||
$matched = 'by-received_uuid';
|
$matched = 'by-received_uuid';
|
||||||
}
|
}
|
||||||
if($matched) {
|
next unless($matched);
|
||||||
push(@all_receive_targets, $_);
|
|
||||||
if($opts{exact_match} && (not exists($_->{BTRBK_RAW}))) {
|
TRACE "get_receive_targets: $matched: Found receive target: $_->{SUBVOL_PATH}";
|
||||||
unless($_->{direct_leaf} && ($_->{NAME} eq $src_vol->{NAME})) {
|
push(@{$opts{seen}}, $_) if($opts{seen});
|
||||||
|
if($opts{exact_match} && !exists($_->{BTRBK_RAW})) {
|
||||||
|
if($_->{direct_leaf} && ($_->{NAME} eq $src_vol->{NAME})) {
|
||||||
|
TRACE "get_receive_targets: exact_match: $_->{SUBVOL_PATH}";
|
||||||
|
}
|
||||||
|
else {
|
||||||
TRACE "get_receive_targets: $matched: skip non-exact match: $_->{PRINT}";
|
TRACE "get_receive_targets: $matched: skip non-exact match: $_->{PRINT}";
|
||||||
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $_->{PRINT}" if($opts{warn_unexpected});
|
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $_->{PRINT}" if($opts{warn});
|
||||||
$unexpected_count++;
|
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TRACE "get_receive_targets: $matched: Found receive target: $_->{SUBVOL_PATH}";
|
push(@ret, $_);
|
||||||
push(@ret_receive_targets, $_);
|
}
|
||||||
|
DEBUG "Found " . scalar(@ret) . " receive targets in \"$droot->{PRINT}/\" for: $src_vol->{PRINT}";
|
||||||
|
return @ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($opts{warn_unexpected}) {
|
|
||||||
|
sub get_receive_targets_fsroot($$@)
|
||||||
|
{
|
||||||
|
my $droot = shift // die;
|
||||||
|
my $src_vol = shift // die;
|
||||||
|
my %opts = @_;
|
||||||
|
my $id = $src_vol->{node}{id};
|
||||||
|
my $uuid = $src_vol->{node}{uuid};
|
||||||
|
my $received_uuid = $src_vol->{node}{received_uuid};
|
||||||
|
$received_uuid = undef if(defined($received_uuid) && ($received_uuid eq '-'));
|
||||||
|
|
||||||
|
my @unexpected;
|
||||||
|
my @exclude;
|
||||||
|
@exclude = map { $_->{node}{id} } @{$opts{exclude}} if($opts{exclude});
|
||||||
|
|
||||||
|
TRACE "get_receive_target_fsroot: uuid=$uuid, received_uuid=" . ($received_uuid // '-') . " exclude id={ " . join(', ', @exclude) . " }";
|
||||||
|
|
||||||
# search in filesystem for matching received_uuid
|
# search in filesystem for matching received_uuid
|
||||||
my @fs_match = grep({ (not $_->{is_root}) &&
|
foreach my $node (
|
||||||
|
grep({ (not $_->{is_root}) &&
|
||||||
(($_->{received_uuid} eq $uuid) ||
|
(($_->{received_uuid} eq $uuid) ||
|
||||||
(defined($received_uuid) && ($_->{received_uuid} eq $received_uuid)))
|
(defined($received_uuid) && ($_->{received_uuid} eq $received_uuid)))
|
||||||
} values(%{$droot->{node}{TREE_ROOT}{ID_HASH}}) );
|
} values(%{$droot->{node}{TREE_ROOT}{ID_HASH}}) ) )
|
||||||
foreach my $node (@fs_match) {
|
{
|
||||||
next if(scalar grep( { $_->{node}{id} == $node->{id} } @all_receive_targets));
|
next if(scalar grep($_ == $node->{id}, @exclude));
|
||||||
|
push @unexpected, $node;
|
||||||
|
if($opts{warn}) {
|
||||||
my $text;
|
my $text;
|
||||||
my @url = get_cached_url_by_uuid($node->{uuid});
|
my @url = get_cached_url_by_uuid($node->{uuid});
|
||||||
if(scalar(@url)) {
|
if(scalar(@url)) {
|
||||||
|
@ -1876,13 +1917,9 @@ sub get_receive_targets($$;@)
|
||||||
$text = '"' . _fs_path($node) . "\" (in filesystem at \"$droot->{PRINT}\")";
|
$text = '"' . _fs_path($node) . "\" (in filesystem at \"$droot->{PRINT}\")";
|
||||||
}
|
}
|
||||||
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $text";
|
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $text";
|
||||||
$unexpected_count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${$opts{ret_unexpected}} = $unexpected_count if(ref $opts{ret_unexpected});
|
return \@unexpected;
|
||||||
}
|
|
||||||
DEBUG "Found " . scalar(@ret_receive_targets) . " receive targets in \"$droot->{PRINT}/\" for: $src_vol->{PRINT}";
|
|
||||||
return @ret_receive_targets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2479,7 +2516,7 @@ sub macro_send_receive(@)
|
||||||
{
|
{
|
||||||
# create backup from latest common
|
# create backup from latest common
|
||||||
if($parent) {
|
if($parent) {
|
||||||
INFO "Incremental from parent subvolume: $parent->{PRINT}";
|
INFO "Incremental from parent: $parent->{PRINT}";
|
||||||
}
|
}
|
||||||
elsif($incremental ne "strict") {
|
elsif($incremental ne "strict") {
|
||||||
INFO "No common parent subvolume present, creating full backup";
|
INFO "No common parent subvolume present, creating full backup";
|
||||||
|
@ -2614,6 +2651,90 @@ sub macro_delete($$$$$;@)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub macro_archive_target($$$;$)
|
||||||
|
{
|
||||||
|
my $sroot = shift || die;
|
||||||
|
my $droot = shift || die;
|
||||||
|
my $snapshot_name = shift // die;
|
||||||
|
my $schedule_options = shift // {};
|
||||||
|
my @schedule;
|
||||||
|
|
||||||
|
# NOTE: this is pretty much the same as "resume missing"
|
||||||
|
my @unexpected_location;
|
||||||
|
foreach my $svol (@{vinfo_subvol_list($sroot, sort => 'path')})
|
||||||
|
{
|
||||||
|
next unless($svol->{node}{readonly});
|
||||||
|
next unless($svol->{btrbk_direct_leaf} && ($svol->{BTRBK_BASENAME} eq $snapshot_name));
|
||||||
|
|
||||||
|
my $warning_seen = [];
|
||||||
|
my @receive_targets = get_receive_targets($droot, $svol, exact_match => 1, warn => 1, seen => $warning_seen );
|
||||||
|
push @unexpected_location, get_receive_targets_fsroot($droot, $svol, exclude => $warning_seen, warn => 1); # warn if unexpected on fs
|
||||||
|
|
||||||
|
next if(scalar(@receive_targets));
|
||||||
|
DEBUG "Adding archive candidate: $svol->{PRINT}";
|
||||||
|
|
||||||
|
push @schedule, { value => $svol,
|
||||||
|
btrbk_date => $svol->{BTRBK_DATE},
|
||||||
|
preserve => $svol->{node}{FORCE_PRESERVE},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
# this is a bit harsh, disabled for now
|
||||||
|
# if(scalar(@unexpected_location) {
|
||||||
|
# ABORTED($droot, "Receive targets of archive candidates exist at unexpected location");
|
||||||
|
# WARN "Skipping archiving of \"$sroot->{PRINT}/${snapshot_name}.*\": $abrt";
|
||||||
|
# return undef;
|
||||||
|
# }
|
||||||
|
|
||||||
|
foreach my $dvol (@{vinfo_subvol_list($droot, sort => 'path')})
|
||||||
|
{
|
||||||
|
next unless($dvol->{btrbk_direct_leaf} && ($dvol->{BTRBK_BASENAME} eq $snapshot_name));
|
||||||
|
next unless($dvol->{node}{readonly});
|
||||||
|
# add all present archives to schedule, with no value.
|
||||||
|
# these are needed for correct results of schedule()
|
||||||
|
push @schedule, { informative_only => 1,
|
||||||
|
value => $dvol,
|
||||||
|
btrbk_date => $dvol->{BTRBK_DATE},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
my ($preserve, undef) = schedule(
|
||||||
|
schedule => \@schedule,
|
||||||
|
preserve => config_preserve_hash($droot, "archive"),
|
||||||
|
result_preserve_action_text => 'archive',
|
||||||
|
result_delete_action_text => '',
|
||||||
|
%$schedule_options
|
||||||
|
);
|
||||||
|
my @archive = grep defined, @$preserve; # remove entries with no value from list (archive subvolumes)
|
||||||
|
my $archive_total = scalar @archive;
|
||||||
|
my $archive_success = 0;
|
||||||
|
foreach my $svol (@archive)
|
||||||
|
{
|
||||||
|
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, "");
|
||||||
|
if(macro_send_receive(source => $svol,
|
||||||
|
target => $droot,
|
||||||
|
parent => $latest_common_src,
|
||||||
|
latest_common_target => $latest_common_target,
|
||||||
|
))
|
||||||
|
{
|
||||||
|
$archive_success++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ERROR("Error while cloning, aborting");
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($archive_total) {
|
||||||
|
INFO "Archived $archive_success/$archive_total subvolumes";
|
||||||
|
} else {
|
||||||
|
INFO "No missing archived subvolume found";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $archive_success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub cmp_date($$)
|
sub cmp_date($$)
|
||||||
{
|
{
|
||||||
my ($a,$b) = @_;
|
my ($a,$b) = @_;
|
||||||
|
@ -2633,6 +2754,8 @@ sub schedule(@)
|
||||||
my $preserve = $args{preserve} || die;
|
my $preserve = $args{preserve} || die;
|
||||||
my $results_list = $args{results};
|
my $results_list = $args{results};
|
||||||
my $result_hints = $args{result_hints} // {};
|
my $result_hints = $args{result_hints} // {};
|
||||||
|
my $result_preserve_action_text = $args{result_preserve_action_text};
|
||||||
|
my $result_delete_action_text = $args{result_delete_action_text} // 'delete';
|
||||||
|
|
||||||
my $preserve_day_of_week = $preserve->{dow} || die;
|
my $preserve_day_of_week = $preserve->{dow} || die;
|
||||||
my $preserve_min_n = $preserve->{min_n};
|
my $preserve_min_n = $preserve->{min_n};
|
||||||
|
@ -2649,7 +2772,10 @@ sub schedule(@)
|
||||||
DEBUG "Schedule: " . format_preserve_matrix($preserve, format => "debug_text");
|
DEBUG "Schedule: " . format_preserve_matrix($preserve, format => "debug_text");
|
||||||
|
|
||||||
# sort the schedule, ascending by date
|
# sort the schedule, ascending by date
|
||||||
my @sorted_schedule = sort { cmp_date($a->{btrbk_date}, $b->{btrbk_date} ) } @$schedule;
|
# regular entries come in front of informative_only
|
||||||
|
my @sorted_schedule = sort { cmp_date($a->{btrbk_date}, $b->{btrbk_date} ) ||
|
||||||
|
(($a->{informative_only} ? ($b->{informative_only} ? 0 : 1) : ($b->{informative_only} ? -1 : 0)))
|
||||||
|
} @$schedule;
|
||||||
|
|
||||||
# first, do our calendar calculations
|
# first, do our calendar calculations
|
||||||
# note: our week starts on $preserve_day_of_week
|
# note: our week starts on $preserve_day_of_week
|
||||||
|
@ -2755,23 +2881,22 @@ sub schedule(@)
|
||||||
my $count_defined = 0;
|
my $count_defined = 0;
|
||||||
foreach my $href (@sorted_schedule)
|
foreach my $href (@sorted_schedule)
|
||||||
{
|
{
|
||||||
next unless(defined($href->{value}));
|
$count_defined++ unless($href->{informative_only});
|
||||||
$count_defined++;
|
|
||||||
if($href->{preserve}) {
|
if($href->{preserve}) {
|
||||||
push(@preserve, $href->{value});
|
push(@preserve, $href->{value}) unless($href->{informative_only});
|
||||||
DEBUG "Schedule: $href->{name}: $href->{preserve}" if($href->{name});
|
DEBUG "Schedule: $href->{name}: $href->{preserve}" if($href->{name});
|
||||||
push @$results_list, { %result_base,
|
push @$results_list, { %result_base,
|
||||||
# action => "preserve",
|
action => $href->{informative_only} ? 'seen' : $result_preserve_action_text,
|
||||||
reason => $href->{preserve},
|
reason => $href->{preserve},
|
||||||
value => $href->{value},
|
value => $href->{value},
|
||||||
} if($results_list);
|
} if($results_list);
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
push(@delete, $href->{value});
|
push(@delete, $href->{value}) unless($href->{informative_only});
|
||||||
DEBUG "Schedule: $href->{name}: delete" if($href->{name});
|
DEBUG "Schedule: $href->{name}: delete" if($href->{name});
|
||||||
push @$results_list, { %result_base,
|
push @$results_list, { %result_base,
|
||||||
action => "delete",
|
action => $result_delete_action_text,
|
||||||
value => $href->{value},
|
value => $href->{value},
|
||||||
} if($results_list);
|
} if($results_list);
|
||||||
}
|
}
|
||||||
|
@ -3074,7 +3199,7 @@ MAIN:
|
||||||
WARN 'Found option "--progress", but "pv" is not present: (please install "pv")';
|
WARN 'Found option "--progress", but "pv" is not present: (please install "pv")';
|
||||||
$show_progress = 0;
|
$show_progress = 0;
|
||||||
}
|
}
|
||||||
my ($action_run, $action_usage, $action_resolve, $action_diff, $action_origin, $action_config_print, $action_list, $action_clean);
|
my ($action_run, $action_usage, $action_resolve, $action_diff, $action_origin, $action_config_print, $action_list, $action_clean, $action_archive);
|
||||||
my @filter_args;
|
my @filter_args;
|
||||||
my $args_allow_group = 1;
|
my $args_allow_group = 1;
|
||||||
my $args_expected_min = 0;
|
my $args_expected_min = 0;
|
||||||
|
@ -3089,6 +3214,12 @@ MAIN:
|
||||||
$action_clean = 1;
|
$action_clean = 1;
|
||||||
@filter_args = @ARGV;
|
@filter_args = @ARGV;
|
||||||
}
|
}
|
||||||
|
elsif ($command eq "archive") {
|
||||||
|
$action_archive = 1;
|
||||||
|
$args_expected_min = $args_expected_max = 2;
|
||||||
|
$args_allow_group = 0;
|
||||||
|
@filter_args = @ARGV;
|
||||||
|
}
|
||||||
elsif ($command eq "usage") {
|
elsif ($command eq "usage") {
|
||||||
$action_usage = 1;
|
$action_usage = 1;
|
||||||
@filter_args = @ARGV;
|
@filter_args = @ARGV;
|
||||||
|
@ -3299,6 +3430,198 @@ MAIN:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if($action_archive)
|
||||||
|
{
|
||||||
|
#
|
||||||
|
# archive (clone) tree
|
||||||
|
#
|
||||||
|
# NOTE: This is intended to work without a config file! The only
|
||||||
|
# thing used from the configuration is the SSH and transaction log
|
||||||
|
# stuff.
|
||||||
|
#
|
||||||
|
init_transaction_log(config_key($config, "transaction_log"));
|
||||||
|
|
||||||
|
my $src_url = $filter_args[0] || die;
|
||||||
|
my $archive_url = $filter_args[1] || die;
|
||||||
|
|
||||||
|
# FIXME: add command line options for preserve logic
|
||||||
|
$config->{SUBSECTION} = []; # clear configured subsections, we build them dynamically
|
||||||
|
|
||||||
|
my $src_root = vinfo($src_url, $config);
|
||||||
|
unless(vinfo_init_root($src_root, resolve_subdir => 1)) {
|
||||||
|
ERROR "Failed to fetch subvolume detail for '$src_root->{PRINT}'" . ($err ? ": $err" : "");
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
my $archive_root = vinfo($archive_url, $config);
|
||||||
|
unless(vinfo_init_root($archive_root, resolve_subdir => 1)) {
|
||||||
|
ERROR "Failed to fetch subvolume detail for '$archive_root->{PRINT}'" . ($err ? ": $err" : "");
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
my %name_uniq;
|
||||||
|
my @subvol_list = @{vinfo_subvol_list($src_root)};
|
||||||
|
my @sorted = sort { ($a->{subtree_depth} <=> $b->{subtree_depth}) || ($a->{SUBVOL_DIR} cmp $b->{SUBVOL_DIR}) } @subvol_list;
|
||||||
|
foreach my $vol (@sorted) {
|
||||||
|
next unless($vol->{node}{readonly});
|
||||||
|
my $snapshot_name = $vol->{BTRBK_BASENAME};
|
||||||
|
unless(defined($snapshot_name)) {
|
||||||
|
WARN "Skipping subvolume (not a btrbk subvolume): $vol->{PRINT}";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
my $subvol_dir = $vol->{SUBVOL_DIR};
|
||||||
|
next if($name_uniq{"$subvol_dir/$snapshot_name"});
|
||||||
|
$name_uniq{"$subvol_dir/$snapshot_name"} = 1;
|
||||||
|
my $droot_url = $archive_url . ($subvol_dir eq "" ? "" : "/$subvol_dir");
|
||||||
|
my $sroot_url = $src_url . ($subvol_dir eq "" ? "" : "/$subvol_dir");
|
||||||
|
my $config_sroot = { CONTEXT => "archive_source",
|
||||||
|
PARENT => $config,
|
||||||
|
url => $sroot_url, # ABORTED() needs this
|
||||||
|
snapshot_name => $snapshot_name,
|
||||||
|
};
|
||||||
|
my $config_droot = { CONTEXT => "target",
|
||||||
|
PARENT => $config_sroot,
|
||||||
|
target_type => "send-receive", # macro_send_receive checks this
|
||||||
|
url => $droot_url, # ABORTED() needs this
|
||||||
|
};
|
||||||
|
$config_sroot->{SUBSECTION} = [ $config_droot ];
|
||||||
|
push(@{$config->{SUBSECTION}}, $config_sroot);
|
||||||
|
|
||||||
|
my $sroot = vinfo($sroot_url, $config_sroot);
|
||||||
|
vinfo_assign_config($sroot, $config_sroot);
|
||||||
|
unless(vinfo_init_root($sroot, resolve_subdir => 1)) {
|
||||||
|
ABORTED($sroot, "Failed to fetch subvolume detail" . ($err ? ": $err" : ""));
|
||||||
|
WARN "Skipping archive source \"$sroot->{PRINT}\": $abrt";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $droot = vinfo($droot_url, $config_droot);
|
||||||
|
vinfo_assign_config($droot, $config_droot);
|
||||||
|
unless(vinfo_init_root($droot, resolve_subdir => 1)) {
|
||||||
|
DEBUG("Failed to fetch subvolume detail" . ($err ? ": $err" : ""));
|
||||||
|
unless(system_mkdir($droot)) {
|
||||||
|
ABORTED($droot, "Failed to create directory: $droot->{PRINT}/");
|
||||||
|
WARN "Skipping archive target \"$droot->{PRINT}\": $abrt";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$droot->{SUBDIR_CREATED} = 1;
|
||||||
|
if($dryrun) {
|
||||||
|
# we need to fake this directory on dryrun
|
||||||
|
$droot->{node} = $archive_root->{node};
|
||||||
|
$droot->{NODE_SUBDIR} = $subvol_dir;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# after directory is created, try to init again
|
||||||
|
unless(vinfo_init_root($droot, resolve_subdir => 1)) {
|
||||||
|
ABORTED($droot, "Failed to fetch subvolume detail" . ($err ? ": $err" : ""));
|
||||||
|
WARN "Skipping archive target \"$droot->{PRINT}\": $abrt";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(_is_child_of($droot->{node}->{TREE_ROOT}, $vol->{node}{uuid})) {
|
||||||
|
ERROR "Source and target subvolumes are on the same btrfs filesystem!";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $schedule_results = [];
|
||||||
|
foreach my $sroot (vinfo_subsection($config, 'archive_source')) {
|
||||||
|
foreach my $droot (vinfo_subsection($sroot, 'target')) {
|
||||||
|
my $snapshot_name = config_key($droot, "snapshot_name") // die;
|
||||||
|
INFO "Archiving subvolumes: $sroot->{PRINT}/${snapshot_name}.*";
|
||||||
|
macro_archive_target($sroot, $droot, $snapshot_name, { results => $schedule_results });
|
||||||
|
if(ABORTED($droot)) {
|
||||||
|
# also abort $sroot
|
||||||
|
ABORTED($sroot, "At least one target aborted");
|
||||||
|
WARN "Skipping archiving of \"$sroot->{PRINT}/\": $abrt";
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last if(ABORTED($sroot));
|
||||||
|
}
|
||||||
|
|
||||||
|
my $exit_status = exit_status($config);
|
||||||
|
my $time_elapsed = time - $start_time;
|
||||||
|
INFO "Completed within: ${time_elapsed}s (" . localtime(time) . ")";
|
||||||
|
action("finished",
|
||||||
|
status => $exit_status ? "partial" : "success",
|
||||||
|
duration => $time_elapsed,
|
||||||
|
message => $exit_status ? "At least one backup task aborted" : undef,
|
||||||
|
);
|
||||||
|
close_transaction_log();
|
||||||
|
|
||||||
|
unless($quiet)
|
||||||
|
{
|
||||||
|
# print scheduling results
|
||||||
|
if($print_schedule) {
|
||||||
|
my @data = map { { %$_, vinfo_prefixed_keys("", $_->{value}) }; } @$schedule_results;
|
||||||
|
print_formatted("schedule", \@data, title => "ARCHIVE SCHEDULE");
|
||||||
|
print "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# print summary
|
||||||
|
$output_format ||= "custom";
|
||||||
|
if($output_format eq "custom")
|
||||||
|
{
|
||||||
|
my @unrecoverable;
|
||||||
|
my @out;
|
||||||
|
foreach my $sroot (vinfo_subsection($config, 'archive_source')) {
|
||||||
|
foreach my $droot (vinfo_subsection($sroot, 'target', 1)) {
|
||||||
|
my @subvol_out;
|
||||||
|
if($droot->{SUBDIR_CREATED}) {
|
||||||
|
push @subvol_out, "++. $droot->{PRINT}/";
|
||||||
|
}
|
||||||
|
foreach(@{$droot->{SUBVOL_RECEIVED} // []}) {
|
||||||
|
my $create_mode = "***";
|
||||||
|
$create_mode = ">>>" if($_->{parent});
|
||||||
|
$create_mode = "!!!" if($_->{ERROR});
|
||||||
|
push @subvol_out, "$create_mode $_->{received_subvolume}->{PRINT}";
|
||||||
|
}
|
||||||
|
if(ABORTED($droot) && (ABORTED($droot) ne "USER_SKIP")) {
|
||||||
|
push @subvol_out, "!!! Target \"$droot->{PRINT}\" aborted: " . ABORTED($droot);
|
||||||
|
}
|
||||||
|
if($droot->{CONFIG}->{UNRECOVERABLE}) {
|
||||||
|
push(@unrecoverable, $droot->{CONFIG}->{UNRECOVERABLE});
|
||||||
|
}
|
||||||
|
if(@subvol_out) {
|
||||||
|
push @out, "$sroot->{PRINT}/$sroot->{CONFIG}->{snapshot_name}.*", @subvol_out, "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_header(title => "Archive Summary",
|
||||||
|
time => $start_time,
|
||||||
|
legend => [
|
||||||
|
# "--- deleted subvolume",
|
||||||
|
"++. created directory",
|
||||||
|
"*** received subvolume (non-incremental)",
|
||||||
|
">>> received subvolume (incremental)",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
print join("\n", @out);
|
||||||
|
|
||||||
|
if($exit_status) {
|
||||||
|
print "\nNOTE: Some errors occurred, which may result in missing backups!\n";
|
||||||
|
print "Please check warning and error messages above.\n";
|
||||||
|
print join("\n", @unrecoverable) . "\n" if(@unrecoverable);
|
||||||
|
}
|
||||||
|
if($dryrun) {
|
||||||
|
print "\nNOTE: Dryrun was active, none of the operations above were actually executed!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
# print action log (without transaction start messages)
|
||||||
|
my @data = grep { $_->{status} ne "starting" } @transaction_log;
|
||||||
|
print_formatted("transaction", \@data, title => "TRANSACTION LOG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exit_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# expand subvolume globs (wildcards)
|
# expand subvolume globs (wildcards)
|
||||||
#
|
#
|
||||||
|
@ -4196,7 +4519,10 @@ MAIN:
|
||||||
|
|
||||||
foreach my $child (@snapshot_children)
|
foreach my $child (@snapshot_children)
|
||||||
{
|
{
|
||||||
next if(scalar(get_receive_targets($droot, $child, exact_match => 1, warn_unexpected => 1)));
|
my $warning_seen = [];
|
||||||
|
my @receive_targets = get_receive_targets($droot, $child, exact_match => 1, warn => 1, seen => $warning_seen );
|
||||||
|
get_receive_targets_fsroot($droot, $child, exclude => $warning_seen, warn => 1); # warn on unexpected on fs
|
||||||
|
next if(scalar(@receive_targets));
|
||||||
|
|
||||||
if(my $err_vol = vinfo_subvol($droot, $child->{NAME})) {
|
if(my $err_vol = vinfo_subvol($droot, $child->{NAME})) {
|
||||||
WARN "Target subvolume \"$err_vol->{PRINT}\" exists, but is not a receive target of \"$child->{PRINT}\"";
|
WARN "Target subvolume \"$err_vol->{PRINT}\" exists, but is not a receive target of \"$child->{PRINT}\"";
|
||||||
|
@ -4221,10 +4547,9 @@ MAIN:
|
||||||
TRACE "Receive target does not match btrbk filename scheme, skipping: $vol->{PRINT}";
|
TRACE "Receive target does not match btrbk filename scheme, skipping: $vol->{PRINT}";
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
push(@schedule, { value => undef,
|
push(@schedule, { informative_only => 1,
|
||||||
|
value => $vol,
|
||||||
btrbk_date => $vol->{BTRBK_DATE},
|
btrbk_date => $vol->{BTRBK_DATE},
|
||||||
# not enforcing resuming of latest snapshot anymore (since v0.23.0)
|
|
||||||
# preserve => $vol->{node}{FORCE_PRESERVE},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
my ($preserve, undef) = schedule(
|
my ($preserve, undef) = schedule(
|
||||||
|
|
47
doc/FAQ.md
47
doc/FAQ.md
|
@ -199,7 +199,23 @@ data physically, either to the datacenter or to your safe in the
|
||||||
basement.
|
basement.
|
||||||
|
|
||||||
|
|
||||||
### Answer 1: Use external storage as "stream-fifo"
|
### Answer 1: Use "btrbk archive"
|
||||||
|
|
||||||
|
A robust approach is to use external disks as archives (secondary
|
||||||
|
backups), and regularly run "btrbk archive" on them. As a nice side
|
||||||
|
effect, this also detects possible read-errors on your backup targets
|
||||||
|
(Note that a "btrfs scrub" is still more effective for that purpose).
|
||||||
|
|
||||||
|
See **btrbk archive** command in [btrbk(1)] for more details.
|
||||||
|
|
||||||
|
**Note that kernels >=4.1 and <4.4 have a bug when re-sending
|
||||||
|
subvolumes**, make sure you run a recent/patched kernel or step 3 will
|
||||||
|
fail. Read
|
||||||
|
[this thread on gmane](http://thread.gmane.org/gmane.comp.file-systems.btrfs/48798)
|
||||||
|
(the patch provided is confirmed working on kernels 4.2.x and 4.3.x).
|
||||||
|
|
||||||
|
|
||||||
|
### Answer 2: Use external storage as "stream-fifo"
|
||||||
|
|
||||||
This example uses a USB disk as "stream-fifo" for transferring
|
This example uses a USB disk as "stream-fifo" for transferring
|
||||||
(cloning) of btrfs subvolumes:
|
(cloning) of btrfs subvolumes:
|
||||||
|
@ -217,32 +233,3 @@ This approach has the advantage that you don't need to reformat your
|
||||||
USB disk. This works fine, but be aware that you may run into trouble
|
USB disk. This works fine, but be aware that you may run into trouble
|
||||||
if a single stream gets corrupted, making all subsequent streams
|
if a single stream gets corrupted, making all subsequent streams
|
||||||
unusable.
|
unusable.
|
||||||
|
|
||||||
|
|
||||||
### Answer 2: Clone btrfs subvolumes
|
|
||||||
|
|
||||||
A more robust approach is to use the USB disk as secondary backup.
|
|
||||||
This has the advantage that possible errors can already be detected by
|
|
||||||
btrfs on the source side:
|
|
||||||
|
|
||||||
1. Initialize USB disk:
|
|
||||||
|
|
||||||
`mkfs.btrfs /dev/usbX`
|
|
||||||
|
|
||||||
2. For all source subvolumes (in order of generation):
|
|
||||||
|
|
||||||
`btrfs send /source/subvolX -p PARENT | btrfs receive /usbdisk/`
|
|
||||||
|
|
||||||
3. At the target location (in order of generation):
|
|
||||||
|
|
||||||
`btrfs send /usbdisk/subvolX -p PARENT | btrfs receive /target`
|
|
||||||
|
|
||||||
If you simply want to have a clone of the source disk, skip step 3 and
|
|
||||||
store your USB disk in a safe. You will be able to use it for
|
|
||||||
restoring backups later, or *as a replacement for your backup disks*.
|
|
||||||
|
|
||||||
**Note that kernels >=4.1 and <4.4 have a bug when re-sending
|
|
||||||
subvolumes**, make sure you run a recent/patched kernel or step 3 will
|
|
||||||
fail. Read
|
|
||||||
[this thread on gmane](http://thread.gmane.org/gmane.comp.file-systems.btrfs/48798)
|
|
||||||
(the patch provided is confirmed working on kernels 4.2.x and 4.3.x).
|
|
||||||
|
|
23
doc/btrbk.1
23
doc/btrbk.1
|
@ -167,6 +167,29 @@ by the \fBrun\fR command. Use in conjunction with \fI\-l debug\fR to
|
||||||
see the btrfs commands that would be executed.
|
see the btrfs commands that would be executed.
|
||||||
.RE
|
.RE
|
||||||
.PP
|
.PP
|
||||||
|
.B archive
|
||||||
|
<source> <target>
|
||||||
|
.I *experimental*
|
||||||
|
.RS 4
|
||||||
|
Recursively copy all subvolumes created by btrbk from <source> to
|
||||||
|
<target> directory, optionally rescheduled using
|
||||||
|
\fIarchive_preserve_*\fR configuration options. Also creates directory
|
||||||
|
tree on <target> (see bugs below). Useful for creating extra archive
|
||||||
|
copies (clones) from your backup disks. Note that you can continue
|
||||||
|
using btrbk after swapping your backup disk with the archive disk.
|
||||||
|
.PP
|
||||||
|
Note that this feature needs a \fBlinux kernel >=4.4\fR to work
|
||||||
|
correctly! Kernels >=4.1 and <4.4 have a bug when re-sending
|
||||||
|
subvolumes (the archived subvolumes will have incorrect received_uuid,
|
||||||
|
see <http://thread.gmane.org/gmane.comp.file-systems.btrfs/48798>), so
|
||||||
|
make sure you run a recent kernel.
|
||||||
|
.PP
|
||||||
|
Known bugs: If you want to use nested subvolumes on the target
|
||||||
|
filesystem, you need to create them by hand (e.g. by running "btrfs
|
||||||
|
subvolume create <target>/dir"). Check the output of --dry-run if
|
||||||
|
unsure.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
.B stats
|
.B stats
|
||||||
[filter...]
|
[filter...]
|
||||||
.RS 4
|
.RS 4
|
||||||
|
|
Loading…
Reference in New Issue