From e7c6e37bd0a976e33ea076c9a3849d1b5ba082e0 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Wed, 2 Sep 2015 11:04:22 +0200 Subject: [PATCH] btrbk: implemented "group" configuration option and filtering for volume/subvolume --- btrbk | 106 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 37 deletions(-) diff --git a/btrbk b/btrbk index 31a2e92..eee5179 100755 --- a/btrbk +++ b/btrbk @@ -47,7 +47,7 @@ use Date::Calc qw(Today Delta_Days Day_of_Week); use Getopt::Long qw(GetOptions); use Data::Dumper; -our $VERSION = "0.20.0"; +our $VERSION = "0.21.0-dev"; our $AUTHOR = 'Axel Burri '; our $PROJECT_HOME = ''; @@ -55,6 +55,13 @@ my $version_info = "btrbk command line client, version $VERSION"; 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: +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 %config_options = ( @@ -78,6 +85,7 @@ my %config_options = ( ssh_port => { default => "default", accept => [ "default" ], accept_numeric => 1 }, ssh_compression => { 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 snapshot_create_always => { default => undef, accept => [ "yes", "no" ], @@ -108,12 +116,6 @@ my $loglevel = 1; my $show_progress = 0; 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: -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 { print STDERR "\nERROR: process died unexpectedly (btrbk v$VERSION)"; @@ -145,12 +147,12 @@ sub HELP_MESSAGE print STDERR " --progress show progress bar on send-receive operation\n"; print STDERR "\n"; print STDERR "commands:\n"; - print STDERR " run [subvol...] 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 " tree [subvol...] shows backup tree\n"; - print STDERR " info [subvol...] print useful filesystem information\n"; - print STDERR " origin print origin information for subvolume\n"; - print STDERR " diff shows new files since subvolume for subvolume \n"; + print STDERR " run [subvol|group...] perform backup operations as defined in the config file\n"; + print STDERR " dryrun [subvol|group...] don't run btrfs commands; show what would be executed\n"; + print STDERR " tree [subvol|group...] shows backup tree\n"; + print STDERR " info [subvol|group...] print useful filesystem information\n"; + print STDERR " origin print origin information for subvolume\n"; + print STDERR " diff shows new files since subvolume for subvolume \n"; print STDERR "\n"; print STDERR "For additional information, see $PROJECT_HOME\n"; } @@ -568,6 +570,11 @@ sub parse_config(@) 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}})) { ERROR "Option \"$key\" is only allowed in " . join(" or ", map("\"$_\"", @{$config_options{$key}->{context}})) . " context, in \"$file\" line $."; return undef; @@ -1498,36 +1505,40 @@ MAIN: $show_progress = 0; } 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); if(($command eq "run") || ($command eq "dryrun")) { $action_run = 1; $dryrun = 1 if($command eq "dryrun"); $args_expected_min = 0; $args_expected_max = 9999; - @subvol_args = @ARGV; + $args_allow_group = 1; + @filter_args = @ARGV; } elsif ($command eq "info") { $action_info = 1; $args_expected_min = 0; $args_expected_max = 9999; - @subvol_args = @ARGV; + $args_allow_group = 1; + @filter_args = @ARGV; } elsif ($command eq "tree") { $action_tree = 1; $args_expected_min = 0; $args_expected_max = 9999; - @subvol_args = @ARGV; + $args_allow_group = 1; + @filter_args = @ARGV; } elsif ($command eq "diff") { $action_diff = 1; $args_expected_min = $args_expected_max = 2; - @subvol_args = @ARGV; + @filter_args = @ARGV; } elsif ($command eq "origin") { $action_origin = 1; $args_expected_min = $args_expected_max = 1; - @subvol_args = @ARGV; + @filter_args = @ARGV; } else { ERROR "Unrecognized command: $command"; @@ -1541,16 +1552,22 @@ MAIN: } # input validation - foreach (@subvol_args) { + foreach (@filter_args) { 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 } elsif(/^(?$ip_addr_match|$host_name_match):\/(?$file_match)$/) { # convert "my.host.com:/my/path" to ssh url $_ = "ssh://$+{host}/$+{file}"; } + elsif(/^\{(?$ip_addr_match|$host_name_match)\}\/(?$file_match)$/) { # convert "{my.host.com}/my/path" to ssh url + $_ = "ssh://$+{host}/$+{file}"; + } else { - ERROR "Bad argument: not a subvolume declaration: $_"; + ERROR "Bad argument: not a subvolume" . ($args_allow_group ? "/group" : "") . " declaration: $_"; HELP_MESSAGE(0); exit 1; } @@ -1564,8 +1581,8 @@ MAIN: # # print snapshot diff # - my $src_url = $subvol_args[0] || die; - my $target_url = $subvol_args[1] || die; + my $src_url = $filter_args[0] || die; + my $target_url = $filter_args[1] || die; # FIXME: allow ssh:// src/dest (does not work since the configuration is not yet read). my $src_vol = vinfo($src_url, { CONTEXT => "cmdline" }); @@ -1675,37 +1692,52 @@ MAIN: # # 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; my %match; foreach my $config_vol (@{$config->{VOLUME}}) { my $vol_url = $config_vol->{url} // die; - if(grep(/^\Q$vol_url\E$/, @subvol_args)) { - push(@filter, vinfo($vol_url, $config_vol)); - $match{$vol_url} = 1; - next; + 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)); + $match{$filter} = 1; + 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; foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) { my $subvol_url = $config_subvol->{url} // die; - if(grep(/^\Q$subvol_url\E$/, @subvol_args)) { - push(@filter_subvol, vinfo($subvol_url, $config_subvol)); - $match{$subvol_url} = 1; - } else { - DEBUG "No match on subvolume command line argument, skipping subvolume: $subvol_url"; + + $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)); + $match{$filter} = 1; + TRACE "filter argument \"$filter\" matches subvolume: $subvol_url\n"; + $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"; } } 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"; } push(@filter, @filter_subvol); } # make sure all args have a match - my @nomatch = map { $match{$_} ? () : $_ } @subvol_args; + my @nomatch = map { $match{$_} ? () : $_ } @filter_args; if(@nomatch) { foreach(@nomatch) { ERROR "Command line argument does not match any volume/subvolume declaration: $_"; @@ -1857,7 +1889,7 @@ MAIN: # # print origin information # - my $url = $subvol_args[0] || die; + my $url = $filter_args[0] || die; my $dump_uuid = 0; my $vol = $vinfo_cache{$url};