mirror of https://github.com/digint/btrbk
Merge branch 'target_raw'
commit
e177ae1c87
|
@ -6,8 +6,12 @@ btrbk-current
|
||||||
(machine-readable) output for "(dry)run" and "tree" commands.
|
(machine-readable) output for "(dry)run" and "tree" commands.
|
||||||
* Added "config dump" command (experimental).
|
* Added "config dump" command (experimental).
|
||||||
* Added configuration option "ssh_cipher_spec" (close: #47).
|
* Added configuration option "ssh_cipher_spec" (close: #47).
|
||||||
|
* Added "target raw", with GnuPG and compression support
|
||||||
|
(experimental).
|
||||||
|
* Added configuration option "timestamp_format short|long".
|
||||||
|
* Bugfix: correctly handle "incremental no" option.
|
||||||
* Hardened ssh_filter_btrbk.sh script: fine-grained access control,
|
* Hardened ssh_filter_btrbk.sh script: fine-grained access control,
|
||||||
restrict-path option, sudo option (close: #45)
|
restrict-path option, sudo option (close: #45).
|
||||||
|
|
||||||
btrbk-0.20.0
|
btrbk-0.20.0
|
||||||
|
|
||||||
|
|
37
README.md
37
README.md
|
@ -78,10 +78,10 @@ btrbk is in AUR: https://aur.archlinux.org/packages/btrbk/
|
||||||
Synopsis
|
Synopsis
|
||||||
========
|
========
|
||||||
|
|
||||||
Please consult the [btrbk(1) man-page] provided with this package for a
|
Please consult the [btrbk(1)] man-page provided with this package for a
|
||||||
full description of the command line options.
|
full description of the command line options.
|
||||||
|
|
||||||
[btrbk(1) man-page]: http://www.digint.ch/btrbk/doc/btrbk.html
|
[btrbk(1)]: http://www.digint.ch/btrbk/doc/btrbk.html
|
||||||
|
|
||||||
|
|
||||||
Configuration File
|
Configuration File
|
||||||
|
@ -90,7 +90,7 @@ Configuration File
|
||||||
Before running `btrbk`, you will need to create a configuration
|
Before running `btrbk`, you will need to create a configuration
|
||||||
file. You might want to take a look at `btrbk.conf.example` provided
|
file. You might want to take a look at `btrbk.conf.example` provided
|
||||||
with this package. For a detailed description, please consult the
|
with this package. For a detailed description, please consult the
|
||||||
[btrbk.conf(5) man-page].
|
[btrbk.conf(5)] man-page.
|
||||||
|
|
||||||
When playing around with config-files, it is highly recommended to
|
When playing around with config-files, it is highly recommended to
|
||||||
check the output using the `dryrun` command before executing the
|
check the output using the `dryrun` command before executing the
|
||||||
|
@ -102,7 +102,7 @@ This will read all btrfs information on the source/target filesystems
|
||||||
and show what actions would be performed (without writing anything to
|
and show what actions would be performed (without writing anything to
|
||||||
the disks).
|
the disks).
|
||||||
|
|
||||||
[btrbk.conf(5) man-page]: http://www.digint.ch/btrbk/doc/btrbk.conf.html
|
[btrbk.conf(5)]: http://www.digint.ch/btrbk/doc/btrbk.conf.html
|
||||||
|
|
||||||
|
|
||||||
Example: laptop with usb-disk for backups
|
Example: laptop with usb-disk for backups
|
||||||
|
@ -262,6 +262,35 @@ from 192.168.0.42. The source filesystem is never altered because of
|
||||||
`snapshot_preserve_daily all`.
|
`snapshot_preserve_daily all`.
|
||||||
|
|
||||||
|
|
||||||
|
Example: encrypted backup to non-btrfs target
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
If your backup server does not support btrfs, you can send your
|
||||||
|
subvolumes to a raw file.
|
||||||
|
|
||||||
|
Note: this is an _experimental_ feature!
|
||||||
|
|
||||||
|
/etc/btrbk/btrbk.conf:
|
||||||
|
|
||||||
|
raw_target_compress xz
|
||||||
|
raw_target_encrypt gpg
|
||||||
|
gpg_keyring /etc/btrbk/gpg/pubring.gpg
|
||||||
|
gpg_recipient btrbk@mydomain.com
|
||||||
|
|
||||||
|
volume /mnt/btr_pool
|
||||||
|
subvolume home
|
||||||
|
target raw ssh://myserver.mydomain.com/backup
|
||||||
|
ssh_user btrbk
|
||||||
|
incremental no
|
||||||
|
|
||||||
|
This will create a GnuPG encrypted, compressed file
|
||||||
|
`/backup/home.YYYYMMDD.btrfs_<received_uuid>.xz.gpg` on the target
|
||||||
|
host.
|
||||||
|
|
||||||
|
While incremental backups are also supported for raw targets, this is
|
||||||
|
not recommended (see [btrbk.conf(5)] for details).
|
||||||
|
|
||||||
|
|
||||||
Setting up SSH
|
Setting up SSH
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|
584
btrbk
584
btrbk
|
@ -43,7 +43,7 @@ use strict;
|
||||||
use warnings FATAL => qw( all );
|
use warnings FATAL => qw( all );
|
||||||
|
|
||||||
use Carp qw(confess);
|
use Carp qw(confess);
|
||||||
use Date::Calc qw(Today Delta_Days Day_of_Week);
|
use Date::Calc qw(Today_and_Now Delta_Days Day_of_Week);
|
||||||
use Getopt::Long qw(GetOptions);
|
use Getopt::Long qw(GetOptions);
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
|
|
||||||
|
@ -59,7 +59,9 @@ my $ip_addr_match = qr/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([
|
||||||
my $host_name_match = qr/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/;
|
my $host_name_match = qr/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/;
|
||||||
my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
|
my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
|
||||||
my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
|
my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
|
||||||
my $snapshot_postfix_match = qr/\.[0-9]{8}(_[0-9]+)?/;
|
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
|
||||||
|
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2}))?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm][_NN]"
|
||||||
|
my $raw_postfix_match = qr/--(?<received_uuid>$uuid_match)(\@(?<parent_uuid>$uuid_match))\.btrfs?(\.(?<compress>(gz|bz2|xz)))?(\.(?<encrypt>gpg))?/; # matches ".btrfs_<received_uuid>[@<parent_uuid>][.gz|bz2|xz][.gpg]"
|
||||||
my $group_match = qr/[a-zA-Z0-9_:-]+/;
|
my $group_match = qr/[a-zA-Z0-9_:-]+/;
|
||||||
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
|
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ my %day_of_week_map = ( monday => 1, tuesday => 2, wednesday => 3, thursday => 4
|
||||||
my %config_options = (
|
my %config_options = (
|
||||||
# NOTE: the parser always maps "no" to undef
|
# NOTE: the parser always maps "no" to undef
|
||||||
# NOTE: keys "volume", "subvolume" and "target" are hardcoded
|
# NOTE: keys "volume", "subvolume" and "target" are hardcoded
|
||||||
|
timestamp_format => { default => "short", accept => [ "short", "long" ], context => [ "root", "volume", "subvolume" ] },
|
||||||
snapshot_dir => { default => undef, accept_file => { relative => 1 } },
|
snapshot_dir => { default => undef, accept_file => { relative => 1 } },
|
||||||
snapshot_name => { default => undef, accept_file => { name_only => 1 }, context => [ "subvolume" ] }, # NOTE: defaults to the subvolume name (hardcoded)
|
snapshot_name => { default => undef, accept_file => { name_only => 1 }, context => [ "subvolume" ] }, # NOTE: defaults to the subvolume name (hardcoded)
|
||||||
snapshot_create => { default => "always", accept => [ "no", "always", "ondemand", "onchange" ] },
|
snapshot_create => { default => "always", accept => [ "no", "always", "ondemand", "onchange" ] },
|
||||||
|
@ -86,6 +89,12 @@ my %config_options = (
|
||||||
ssh_port => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
ssh_port => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
||||||
ssh_compression => { default => undef, accept => [ "yes", "no" ] },
|
ssh_compression => { default => undef, accept => [ "yes", "no" ] },
|
||||||
ssh_cipher_spec => { default => "default", accept_regexp => qr/^$ssh_cipher_match(,$ssh_cipher_match)*$/ },
|
ssh_cipher_spec => { default => "default", accept_regexp => qr/^$ssh_cipher_match(,$ssh_cipher_match)*$/ },
|
||||||
|
|
||||||
|
raw_target_compress => { default => undef, accept => [ "no", "gzip", "bzip2", "xz" ] },
|
||||||
|
raw_target_encrypt => { default => undef, accept => [ "no", "gpg" ] },
|
||||||
|
gpg_keyring => { default => undef, accept_file => { absolute => 1 } },
|
||||||
|
gpg_recipient => { default => undef, accept_regexp => qr/^[0-9a-zA-Z_@\+\-\.]+$/ },
|
||||||
|
|
||||||
btrfs_progs_compat => { default => undef, accept => [ "yes", "no" ] },
|
btrfs_progs_compat => { default => undef, accept => [ "yes", "no" ] },
|
||||||
group => { default => undef, accept_regexp => qr/^$group_match(\s*,\s*$group_match)*$/, split => qr/\s*,\s*/ },
|
group => { default => undef, accept_regexp => qr/^$group_match(\s*,\s*$group_match)*$/, split => qr/\s*,\s*/ },
|
||||||
|
|
||||||
|
@ -106,7 +115,7 @@ my %config_options = (
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
my @config_target_types = qw(send-receive);
|
my @config_target_types = qw(send-receive raw);
|
||||||
|
|
||||||
my %root_tree_cache; # map URL to SUBTREE (needed since "btrfs subvolume list" does not provide us with the uuid of the btrfs root node)
|
my %root_tree_cache; # map URL to SUBTREE (needed since "btrfs subvolume list" does not provide us with the uuid of the btrfs root node)
|
||||||
my %vinfo_cache; # map URL to vinfo
|
my %vinfo_cache; # map URL to vinfo
|
||||||
|
@ -232,7 +241,7 @@ sub vinfo($$)
|
||||||
my $name = $url;
|
my $name = $url;
|
||||||
$name =~ s/^.*\///;
|
$name =~ s/^.*\///;
|
||||||
my %info = (
|
my %info = (
|
||||||
URL => $url,
|
URL => $url,
|
||||||
NAME => $name,
|
NAME => $name,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -927,21 +936,21 @@ sub btrfs_subvolume_delete($@)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btrfs_send_receive($$$)
|
sub btrfs_send_receive($$$$)
|
||||||
{
|
{
|
||||||
my $snapshot = shift || die;
|
my $snapshot = shift || die;
|
||||||
my $target = shift || die;
|
my $target = shift || die;
|
||||||
my $parent = shift;
|
my $parent = shift;
|
||||||
|
my $ret_vol_received = shift;
|
||||||
my $snapshot_path = $snapshot->{PATH} // die;
|
my $snapshot_path = $snapshot->{PATH} // die;
|
||||||
my $snapshot_rsh = $snapshot->{RSH};
|
|
||||||
my $target_path = $target->{PATH} // die;
|
my $target_path = $target->{PATH} // die;
|
||||||
my $target_rsh = $target->{RSH};
|
|
||||||
my $parent_path = $parent ? $parent->{PATH} : undef;
|
my $parent_path = $parent ? $parent->{PATH} : undef;
|
||||||
|
|
||||||
my $snapshot_name = $snapshot_path;
|
my $vol_received = vinfo_child($target, $snapshot->{NAME});
|
||||||
$snapshot_name =~ s/^.*\///;
|
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
|
||||||
INFO ">>> $target->{PRINT}/$snapshot_name";
|
|
||||||
print STDOUT "Receiving subvol: $target->{PRINT}/$snapshot_name\n" if($show_progress && (not $dryrun));
|
INFO ">>> $vol_received->{PRINT}";
|
||||||
|
print STDOUT "Receiving subvol: $vol_received->{PRINT}\n" if($show_progress && (not $dryrun));
|
||||||
|
|
||||||
DEBUG "[btrfs] send/receive" . ($parent ? " (incremental)" : " (complete)") . ":";
|
DEBUG "[btrfs] send/receive" . ($parent ? " (incremental)" : " (complete)") . ":";
|
||||||
DEBUG "[btrfs] source: $snapshot->{PRINT}";
|
DEBUG "[btrfs] source: $snapshot->{PRINT}";
|
||||||
|
@ -957,7 +966,7 @@ sub btrfs_send_receive($$$)
|
||||||
my @cmd_pipe;
|
my @cmd_pipe;
|
||||||
push @cmd_pipe, {
|
push @cmd_pipe, {
|
||||||
cmd => [ qw(btrfs send), @send_options, $snapshot_path ],
|
cmd => [ qw(btrfs send), @send_options, $snapshot_path ],
|
||||||
rsh => $snapshot_rsh,
|
rsh => $snapshot->{RSH},
|
||||||
name => "btrfs send",
|
name => "btrfs send",
|
||||||
};
|
};
|
||||||
push @cmd_pipe, {
|
push @cmd_pipe, {
|
||||||
|
@ -965,12 +974,116 @@ sub btrfs_send_receive($$$)
|
||||||
} if($show_progress);
|
} if($show_progress);
|
||||||
push @cmd_pipe, {
|
push @cmd_pipe, {
|
||||||
cmd => [ qw(btrfs receive), @receive_options, $target_path . '/' ],
|
cmd => [ qw(btrfs receive), @receive_options, $target_path . '/' ],
|
||||||
rsh => $target_rsh,
|
rsh => $target->{RSH},
|
||||||
name => "btrfs receive",
|
name => "btrfs receive",
|
||||||
};
|
};
|
||||||
my $ret = run_cmd(@cmd_pipe);
|
my $ret = run_cmd(@cmd_pipe);
|
||||||
unless(defined($ret)) {
|
unless(defined($ret)) {
|
||||||
ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}";
|
ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}";
|
||||||
|
|
||||||
|
# NOTE: btrfs-progs v3.19.1 does not delete garbled received subvolume,
|
||||||
|
# we need to do this by hand.
|
||||||
|
# TODO: remove this as soon as btrfs-progs handle receive errors correctly.
|
||||||
|
DEBUG "send/received failed, deleting (possibly present and garbled) received subvolume: $vol_received->{PRINT}";
|
||||||
|
my $ret = btrfs_subvolume_delete($vol_received, commit => "after");
|
||||||
|
if(defined($ret)) {
|
||||||
|
WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WARN "Deletion of partially received (garbled) subvolume failed, assuming clean environment: $vol_received->{PRINT}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub btrfs_send_to_file($$$$;@)
|
||||||
|
{
|
||||||
|
my $snapshot = shift || die;
|
||||||
|
my $target = shift || die;
|
||||||
|
my $parent = shift;
|
||||||
|
my $ret_vol_received = shift;
|
||||||
|
my %opts = @_;
|
||||||
|
my $snapshot_path = $snapshot->{PATH} // die;
|
||||||
|
my $target_path = $target->{PATH} // die;
|
||||||
|
my $parent_path = $parent ? $parent->{PATH} : undef;
|
||||||
|
my $parent_uuid = $parent ? $parent->{uuid} : undef ;
|
||||||
|
my $received_uuid = $snapshot->{uuid};
|
||||||
|
$received_uuid = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" if((not $received_uuid) && $dryrun);
|
||||||
|
die unless($received_uuid);
|
||||||
|
die if($parent && !$parent_uuid);
|
||||||
|
|
||||||
|
my $target_filename = $snapshot->{NAME} || die;
|
||||||
|
$target_filename .= "--$received_uuid";
|
||||||
|
$target_filename .= '@' . $parent_uuid if($parent_uuid);
|
||||||
|
$target_filename .= ".btrfs";
|
||||||
|
|
||||||
|
my %compress = ( gzip => { pipe => { cmd => [ 'gzip' ], name => 'gzip' }, postfix => '.gz' },
|
||||||
|
bzip2 => { pipe => { cmd => [ 'bzip2' ], name => 'bzip2' }, postfix => '.bz2' },
|
||||||
|
xz => { pipe => { cmd => [ 'xz' ], name => 'xz' }, postfix => '.xz' },
|
||||||
|
);
|
||||||
|
|
||||||
|
my @send_options;
|
||||||
|
push(@send_options, '-v') if($loglevel >= 3);
|
||||||
|
push(@send_options, '-p', $parent_path) if($parent_path);
|
||||||
|
|
||||||
|
my @cmd_pipe;
|
||||||
|
push @cmd_pipe, {
|
||||||
|
cmd => [ qw(btrfs send), @send_options, $snapshot_path ],
|
||||||
|
rsh => $snapshot->{RSH},
|
||||||
|
name => "btrfs send",
|
||||||
|
};
|
||||||
|
push @cmd_pipe, {
|
||||||
|
cmd => [ '/usr/bin/pv' ],
|
||||||
|
} if($show_progress);
|
||||||
|
if($opts{compress}) {
|
||||||
|
die unless($compress{$opts{compress}});
|
||||||
|
$target_filename .= $compress{$opts{compress}}->{postfix};
|
||||||
|
push @cmd_pipe, $compress{$opts{compress}}->{pipe};
|
||||||
|
}
|
||||||
|
if($opts{encrypt}) {
|
||||||
|
die unless($opts{encrypt}->{type} eq "gpg");
|
||||||
|
$target_filename .= '.gpg';
|
||||||
|
my @gpg_options = ( '--batch', '--no-tty', '--trust-model', 'always' );
|
||||||
|
push(@gpg_options, ( '--no-default-keyring', '--keyring', $opts{encrypt}->{keyring} )) if($opts{encrypt}->{keyring});
|
||||||
|
push(@gpg_options, ( '--default-recipient', $opts{encrypt}->{recipient} )) if($opts{encrypt}->{recipient});
|
||||||
|
push @cmd_pipe, {
|
||||||
|
cmd => [ 'gpg', @gpg_options, '--encrypt' ],
|
||||||
|
name => 'gpg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
push @cmd_pipe, {
|
||||||
|
cmd => [ 'dd', 'status=none', "of=$target_path/$target_filename" ],
|
||||||
|
rsh => $target->{RSH},
|
||||||
|
name => 'dd',
|
||||||
|
};
|
||||||
|
|
||||||
|
my $vol_received = vinfo_child($target, $target_filename);
|
||||||
|
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
|
||||||
|
|
||||||
|
INFO ">>> $vol_received->{PRINT}";
|
||||||
|
print STDOUT "Receiving subvol (raw): $vol_received->{PRINT}\n" if($show_progress && (not $dryrun));
|
||||||
|
|
||||||
|
DEBUG "[btrfs] send-to-file" . ($parent ? " (incremental)" : " (complete)") . ":";
|
||||||
|
DEBUG "[btrfs] source: $snapshot->{PRINT}";
|
||||||
|
DEBUG "[btrfs] parent: $parent->{PRINT}" if($parent);
|
||||||
|
DEBUG "[btrfs] target: $target->{PRINT}";
|
||||||
|
|
||||||
|
my $ret = run_cmd(@cmd_pipe);
|
||||||
|
if(defined($ret)) {
|
||||||
|
# Test target file for "exists and size > 0" after writing,
|
||||||
|
# as we can not rely on the exit status of 'dd'
|
||||||
|
DEBUG "Testing target file (non-zero size): $target->{PRINT}";
|
||||||
|
$ret = run_cmd({
|
||||||
|
cmd => ['test', '-s', "$target_path/$target_filename"],
|
||||||
|
rsh => $target->{RSH},
|
||||||
|
name => "test",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unless(defined($ret)) {
|
||||||
|
ERROR "Failed to send btrfs subvolume to raw file: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $vol_received->{PRINT}";
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -1122,6 +1235,7 @@ sub macro_send_receive($@)
|
||||||
my $snapshot = $info{snapshot} || die;
|
my $snapshot = $info{snapshot} || die;
|
||||||
my $target = $info{target} || die;
|
my $target = $info{target} || die;
|
||||||
my $parent = $info{parent};
|
my $parent = $info{parent};
|
||||||
|
my $target_type = $config_target->{target_type} || die;
|
||||||
my $incremental = config_key($config_target, "incremental");
|
my $incremental = config_key($config_target, "incremental");
|
||||||
|
|
||||||
INFO "Receiving from snapshot: $snapshot->{PRINT}";
|
INFO "Receiving from snapshot: $snapshot->{PRINT}";
|
||||||
|
@ -1136,12 +1250,6 @@ sub macro_send_receive($@)
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
# add info to $config->{SUBVOL_RECEIVED}
|
|
||||||
my $vol_received = vinfo_child($target, $snapshot->{NAME});
|
|
||||||
$info{received_subvolume} = $vol_received;
|
|
||||||
$config_target->{SUBVOL_RECEIVED} //= [];
|
|
||||||
push(@{$config_target->{SUBVOL_RECEIVED}}, \%info);
|
|
||||||
|
|
||||||
if($incremental)
|
if($incremental)
|
||||||
{
|
{
|
||||||
# create backup from latest common
|
# create backup from latest common
|
||||||
|
@ -1160,42 +1268,86 @@ sub macro_send_receive($@)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
INFO "Option \"incremental\" is not set, creating full backup";
|
INFO "Option \"incremental\" is not set, creating full backup";
|
||||||
|
$parent = undef;
|
||||||
delete $info{parent};
|
delete $info{parent};
|
||||||
}
|
}
|
||||||
|
|
||||||
if(btrfs_send_receive($snapshot, $target, $parent)) {
|
my $ret;
|
||||||
return 1;
|
my $vol_received;
|
||||||
} else {
|
if($target_type eq "send-receive")
|
||||||
|
{
|
||||||
|
$ret = btrfs_send_receive($snapshot, $target, $parent, \$vol_received);
|
||||||
|
$config_target->{ABORTED} = "Failed to send/receive subvolume" unless($ret);
|
||||||
|
}
|
||||||
|
elsif($target_type eq "raw")
|
||||||
|
{
|
||||||
|
unless($dryrun) {
|
||||||
|
# make sure we know the snapshot uuid
|
||||||
|
unless($snapshot->{uuid}) {
|
||||||
|
DEBUG "Fetching uuid of new snapshot: $snapshot->{PRINT}";
|
||||||
|
my $detail = btrfs_subvolume_detail($snapshot);
|
||||||
|
die unless($detail->{uuid});
|
||||||
|
vinfo_set_detail($snapshot, { uuid => $detail->{uuid} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $compress = config_key($config_target, "raw_target_compress");
|
||||||
|
my $encrypt = undef;
|
||||||
|
my $encrypt_type = config_key($config_target, "raw_target_encrypt");
|
||||||
|
if($encrypt_type) {
|
||||||
|
die unless($encrypt_type eq "gpg");
|
||||||
|
$encrypt = { type => $encrypt_type,
|
||||||
|
keyring => config_key($config_target, "gpg_keyring"),
|
||||||
|
recipient => config_key($config_target, "gpg_recipient"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ret = btrfs_send_to_file($snapshot, $target, $parent, \$vol_received, compress => $compress, encrypt => $encrypt);
|
||||||
|
$config_target->{ABORTED} = "Failed to send subvolume to raw file" unless($ret);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
die "Illegal target type \"$target_type\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
# add info to $config->{SUBVOL_RECEIVED}
|
||||||
|
$info{received_type} = $target_type || die;
|
||||||
|
$info{received_subvolume} = $vol_received || die;
|
||||||
|
$config_target->{SUBVOL_RECEIVED} //= [];
|
||||||
|
push(@{$config_target->{SUBVOL_RECEIVED}}, \%info);
|
||||||
|
|
||||||
|
unless($ret) {
|
||||||
$info{ERROR} = 1;
|
$info{ERROR} = 1;
|
||||||
$config_target->{ABORTED} = "Failed to send/receive subvolume";
|
|
||||||
|
|
||||||
# NOTE: btrfs-progs v3.19.1 does not delete garbled received subvolume,
|
|
||||||
# we need to do this by hand.
|
|
||||||
# TODO: remove this as soon as btrfs-progs handle receive errors correctly.
|
|
||||||
DEBUG "send/received failed, deleting (possibly present and garbled) received subvolume: $vol_received->{PRINT}";
|
|
||||||
my $ret = btrfs_subvolume_delete($vol_received, commit => "after");
|
|
||||||
if(defined($ret)) {
|
|
||||||
WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
WARN "Deletion of partially received (garbled) subvolume failed, assuming clean environment: $vol_received->{PRINT}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub get_date_tag($)
|
# returns { btrbk_date => [ yyyy, mm, dd, hh, mm, <date_ext> ] } or undef
|
||||||
|
# fixed array length of 6, all individually defaulting to 0
|
||||||
|
sub parse_filename($$;$)
|
||||||
{
|
{
|
||||||
my $name = shift;
|
my $file = shift;
|
||||||
$name =~ s/_([0-9]+)$//;
|
my $name_match = shift;
|
||||||
my $postfix_counter = $1 // 0;
|
my $raw_format = shift || 0;
|
||||||
my $date = undef;
|
my %raw_info;
|
||||||
if($name =~ /\.([0-9]{4})([0-9]{2})([0-9]{2})$/) {
|
if($raw_format)
|
||||||
$date = [ $1, $2, $3 ];
|
{
|
||||||
|
return undef unless($file =~ /^\Q$name_match\E$timestamp_postfix_match$raw_postfix_match$/);
|
||||||
|
die unless($+{YYYY} && $+{MM} && $+{DD});
|
||||||
|
return { btrbk_date => [ $+{YYYY}, $+{MM}, $+{DD}, ($+{hh} // 0), ($+{mm} // 0), ($+{NN} // 0) ],
|
||||||
|
received_uuid => $+{received_uuid} // die,
|
||||||
|
parent_uuid => $+{parent_uuid} // '-',
|
||||||
|
ENCRYPT => $+{encrypt} // "",
|
||||||
|
COMPRESS => $+{compress} // "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return undef unless($file =~ /^\Q$name_match\E$timestamp_postfix_match$/);
|
||||||
|
die unless($+{YYYY} && $+{MM} && $+{DD});
|
||||||
|
return { btrbk_date => [ $+{YYYY}, $+{MM}, $+{DD}, ($+{hh} // 0), ($+{mm} // 0), ($+{NN} // 0) ] };
|
||||||
}
|
}
|
||||||
return ($date, $postfix_counter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1360,10 +1512,12 @@ sub schedule(@)
|
||||||
}
|
}
|
||||||
|
|
||||||
# sort the schedule, ascending by date
|
# sort the schedule, ascending by date
|
||||||
my @sorted_schedule = sort { ($a->{date}->[0] <=> $b->{date}->[0]) ||
|
my @sorted_schedule = sort { ($a->{btrbk_date}->[0] <=> $b->{btrbk_date}->[0]) ||
|
||||||
($a->{date}->[1] <=> $b->{date}->[1]) ||
|
($a->{btrbk_date}->[1] <=> $b->{btrbk_date}->[1]) ||
|
||||||
($a->{date}->[2] <=> $b->{date}->[2]) ||
|
($a->{btrbk_date}->[2] <=> $b->{btrbk_date}->[2]) ||
|
||||||
($a->{date_ext} <=> $b->{date_ext})
|
($a->{btrbk_date}->[3] <=> $b->{btrbk_date}->[3]) ||
|
||||||
|
($a->{btrbk_date}->[4] <=> $b->{btrbk_date}->[4]) ||
|
||||||
|
($a->{btrbk_date}->[5] <=> $b->{btrbk_date}->[5])
|
||||||
} @$schedule;
|
} @$schedule;
|
||||||
|
|
||||||
# first, do our calendar calculations
|
# first, do our calendar calculations
|
||||||
|
@ -1373,7 +1527,7 @@ sub schedule(@)
|
||||||
TRACE "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 (@sorted_schedule)
|
foreach my $href (@sorted_schedule)
|
||||||
{
|
{
|
||||||
my @date = @{$href->{date}};
|
my @date = @{$href->{btrbk_date}}[0..2]; # Date::Calc takes: @date = ( yy, mm, dd )
|
||||||
my $delta_days = Delta_Days(@date, @today);
|
my $delta_days = Delta_Days(@date, @today);
|
||||||
my $delta_days_to_eow = $delta_days + $delta_days_to_eow_from_today;
|
my $delta_days_to_eow = $delta_days + $delta_days_to_eow_from_today;
|
||||||
{
|
{
|
||||||
|
@ -1388,7 +1542,7 @@ sub schedule(@)
|
||||||
|
|
||||||
if($preserve_latest && (scalar @sorted_schedule)) {
|
if($preserve_latest && (scalar @sorted_schedule)) {
|
||||||
my $href = $sorted_schedule[-1];
|
my $href = $sorted_schedule[-1];
|
||||||
$href->{preserve} ||= "preserve forced: latest in list";
|
$href->{preserve} ||= $preserve_latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
# filter daily, weekly, monthly
|
# filter daily, weekly, monthly
|
||||||
|
@ -1481,7 +1635,8 @@ MAIN:
|
||||||
Getopt::Long::Configure qw(gnu_getopt);
|
Getopt::Long::Configure qw(gnu_getopt);
|
||||||
$Data::Dumper::Sortkeys = 1;
|
$Data::Dumper::Sortkeys = 1;
|
||||||
my $start_time = time;
|
my $start_time = time;
|
||||||
my @today = Today();
|
my @today_and_now = Today_and_Now();
|
||||||
|
my @today = @today_and_now[0..2];
|
||||||
|
|
||||||
|
|
||||||
my ($config_cmdline, $quiet, $verbose, $preserve_backups, $resume_only);
|
my ($config_cmdline, $quiet, $verbose, $preserve_backups, $resume_only);
|
||||||
|
@ -1969,10 +2124,91 @@ MAIN:
|
||||||
{
|
{
|
||||||
next if($config_target->{ABORTED});
|
next if($config_target->{ABORTED});
|
||||||
my $droot = vinfo($config_target->{url}, $config_target);
|
my $droot = vinfo($config_target->{url}, $config_target);
|
||||||
unless(vinfo_root($droot)) {
|
|
||||||
$config_target->{ABORTED} = "Failed to fetch subvolume detail" . ($err ? ": $err" : "");
|
my $target_type = $config_target->{target_type} || die;
|
||||||
WARN "Skipping target \"$droot->{PRINT}\": $config_target->{ABORTED}";
|
if($target_type eq "send-receive")
|
||||||
next;
|
{
|
||||||
|
unless(vinfo_root($droot)) {
|
||||||
|
$config_target->{ABORTED} = "Failed to fetch subvolume detail" . ($err ? ": $err" : "");
|
||||||
|
WARN "Skipping target \"$droot->{PRINT}\": $config_target->{ABORTED}";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elsif($target_type eq "raw")
|
||||||
|
{
|
||||||
|
DEBUG "Creating raw subvolume list: $droot->{PRINT}";
|
||||||
|
my $ret = run_cmd(
|
||||||
|
# NOTE: check for file size >0, which causes bad (zero-sized) images to be resumed
|
||||||
|
# TODO: fix btrfs_send_to_file() to never create bad images
|
||||||
|
cmd => [ 'find', $droot->{PATH} . '/', '-maxdepth', '1', '-type', 'f', '-size', '+0' ],
|
||||||
|
rsh => $droot->{RSH},
|
||||||
|
# note: use something like this to get the real (link resolved) path
|
||||||
|
# cmd => [ "find", $droot->{PATH} . '/', "-maxdepth", "1", "-name", "$snapshot_basename.\*.raw\*", '-printf', '%f\0', '-exec', 'realpath', '-z', '{}', ';' ],
|
||||||
|
non_destructive => 1,
|
||||||
|
);
|
||||||
|
unless(defined($ret)) {
|
||||||
|
$config_target->{ABORTED} = "Failed to list files from: $droot->{PATH}";
|
||||||
|
WARN "Skipping target \"$droot->{PRINT}\": $config_target->{ABORTED}";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
my %subvol_list;
|
||||||
|
my %parent_uuid_list;
|
||||||
|
foreach my $file (split("\n", $ret))
|
||||||
|
{
|
||||||
|
unless($file =~ /^$file_match$/) {
|
||||||
|
DEBUG "Skipping non-parseable file: \"$file\"";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
unless($file =~ s/^\Q$droot->{PATH}\E\///) {
|
||||||
|
$config_target->{ABORTED} = "Unexpected result from 'find': file \"$file\" is not under \"$droot->{PATH}\"";
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
my $filename_info = parse_filename($file, $snapshot_basename, 1);
|
||||||
|
unless($filename_info) {
|
||||||
|
DEBUG "Skipping file (not btrbk raw): \"$file\"";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fake btrfs subvolume information (received_uuid, parent_uuid) from filename info.
|
||||||
|
#
|
||||||
|
# NOTE: parent_uuid in $filename_info is the "parent of the source subvolume", NOT the
|
||||||
|
# "parent of the received subvolume". We fake the real parent_uuid with the one from
|
||||||
|
# the filename here.
|
||||||
|
my $subvol = vinfo_child($droot, $file);
|
||||||
|
vinfo_set_detail($subvol, $filename_info);
|
||||||
|
|
||||||
|
$subvol_list{$file} = $subvol;
|
||||||
|
$parent_uuid_list{$filename_info->{parent_uuid}} = $subvol if($filename_info->{parent_uuid} ne '-');
|
||||||
|
}
|
||||||
|
if($config_target->{ABORTED}) {
|
||||||
|
WARN "Skipping target \"$droot->{PRINT}\": $config_target->{ABORTED}";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
DEBUG "Found " . scalar(keys %subvol_list) . " raw subvolume backups of: $svol->{PRINT}";
|
||||||
|
$droot->{SUBVOL_LIST} = \%subvol_list;
|
||||||
|
$droot->{REAL_URL} = $droot->{URL}; # ignore links here
|
||||||
|
|
||||||
|
# Make sure that incremental backup chains are never broken:
|
||||||
|
foreach my $subvol (values %subvol_list)
|
||||||
|
{
|
||||||
|
# If restoring a backup from raw btrfs images (using "incremental yes|strict"):
|
||||||
|
# "btrfs send -p parent source > svol.btrfs", the backups
|
||||||
|
# on the target will get corrupted (unusable!) as soon as
|
||||||
|
# an any files in the chain gets deleted.
|
||||||
|
#
|
||||||
|
# We need to make sure btrbk will NEVER delete those:
|
||||||
|
# - svol.<timestamp>--<received_uuid_0>.btrfs : root (full) image
|
||||||
|
# - svol.<timestamp>--<received_uuid-n>[@<received_uuid_n-1>].btrfs : incremental image
|
||||||
|
|
||||||
|
if(my $child = $parent_uuid_list{$subvol->{received_uuid}}) {
|
||||||
|
DEBUG "Found parent/child partners, forcing preserve of: \"$subvol->{PRINT}\", \"$child->{PRINT}\"";
|
||||||
|
$subvol->{FORCE_PRESERVE} = "preserve forced: parent of another raw target";
|
||||||
|
$child->{FORCE_PRESERVE} ||= "preserve forced: child of another raw target";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# TRACE(Data::Dumper->Dump([\%subvol_list], ["vinfo_raw_subvol_list{$droot}"]));
|
||||||
}
|
}
|
||||||
$config_target->{droot} = $droot;
|
$config_target->{droot} = $droot;
|
||||||
|
|
||||||
|
@ -2113,7 +2349,6 @@ MAIN:
|
||||||
#
|
#
|
||||||
# create snapshots
|
# create snapshots
|
||||||
#
|
#
|
||||||
my $timestamp = sprintf("%04d%02d%02d", @today);
|
|
||||||
foreach my $config_vol (@{$config->{VOLUME}})
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
{
|
{
|
||||||
next if($config_vol->{ABORTED});
|
next if($config_vol->{ABORTED});
|
||||||
|
@ -2152,10 +2387,10 @@ MAIN:
|
||||||
elsif($snapshot_create eq "ondemand") {
|
elsif($snapshot_create eq "ondemand") {
|
||||||
# check if at least one target is present
|
# check if at least one target is present
|
||||||
if(scalar grep { not $_->{ABORTED} } @{$config_subvol->{TARGET}}) {
|
if(scalar grep { not $_->{ABORTED} } @{$config_subvol->{TARGET}}) {
|
||||||
DEBUG "Snapshot creation enabled (snapshot_create=ondemand): at least one send-receive target is present";
|
DEBUG "Snapshot creation enabled (snapshot_create=ondemand): at least one target is present";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
INFO "Snapshot creation skipped: snapshot_create=ondemand, and no send-receive target is present for: $svol->{PRINT}";
|
INFO "Snapshot creation skipped: snapshot_create=ondemand, and no target is present for: $svol->{PRINT}";
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2164,6 +2399,9 @@ MAIN:
|
||||||
}
|
}
|
||||||
|
|
||||||
# find unique snapshot name
|
# find unique snapshot name
|
||||||
|
my $timestamp = ((config_key($config_subvol, "timestamp_format") eq "short") ?
|
||||||
|
sprintf("%04d%02d%02d", @today) :
|
||||||
|
sprintf("%04d%02d%02dT%02d%02d", @today_and_now));
|
||||||
my @unconfirmed_target_name;
|
my @unconfirmed_target_name;
|
||||||
my @lookup = keys %{vinfo_subvol_list($sroot)};
|
my @lookup = keys %{vinfo_subvol_list($sroot)};
|
||||||
@lookup = grep s/^\Q$snapdir\E// , @lookup;
|
@lookup = grep s/^\Q$snapdir\E// , @lookup;
|
||||||
|
@ -2219,110 +2457,109 @@ MAIN:
|
||||||
{
|
{
|
||||||
next if($config_target->{ABORTED});
|
next if($config_target->{ABORTED});
|
||||||
my $droot = $config_target->{droot} || die;
|
my $droot = $config_target->{droot} || die;
|
||||||
my $target_type = $config_target->{target_type} || die;
|
|
||||||
|
|
||||||
if($target_type eq "send-receive")
|
#
|
||||||
|
# resume missing backups (resume_missing)
|
||||||
|
#
|
||||||
|
if(config_key($config_target, "resume_missing"))
|
||||||
{
|
{
|
||||||
#
|
INFO "Checking for missing backups of subvolume \"$svol->{PRINT}\" in: $droot->{PRINT}/";
|
||||||
# resume missing backups (resume_missing)
|
my @schedule;
|
||||||
#
|
my $resume_total = 0;
|
||||||
if(config_key($config_target, "resume_missing"))
|
my $resume_success = 0;
|
||||||
{
|
|
||||||
INFO "Checking for missing backups of subvolume \"$svol->{PRINT}\" in: $droot->{PRINT}/";
|
|
||||||
my @schedule;
|
|
||||||
my $resume_total = 0;
|
|
||||||
my $resume_success = 0;
|
|
||||||
|
|
||||||
foreach my $child (sort { $a->{cgen} <=> $b->{cgen} } get_snapshot_children($sroot, $svol))
|
foreach my $child (sort { $a->{cgen} <=> $b->{cgen} } get_snapshot_children($sroot, $svol))
|
||||||
|
{
|
||||||
|
my $filename_info = parse_filename($child->{SUBVOL_PATH}, $snapdir . $snapshot_basename);
|
||||||
|
next unless($filename_info); # ignore non-btrbk files
|
||||||
|
|
||||||
|
if(scalar get_receive_targets($droot, $child)) {
|
||||||
|
DEBUG "Found matching receive target, skipping: $child->{PRINT}";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DEBUG "No matching receive targets found, adding resume candidate: $child->{PRINT}";
|
||||||
|
|
||||||
|
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}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
# check if the target would be preserved
|
||||||
|
push(@schedule, { value => $child,
|
||||||
|
btrbk_date => $filename_info->{btrbk_date},
|
||||||
|
preserve => $child->{FORCE_PRESERVE},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scalar @schedule)
|
||||||
|
{
|
||||||
|
DEBUG "Checking schedule for resume candidates";
|
||||||
|
# add all present backups to schedule, with no value
|
||||||
|
# these are needed for correct results of schedule()
|
||||||
|
foreach my $vol (values %{vinfo_subvol_list($droot)}) {
|
||||||
|
my $filename_info = parse_filename($vol->{SUBVOL_PATH}, $snapshot_basename, ($config_target->{target_type} eq "raw"));
|
||||||
|
next unless($filename_info); # ignore non-btrbk files
|
||||||
|
push(@schedule, { value => undef,
|
||||||
|
btrbk_date => $filename_info->{btrbk_date},
|
||||||
|
preserve => $vol->{FORCE_PRESERVE},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
my ($preserve, undef) = schedule(
|
||||||
|
schedule => \@schedule,
|
||||||
|
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"),
|
||||||
|
preserve_latest => $preserve_latest,
|
||||||
|
);
|
||||||
|
my @resume = grep defined, @$preserve; # remove entries with no value from list (target subvolumes)
|
||||||
|
$resume_total = scalar @resume;
|
||||||
|
|
||||||
|
foreach my $child (sort { $a->{cgen} <=> $b->{cgen} } @resume)
|
||||||
{
|
{
|
||||||
if(scalar get_receive_targets($droot, $child)) {
|
INFO "Resuming subvolume backup (send-receive) for: $child->{PRINT}";
|
||||||
DEBUG "Found matching receive target, skipping: $child->{PRINT}";
|
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{cgen});
|
||||||
|
if(macro_send_receive($config_target,
|
||||||
|
snapshot => $child,
|
||||||
|
target => $droot,
|
||||||
|
parent => $latest_common_src, # this is <undef> if no common found
|
||||||
|
resume => 1, # propagated to $config_target->{SUBVOL_RECEIVED}
|
||||||
|
))
|
||||||
|
{
|
||||||
|
# tag the source snapshot, so that get_latest_common() above can make use of the newly received subvolume
|
||||||
|
$child->{RECEIVE_TARGET_PRESENT} = $droot->{URL};
|
||||||
|
$resume_success++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
DEBUG "No matching receive targets found, adding resume candidate: $child->{PRINT}";
|
# note: ABORTED flag is already set by macro_send_receive()
|
||||||
|
ERROR("Error while resuming backups, aborting");
|
||||||
if(my $err_vol = vinfo_subvol($droot, $child->{NAME})) {
|
last;
|
||||||
WARN "Target subvolume \"$err_vol->{PRINT}\" exists, but is not a receive target of \"$child->{PRINT}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if the target would be preserved
|
|
||||||
my ($date, $date_ext) = get_date_tag($child->{SUBVOL_PATH});
|
|
||||||
next unless($date && ($child->{SUBVOL_PATH} =~ /^\Q$snapdir$snapshot_basename\E$snapshot_postfix_match$/));
|
|
||||||
push(@schedule, { value => $child, date => $date, date_ext => $date_ext }),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(scalar @schedule)
|
|
||||||
{
|
|
||||||
DEBUG "Checking schedule for resume candidates";
|
|
||||||
# add all present backups to schedule, with no value
|
|
||||||
# these are needed for correct results of schedule()
|
|
||||||
foreach my $vol (values %{vinfo_subvol_list($droot)}) {
|
|
||||||
next unless($vol->{SUBVOL_PATH} =~ /^\Q$snapshot_basename\E$snapshot_postfix_match$/);
|
|
||||||
my ($date, $date_ext) = get_date_tag($vol->{NAME});
|
|
||||||
next unless($date);
|
|
||||||
push(@schedule, { value => undef, date => $date, date_ext => $date_ext });
|
|
||||||
}
|
|
||||||
my ($preserve, undef) = schedule(
|
|
||||||
schedule => \@schedule,
|
|
||||||
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"),
|
|
||||||
preserve_latest => $preserve_latest,
|
|
||||||
);
|
|
||||||
my @resume = grep defined, @$preserve; # remove entries with no value from list (target subvolumes)
|
|
||||||
$resume_total = scalar @resume;
|
|
||||||
|
|
||||||
foreach my $child (sort { $a->{cgen} <=> $b->{cgen} } @resume) {
|
|
||||||
INFO "Resuming subvolume backup (send-receive) for: $child->{PRINT}";
|
|
||||||
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{cgen});
|
|
||||||
if(macro_send_receive($config_target,
|
|
||||||
snapshot => $child,
|
|
||||||
target => $droot,
|
|
||||||
parent => $latest_common_src, # this is <undef> if no common found
|
|
||||||
resume => 1, # propagated to $config_target->{SUBVOL_RECEIVED}
|
|
||||||
))
|
|
||||||
{
|
|
||||||
# tag the source snapshot, so that get_latest_common() above can make use of the newly received subvolume
|
|
||||||
$child->{RECEIVE_TARGET_PRESENT} = $droot->{URL};
|
|
||||||
$resume_success++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# note: ABORTED flag is already set by macro_send_receive()
|
|
||||||
ERROR("Error while resuming backups, aborting");
|
|
||||||
last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($resume_total) {
|
|
||||||
INFO "Resumed $resume_success/$resume_total missing backups";
|
|
||||||
} else {
|
|
||||||
INFO "No missing backups found";
|
|
||||||
}
|
|
||||||
} # /resume_missing
|
|
||||||
|
|
||||||
unless($resume_only)
|
|
||||||
{
|
|
||||||
# skip creation if resume_missing failed
|
|
||||||
next if($config_target->{ABORTED});
|
|
||||||
next unless($config_subvol->{SNAPSHOT});
|
|
||||||
|
|
||||||
# finally receive the previously created snapshot
|
|
||||||
INFO "Creating subvolume backup (send-receive) for: $svol->{PRINT}";
|
|
||||||
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot);
|
|
||||||
macro_send_receive($config_target,
|
|
||||||
snapshot => $config_subvol->{SNAPSHOT},
|
|
||||||
target => $droot,
|
|
||||||
parent => $latest_common_src, # this is <undef> if no common found
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
if($resume_total) {
|
||||||
ERROR "Unknown target type \"$target_type\", skipping: $svol->{PRINT}";
|
INFO "Resumed $resume_success/$resume_total missing backups";
|
||||||
$config_target->{ABORTED} = "Unknown target type \"$target_type\"";
|
} else {
|
||||||
|
INFO "No missing backups found";
|
||||||
|
}
|
||||||
|
} # /resume_missing
|
||||||
|
|
||||||
|
unless($resume_only)
|
||||||
|
{
|
||||||
|
# skip creation if resume_missing failed
|
||||||
|
next if($config_target->{ABORTED});
|
||||||
|
next unless($config_subvol->{SNAPSHOT});
|
||||||
|
|
||||||
|
# finally receive the previously created snapshot
|
||||||
|
INFO "Creating subvolume backup (send-receive) for: $svol->{PRINT}";
|
||||||
|
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot);
|
||||||
|
macro_send_receive($config_target,
|
||||||
|
snapshot => $config_subvol->{SNAPSHOT},
|
||||||
|
target => $droot,
|
||||||
|
parent => $latest_common_src, # this is <undef> if no common found
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2347,7 +2584,8 @@ MAIN:
|
||||||
my $svol = $config_subvol->{svol} || die;
|
my $svol = $config_subvol->{svol} || die;
|
||||||
my $snapdir = config_key($config_subvol, "snapshot_dir", postfix => '/') // "";
|
my $snapdir = config_key($config_subvol, "snapshot_dir", postfix => '/') // "";
|
||||||
my $snapshot_basename = config_key($config_subvol, "snapshot_name") // die;
|
my $snapshot_basename = config_key($config_subvol, "snapshot_name") // die;
|
||||||
my $preserve_latest = $config_subvol->{SNAPSHOT} ? 0 : 1;
|
my $preserve_latest_snapshot = $config_subvol->{SNAPSHOT} ? 0 : "preserve forced: latest in list";
|
||||||
|
my $preserve_latest_backup = $preserve_latest_snapshot;
|
||||||
my $target_aborted = 0;
|
my $target_aborted = 0;
|
||||||
|
|
||||||
foreach my $config_target (@{$config_subvol->{TARGET}})
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
|
@ -2361,6 +2599,18 @@ MAIN:
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
my $droot = $config_target->{droot} || die;
|
my $droot = $config_target->{droot} || die;
|
||||||
|
if($config_target->{target_type} eq "raw") {
|
||||||
|
if(config_key($config_target, "incremental")) {
|
||||||
|
# In incremental mode, the latest backup is most certainly our parent.
|
||||||
|
# (see note on FORCE_PRESERVE above)
|
||||||
|
$preserve_latest_backup ||= "preserve forced: possibly parent of latest backup";
|
||||||
|
# Note that we could check against $config_subvol->{SNAPSHOT}->{parent_uuid} to be certain,
|
||||||
|
# but this information is not available in $dryrun:
|
||||||
|
# foreach my $vol (values %{vinfo_subvol_list($droot)}) {
|
||||||
|
# $vol->{FORCE_PRESERVE} = 1 if($vol->{received_uuid} eq $config_subvol->{SNAPSHOT}->{parent_uuid});
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# delete backups
|
# delete backups
|
||||||
|
@ -2368,15 +2618,19 @@ MAIN:
|
||||||
INFO "Cleaning backups of subvolume \"$svol->{PRINT}\": $droot->{PRINT}/$snapshot_basename.*";
|
INFO "Cleaning backups of subvolume \"$svol->{PRINT}\": $droot->{PRINT}/$snapshot_basename.*";
|
||||||
my @schedule;
|
my @schedule;
|
||||||
foreach my $vol (values %{vinfo_subvol_list($droot)}) {
|
foreach my $vol (values %{vinfo_subvol_list($droot)}) {
|
||||||
next unless($vol->{SUBVOL_PATH} =~ /^\Q$snapshot_basename\E$snapshot_postfix_match$/);
|
my $filename_info = parse_filename($vol->{SUBVOL_PATH}, $snapshot_basename, ($config_target->{target_type} eq "raw"));
|
||||||
|
next unless($filename_info); # ignore non-btrbk files
|
||||||
|
|
||||||
# NOTE: checking received_uuid does not make much sense, as this received_uuid is propagated to snapshots
|
# NOTE: checking received_uuid does not make much sense, as this received_uuid is propagated to snapshots
|
||||||
# if($vol->{received_uuid} && ($vol->{received_uuid} eq '-')) {
|
# if($vol->{received_uuid} && ($vol->{received_uuid} eq '-')) {
|
||||||
# INFO "Target subvolume is not a received backup, skipping deletion of: $vol->{PRINT}";
|
# INFO "Target subvolume is not a received backup, skipping deletion of: $vol->{PRINT}";
|
||||||
# next;
|
# next;
|
||||||
# }
|
# }
|
||||||
my ($date, $date_ext) = get_date_tag($vol->{NAME});
|
push(@schedule, { value => $vol,
|
||||||
next unless($date);
|
name => $vol->{PRINT},
|
||||||
push(@schedule, { value => $vol, name => $vol->{PRINT}, date => $date, date_ext => $date_ext });
|
btrbk_date => $filename_info->{btrbk_date},
|
||||||
|
preserve => $vol->{FORCE_PRESERVE}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
my (undef, $delete) = schedule(
|
my (undef, $delete) = schedule(
|
||||||
schedule => \@schedule,
|
schedule => \@schedule,
|
||||||
|
@ -2385,7 +2639,7 @@ MAIN:
|
||||||
preserve_daily => config_key($config_target, "target_preserve_daily"),
|
preserve_daily => config_key($config_target, "target_preserve_daily"),
|
||||||
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
||||||
preserve_monthly => config_key($config_target, "target_preserve_monthly"),
|
preserve_monthly => config_key($config_target, "target_preserve_monthly"),
|
||||||
preserve_latest => $preserve_latest,
|
preserve_latest => $preserve_latest_backup,
|
||||||
log_verbose => 1,
|
log_verbose => 1,
|
||||||
);
|
);
|
||||||
my $ret = btrfs_subvolume_delete($delete, commit => config_key($config_target, "btrfs_commit_delete"));
|
my $ret = btrfs_subvolume_delete($delete, commit => config_key($config_target, "btrfs_commit_delete"));
|
||||||
|
@ -2413,10 +2667,12 @@ MAIN:
|
||||||
INFO "Cleaning snapshots: $sroot->{PRINT}/$snapdir$snapshot_basename.*";
|
INFO "Cleaning snapshots: $sroot->{PRINT}/$snapdir$snapshot_basename.*";
|
||||||
my @schedule;
|
my @schedule;
|
||||||
foreach my $vol (values %{vinfo_subvol_list($sroot)}) {
|
foreach my $vol (values %{vinfo_subvol_list($sroot)}) {
|
||||||
next unless($vol->{SUBVOL_PATH} =~ /^\Q$snapdir$snapshot_basename\E$snapshot_postfix_match$/);
|
my $filename_info = parse_filename($vol->{SUBVOL_PATH}, $snapdir . $snapshot_basename);
|
||||||
my ($date, $date_ext) = get_date_tag($vol->{NAME});
|
next unless($filename_info); # ignore non-btrbk files
|
||||||
next unless($date);
|
push(@schedule, { value => $vol,
|
||||||
push(@schedule, { value => $vol, name => $vol->{PRINT}, date => $date, date_ext => $date_ext });
|
name => $vol->{PRINT},
|
||||||
|
btrbk_date => $filename_info->{btrbk_date}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
my (undef, $delete) = schedule(
|
my (undef, $delete) = schedule(
|
||||||
schedule => \@schedule,
|
schedule => \@schedule,
|
||||||
|
@ -2425,7 +2681,7 @@ MAIN:
|
||||||
preserve_daily => config_key($config_subvol, "snapshot_preserve_daily"),
|
preserve_daily => config_key($config_subvol, "snapshot_preserve_daily"),
|
||||||
preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"),
|
preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"),
|
||||||
preserve_monthly => config_key($config_subvol, "snapshot_preserve_monthly"),
|
preserve_monthly => config_key($config_subvol, "snapshot_preserve_monthly"),
|
||||||
preserve_latest => $preserve_latest,
|
preserve_latest => $preserve_latest_snapshot,
|
||||||
log_verbose => 1,
|
log_verbose => 1,
|
||||||
);
|
);
|
||||||
my $ret = btrfs_subvolume_delete($delete, commit => config_key($config_subvol, "btrfs_commit_delete"));
|
my $ret = btrfs_subvolume_delete($delete, commit => config_key($config_subvol, "btrfs_commit_delete"));
|
||||||
|
|
15
doc/btrbk.1
15
doc/btrbk.1
|
@ -1,4 +1,4 @@
|
||||||
.TH "btrbk" "1" "2015-09-02" "btrbk v0.20.0" ""
|
.TH "btrbk" "1" "2015-09-29" "btrbk v0.21.0-dev" ""
|
||||||
.\" disable hyphenation
|
.\" disable hyphenation
|
||||||
.nh
|
.nh
|
||||||
.\" disable justification (adjust text to left margin only)
|
.\" disable justification (adjust text to left margin only)
|
||||||
|
@ -21,12 +21,17 @@ from one source to multiple destinations.
|
||||||
Snapshots as well as backup subvolume names are created in form:
|
Snapshots as well as backup subvolume names are created in form:
|
||||||
.PP
|
.PP
|
||||||
.RS 4
|
.RS 4
|
||||||
<source_name>.YYYYMMDD[_N]
|
<snapshot_name>.<timestamp>[_N]
|
||||||
.RE
|
.RE
|
||||||
.PP
|
.PP
|
||||||
Where YYYY is the year, MM is the month, and DD is the day of
|
Where <snapshot_name> is identical to the source subvolume name,
|
||||||
creation, and, if multiple backups are created on the same day, N will
|
unless the configuration option \fIsnapshot_name\fR is set. The
|
||||||
be incremented on each backup, starting at 1.
|
<timestamp> is either "YYYYMMDD" or "YYYYMMDDThhmm" (dependent of the
|
||||||
|
\fItimestamp_format\fR configuration option), where "YYYY" is the
|
||||||
|
year, "MM" is the month, "DD" is the day, "hh" is the hour and "mm" is
|
||||||
|
the minute of the creation time (local time of the host running
|
||||||
|
btrbk). If multiple snapshots/backups are created on the same
|
||||||
|
date/time, N will be incremented on each backup, starting at 1.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.PP
|
.PP
|
||||||
\-h, \-\-help
|
\-h, \-\-help
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.TH "btrbk.conf" "5" "2015-09-02" "btrbk v0.20.0" ""
|
.TH "btrbk.conf" "5" "2015-09-29" "btrbk v0.21.0-dev" ""
|
||||||
.\" disable hyphenation
|
.\" disable hyphenation
|
||||||
.nh
|
.nh
|
||||||
.\" disable justification (adjust text to left margin only)
|
.\" disable justification (adjust text to left margin only)
|
||||||
|
@ -19,8 +19,7 @@ defined either globally or within a section.
|
||||||
The options specified always apply to the last section encountered,
|
The options specified always apply to the last section encountered,
|
||||||
superseding the values set in upper-level sections. This means that
|
superseding the values set in upper-level sections. This means that
|
||||||
global options must be set before any sections are defined.
|
global options must be set before any sections are defined.
|
||||||
.PP
|
.SH SECTIONS
|
||||||
The sections are:
|
|
||||||
.PP
|
.PP
|
||||||
\fBvolume\fR <volume-directory>|<url>
|
\fBvolume\fR <volume-directory>|<url>
|
||||||
.RS 4
|
.RS 4
|
||||||
|
@ -33,17 +32,19 @@ filesystem mounted with the \fIsubvolid=0\fR option.
|
||||||
\fBsubvolume\fR <subvolume-name>
|
\fBsubvolume\fR <subvolume-name>
|
||||||
.RS 4
|
.RS 4
|
||||||
Subvolume to be backed up, relative to the \fI<volume-directory>\fR
|
Subvolume to be backed up, relative to the \fI<volume-directory>\fR
|
||||||
specified in the \fIvolume\fR section.
|
specified in the \fIvolume\fR section. Multiple \fIsubvolume\fR
|
||||||
|
sections are allowed within \fIvolume\fR sections.
|
||||||
.RE
|
.RE
|
||||||
.PP
|
.PP
|
||||||
\fBtarget\fR <type> <volume-directory>|<url>
|
\fBtarget\fR <type> <target-directory>|<url>
|
||||||
.RS 4
|
.RS 4
|
||||||
Target type and directory where the backup subvolumes are to be
|
Target type and directory where the backup subvolumes are to be
|
||||||
created. \fI<volume-directory>\fR must be an absolute path and point
|
created. See the TARGET TYPES section for supported
|
||||||
to a btrfs volume (or subvolume). Currently the the only valid
|
\fI<type>\fR. Multiple \fItarget\fR sections are allowed within
|
||||||
\fI<type>\fR is \[lq]send\-receive\[rq].
|
\fIsubvolume\fR sections.
|
||||||
|
.RE
|
||||||
.PP
|
.PP
|
||||||
For the \fIvolume\fR and \fItarget\fR sections, you can also specify a
|
For the \fIvolume\fR and \fItarget\fR sections, you can specify a
|
||||||
ssh-url instead of a local directory. The syntax for \fI<url>\fR is:
|
ssh-url instead of a local directory. The syntax for \fI<url>\fR is:
|
||||||
.PP
|
.PP
|
||||||
.RS 4
|
.RS 4
|
||||||
|
@ -56,8 +57,67 @@ Note that btrfs is very picky on file names (mainly for security
|
||||||
reasons), only the characters [0-9] [a-z] [A-Z] and "._+-@" are
|
reasons), only the characters [0-9] [a-z] [A-Z] and "._+-@" are
|
||||||
allowed.
|
allowed.
|
||||||
.RE
|
.RE
|
||||||
|
.SH TARGET TYPES
|
||||||
.PP
|
.PP
|
||||||
The configuration options are:
|
\fBsend-receive\fR
|
||||||
|
.RS 4
|
||||||
|
Backup to a btrfs filesystem, using "btrfs send/receive". The
|
||||||
|
\fI<target-directory>\fR must be an absolute path and point to a btrfs
|
||||||
|
volume (or subvolume). See btrfs-send(8), btrfs-receive(8).
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fBraw\fR \fI*experimental*\fR
|
||||||
|
.RS 4
|
||||||
|
Backup to a raw (filesystem independent) file from the output of
|
||||||
|
btrfs-send(8), with optional compression and encryption.
|
||||||
|
.PP
|
||||||
|
Additional options for raw target:
|
||||||
|
.PP
|
||||||
|
.RS 4
|
||||||
|
raw_target_compress gzip|bzip2|xz|no
|
||||||
|
.PD 0
|
||||||
|
.PP
|
||||||
|
raw_target_encrypt gpg|no
|
||||||
|
.PP
|
||||||
|
gpg_keyring <file>
|
||||||
|
.PP
|
||||||
|
gpg_recipient <name>
|
||||||
|
.RE
|
||||||
|
.PD
|
||||||
|
.PP
|
||||||
|
The target file name is:
|
||||||
|
.PP
|
||||||
|
.RS 4
|
||||||
|
<snapshot-name>--<received_uuid>[@<parent_uuid>].btrfs[.gz|.bz2|.xz][.gpg]
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
It is recommended to set "incremental no" for raw backups, for the
|
||||||
|
following reasons:
|
||||||
|
.IP \[bu] 2
|
||||||
|
The target preserve mechanism is disabled for raw \fIincremental\fR
|
||||||
|
backups, since deleting of any intermediate backup would destroy the
|
||||||
|
consistency of all subsequent backups (btrbk will never delete any
|
||||||
|
backup in a raw incremental chain).
|
||||||
|
.IP \[bu]
|
||||||
|
No rotation of incremental backups: if \fIincremental\fR is set, a
|
||||||
|
full backup must be triggered manually from time to time in order to
|
||||||
|
be able to delete old backups.
|
||||||
|
.RE
|
||||||
|
.SH OPTIONS
|
||||||
|
.PP
|
||||||
|
\fBtimestamp_format\fR short|long
|
||||||
|
.RS 4
|
||||||
|
Timestamp format used as postfix for new snapshot subvolume names.
|
||||||
|
defaults to \[lq]short\[rq].
|
||||||
|
.IP \[bu] 2
|
||||||
|
\fIshort\fR: YYYYMMDD[_N] (e.g. "20150825", "20150825_1")
|
||||||
|
.IP \[bu]
|
||||||
|
\fIlong\fR: YYYYMMDD<T>hhmm[_N] (e.g. "20150825T1531")
|
||||||
|
.PP
|
||||||
|
Note that a postfix "_N" is only appended to the timestamp if a
|
||||||
|
snapshot/backup already exists with the timestamp of current
|
||||||
|
date/time.
|
||||||
|
.RE
|
||||||
.PP
|
.PP
|
||||||
\fBsnapshot_dir\fR <directory>
|
\fBsnapshot_dir\fR <directory>
|
||||||
.RS 4
|
.RS 4
|
||||||
|
|
Loading…
Reference in New Issue