mirror of https://github.com/digint/btrbk
btrbk: added support for ssh targets (identity file only, no password support yet)
parent
92ee8b0454
commit
17266d90aa
165
btrbk
165
btrbk
|
@ -10,9 +10,9 @@ btrbk - backup btrfs volumes at file-system level
|
|||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Backup tool for btrfs (sub-)volumes, taking advantage of btrfs
|
||||
specific send-receive mechanism, allowing incremental backups at
|
||||
file-system level.
|
||||
Backup tool for btrfs subvolumes, taking advantage of btrfs specific
|
||||
send-receive mechanism, allowing incremental backups at file-system
|
||||
level.
|
||||
|
||||
The full btrbk documentation is available at L<http://www.digint.ch/btrbk>.
|
||||
|
||||
|
@ -72,6 +72,8 @@ my %config_options = (
|
|||
target_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1 },
|
||||
target_preserve_monthly => { default => "all", accept => [ "all" ], accept_numeric => 1 },
|
||||
btrfs_commit_delete => { default => undef, accept => [ "after", "each", "no" ] },
|
||||
ssh_identity => { default => undef, accept_file => "absolute" },
|
||||
ssh_user => { default => "root", accept_regexp => qr/^[a-z_][a-z0-9_-]*$/ },
|
||||
);
|
||||
|
||||
my @config_target_types = qw(send-receive);
|
||||
|
@ -125,19 +127,26 @@ sub ERROR { my $t = shift; print STDOUT "ERROR: $t\n"; }
|
|||
|
||||
sub run_cmd($;$)
|
||||
{
|
||||
my $cmd = shift;
|
||||
my $cmd = shift || die;
|
||||
my $non_destructive = shift;
|
||||
my $ret = "";
|
||||
DEBUG "### $cmd" unless($non_destructive);
|
||||
$cmd =~ s/^\s+//;
|
||||
$cmd =~ s/\s+$//;
|
||||
if($non_destructive || (not $dryrun)) {
|
||||
TRACE "### $cmd";
|
||||
DEBUG "### $cmd";
|
||||
$ret = `$cmd`;
|
||||
chomp($ret);
|
||||
TRACE "command output:\n$ret";
|
||||
TRACE "Command output:\n$ret";
|
||||
if($?) {
|
||||
WARN "command execution failed (exitcode=$?): \"$cmd\"";
|
||||
WARN "Command execution failed (exitcode=$?): \"$cmd\"";
|
||||
return undef;
|
||||
}
|
||||
else {
|
||||
DEBUG "Command execution successful";
|
||||
}
|
||||
}
|
||||
else {
|
||||
DEBUG "### (dryrun) $cmd";
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
@ -145,8 +154,8 @@ sub run_cmd($;$)
|
|||
|
||||
sub subvol($$)
|
||||
{
|
||||
my $root = shift;
|
||||
my $vol = shift;
|
||||
my $root = shift || die;
|
||||
my $vol = shift || die;
|
||||
if($vol_info{$root} && $vol_info{$root}->{$vol}) {
|
||||
return $vol_info{$root}->{$vol};
|
||||
}
|
||||
|
@ -156,9 +165,9 @@ sub subvol($$)
|
|||
|
||||
sub create_snapdir($$$)
|
||||
{
|
||||
my $root = shift;
|
||||
my $vol = shift;
|
||||
my $snapdir = shift;
|
||||
my $root = shift || die;
|
||||
my $vol = shift || die;
|
||||
my $snapdir = shift || die;
|
||||
if($snapdir && (not $dryrun))
|
||||
{
|
||||
my $dir = "$root/$snapdir";
|
||||
|
@ -170,17 +179,32 @@ sub create_snapdir($$$)
|
|||
}
|
||||
|
||||
|
||||
sub btr_subvolume_detail($)
|
||||
sub get_rsh($$)
|
||||
{
|
||||
my $vol = shift;
|
||||
my $ret = run_cmd("/sbin/btrfs subvolume show $vol 2>/dev/null", 1);
|
||||
my $vol = shift || die;
|
||||
my $config = shift;
|
||||
if($config && ($vol =~ /^ssh:\/\/(\S+?)(\/\S+)$/)) {
|
||||
my ($ssh_host, $real_vol) = ($1, $2);
|
||||
my $rsh = "/usr/bin/ssh -i " . config_key($config, "ssh_identity") . ' ' . config_key($config, "ssh_user") . '@' . $ssh_host;
|
||||
return ($rsh, $real_vol);
|
||||
}
|
||||
return ("", $vol);
|
||||
}
|
||||
|
||||
|
||||
sub btr_subvolume_detail($;$)
|
||||
{
|
||||
my $vol = shift || die;
|
||||
my $config = shift;
|
||||
my ($rsh, $real_vol) = get_rsh($vol, $config);
|
||||
my $ret = run_cmd("$rsh /sbin/btrfs subvolume show $real_vol 2>/dev/null", 1);
|
||||
if($ret)
|
||||
{
|
||||
if($ret eq "$vol is btrfs root") {
|
||||
TRACE "btr_detail: found btrfs root: $vol";
|
||||
if($ret eq "$real_vol is btrfs root") {
|
||||
DEBUG "found btrfs root: $vol";
|
||||
return { id => 5, is_root => 1 };
|
||||
}
|
||||
elsif($ret =~ /^$vol/) {
|
||||
elsif($ret =~ /^$real_vol/) {
|
||||
TRACE "btr_detail: found btrfs subvolume: $vol";
|
||||
my %trans = (
|
||||
name => "Name",
|
||||
|
@ -202,6 +226,7 @@ sub btr_subvolume_detail($)
|
|||
WARN "Failed to parse subvolume detail \"$trans{$_}\": $ret";
|
||||
}
|
||||
}
|
||||
DEBUG "parsed " . scalar(keys %detail) . " subvolume detail items: $vol";
|
||||
TRACE "btr_detail for $vol: " . Dumper \%detail;
|
||||
return \%detail;
|
||||
}
|
||||
|
@ -213,8 +238,8 @@ sub btr_subvolume_detail($)
|
|||
|
||||
sub config_key($$)
|
||||
{
|
||||
my $node = shift;
|
||||
my $key = shift;
|
||||
my $node = shift || die;
|
||||
my $key = shift || die;
|
||||
TRACE "config_key: context=$node->{CONTEXT}, key=$key";
|
||||
while(not exists($node->{$key})) {
|
||||
return undef unless($node->{PARENT});
|
||||
|
@ -227,7 +252,7 @@ sub config_key($$)
|
|||
|
||||
sub parse_config($)
|
||||
{
|
||||
my $file = shift;
|
||||
my $file = shift || die;
|
||||
my $root = { CONTEXT => "root" };
|
||||
my $cur = $root;
|
||||
# set defaults
|
||||
|
@ -355,6 +380,16 @@ sub parse_config($)
|
|||
$value .= '/';
|
||||
}
|
||||
}
|
||||
elsif($config_options{$key}->{accept_regexp}) {
|
||||
my $match = $config_options{$key}->{accept_regexp};
|
||||
if($value =~ m/$match/) {
|
||||
TRACE "option \"$key=$value\" is numeric, accepted";
|
||||
}
|
||||
else {
|
||||
ERROR "Value \"$value\" failed input validation for option \"$key\" in \"$file\" line $.";
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ERROR "Unsupported value \"$value\" for option \"$key\" in \"$file\" line $.";
|
||||
|
@ -384,14 +419,16 @@ sub parse_config($)
|
|||
}
|
||||
|
||||
|
||||
sub btr_subvolume_list($;@)
|
||||
sub btr_subvolume_list($;$@)
|
||||
{
|
||||
my $vol = shift;
|
||||
my $vol = shift || die;
|
||||
my $config = shift;
|
||||
my %opts = @_;
|
||||
my $filter_option = "-a";
|
||||
$filter_option = "-o" if($opts{subvol_only});
|
||||
my $ret = run_cmd("/sbin/btrfs subvolume list $filter_option -c -u -q -R $vol", 1);
|
||||
unless($ret) {
|
||||
my ($rsh, $real_vol) = get_rsh($vol, $config);
|
||||
my $ret = run_cmd("$rsh /sbin/btrfs subvolume list $filter_option -c -u -q -R $real_vol", 1);
|
||||
unless(defined($ret)) {
|
||||
WARN "Failed to fetch btrfs subvolume list for: $vol";
|
||||
return undef;
|
||||
}
|
||||
|
@ -416,14 +453,18 @@ sub btr_subvolume_list($;@)
|
|||
};
|
||||
# $node{parent_uuid} = undef if($node{parent_uuid} eq '-');
|
||||
}
|
||||
DEBUG "found " . scalar(@nodes) . " subvolumes in: $vol";
|
||||
return \@nodes;
|
||||
}
|
||||
|
||||
sub btr_subvolume_find_new($$)
|
||||
|
||||
sub btr_subvolume_find_new($$;$)
|
||||
{
|
||||
my $vol = shift;
|
||||
my $lastgen = shift;
|
||||
my $ret = run_cmd("/sbin/btrfs subvolume find-new $vol $lastgen");
|
||||
my $vol = shift || die;
|
||||
my $lastgen = shift // die;
|
||||
my $config = shift;
|
||||
my ($rsh, $real_vol) = get_rsh($vol, $config);
|
||||
my $ret = run_cmd("$rsh /sbin/btrfs subvolume find-new $real_vol $lastgen");
|
||||
unless(defined($ret)) {
|
||||
ERROR "Failed to fetch modified files for: $vol";
|
||||
return undef;
|
||||
|
@ -474,12 +515,13 @@ sub btr_subvolume_find_new($$)
|
|||
}
|
||||
|
||||
|
||||
sub btr_tree($)
|
||||
sub btr_tree($;$)
|
||||
{
|
||||
my $vol = shift;
|
||||
my $vol = shift || die;
|
||||
my $config = shift;
|
||||
my %tree;
|
||||
my %id;
|
||||
my $subvol_list = btr_subvolume_list($vol, subvol_only => 0);
|
||||
my $subvol_list = btr_subvolume_list($vol, $config, subvol_only => 0);
|
||||
return undef unless(ref($subvol_list) eq "ARRAY");
|
||||
foreach my $node (@$subvol_list)
|
||||
{
|
||||
|
@ -513,10 +555,11 @@ sub btr_tree($)
|
|||
}
|
||||
|
||||
|
||||
sub btr_subtree($)
|
||||
sub btr_subtree($;$)
|
||||
{
|
||||
my $vol = shift;
|
||||
my $detail = btr_subvolume_detail($vol);
|
||||
my $vol = shift || die;
|
||||
my $config = shift;
|
||||
my $detail = btr_subvolume_detail($vol, $config);
|
||||
unless($detail) {
|
||||
WARN "Failed to build btrfs subtree for volume: $vol";
|
||||
return undef;
|
||||
|
@ -524,7 +567,7 @@ sub btr_subtree($)
|
|||
|
||||
my $volname = $detail->{name} || "";
|
||||
my %tree;
|
||||
my $subvol_list = btr_subvolume_list($vol, subvol_only => 1);
|
||||
my $subvol_list = btr_subvolume_list($vol, $config, subvol_only => 1);
|
||||
return undef unless(ref($subvol_list) eq "ARRAY");
|
||||
foreach my $node (@$subvol_list)
|
||||
{
|
||||
|
@ -559,8 +602,8 @@ sub btr_subtree($)
|
|||
# returns $target, or undef on error
|
||||
sub btrfs_snapshot($$)
|
||||
{
|
||||
my $src = shift;
|
||||
my $target = shift;
|
||||
my $src = shift || die;
|
||||
my $target = shift || die;
|
||||
DEBUG "[btrfs] snapshot (ro):";
|
||||
DEBUG "[btrfs] source: $src";
|
||||
DEBUG "[btrfs] target: $target";
|
||||
|
@ -573,9 +616,10 @@ sub btrfs_snapshot($$)
|
|||
|
||||
sub btrfs_subvolume_delete($@)
|
||||
{
|
||||
my $commit_delete = shift;
|
||||
my $config = shift;
|
||||
my @targets = @_;
|
||||
return 0 unless(scalar(@targets));
|
||||
my $commit_delete = config_key($config, "btrfs_commit_delete");
|
||||
DEBUG "[btrfs] delete:";
|
||||
DEBUG "[btrfs] commit-delete: " . ($commit_delete ? $commit_delete : "no");
|
||||
DEBUG "[btrfs] subvolume: $_" foreach(@targets);
|
||||
|
@ -588,12 +632,14 @@ sub btrfs_subvolume_delete($@)
|
|||
}
|
||||
|
||||
|
||||
sub btrfs_send_receive($$;$$)
|
||||
sub btrfs_send_receive($$$$;$)
|
||||
{
|
||||
my $src = shift;
|
||||
my $target = shift;
|
||||
my $src = shift || die;
|
||||
my $target = shift || die;
|
||||
my $parent = shift // "";
|
||||
my $changelog = shift // "";
|
||||
my $config = shift;
|
||||
my ($rsh, $real_target) = get_rsh($target, $config);
|
||||
my $now = localtime;
|
||||
|
||||
my $src_name = $src;
|
||||
|
@ -612,7 +658,7 @@ sub btrfs_send_receive($$;$$)
|
|||
my $receive_option = "";
|
||||
$receive_option = "-v" if($changelog || ($loglevel >= 2));
|
||||
$receive_option = "-v -v" if($parent && $changelog);
|
||||
my $cmd = "/sbin/btrfs send $parent_option $src | /sbin/btrfs receive $receive_option $target/ 2>&1";
|
||||
my $cmd = "/sbin/btrfs send $parent_option $src | $rsh /sbin/btrfs receive $receive_option $real_target/ 2>&1";
|
||||
my $ret = run_cmd($cmd);
|
||||
unless(defined($ret)) {
|
||||
ERROR "Failed to send/receive btrfs subvolume: $src " . ($parent ? "[$parent]" : "") . " -> $target";
|
||||
|
@ -639,8 +685,8 @@ sub btrfs_send_receive($$;$$)
|
|||
|
||||
sub get_children($$)
|
||||
{
|
||||
my $sroot = shift;
|
||||
my $svol = shift;
|
||||
my $sroot = shift || die;
|
||||
my $svol = shift || die;
|
||||
my $svol_href = subvol($sroot, $svol);
|
||||
die("subvolume info not present: $sroot/$svol") unless($svol_href);
|
||||
DEBUG "Getting snapshot children of: $sroot/$svol";
|
||||
|
@ -656,8 +702,8 @@ sub get_children($$)
|
|||
|
||||
sub get_receive_targets_by_uuid($$)
|
||||
{
|
||||
my $droot = shift;
|
||||
my $uuid = shift;
|
||||
my $droot = shift || die;
|
||||
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}->{FS_PATH}";
|
||||
|
@ -673,9 +719,9 @@ sub get_receive_targets_by_uuid($$)
|
|||
|
||||
sub get_latest_common($$$)
|
||||
{
|
||||
my $sroot = shift;
|
||||
my $svol = shift;
|
||||
my $droot = shift;
|
||||
my $sroot = shift || die;
|
||||
my $svol = shift || die;
|
||||
my $droot = shift || die;
|
||||
|
||||
die("source subvolume info not present: $sroot") unless($vol_info{$sroot});
|
||||
die("target subvolume info not present: $droot") unless($vol_info{$droot});
|
||||
|
@ -930,7 +976,7 @@ MAIN:
|
|||
foreach my $config_vol (@{$config->{VOLUME}})
|
||||
{
|
||||
my $sroot = $config_vol->{sroot} || die;
|
||||
$vol_info{$sroot} //= btr_subtree($sroot);
|
||||
$vol_info{$sroot} //= btr_subtree($sroot, $config_vol);
|
||||
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||
{
|
||||
my $svol = $config_subvol->{svol} || die;
|
||||
|
@ -942,7 +988,7 @@ MAIN:
|
|||
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||
{
|
||||
my $droot = $config_target->{droot} || die;
|
||||
$vol_info{$droot} //= btr_subtree($droot);
|
||||
$vol_info{$droot} //= btr_subtree($droot, $config_target);
|
||||
unless($vol_info{$droot}) {
|
||||
$config_target->{ABORTED} = "Failed to read btrfs subvolume list for \"$droot\"";
|
||||
WARN "Skipping target: $config_target->{ABORTED}";
|
||||
|
@ -1115,11 +1161,11 @@ MAIN:
|
|||
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, $receive_log);
|
||||
$success = btrfs_send_receive($snapshot, $droot, $parent_snap, $receive_log, $config_target);
|
||||
}
|
||||
elsif($incremental ne "strict") {
|
||||
INFO "No common parent subvolume present, creating full backup";
|
||||
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log);
|
||||
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log, $config_target);
|
||||
}
|
||||
else {
|
||||
WARN "Backup to $droot failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\"";
|
||||
|
@ -1127,7 +1173,7 @@ MAIN:
|
|||
}
|
||||
else {
|
||||
INFO "Creating full backup (option \"incremental\" is not set)";
|
||||
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log);
|
||||
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log, $config_target);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -1179,8 +1225,7 @@ MAIN:
|
|||
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
||||
preserve_monthly => config_key($config_target, "target_preserve_monthly"),
|
||||
);
|
||||
my $commit_delete = config_key($config_target, "btrfs_commit_delete");
|
||||
my $ret = btrfs_subvolume_delete($commit_delete, @delete);
|
||||
my $ret = btrfs_subvolume_delete($config_target, @delete);
|
||||
if(defined($ret)) {
|
||||
INFO "Deleted $ret subvolumes in: $droot/$svol.*";
|
||||
$config_target->{subvol_deleted} = \@delete;
|
||||
|
@ -1194,6 +1239,7 @@ MAIN:
|
|||
#
|
||||
# delete snapshots
|
||||
#
|
||||
# TODO: don't delete any snapshots if on of the above failed
|
||||
INFO "Cleaning snapshots: $sroot/$snapdir$svol.*";
|
||||
my @schedule;
|
||||
foreach my $vol (keys %{$vol_info{$sroot}}) {
|
||||
|
@ -1209,8 +1255,7 @@ MAIN:
|
|||
preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"),
|
||||
preserve_monthly => config_key($config_subvol, "snapshot_preserve_monthly"),
|
||||
);
|
||||
my $commit_delete = config_key($config_subvol, "btrfs_commit_delete");
|
||||
my $ret = btrfs_subvolume_delete($commit_delete, @delete);
|
||||
my $ret = btrfs_subvolume_delete($config_subvol, @delete);
|
||||
if(defined($ret)) {
|
||||
INFO "Deleted $ret subvolumes in: $sroot/$snapdir$svol.*";
|
||||
$config_subvol->{subvol_deleted} = \@delete;
|
||||
|
|
Loading…
Reference in New Issue