btrbk: proper input validation of config file

pull/30/head
Axel Burri 2015-01-12 15:46:24 +01:00
parent a269231cf9
commit 1aaa72ebfe
1 changed files with 62 additions and 45 deletions

107
btrbk
View File

@ -61,17 +61,18 @@ my %uuid_info;
my $dryrun; my $dryrun;
my $loglevel = 1; my $loglevel = 1;
my %config_defaults = ( my %config_options = (
snapshot_dir => "_btrbk_snap", # NOTE: the parser always maps "no" to undef
incremental => 1, snapshot_dir => { default => "_btrbk_snap", accept_file => "relative" },
receive_log => undef, incremental => { default => "yes", accept => [ "yes", "no", "strict" ] },
snapshot_create_always => 0, receive_log => { default => undef, accept => [ "sidecar", "no" ], accept_file => "absolute" },
snapshot_preserve_all => 1, # TODO: honor this snapshot_create_always => { default => undef, accept => [ "yes", "no" ] },
snapshot_preserve_days => undef, snapshot_preserve_all => { default => "yes", accept => [ "yes", "no" ] }, # TODO: honor this
snapshot_preserve_weekly => undef, snapshot_preserve_days => { default => undef, accept => [ "no" ], accept_number => 1 },
target_preserve_all => 1, # TODO: honor this snapshot_preserve_weekly => { default => undef, accept => [ "no" ], accept_number => 1 },
target_preserve_days => undef, target_preserve_all => { default => "yes", accept => [ "yes", "no" ] }, # TODO: honor this
target_preserve_weekly => undef, target_preserve_days => { default => undef, accept => [ "no" ], accept_number => 1 },
target_preserve_weekly => { default => undef, accept => [ "no" ], accept_number => 1 },
); );
my @config_target_types = qw(send-receive); my @config_target_types = qw(send-receive);
@ -204,11 +205,11 @@ sub config_key($$)
my $node = shift; my $node = shift;
my $key = shift; my $key = shift;
TRACE "config_key: $node->{CONTEXT}"; TRACE "config_key: $node->{CONTEXT}";
while(not defined($node->{$key})) { while(not exists($node->{$key})) {
return undef unless($node->{PARENT}); return undef unless($node->{PARENT});
$node = $node->{PARENT}; $node = $node->{PARENT};
} }
TRACE "config_key: returning $node->{$key}"; TRACE "config_key: returning " . ($node->{$key} // "<undef>");
return $node->{$key}; return $node->{$key};
} }
@ -216,10 +217,12 @@ sub config_key($$)
sub parse_config($) sub parse_config($)
{ {
my $file = shift; my $file = shift;
my $root = { CONTEXT => "root", my $root = { CONTEXT => "root" };
%config_defaults
};
my $cur = $root; my $cur = $root;
# set defaults
foreach (keys %config_options) {
$root->{$_} = $config_options{$_}->{default};
}
unless(-r "$file") { unless(-r "$file") {
WARN "Configuration file not found: $file"; WARN "Configuration file not found: $file";
@ -315,9 +318,35 @@ sub parse_config($)
return undef; return undef;
} }
} }
elsif(grep(/^$key$/, keys %config_defaults)) # accept only keys listed in %config_default elsif(grep(/^$key$/, keys %config_options)) # accept only keys listed in %config_options
{ {
if(grep(/^$value$/, @{$config_options{$key}->{accept}})) {
TRACE "option \"$key=$value\" found in accept list";
}
elsif($config_options{$key}->{accept_number} && ($value =~ /^[0-9]+$/)) {
TRACE "option \"$key=$value\" is a number, accepted";
}
elsif($config_options{$key}->{accept_file})
{
if(($config_options{$key}->{accept_file} eq "relative") && ($value =~ /^\//)) {
ERROR "Only relative files allowed for option \"$key\" in \"$file\" line $.";
return undef;
}
elsif(($config_options{$key}->{accept_file} eq "absolute") && ($value !~ /^\//)) {
ERROR "Only absolute files allowed for option \"$key\" in \"$file\" line $.";
return undef;
}
TRACE "option \"$key=$value\" is a file, accepted";
$value =~ s/\/+$//; # remove trailing slash
$value =~ s/^\/+/\//; # sanitize leading slash
}
else
{
ERROR "Unsupported value \"$value\" for option \"$key\" in \"$file\" line $.";
return undef;
}
DEBUG "config: adding option \"$key=$value\" to $cur->{CONTEXT} context"; DEBUG "config: adding option \"$key=$value\" to $cur->{CONTEXT} context";
$value = undef if($value eq "no"); # we don't want to check for "no" all the time
$cur->{$key} = $value; $cur->{$key} = $value;
} }
else else
@ -963,7 +992,7 @@ MAIN:
$snapshot_name = "$svol$postfix"; $snapshot_name = "$svol$postfix";
} }
my $create_snapshot = config_key($config_subvol, "snapshot_create_always"); # TODO: check for "yes", ... my $create_snapshot = config_key($config_subvol, "snapshot_create_always");
foreach my $config_target (@{$config_subvol->{TARGET}}) foreach my $config_target (@{$config_subvol->{TARGET}})
{ {
next if($config_target->{ABORTED}); next if($config_target->{ABORTED});
@ -1025,45 +1054,33 @@ MAIN:
if($target_type eq "send-receive") if($target_type eq "send-receive")
{ {
DEBUG "***";
# DEBUG "*** $type\[" . join(',', map { "$_=$job_opts->{$_}" } keys(%$job_opts)) . "]";
DEBUG "*** $target_type";
DEBUG "*** source: $sroot/$svol";
DEBUG "*** dest : $droot/";
DEBUG "***";
INFO "Creating subvolume backup ($target_type) for: $sroot/$svol"; INFO "Creating subvolume backup ($target_type) for: $sroot/$svol";
INFO "Using previously created snapshot: $snapshot";
my $receive_log = config_key($config_target, "receive_log"); my $receive_log = config_key($config_target, "receive_log");
if($receive_log) if($receive_log && ($receive_log eq "sidecar")) {
{ # log to sidecar of destination snapshot
if(lc($receive_log) eq "sidecar" ) { $receive_log = "$droot/$snapshot_name.btrbk.log";
# log defaults to sidecar of destination snapshot
$receive_log = "$droot/$snapshot_name.btrbk.log";
}
else {
$receive_log =~ s/\/+$//; # remove trailing slash
$receive_log =~ s/^\/+/\//; # sanitize leading slash
unless($receive_log =~ /^\//) {
WARN "Receive logfile is not absolute, ignoring: $receive_log";
$receive_log = undef;
}
}
} }
if(config_key($config_target, "incremental"))
my $incremental = config_key($config_target, "incremental");
if($incremental)
{ {
INFO "Using previously created snapshot: $snapshot";
my ($latest_common_src, $latest_common_dst) = get_latest_common($sroot, $svol, $droot); my ($latest_common_src, $latest_common_dst) = get_latest_common($sroot, $svol, $droot);
if($latest_common_src && $latest_common_dst) { if($latest_common_src && $latest_common_dst) {
my $parent_snap = $latest_common_src->{FS_PATH}; my $parent_snap = $latest_common_src->{FS_PATH};
INFO "Using parent snapshot: $parent_snap"; INFO "Incremental from parent snapshot: $parent_snap";
btrfs_send_receive($snapshot, $droot, $parent_snap, $receive_log); btrfs_send_receive($snapshot, $droot, $parent_snap, $receive_log);
} elsif (lc(config_key($config_target, "incremental")) ne "strict") { }
INFO "No common parent subvolume present, creating initial full backup"; elsif($incremental ne "strict") {
INFO "No common parent subvolume present, creating full backup";
btrfs_send_receive($snapshot, $droot, undef, $receive_log); btrfs_send_receive($snapshot, $droot, undef, $receive_log);
} else { }
else {
WARN "Backup to $droot failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\""; WARN "Backup to $droot failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\"";
} }
} else { }
else {
INFO "Creating full backup (option \"incremental\" is not set)"; INFO "Creating full backup (option \"incremental\" is not set)";
btrfs_send_receive($snapshot, $droot, undef, $receive_log); btrfs_send_receive($snapshot, $droot, undef, $receive_log);
} }