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