From bd219b73735cfe56d09aa675b6002f5f467f5870 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 13:37:56 +0200 Subject: [PATCH 01/10] btrbk: resume missing backups if option "resume_missing" is set --- btrbk | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/btrbk b/btrbk index c331f17..805f616 100755 --- a/btrbk +++ b/btrbk @@ -64,6 +64,7 @@ my %config_options = ( receive_log => { default => undef, accept => [ "sidecar", "no" ], accept_file => { absolute => 1 }, deprecated => "removed" }, incremental => { default => "yes", accept => [ "yes", "no", "strict" ] }, snapshot_create_always => { default => undef, accept => [ "yes", "no" ] }, + resume_missing => { default => undef, accept => [ "yes", "no" ] }, preserve_day_of_week => { default => "sunday", accept => [ (keys %day_of_week_map) ] }, snapshot_preserve_daily => { default => "all", accept => [ "all" ], accept_numeric => 1 }, snapshot_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1 }, @@ -831,7 +832,7 @@ sub btrfs_send_receive($$$$;$) ERROR "Failed to send/receive btrfs subvolume: $src " . ($real_parent ? "[$real_parent]" : "") . " -> $target"; return undef; } - return 1; + return "$target/$src_name"; } @@ -1514,17 +1515,50 @@ MAIN: my $success = 0; if($target_type eq "send-receive") { - INFO "Creating subvolume backup ($target_type) for: $sroot/$svol"; - INFO "Using previously created snapshot: $snapshot"; - if(config_key($config_target, "receive_log")) { WARN "Ignoring deprecated option \"receive_log\" for target: $droot" } my $incremental = config_key($config_target, "incremental"); - if($incremental) + if($incremental) # TODO: fix this { my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); + + # resume missing backups (resume_missing) + # TODO: non-incremental + if(config_key($config_target, "resume_missing")) { + INFO "Checking for missing backups of subvolume \"$sroot/$svol\": $droot/"; + my $found_missing = 0; + foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) { + if(scalar get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { + DEBUG "Found matching receive target for: $child->{FS_PATH}"; + } + else { + DEBUG "No matching receive targets found for: $child->{FS_PATH}"; + INFO "Resuming backup of: $child->{FS_PATH}"; + if(my $received_name = btrfs_send_receive($child->{FS_PATH}, $droot, $latest_common_src->{FS_PATH}, $config_target)) { + $latest_common_src = $child; + DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$child->{FS_PATH}"); + + $config_target->{subvol_created_resume} //= []; + push(@{$config_target->{subvol_created_resume}}, $received_name); + } + else { + $config_target->{ABORTED} = "btrfs send/receive command failed"; + } + $found_missing++; + } + } + + unless($found_missing) { + INFO "No missing backups found"; + } + } + + # create backup from latest common + INFO "Creating subvolume backup ($target_type) for: $sroot/$svol"; + INFO "Using previously created snapshot: $snapshot"; + if($latest_common_src && $latest_common_target) { my $parent_snap = $latest_common_src->{FS_PATH}; INFO "Incremental from parent snapshot: $parent_snap"; @@ -1540,6 +1574,9 @@ MAIN: } } else { + # TODO: fix this + WARN "Option resume_missing not supported for non-incremental backups" if(config_key($config_target, "resume_missing")); + INFO "Creating full backup (option \"incremental\" is not set)"; $config_target->{subvol_non_incremental} = 1; $success = btrfs_send_receive($snapshot, $droot, undef, $config_target); @@ -1663,6 +1700,12 @@ MAIN: print "Backup Summary ($version_info)\n\n"; print " Date: " . localtime($start_time) . "\n"; print " Config: $config->{SRC_FILE}\n"; + print "\nLegend:\n"; + print " +++ created subvolume (snapshot)\n"; + print " --- deleted subvolume\n"; + print " *** received subvolume (non-incremental)\n"; + print " >>> received subvolume (incremental)\n"; + print " %>> received subvolume (incremental, resume_missing)\n"; print "--------------------------------------------------------------------------------"; foreach my $config_vol (@{$config->{VOLUME}}) { @@ -1697,8 +1740,13 @@ MAIN: # print(($_->{preserve} ? "===" : "---") . " $_->{name}\n"); # } # } + + # print the resumed backups (resume_missing) + print "%>> $_\n" foreach(@{$config_target->{subvol_created_resume} // []}); + my $create_mode = ($config_target->{subvol_non_incremental} ? "***" : ">>>"); print "$create_mode $config_target->{subvol_created}\n" if($config_target->{subvol_created}); + if($config_target->{subvol_deleted}) { print "--- $_\n" foreach(sort { $b cmp $a} @{$config_target->{subvol_deleted}}); } From 5255a6b6d18877ae0f484f9fc78c93d7e82e5129 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 13:42:45 +0200 Subject: [PATCH 02/10] documentation: added option "resume_missing" to btrbk.conf.example --- btrbk.conf.example | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/btrbk.conf.example b/btrbk.conf.example index 8d74217..7871492 100644 --- a/btrbk.conf.example +++ b/btrbk.conf.example @@ -16,13 +16,16 @@ # snapshot_dir _btrbk_snap -# Always create snapshots, even if the target volume is unreachable -snapshot_create_always yes - # Perform incremental backups (set to "strict" if you want to prevent # creation of initial backups if no parent is found) incremental yes +# Always create snapshots, even if the target volume is unreachable +snapshot_create_always yes + +# Resume missing backups of snapshots +resume_missing yes + # ssh key for ssh volumes/targets ssh_identity /etc/btrbk/ssh/id_ed25519 ssh_user root From 81aef9e5b51a355c050c6fc5d862716d586386af Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 16:20:45 +0200 Subject: [PATCH 03/10] btrbk: added function get_date_tag() --- btrbk | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/btrbk b/btrbk index 805f616..10954ef 100755 --- a/btrbk +++ b/btrbk @@ -836,6 +836,19 @@ sub btrfs_send_receive($$$$;$) } +sub get_date_tag($) +{ + my $name = shift; + $name =~ s/_([0-9]+)$//; + my $postfix_counter = $1; + my $date = undef; + if($name =~ /\.([0-9]{4})([0-9]{2})([0-9]{2})$/) { + $date = [ $1, $2, $3 ]; + } + return ($date, $postfix_counter); +} + + sub get_snapshot_children($$) { my $sroot = shift || die; @@ -1628,9 +1641,9 @@ MAIN: INFO "Cleaning backups of subvolume \"$sroot/$svol\": $droot/$svol.*"; my @schedule; foreach my $vol (keys %{$vol_info{$droot}}) { - if($vol =~ /^$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/) { - push(@schedule, { name => "$droot/$vol", sort => $vol, date => [ $1, $2, $3 ] }); - } + my ($date, undef) = get_date_tag($vol); + next unless($date && ($vol =~ /^svol\./)); + push(@schedule, { name => "$droot/$vol", sort => $vol, date => $date }); } my @delete = schedule_deletion( schedule => \@schedule, @@ -1662,9 +1675,9 @@ MAIN: INFO "Cleaning snapshots: $sroot/$snapdir$svol.*"; my @schedule; foreach my $vol (keys %{$vol_info{$sroot}}) { - if($vol =~ /^$snapdir$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/) { - push(@schedule, { name => "$sroot/$vol", sort => $vol, date => [ $1, $2, $3 ] }); - } + my ($date, undef) = get_date_tag($vol); + next unless($date && ($vol =~ /^$snapdir$svol\./)); + push(@schedule, { name => "$sroot/$vol", sort => $vol, date => $date }); } my @delete = schedule_deletion( schedule => \@schedule, From 0856a8f68e01b1148a3e20b3596e85bac70ba3b5 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 19:07:33 +0200 Subject: [PATCH 04/10] btrbk: refactored backup creation: check target preserve rules before resuming backups --- btrbk | 168 +++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 102 insertions(+), 66 deletions(-) diff --git a/btrbk b/btrbk index 10954ef..21c6a9f 100755 --- a/btrbk +++ b/btrbk @@ -802,7 +802,7 @@ sub btrfs_subvolume_delete($@) } -sub btrfs_send_receive($$$$;$) +sub btrfs_send_receive($$$;$) { my $src = shift || die; my $target = shift || die; @@ -836,6 +836,49 @@ sub btrfs_send_receive($$$$;$) } +# sets $config->{ABORTED} on failure +# sets $config->{subvol_received} +sub macro_send_receive($@) +{ + my $config = shift || die; + my %info = @_; + + my $incremental = config_key($config, "incremental"); + + INFO "Using previously created snapshot: $info{src}"; + + if($incremental) + { + # create backup from latest common + if($info{parent}) { + INFO "Incremental from parent snapshot: $info{parent}"; + } + elsif($incremental ne "strict") { + INFO "No common parent subvolume present, creating full backup"; + } + else { + WARN "Backup to $info{target} failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\""; + $config->{ABORTED} = "No common parent subvolume found, and option \"incremental\" is set to \"strict\""; + return undef; + } + } + else { + INFO "Option \"incremental\" is not set, creating full backup"; + delete $info{parent}; + } + + $info{received_name} = btrfs_send_receive($info{src}, $info{target}, $info{parent}, $config); + if($info{received_name}) { + $config->{subvol_received} //= []; + push(@{$config->{subvol_received}}, \%info); + return 1; + } else { + $config->{ABORTED} = "btrfs send/receive command failed"; + } + return undef; +} + + sub get_date_tag($) { my $name = shift; @@ -1525,84 +1568,75 @@ MAIN: my $droot = $config_target->{droot} || die; my $target_type = $config_target->{target_type} || die; - my $success = 0; if($target_type eq "send-receive") { if(config_key($config_target, "receive_log")) { WARN "Ignoring deprecated option \"receive_log\" for target: $droot" } - my $incremental = config_key($config_target, "incremental"); - if($incremental) # TODO: fix this + my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); + my $parent_snap; + $parent_snap = $latest_common_src->{FS_PATH} if($latest_common_src); + + # resume missing backups (resume_missing) + if(config_key($config_target, "resume_missing")) { - my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); + INFO "Checking for missing backups of subvolume \"$sroot/$svol\": $droot/"; + my $found_missing = 0; + foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) { + if(scalar get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { + DEBUG "Found matching receive target for: $child->{FS_PATH}"; + } + else { + DEBUG "No matching receive targets found for: $child->{FS_PATH}"; - # resume missing backups (resume_missing) - # TODO: non-incremental - if(config_key($config_target, "resume_missing")) { - INFO "Checking for missing backups of subvolume \"$sroot/$svol\": $droot/"; - my $found_missing = 0; - foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) { - if(scalar get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { - DEBUG "Found matching receive target for: $child->{FS_PATH}"; - } - else { - DEBUG "No matching receive targets found for: $child->{FS_PATH}"; - INFO "Resuming backup of: $child->{FS_PATH}"; - if(my $received_name = btrfs_send_receive($child->{FS_PATH}, $droot, $latest_common_src->{FS_PATH}, $config_target)) { - $latest_common_src = $child; - DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$child->{FS_PATH}"); - - $config_target->{subvol_created_resume} //= []; - push(@{$config_target->{subvol_created_resume}}, $received_name); + # check if the target would be preserved + my ($date, undef) = get_date_tag($child->{SUBVOL_PATH}); + next unless($date); + unless(scalar schedule_deletion( + schedule => [ { name => $child->{FS_PATH}, sort => $child->{SUBVOL_PATH}, date => $date } ], + today => \@today, + preserve_day_of_week => config_key($config_target, "preserve_day_of_week"), + preserve_daily => config_key($config_target, "target_preserve_daily"), + preserve_weekly => config_key($config_target, "target_preserve_weekly"), + preserve_monthly => config_key($config_target, "target_preserve_monthly"), + )) + { + INFO "Resuming subvolume backup of: $child->{FS_PATH}"; + if(macro_send_receive($config_target, + src => $child->{FS_PATH}, + target => $droot, + parent => $parent_snap, + resume => 1, # propagated to $config_target->{subvol_received} + )) { + $parent_snap = $child->{FS_PATH}; + DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$parent_snap"); } else { - $config_target->{ABORTED} = "btrfs send/receive command failed"; + ERROR("Error while resuming backups, aborting resume chain"); + last; } $found_missing++; } } - - unless($found_missing) { - INFO "No missing backups found"; - } } - # create backup from latest common - INFO "Creating subvolume backup ($target_type) for: $sroot/$svol"; - INFO "Using previously created snapshot: $snapshot"; - - if($latest_common_src && $latest_common_target) { - my $parent_snap = $latest_common_src->{FS_PATH}; - INFO "Incremental from parent snapshot: $parent_snap"; - $success = btrfs_send_receive($snapshot, $droot, $parent_snap, $config_target); - } - elsif($incremental ne "strict") { - INFO "No common parent subvolume present, creating full backup"; - $config_target->{subvol_non_incremental} = 1; - $success = btrfs_send_receive($snapshot, $droot, undef, $config_target); - } - else { - WARN "Backup to $droot failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\""; + unless($found_missing) { + INFO "No missing backups found"; } } - else { - # TODO: fix this - WARN "Option resume_missing not supported for non-incremental backups" if(config_key($config_target, "resume_missing")); - INFO "Creating full backup (option \"incremental\" is not set)"; - $config_target->{subvol_non_incremental} = 1; - $success = btrfs_send_receive($snapshot, $droot, undef, $config_target); - } + # finally create the latest backup + INFO "Creating subvolume backup (send-receive) for: $sroot/$svol"; + macro_send_receive($config_target, + src => $snapshot, + target => $droot, + parent => $parent_snap + ); } else { ERROR "Unknown target type \"$target_type\", skipping: $sroot/$svol"; - } - if($success) { - $config_target->{subvol_created} = "$droot/$snapshot_name"; - } - else { - $config_target->{ABORTED} = "btrfs send/receive command failed"; + $config_target->{ABORTED} = "Unknown target type \"$target_type\""; } } } @@ -1744,25 +1778,27 @@ MAIN: } foreach my $config_target (@{$config_subvol->{TARGET}}) { - if($config_target->{ABORTED}) { - print "!!! Target \"$config_target->{droot}\" aborted: $config_target->{ABORTED}\n"; - $err_count++ unless($config_target->{ABORTED_NOERR}); - } # if($config_target->{schedule}) { # foreach (sort { $a->{sort} cmp $b->{sort} } @{$config_target->{schedule}}) { # print(($_->{preserve} ? "===" : "---") . " $_->{name}\n"); # } # } - # print the resumed backups (resume_missing) - print "%>> $_\n" foreach(@{$config_target->{subvol_created_resume} // []}); - - my $create_mode = ($config_target->{subvol_non_incremental} ? "***" : ">>>"); - print "$create_mode $config_target->{subvol_created}\n" if($config_target->{subvol_created}); + foreach(@{$config_target->{subvol_received} // []}) { + next unless $_; + my $create_mode = $_->{parent} ? ">>>" : "***"; + substr($create_mode, 0, 1, '%') if($_->{resume}); + print "$create_mode $_->{received_name}\n"; + } if($config_target->{subvol_deleted}) { print "--- $_\n" foreach(sort { $b cmp $a} @{$config_target->{subvol_deleted}}); } + + if($config_target->{ABORTED}) { + print "!!! Target \"$config_target->{droot}\" aborted: $config_target->{ABORTED}\n"; + $err_count++ unless($config_target->{ABORTED_NOERR}); + } } } } From 889d7f02890506c3e844e9d10d6b2abc3813ea91 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 19:58:24 +0200 Subject: [PATCH 05/10] btrbk: dont log scheduling info when checking for missing backups --- btrbk | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/btrbk b/btrbk index 21c6a9f..ae0f65d 100755 --- a/btrbk +++ b/btrbk @@ -842,10 +842,9 @@ sub macro_send_receive($@) { my $config = shift || die; my %info = @_; - my $incremental = config_key($config, "incremental"); - INFO "Using previously created snapshot: $info{src}"; + INFO "Receiving from snapshot: $info{src}"; if($incremental) { @@ -1006,10 +1005,16 @@ sub schedule_deletion(@) my $preserve_daily = $args{preserve_daily} // die; my $preserve_weekly = $args{preserve_weekly} // die; my $preserve_monthly = $args{preserve_monthly} // die; + my $log_verbose = $args{log_verbose}; - INFO "Filter scheme: preserving all within $preserve_daily days"; - INFO "Filter scheme: preserving first in week (starting on $preserve_day_of_week), for $preserve_weekly weeks"; - INFO "Filter scheme: preserving last weekly of month, for $preserve_monthly months"; + if($log_verbose) { + INFO "Filter scheme: preserving all within $preserve_daily days"; + INFO "Filter scheme: preserving first in week (starting on $preserve_day_of_week), for $preserve_weekly weeks"; + INFO "Filter scheme: preserving last weekly of month, for $preserve_monthly months"; + } + else { + DEBUG "Filter scheme: $preserve_day_of_week: $preserve_daily:$preserve_weekly:$preserve_monthly"; + } # first, do our calendar calculations # note: our week starts on $preserve_day_of_week @@ -1059,10 +1064,10 @@ sub schedule_deletion(@) foreach my $href (sort { $a->{sort} cmp $b->{sort} } @$schedule) { if($href->{preserve}) { - INFO "=== $href->{sort}: $href->{preserve}"; + INFO "=== $href->{sort}: $href->{preserve}" if($log_verbose); } else { - INFO "<<< $href->{sort}"; + INFO "<<< $href->{sort}" if($log_verbose); push(@delete, $href->{name}); } } @@ -1593,7 +1598,7 @@ MAIN: # check if the target would be preserved my ($date, undef) = get_date_tag($child->{SUBVOL_PATH}); next unless($date); - unless(scalar schedule_deletion( + if(scalar schedule_deletion( schedule => [ { name => $child->{FS_PATH}, sort => $child->{SUBVOL_PATH}, date => $date } ], today => \@today, preserve_day_of_week => config_key($config_target, "preserve_day_of_week"), @@ -1602,7 +1607,11 @@ MAIN: preserve_monthly => config_key($config_target, "target_preserve_monthly"), )) { - INFO "Resuming subvolume backup of: $child->{FS_PATH}"; + DEBUG "Target would have been deleted by target_perserve rules, skipping resume of: $child->{FS_PATH}"; + } + else + { + INFO "Resuming subvolume backup (send-receive) for: $child->{FS_PATH}"; if(macro_send_receive($config_target, src => $child->{FS_PATH}, target => $droot, @@ -1621,7 +1630,9 @@ MAIN: } } - unless($found_missing) { + if($found_missing) { + INFO "Resumed $found_missing backups"; + } else { INFO "No missing backups found"; } } @@ -1686,6 +1697,7 @@ MAIN: preserve_daily => config_key($config_target, "target_preserve_daily"), preserve_weekly => config_key($config_target, "target_preserve_weekly"), preserve_monthly => config_key($config_target, "target_preserve_monthly"), + log_verbose => 1, ); my $ret = btrfs_subvolume_delete($config_target, @delete); if(defined($ret)) { @@ -1720,6 +1732,7 @@ MAIN: preserve_daily => config_key($config_subvol, "snapshot_preserve_daily"), preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"), preserve_monthly => config_key($config_subvol, "snapshot_preserve_monthly"), + log_verbose => 1, ); my $ret = btrfs_subvolume_delete($config_subvol, @delete); if(defined($ret)) { From 679a96495ac54ff4ece428529a717c68fad32775 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 20:36:10 +0200 Subject: [PATCH 06/10] btrbk: show subvolumes names failed on send-receive in backup summary --- btrbk | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/btrbk b/btrbk index ae0f65d..5cca8db 100755 --- a/btrbk +++ b/btrbk @@ -832,7 +832,7 @@ sub btrfs_send_receive($$$;$) ERROR "Failed to send/receive btrfs subvolume: $src " . ($real_parent ? "[$real_parent]" : "") . " -> $target"; return undef; } - return "$target/$src_name"; + return 1; } @@ -846,6 +846,13 @@ sub macro_send_receive($@) INFO "Receiving from snapshot: $info{src}"; + # add info to $config->{subvol_received} + my $src_name = $info{src}; + $src_name =~ s/^.*\///; + $info{received_name} = "$info{target}/$src_name"; + $config->{subvol_received} //= []; + push(@{$config->{subvol_received}}, \%info); + if($incremental) { # create backup from latest common @@ -857,6 +864,7 @@ sub macro_send_receive($@) } else { WARN "Backup to $info{target} failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\""; + $info{ERROR} = 1; $config->{ABORTED} = "No common parent subvolume found, and option \"incremental\" is set to \"strict\""; return undef; } @@ -866,15 +874,13 @@ sub macro_send_receive($@) delete $info{parent}; } - $info{received_name} = btrfs_send_receive($info{src}, $info{target}, $info{parent}, $config); - if($info{received_name}) { - $config->{subvol_received} //= []; - push(@{$config->{subvol_received}}, \%info); + if(btrfs_send_receive($info{src}, $info{target}, $info{parent}, $config)) { return 1; } else { + $info{ERROR} = 1; $config->{ABORTED} = "btrfs send/receive command failed"; + return undef; } - return undef; } @@ -1586,9 +1592,12 @@ MAIN: # resume missing backups (resume_missing) if(config_key($config_target, "resume_missing")) { - INFO "Checking for missing backups of subvolume \"$sroot/$svol\": $droot/"; + INFO "Checking for missing backups of subvolume \"$sroot/$svol\" in: $droot/"; my $found_missing = 0; - foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) { + foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) + { + last if($config_target->{ABORTED}); + if(scalar get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { DEBUG "Found matching receive target for: $child->{FS_PATH}"; } @@ -1611,6 +1620,7 @@ MAIN: } else { + $found_missing++; INFO "Resuming subvolume backup (send-receive) for: $child->{FS_PATH}"; if(macro_send_receive($config_target, src => $child->{FS_PATH}, @@ -1622,10 +1632,9 @@ MAIN: DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$parent_snap"); } else { - ERROR("Error while resuming backups, aborting resume chain"); - last; + # note: ABORTED flag is already set by macro_send_receive() + ERROR("Error while resuming backups, aborting"); } - $found_missing++; } } } @@ -1637,6 +1646,9 @@ MAIN: } } + # skip creation if resume_missing failed + next if($config_target->{ABORTED}); + # finally create the latest backup INFO "Creating subvolume backup (send-receive) for: $sroot/$svol"; macro_send_receive($config_target, @@ -1798,9 +1810,10 @@ MAIN: # } foreach(@{$config_target->{subvol_received} // []}) { - next unless $_; - my $create_mode = $_->{parent} ? ">>>" : "***"; + my $create_mode = "***"; + $create_mode = ">>>" if($_->{parent}); substr($create_mode, 0, 1, '%') if($_->{resume}); + $create_mode = "!!!" if($_->{ERROR}); print "$create_mode $_->{received_name}\n"; } From 956c028435853faf22f058943d30b3de9f40a021 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Tue, 31 Mar 2015 21:45:21 +0200 Subject: [PATCH 07/10] btrbk: find correct latest common snapshot when resuming backups. Note that we then chain the backups, assuming that the previous snapshot is automatically the parent for the next one, which is not always true --- btrbk | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/btrbk b/btrbk index 5cca8db..5c57982 100755 --- a/btrbk +++ b/btrbk @@ -931,18 +931,26 @@ sub get_receive_targets_by_uuid($$) } -sub get_latest_common($$$) +sub get_latest_common($$$;$) { my $sroot = shift || die; my $svol = shift || die; my $droot = shift || die; + my $threshold_gen = shift; # skip all snapshot children with generation >= $threshold_gen die("source subvolume info not present: $sroot") unless($vol_info{$sroot}); die("target subvolume info not present: $droot") unless($vol_info{$droot}); + TRACE "get_latest_common: threshold_gen=$threshold_gen" if($threshold_gen); + # sort children of svol descending by generation foreach my $child (sort { $b->{node}->{gen} <=> $a->{node}->{gen} } get_snapshot_children($sroot, $svol)) { TRACE "get_latest_common: checking source snapshot: $child->{SUBVOL_PATH}"; + if($threshold_gen && ($child->{node}->{gen} >= $threshold_gen)) { + TRACE "get_latest_common: skipped gen=$child->{node}->{gen} >= $threshold_gen: $child->{SUBVOL_PATH}"; + next; + } + if($vol_btrfs_progs_compat{$droot}) { # guess matches by subvolume name (node->received_uuid is not available if BTRFS_PROGS_COMPAT is set) my $child_name = $child->{node}->{REL_PATH}; @@ -1585,16 +1593,15 @@ MAIN: WARN "Ignoring deprecated option \"receive_log\" for target: $droot" } - my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); - my $parent_snap; - $parent_snap = $latest_common_src->{FS_PATH} if($latest_common_src); + my $parent_snap = ""; # resume missing backups (resume_missing) if(config_key($config_target, "resume_missing")) { INFO "Checking for missing backups of subvolume \"$sroot/$svol\" in: $droot/"; my $found_missing = 0; - foreach my $child (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_snapshot_children($sroot, $svol)) + # sort children of svol ascending by generation + foreach my $child (sort { $a->{node}->{gen} <=> $b->{node}->{gen} } get_snapshot_children($sroot, $svol)) { last if($config_target->{ABORTED}); @@ -1621,6 +1628,13 @@ MAIN: else { $found_missing++; + + unless($parent_snap) { + my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{node}->{gen}); + $parent_snap = $latest_common_src->{FS_PATH} if($latest_common_src); + DEBUG("Set latest common snapshots for: $child->{FS_PATH}: src=$parent_snap"); + } + INFO "Resuming subvolume backup (send-receive) for: $child->{FS_PATH}"; if(macro_send_receive($config_target, src => $child->{FS_PATH}, @@ -1628,6 +1642,8 @@ MAIN: parent => $parent_snap, resume => 1, # propagated to $config_target->{subvol_received} )) { + + # NOTE: we assume that the previous snapshot is automatically the parent for the next one. $parent_snap = $child->{FS_PATH}; DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$parent_snap"); } From 57db10112a2d93600bb2b5d8729ed1731bb2e19c Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 1 Apr 2015 13:25:24 +0200 Subject: [PATCH 08/10] btrbk: fixed finding of correct latest common snapshot when resuming backups: keep track of received subvolumes and use them for later --- btrbk | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/btrbk b/btrbk index 5c57982..8dd4fdb 100755 --- a/btrbk +++ b/btrbk @@ -941,7 +941,8 @@ sub get_latest_common($$$;$) die("source subvolume info not present: $sroot") unless($vol_info{$sroot}); die("target subvolume info not present: $droot") unless($vol_info{$droot}); - TRACE "get_latest_common: threshold_gen=$threshold_gen" if($threshold_gen); + my $debug_src = "$sroot/$svol"; + $debug_src .= "@" . $threshold_gen if($threshold_gen); # sort children of svol descending by generation foreach my $child (sort { $b->{node}->{gen} <=> $a->{node}->{gen} } get_snapshot_children($sroot, $svol)) { @@ -951,6 +952,12 @@ sub get_latest_common($$$;$) next; } + if($child->{RECEIVE_TARGET_PRESENT} && ($child->{RECEIVE_TARGET_PRESENT} eq $droot)) { + # little hack to keep track of previously received subvolumes + DEBUG("Latest common snapshots for: $debug_src: src=$child->{FS_PATH} target="); + return ($child, undef); + } + if($vol_btrfs_progs_compat{$droot}) { # guess matches by subvolume name (node->received_uuid is not available if BTRFS_PROGS_COMPAT is set) my $child_name = $child->{node}->{REL_PATH}; @@ -959,7 +966,7 @@ sub get_latest_common($$$;$) my $backup_name = $backup->{node}->{REL_PATH}; $backup_name =~ s/^.*\///; # strip path if($backup_name eq $child_name) { - DEBUG("Latest common snapshots for: $sroot/$svol: src=$child->{FS_PATH} target=$backup->{FS_PATH} (NOTE: guessed by subvolume name)"); + DEBUG("Latest common snapshots for: $debug_src: src=$child->{FS_PATH} target=$backup->{FS_PATH} (NOTE: guessed by subvolume name)"); return ($child, $backup); } } @@ -967,13 +974,13 @@ sub get_latest_common($$$;$) else { foreach (get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { TRACE "get_latest_common: found receive target: $_->{FS_PATH}"; - DEBUG("Latest common snapshots for: $sroot/$svol: src=$child->{FS_PATH} target=$_->{FS_PATH}"); + DEBUG("Latest common snapshots for: $debug_src: src=$child->{FS_PATH} target=$_->{FS_PATH}"); return ($child, $_); } } TRACE "get_latest_common: no matching targets found for: $child->{FS_PATH}"; } - DEBUG("No common snapshots for \"$sroot/$svol\" found in src=$sroot/ target=$droot/"); + DEBUG("No common snapshots for \"$debug_src\" found in src=$sroot/ target=$droot/"); return (undef, undef); } @@ -1628,12 +1635,8 @@ MAIN: else { $found_missing++; - - unless($parent_snap) { - my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{node}->{gen}); - $parent_snap = $latest_common_src->{FS_PATH} if($latest_common_src); - DEBUG("Set latest common snapshots for: $child->{FS_PATH}: src=$parent_snap"); - } + my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{node}->{gen}); + $parent_snap = $latest_common_src->{FS_PATH} if($latest_common_src); INFO "Resuming subvolume backup (send-receive) for: $child->{FS_PATH}"; if(macro_send_receive($config_target, @@ -1641,11 +1644,10 @@ MAIN: target => $droot, parent => $parent_snap, resume => 1, # propagated to $config_target->{subvol_received} - )) { - - # NOTE: we assume that the previous snapshot is automatically the parent for the next one. - $parent_snap = $child->{FS_PATH}; - DEBUG("Updated latest common snapshots for: $sroot/$svol: src=$parent_snap"); + )) + { + # tag the source snapshot, so that get_latest_common() above can make use of the newly received subvolume + $child->{RECEIVE_TARGET_PRESENT} = $droot; } else { # note: ABORTED flag is already set by macro_send_receive() @@ -1665,8 +1667,10 @@ MAIN: # skip creation if resume_missing failed next if($config_target->{ABORTED}); - # finally create the latest backup + # finally receive the previously created snapshot INFO "Creating subvolume backup (send-receive) for: $sroot/$svol"; + my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); + $parent_snap = $latest_common_src ? $latest_common_src->{FS_PATH} : undef; macro_send_receive($config_target, src => $snapshot, target => $droot, From 2ca53296a1c84685f4c609f308c2d179eff0d375 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 1 Apr 2015 13:26:10 +0200 Subject: [PATCH 09/10] btrbk: cosmetics: changed some log statements --- btrbk | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/btrbk b/btrbk index 8dd4fdb..00030f4 100755 --- a/btrbk +++ b/btrbk @@ -903,13 +903,13 @@ sub get_snapshot_children($$) my $svol = shift || die; my $svol_node = subvol($sroot, $svol); die("subvolume info not present: $sroot/$svol") unless($svol_node); - DEBUG "Getting snapshot children of: $sroot/$svol"; my @ret; foreach (values %{$vol_info{$sroot}}) { next unless($_->{node}->{parent_uuid} eq $svol_node->{uuid}); - DEBUG "Found snapshot child: $_->{SUBVOL_PATH}"; + TRACE "get_snapshot_children: Found snapshot child: $_->{SUBVOL_PATH}"; push(@ret, $_); } + DEBUG "Found " . scalar(@ret) . " snapshot children of: $sroot/$svol"; return @ret; } @@ -920,13 +920,13 @@ sub get_receive_targets_by_uuid($$) my $uuid = shift || die; die("root subvolume info not present: $droot") unless($vol_info{$droot}); die("subvolume info not present: $uuid") unless($uuid_info{$uuid}); - DEBUG "Getting receive targets in \"$droot/\" for: $uuid_info{$uuid}->{path}"; my @ret; foreach (values %{$vol_info{$droot}}) { next unless($_->{node}->{received_uuid} eq $uuid); - DEBUG "Found receive target: $_->{SUBVOL_PATH}"; + TRACE "get_receive_targets_by_uuid: Found receive target: $_->{SUBVOL_PATH}"; push(@ret, $_); } + DEBUG "Found " . scalar(@ret) . " receive targets in \"$droot/\" for: $uuid_info{$uuid}->{path}"; return @ret; } @@ -1033,15 +1033,12 @@ sub schedule_deletion(@) INFO "Filter scheme: preserving first in week (starting on $preserve_day_of_week), for $preserve_weekly weeks"; INFO "Filter scheme: preserving last weekly of month, for $preserve_monthly months"; } - else { - DEBUG "Filter scheme: $preserve_day_of_week: $preserve_daily:$preserve_weekly:$preserve_monthly"; - } # first, do our calendar calculations # note: our week starts on $preserve_day_of_week my $delta_days_to_eow_from_today = $day_of_week_map{$preserve_day_of_week} - Day_of_Week(@today) - 1; $delta_days_to_eow_from_today = $delta_days_to_eow_from_today + 7 if($delta_days_to_eow_from_today < 0); - DEBUG "last day before next $preserve_day_of_week is in $delta_days_to_eow_from_today days"; + TRACE "last day before next $preserve_day_of_week is in $delta_days_to_eow_from_today days"; foreach my $href (@$schedule) { my @date = @{$href->{date}}; @@ -1092,6 +1089,7 @@ sub schedule_deletion(@) push(@delete, $href->{name}); } } + DEBUG "Preserving " . (@$schedule - @delete) . "/" . @$schedule . " items" unless($log_verbose); return @delete; } @@ -1608,15 +1606,18 @@ MAIN: INFO "Checking for missing backups of subvolume \"$sroot/$svol\" in: $droot/"; my $found_missing = 0; # sort children of svol ascending by generation - foreach my $child (sort { $a->{node}->{gen} <=> $b->{node}->{gen} } get_snapshot_children($sroot, $svol)) + foreach my $child (sort { $a->{node}->{gen} <=> $b->{node}->{gen} } get_snapshot_children($sroot, $svol)) { last if($config_target->{ABORTED}); + DEBUG "Checking for missing receive targets for \"$child->{FS_PATH}\" in: $droot/"; + + # TODO: fix for btrfs_progs_compat if(scalar get_receive_targets_by_uuid($droot, $child->{node}->{uuid})) { - DEBUG "Found matching receive target for: $child->{FS_PATH}"; + DEBUG "Found matching receive target, skipping: $child->{FS_PATH}"; } else { - DEBUG "No matching receive targets found for: $child->{FS_PATH}"; + DEBUG "No matching receive targets found, checking schedule for: $child->{FS_PATH}"; # check if the target would be preserved my ($date, undef) = get_date_tag($child->{SUBVOL_PATH}); @@ -1793,11 +1794,11 @@ MAIN: print " Date: " . localtime($start_time) . "\n"; print " Config: $config->{SRC_FILE}\n"; print "\nLegend:\n"; - print " +++ created subvolume (snapshot)\n"; - print " --- deleted subvolume\n"; + print " +++ created subvolume (source snapshot)\n"; + print " --- deleted subvolume (source snapshot)\n"; print " *** received subvolume (non-incremental)\n"; print " >>> received subvolume (incremental)\n"; - print " %>> received subvolume (incremental, resume_missing)\n"; + # print " %>> received subvolume (incremental, resume_missing)\n"; print "--------------------------------------------------------------------------------"; foreach my $config_vol (@{$config->{VOLUME}}) { @@ -1832,7 +1833,7 @@ MAIN: foreach(@{$config_target->{subvol_received} // []}) { my $create_mode = "***"; $create_mode = ">>>" if($_->{parent}); - substr($create_mode, 0, 1, '%') if($_->{resume}); + # substr($create_mode, 0, 1, '%') if($_->{resume}); $create_mode = "!!!" if($_->{ERROR}); print "$create_mode $_->{received_name}\n"; } From 10b89832966201d5008448bc86771abe70e877e1 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 1 Apr 2015 14:21:50 +0200 Subject: [PATCH 10/10] documentation: added documentation of "resume_missing" option --- ChangeLog | 4 ++++ README.md | 3 ++- btrbk.conf.example | 3 ++- doc/btrbk.1 | 3 ++- doc/btrbk.conf.5 | 20 ++++++++++++++------ 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/ChangeLog b/ChangeLog index f748362..77428d1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -24,5 +24,9 @@ * btrbk-current - added configuration option "btrfs_progs_compat", to be enabled if using btrfs-progs < 3.17. Fixes issue #6 + - added configuration option "resume_missing", for automatic resume + of missing backups - removed configuration option "receive_log" in favor of printing errors from "btrfs receive" + - bugfix: show correct exit code on external command failure + - bugfix: no crash if "commit_delete" option is set to "no" diff --git a/README.md b/README.md index 332f18b..22e9709 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,10 @@ Key Features: - Atomic snapshots - Incremental backups +- Configurable retention policy - Backups to multiple destinations - Transfer via ssh -- Configurable retention policy +- Resume of backups (if backup disk was not attached for a while) - Display file changes between two backups btrbk is intended to be run as a cron job. diff --git a/btrbk.conf.example b/btrbk.conf.example index 7871492..245155e 100644 --- a/btrbk.conf.example +++ b/btrbk.conf.example @@ -23,7 +23,8 @@ incremental yes # Always create snapshots, even if the target volume is unreachable snapshot_create_always yes -# Resume missing backups of snapshots +# Resume missing backups if the target volume is reachable again. +# Useful in conjunction with "snapshot_create_always". resume_missing yes # ssh key for ssh volumes/targets diff --git a/doc/btrbk.1 b/doc/btrbk.1 index feea8d2..efbed72 100644 --- a/doc/btrbk.1 +++ b/doc/btrbk.1 @@ -62,7 +62,8 @@ subvolumes specified in the configuration file. Then, for each specified target, btrbk creates a new backup subvolume, incremental from the latest common snapshot / backup subvolume found. If no common parent subvolume is found, a full backup is -created. +created. This is also done for all missing backups if the +\fIresume_missing\fR option is set. .PP In a last step, previous snapshots and backup subvolumes that are not preserved by their configured retention policy will be deleted. This diff --git a/doc/btrbk.conf.5 b/doc/btrbk.conf.5 index 4fff2f9..34d8298 100644 --- a/doc/btrbk.conf.5 +++ b/doc/btrbk.conf.5 @@ -45,16 +45,24 @@ Directory in which the btrfs snapshots are created. Relative to does not autmatically create this directory, and the snapshot creation will fail if it is not present. .TP -\fBsnapshot_create_always\fR yes|no -If set, the snapshots are always created, even if the backup subvolume -cannot be created (e.g. if the target subvolume cannot be -reached). Defaults to \(lqno\(rq. Useful for subvolumes on laptops to -make sure the snapshots are created even if you are on the road. -.TP \fBincremental\fR yes|no|strict Perform incremental backups. Defaults to \(lqyes\(rq. If set to \(lqstrict\(rq, non-incremental (initial) backups are never created. .TP +\fBsnapshot_create_always\fR yes|no +If set, the snapshots are always created, even if the backup subvolume +cannot be created (e.g. if the target subvolume cannot be +reached). Use in conjunction with the \fIresume_missing\fR option to +make sure that the backups are created as soon as the target subvolume +is reachable again. Useful for laptop filesystems in order to make +sure the snapshots are created even if you are on the road. Defaults +to \(lqno\(rq. +.TP +\fBresume_missing\fR yes|no +If set, the backups in the target directory are compared to the source +snapshots, and missing backups are created if needed (complying to the +target preserve matrix). Defaults to \(lqyes\(rq. +.TP \fBtarget_preserve_daily\fR all| How many days of backups should be preserved. Defaults to \(lqall\(rq. .TP