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

106
btrbk
View File

@ -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 <axel@tty0.ch>';
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 $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 %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: <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 {
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 <subvol> print origin information for subvolume\n";
print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\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 <subvol> print origin information for subvolume\n";
print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\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(/^(?<host>$ip_addr_match|$host_name_match):\/(?<file>$file_match)$/) { # convert "my.host.com:/my/path" to ssh url
$_ = "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 {
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};