From 17266d90aae91657fb24c1e596cdadd83d12990e Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 14 Jan 2015 14:10:41 +0100 Subject: [PATCH] btrbk: added support for ssh targets (identity file only, no password support yet) --- btrbk | 165 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 60 deletions(-) diff --git a/btrbk b/btrbk index c43310c..6bfd03b 100755 --- a/btrbk +++ b/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. @@ -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;