btrbk: implemented "group" configuration option and filtering for volume/subvolume

pull/48/merge
Axel Burri 2015-09-02 11:04:22 +02:00
parent 8bc1acc672
commit e7c6e37bd0
1 changed files with 69 additions and 37 deletions

98
btrbk
View File

@ -47,7 +47,7 @@ use Date::Calc qw(Today Delta_Days Day_of_Week);
use Getopt::Long qw(GetOptions); use Getopt::Long qw(GetOptions);
use Data::Dumper; use Data::Dumper;
our $VERSION = "0.20.0"; our $VERSION = "0.21.0-dev";
our $AUTHOR = 'Axel Burri <axel@tty0.ch>'; our $AUTHOR = 'Axel Burri <axel@tty0.ch>';
our $PROJECT_HOME = '<http://www.digint.ch/btrbk/>'; our $PROJECT_HOME = '<http://www.digint.ch/btrbk/>';
@ -55,6 +55,13 @@ my $version_info = "btrbk command line client, version $VERSION";
my @config_src = ("/etc/btrbk.conf", "/etc/btrbk/btrbk.conf"); my @config_src = ("/etc/btrbk.conf", "/etc/btrbk/btrbk.conf");
my $ip_addr_match = qr/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/;
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 $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
my $snapshot_postfix_match = qr/\.[0-9]{8}(_[0-9]+)?/;
my $group_match = qr/[a-zA-Z0-9_:-]+/;
my %day_of_week_map = ( monday => 1, tuesday => 2, wednesday => 3, thursday => 4, friday => 5, saturday => 6, sunday => 7 ); my %day_of_week_map = ( monday => 1, tuesday => 2, wednesday => 3, thursday => 4, friday => 5, saturday => 6, sunday => 7 );
my %config_options = ( my %config_options = (
@ -78,6 +85,7 @@ 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" ] },
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*/, context => [ "volume", "subvolume" ] },
# deprecated options # deprecated options
snapshot_create_always => { default => undef, accept => [ "yes", "no" ], snapshot_create_always => { default => undef, accept => [ "yes", "no" ],
@ -108,12 +116,6 @@ my $loglevel = 1;
my $show_progress = 0; my $show_progress = 0;
my $err = ""; my $err = "";
my $ip_addr_match = qr/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/;
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 $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
my $snapshot_postfix_match = qr/\.[0-9]{8}(_[0-9]+)?/;
$SIG{__DIE__} = sub { $SIG{__DIE__} = sub {
print STDERR "\nERROR: process died unexpectedly (btrbk v$VERSION)"; print STDERR "\nERROR: process died unexpectedly (btrbk v$VERSION)";
@ -145,10 +147,10 @@ sub HELP_MESSAGE
print STDERR " --progress show progress bar on send-receive operation\n"; print STDERR " --progress show progress bar on send-receive operation\n";
print STDERR "\n"; print STDERR "\n";
print STDERR "commands:\n"; print STDERR "commands:\n";
print STDERR " run [subvol...] perform backup operations as defined in the config file\n"; print STDERR " run [subvol|group...] perform backup operations as defined in the config file\n";
print STDERR " dryrun [subvol...] don't run btrfs commands; show what would be executed\n"; print STDERR " dryrun [subvol|group...] don't run btrfs commands; show what would be executed\n";
print STDERR " tree [subvol...] shows backup tree\n"; print STDERR " tree [subvol|group...] shows backup tree\n";
print STDERR " info [subvol...] print useful filesystem information\n"; print STDERR " info [subvol|group...] print useful filesystem information\n";
print STDERR " origin <subvol> print origin information for subvolume\n"; print STDERR " origin <subvol> print origin information for subvolume\n";
print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\n"; print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\n";
print STDERR "\n"; print STDERR "\n";
@ -568,6 +570,11 @@ sub parse_config(@)
return undef; return undef;
} }
if($config_options{$key}->{split}) {
$value = [ split($config_options{$key}->{split}, $value) ];
TRACE "splitted option \"$key\": " . join(',', @$value);
}
if($config_options{$key}->{context} && !grep(/^$cur->{CONTEXT}$/, @{$config_options{$key}->{context}})) { if($config_options{$key}->{context} && !grep(/^$cur->{CONTEXT}$/, @{$config_options{$key}->{context}})) {
ERROR "Option \"$key\" is only allowed in " . join(" or ", map("\"$_\"", @{$config_options{$key}->{context}})) . " context, in \"$file\" line $."; ERROR "Option \"$key\" is only allowed in " . join(" or ", map("\"$_\"", @{$config_options{$key}->{context}})) . " context, in \"$file\" line $.";
return undef; return undef;
@ -1498,36 +1505,40 @@ MAIN:
$show_progress = 0; $show_progress = 0;
} }
my ($action_run, $action_info, $action_tree, $action_diff, $action_origin); my ($action_run, $action_info, $action_tree, $action_diff, $action_origin);
my @subvol_args; my @filter_args;
my $args_allow_group = 0;
my ($args_expected_min, $args_expected_max) = (0, 0); my ($args_expected_min, $args_expected_max) = (0, 0);
if(($command eq "run") || ($command eq "dryrun")) { if(($command eq "run") || ($command eq "dryrun")) {
$action_run = 1; $action_run = 1;
$dryrun = 1 if($command eq "dryrun"); $dryrun = 1 if($command eq "dryrun");
$args_expected_min = 0; $args_expected_min = 0;
$args_expected_max = 9999; $args_expected_max = 9999;
@subvol_args = @ARGV; $args_allow_group = 1;
@filter_args = @ARGV;
} }
elsif ($command eq "info") { elsif ($command eq "info") {
$action_info = 1; $action_info = 1;
$args_expected_min = 0; $args_expected_min = 0;
$args_expected_max = 9999; $args_expected_max = 9999;
@subvol_args = @ARGV; $args_allow_group = 1;
@filter_args = @ARGV;
} }
elsif ($command eq "tree") { elsif ($command eq "tree") {
$action_tree = 1; $action_tree = 1;
$args_expected_min = 0; $args_expected_min = 0;
$args_expected_max = 9999; $args_expected_max = 9999;
@subvol_args = @ARGV; $args_allow_group = 1;
@filter_args = @ARGV;
} }
elsif ($command eq "diff") { elsif ($command eq "diff") {
$action_diff = 1; $action_diff = 1;
$args_expected_min = $args_expected_max = 2; $args_expected_min = $args_expected_max = 2;
@subvol_args = @ARGV; @filter_args = @ARGV;
} }
elsif ($command eq "origin") { elsif ($command eq "origin") {
$action_origin = 1; $action_origin = 1;
$args_expected_min = $args_expected_max = 1; $args_expected_min = $args_expected_max = 1;
@subvol_args = @ARGV; @filter_args = @ARGV;
} }
else { else {
ERROR "Unrecognized command: $command"; ERROR "Unrecognized command: $command";
@ -1541,16 +1552,22 @@ MAIN:
} }
# input validation # input validation
foreach (@subvol_args) { foreach (@filter_args) {
s/\/+$//; # remove trailing slash s/\/+$//; # remove trailing slash
if(/^(($ssh_prefix_match)?\/$file_match)$/) { # matches ssh statement or absolute file if($args_allow_group && /^($group_match)$/) { # matches group
$_ = $1; # untaint argument
}
elsif(/^(($ssh_prefix_match)?\/$file_match)$/) { # matches ssh statement or absolute file
$_ = $1; # untaint argument $_ = $1; # untaint argument
} }
elsif(/^(?<host>$ip_addr_match|$host_name_match):\/(?<file>$file_match)$/) { # convert "my.host.com:/my/path" to ssh url elsif(/^(?<host>$ip_addr_match|$host_name_match):\/(?<file>$file_match)$/) { # convert "my.host.com:/my/path" to ssh url
$_ = "ssh://$+{host}/$+{file}"; $_ = "ssh://$+{host}/$+{file}";
} }
elsif(/^\{(?<host>$ip_addr_match|$host_name_match)\}\/(?<file>$file_match)$/) { # convert "{my.host.com}/my/path" to ssh url
$_ = "ssh://$+{host}/$+{file}";
}
else { else {
ERROR "Bad argument: not a subvolume declaration: $_"; ERROR "Bad argument: not a subvolume" . ($args_allow_group ? "/group" : "") . " declaration: $_";
HELP_MESSAGE(0); HELP_MESSAGE(0);
exit 1; exit 1;
} }
@ -1564,8 +1581,8 @@ MAIN:
# #
# print snapshot diff # print snapshot diff
# #
my $src_url = $subvol_args[0] || die; my $src_url = $filter_args[0] || die;
my $target_url = $subvol_args[1] || die; my $target_url = $filter_args[1] || die;
# FIXME: allow ssh:// src/dest (does not work since the configuration is not yet read). # FIXME: allow ssh:// src/dest (does not work since the configuration is not yet read).
my $src_vol = vinfo($src_url, { CONTEXT => "cmdline" }); my $src_vol = vinfo($src_url, { CONTEXT => "cmdline" });
@ -1675,37 +1692,52 @@ MAIN:
# #
# filter subvolumes matching command line arguments # filter subvolumes matching command line arguments
# #
if(($action_run || $action_tree || $action_info) && scalar(@subvol_args)) if(($action_run || $action_tree || $action_info) && scalar(@filter_args))
{ {
my $filter_count = undef; my $filter_count = undef;
my @filter; my @filter;
my %match; my %match;
foreach my $config_vol (@{$config->{VOLUME}}) { foreach my $config_vol (@{$config->{VOLUME}}) {
my $vol_url = $config_vol->{url} // die; my $vol_url = $config_vol->{url} // die;
if(grep(/^\Q$vol_url\E$/, @subvol_args)) { my $found = 0;
foreach my $filter (@filter_args) {
if(($vol_url eq $filter) || (map { ($filter eq $_) || () } @{$config_vol->{group}})) {
push(@filter, vinfo($vol_url, $config_vol)); push(@filter, vinfo($vol_url, $config_vol));
$match{$vol_url} = 1; $match{$filter} = 1;
next; TRACE "filter argument \"$filter\" matches volume: $vol_url\n";
$found = 1;
# last; # need to cycle through all filter_args for correct %match
} }
}
next if($found);
my @filter_subvol; my @filter_subvol;
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) { foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) {
my $subvol_url = $config_subvol->{url} // die; my $subvol_url = $config_subvol->{url} // die;
if(grep(/^\Q$subvol_url\E$/, @subvol_args)) {
$found = 0;
foreach my $filter (@filter_args) {
if(($subvol_url eq $filter) || (map { ($filter eq $_) || () } @{$config_subvol->{group}})) {
push(@filter_subvol, vinfo($subvol_url, $config_subvol)); push(@filter_subvol, vinfo($subvol_url, $config_subvol));
$match{$subvol_url} = 1; $match{$filter} = 1;
} else { TRACE "filter argument \"$filter\" matches subvolume: $subvol_url\n";
DEBUG "No match on subvolume command line argument, skipping subvolume: $subvol_url"; $found = 1;
# last; # need to cycle through all filter_args for correct %match
}
}
unless($found) {
DEBUG "No match on subvolume/group command line argument, skipping subvolume: $subvol_url";
$config_subvol->{ABORTED} = "USER_SKIP"; $config_subvol->{ABORTED} = "USER_SKIP";
} }
} }
unless(@filter_subvol) { unless(@filter_subvol) {
DEBUG "No match on subvolume command line argument, skipping volume: $vol_url"; DEBUG "No match on subvolume/group command line argument, skipping volume: $vol_url";
$config_vol->{ABORTED} = "USER_SKIP"; $config_vol->{ABORTED} = "USER_SKIP";
} }
push(@filter, @filter_subvol); push(@filter, @filter_subvol);
} }
# make sure all args have a match # make sure all args have a match
my @nomatch = map { $match{$_} ? () : $_ } @subvol_args; my @nomatch = map { $match{$_} ? () : $_ } @filter_args;
if(@nomatch) { if(@nomatch) {
foreach(@nomatch) { foreach(@nomatch) {
ERROR "Command line argument does not match any volume/subvolume declaration: $_"; ERROR "Command line argument does not match any volume/subvolume declaration: $_";
@ -1857,7 +1889,7 @@ MAIN:
# #
# print origin information # print origin information
# #
my $url = $subvol_args[0] || die; my $url = $filter_args[0] || die;
my $dump_uuid = 0; my $dump_uuid = 0;
my $vol = $vinfo_cache{$url}; my $vol = $vinfo_cache{$url};