mirror of https://github.com/digint/btrbk
btrbk: implemented "group" configuration option and filtering for volume/subvolume
parent
8bc1acc672
commit
e7c6e37bd0
106
btrbk
106
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 <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};
|
||||
|
|
Loading…
Reference in New Issue