mirror of https://github.com/digint/btrbk
Merge branch 'config-file-refactoring'
commit
77d0a95d33
703
btrbk
703
btrbk
|
@ -61,6 +61,20 @@ my %uuid_info;
|
||||||
my $dryrun;
|
my $dryrun;
|
||||||
my $loglevel = 1;
|
my $loglevel = 1;
|
||||||
|
|
||||||
|
my %config_options = (
|
||||||
|
# NOTE: the parser always maps "no" to undef
|
||||||
|
snapshot_dir => { default => "_btrbk_snap", accept_file => "relative" },
|
||||||
|
incremental => { default => "yes", accept => [ "yes", "no", "strict" ] },
|
||||||
|
receive_log => { default => undef, accept => [ "sidecar", "no" ], accept_file => "absolute" },
|
||||||
|
snapshot_create_always => { default => undef, accept => [ "yes", "no" ] },
|
||||||
|
snapshot_preserve_days => { default => "all", accept => [ "no", "all" ], accept_number => 1 },
|
||||||
|
snapshot_preserve_weekly => { default => 0, accept => [ "no" ], accept_number => 1 },
|
||||||
|
target_preserve_days => { default => "all", accept => [ "no", "all" ], accept_number => 1 },
|
||||||
|
target_preserve_weekly => { default => 0, accept => [ "no" ], accept_number => 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
my @config_target_types = qw(send-receive);
|
||||||
|
|
||||||
|
|
||||||
sub VERSION_MESSAGE
|
sub VERSION_MESSAGE
|
||||||
{
|
{
|
||||||
|
@ -184,70 +198,172 @@ sub btr_subvolume_detail($)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub config_key($$)
|
||||||
|
{
|
||||||
|
my $node = shift;
|
||||||
|
my $key = shift;
|
||||||
|
TRACE "config_key: $node->{CONTEXT}";
|
||||||
|
while(not exists($node->{$key})) {
|
||||||
|
return undef unless($node->{PARENT});
|
||||||
|
$node = $node->{PARENT};
|
||||||
|
}
|
||||||
|
TRACE "config_key: returning " . ($node->{$key} // "<undef>");
|
||||||
|
return $node->{$key};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub parse_config($)
|
sub parse_config($)
|
||||||
{
|
{
|
||||||
my $file = shift;
|
my $file = shift;
|
||||||
my @jobs;
|
my $root = { CONTEXT => "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";
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
TRACE "parsing config file: $file";
|
DEBUG "config: parsing file: $file";
|
||||||
open(FILE, '<', $file) or die $!;
|
open(FILE, '<', $file) or die $!;
|
||||||
while (<FILE>) {
|
while (<FILE>) {
|
||||||
chomp;
|
chomp;
|
||||||
next if /^\s*#/; # ignore comments
|
next if /^\s*#/; # ignore comments
|
||||||
next if /^\s*$/; # ignore empty lines
|
next if /^\s*$/; # ignore empty lines
|
||||||
TRACE "parse_config: parsing line: $_";
|
TRACE "config: parsing line $. with context=$cur->{CONTEXT}: \"$_\"";
|
||||||
if(/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/)
|
if(/^(\s*)([a-zA-Z_]+)\s+(.*)$/)
|
||||||
{
|
{
|
||||||
my %job = ( type => "subvol_backup",
|
my ($indent, $key, $value) = (length($1), lc($2), $3);
|
||||||
sroot => $1,
|
$value =~ s/\s*$//;
|
||||||
svol => $2,
|
# NOTE: we do not perform checks on indentation!
|
||||||
droot => $3,
|
|
||||||
);
|
|
||||||
my @options = split(/,/, $4);
|
|
||||||
|
|
||||||
$job{sroot} =~ s/\/+$//; # remove trailing slash
|
if($key eq "volume")
|
||||||
$job{sroot} =~ s/^\/+/\//; # sanitize leading slash
|
{
|
||||||
$job{svol} =~ s/\/+$//; # remove trailing slash
|
$cur = $root;
|
||||||
$job{svol} =~ s/^\/+//; # remove leading slash
|
DEBUG "config: context forced to: $cur->{CONTEXT}";
|
||||||
if($job{svol} =~ /\//) {
|
DEBUG "config: adding volume \"$value\" to root context";
|
||||||
ERROR "src_subvol contains slashes: $job{svol}";
|
$value =~ s/\/+$//; # remove trailing slash
|
||||||
|
$value =~ s/^\/+/\//; # sanitize leading slash
|
||||||
|
my $volume = { CONTEXT => "volume",
|
||||||
|
PARENT => $cur,
|
||||||
|
sroot => $value,
|
||||||
|
};
|
||||||
|
$cur->{VOLUME} //= [];
|
||||||
|
push(@{$cur->{VOLUME}}, $volume);
|
||||||
|
$cur = $volume;
|
||||||
|
}
|
||||||
|
elsif($key eq "subvolume")
|
||||||
|
{
|
||||||
|
while($cur->{CONTEXT} ne "volume") {
|
||||||
|
if(($cur->{CONTEXT} eq "root") || (not $cur->{PARENT})) {
|
||||||
|
ERROR "subvolume keyword outside volume context, in \"$file\" line $.";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
$cur = $cur->{PARENT} || die;
|
||||||
|
DEBUG "config: context changed to: $cur->{CONTEXT}";
|
||||||
|
}
|
||||||
|
$value =~ s/\/+$//; # remove trailing slash
|
||||||
|
$value =~ s/^\/+//; # remove leading slash
|
||||||
|
if($value =~ /\//) {
|
||||||
|
ERROR "subvolume contains slashes: \"$value\" in \"$file\" line $.";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG "config: adding subvolume \"$value\" to volume context: $cur->{sroot}";
|
||||||
|
my $subvolume = { CONTEXT => "subvolume",
|
||||||
|
PARENT => $cur,
|
||||||
|
svol => $value,
|
||||||
|
};
|
||||||
|
$cur->{SUBVOLUME} //= [];
|
||||||
|
push(@{$cur->{SUBVOLUME}}, $subvolume);
|
||||||
|
$cur = $subvolume;
|
||||||
|
}
|
||||||
|
elsif($key eq "target")
|
||||||
|
{
|
||||||
|
if($cur->{CONTEXT} eq "target") {
|
||||||
|
$cur = $cur->{PARENT} || die;
|
||||||
|
DEBUG "config: context changed to: $cur->{CONTEXT}";
|
||||||
|
}
|
||||||
|
if($cur->{CONTEXT} ne "subvolume") {
|
||||||
|
ERROR "target keyword outside subvolume context, in \"$file\" line $.";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
if($value =~ /^(\S+)\s+(\S+)$/)
|
||||||
|
{
|
||||||
|
my ($target_type, $droot) = ($1, $2);
|
||||||
|
unless(grep(/^$target_type$/, @config_target_types)) {
|
||||||
|
ERROR "unknown target type \"$target_type\" in \"$file\" line $.";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
$droot =~ s/\/+$//; # remove trailing slash
|
||||||
|
$droot =~ s/^\/+/\//; # sanitize leading slash
|
||||||
|
DEBUG "config: adding target \"$droot\" (type=$target_type) to subvolume context: $cur->{PARENT}->{sroot}/$cur->{svol}";
|
||||||
|
my $target = { CONTEXT => "target",
|
||||||
|
PARENT => $cur,
|
||||||
|
target_type => $target_type,
|
||||||
|
droot => $droot,
|
||||||
|
};
|
||||||
|
$cur->{TARGET} //= [];
|
||||||
|
push(@{$cur->{TARGET}}, $target);
|
||||||
|
$cur = $target;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ERROR "Ambiguous target configuration, in \"$file\" line $.";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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";
|
||||||
|
$value = undef if($value eq "no"); # we don't want to check for "no" all the time
|
||||||
|
$cur->{$key} = $value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ERROR "Unknown option \"$key\" in \"$file\" line $.";
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
$job{droot} =~ s/\/+$//; # remove trailing slash
|
TRACE "line processed: new context=$cur->{CONTEXT}";
|
||||||
$job{droot} =~ s/^\/+/\//; # sanitize leading slash
|
|
||||||
|
|
||||||
$job{mountpoint} = $job{sroot}; # TODO: honor this, automount
|
|
||||||
|
|
||||||
foreach(@options) {
|
|
||||||
if ($_ eq "incremental") { $job{options}->{incremental} = 1; }
|
|
||||||
elsif($_ eq "init") { $job{options}->{init} = 1; }
|
|
||||||
elsif($_ eq "create") { $job{options}->{create} = 1; }
|
|
||||||
elsif($_ eq "log") { $job{options}->{log} = 1; }
|
|
||||||
elsif($_ =~ /^log=(\S+)$/) { $job{options}->{log} = 1; $job{options}->{logfile} = $1; }
|
|
||||||
elsif($_ =~ /^preserve=[dD]([0-9]+)[wW]([0-9]+)$/) { $job{options}->{preserve} = {daily => $1, weekly => $2}; }
|
|
||||||
else {
|
|
||||||
ERROR "Ambiguous option=\"$_\": $file line $.";
|
|
||||||
return undef; # be very strict here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TRACE "parse_config: adding job \"$job{type}\": $job{sroot}/$job{svol} -> $job{droot}/";
|
|
||||||
push @jobs, \%job;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ERROR "Ambiguous configuration: $file line $.";
|
ERROR "Parse error in \"$file\" line $.";
|
||||||
return undef; # be very strict here
|
return undef;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close FILE;
|
|
||||||
TRACE "jobs: " . Dumper(\@jobs);
|
TRACE(Data::Dumper->Dump([$root], ["config_root"]));
|
||||||
return \@jobs;
|
return $root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -561,39 +677,50 @@ sub check_backup_scheme(@)
|
||||||
my %args = @_;
|
my %args = @_;
|
||||||
my $vol_date = $args{vol_date} || die;
|
my $vol_date = $args{vol_date} || die;
|
||||||
my $today = $args{today} || die;
|
my $today = $args{today} || die;
|
||||||
my $keep_days = $args{keep_days} || die;
|
my $week_start = $args{week_start} || die;
|
||||||
my $weekly_threshold = $args{weekly_threshold} || die;
|
my $preserve_days = $args{preserve_days} // die;
|
||||||
my $keep_info = $args{keep_info} || die;
|
my $preserve_weekly = $args{preserve_weekly} // die;
|
||||||
my $keep = 0;
|
my $preserve_info = $args{preserve_info} || die;
|
||||||
|
my $preserve = 0;
|
||||||
my ($vol_y, $vol_m, $vol_d) = @$vol_date;
|
my ($vol_y, $vol_m, $vol_d) = @$vol_date;
|
||||||
|
|
||||||
my $dd = Delta_Days(@$vol_date, @$today);
|
# calculate weekly_threshold
|
||||||
if($dd <= $keep_days)
|
my @weekly_threshold = Add_Delta_Days(@$week_start, (-7 * $preserve_weekly));
|
||||||
{
|
TRACE "weekly_threshold for preserve_weekly=$preserve_weekly: " . join('-', @weekly_threshold);
|
||||||
$keep = "less than $keep_days days old (age=$dd days)";
|
|
||||||
DEBUG "$keep";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Delta_Days(@$vol_date, @$weekly_threshold) < 0)
|
if($preserve_days eq "all") {
|
||||||
{
|
$preserve = "preserve_days is set to \"all\"";
|
||||||
DEBUG "not older than " . join('-', @$weekly_threshold);
|
DEBUG "$preserve";
|
||||||
my ($vol_wnr, $vol_wy) = Week_of_Year(@$vol_date);
|
}
|
||||||
unless($keep_info->{week}->{"$vol_wy-$vol_wnr"})
|
else {
|
||||||
|
my $dd = Delta_Days(@$vol_date, @$today);
|
||||||
|
if($dd <= $preserve_days)
|
||||||
{
|
{
|
||||||
$keep_info->{week}->{"$vol_wy-$vol_wnr"} = 1;
|
$preserve = "less than $preserve_days days old (age: $dd days)";
|
||||||
$keep = "last in week $vol_wy-$vol_wnr";
|
DEBUG "$preserve";
|
||||||
DEBUG "$keep";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unless($keep_info->{month}->{"$vol_y-$vol_m"})
|
if(Delta_Days(@$vol_date, @weekly_threshold) < 0)
|
||||||
{
|
{
|
||||||
$keep_info->{month}->{"$vol_y-$vol_m"} = 1;
|
DEBUG "not older than " . join('-', @weekly_threshold);
|
||||||
$keep = "last in month $vol_y-$vol_m";
|
my ($vol_wnr, $vol_wy) = Week_of_Year(@$vol_date);
|
||||||
DEBUG "$keep";
|
unless($preserve_info->{week}->{"$vol_wy-$vol_wnr"})
|
||||||
|
{
|
||||||
|
$preserve_info->{week}->{"$vol_wy-$vol_wnr"} = 1;
|
||||||
|
$preserve = "last in week #$vol_wnr, $vol_wy";
|
||||||
|
DEBUG "$preserve";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $keep;
|
unless($preserve_info->{month}->{"$vol_y-$vol_m"})
|
||||||
|
{
|
||||||
|
$preserve_info->{month}->{"$vol_y-$vol_m"} = 1;
|
||||||
|
$preserve = "last in month $vol_y-$vol_m";
|
||||||
|
DEBUG "$preserve";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $preserve;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -605,7 +732,11 @@ MAIN:
|
||||||
my @today = Today();
|
my @today = Today();
|
||||||
|
|
||||||
my %opts;
|
my %opts;
|
||||||
getopts('s:t:c:vl:p', \%opts);
|
unless(getopts('s:t:c:vl:p', \%opts)) {
|
||||||
|
VERSION_MESSAGE();
|
||||||
|
HELP_MESSAGE(0);
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
my $command = shift @ARGV;
|
my $command = shift @ARGV;
|
||||||
|
|
||||||
# assign command line options
|
# assign command line options
|
||||||
|
@ -618,7 +749,7 @@ MAIN:
|
||||||
else {
|
else {
|
||||||
$loglevel = $opts{v} ? 2 : 0;
|
$loglevel = $opts{v} ? 2 : 0;
|
||||||
}
|
}
|
||||||
my $config = $opts{c} || $default_config;
|
my $config_file = $opts{c} || $default_config;
|
||||||
my $snapdir = $opts{s} || $default_snapdir;
|
my $snapdir = $opts{s} || $default_snapdir;
|
||||||
$snapdir =~ s/\/+$//; # remove trailing slash
|
$snapdir =~ s/\/+$//; # remove trailing slash
|
||||||
$snapdir =~ s/^\/+//; # remove leading slash
|
$snapdir =~ s/^\/+//; # remove leading slash
|
||||||
|
@ -751,24 +882,39 @@ MAIN:
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# check jobs, fill vol_info hash
|
# fill vol_info hash, basic checks on configuration
|
||||||
#
|
#
|
||||||
my $jobs = parse_config($config);
|
my $config = parse_config($config_file);
|
||||||
unless($jobs) {
|
unless($config) {
|
||||||
ERROR "Failed to parse configuration file";
|
ERROR "Failed to parse configuration file";
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
foreach my $job (@$jobs)
|
unless(ref($config->{VOLUME}) eq "ARRAY") {
|
||||||
|
ERROR "No volumes defined in configuration file";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
{
|
{
|
||||||
my $sroot = $job->{sroot} || die;
|
my $sroot = $config_vol->{sroot} || die;
|
||||||
my $droot = $job->{droot} || die;
|
|
||||||
my $svol = $job->{svol} || die;
|
|
||||||
$vol_info{$sroot} //= btr_subtree($sroot);
|
$vol_info{$sroot} //= btr_subtree($sroot);
|
||||||
$vol_info{$droot} //= btr_subtree($droot);
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||||
unless(subvol($sroot, $svol) && $vol_info{$droot}) {
|
{
|
||||||
ERROR "Failed to read btrfs subvolume information, aborting job";
|
my $svol = $config_subvol->{svol} || die;
|
||||||
$job->{ABORTED} = 1;
|
unless(subvol($sroot, $svol)) {
|
||||||
next;
|
WARN "Subvolume \"$svol\" not present in btrfs subvolume list for \"$sroot\", skipping section";
|
||||||
|
$config_subvol->{ABORTED} = 1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
|
{
|
||||||
|
my $droot = $config_target->{droot} || die;
|
||||||
|
$vol_info{$droot} //= btr_subtree($droot);
|
||||||
|
unless($vol_info{$droot}) {
|
||||||
|
WARN "Failed to read btrfs subvolume list for \"$droot\", skipping target";
|
||||||
|
$config_target->{ABORTED} = 1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TRACE(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
|
TRACE(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
|
||||||
|
@ -779,19 +925,14 @@ MAIN:
|
||||||
#
|
#
|
||||||
# print snapshot tree
|
# print snapshot tree
|
||||||
#
|
#
|
||||||
my %info;
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
foreach my $job (@$jobs)
|
|
||||||
{
|
{
|
||||||
$info{$job->{sroot}}->{$job->{svol}} = $job;
|
my $sroot = $config_vol->{sroot} || die;
|
||||||
}
|
print "$sroot\n";
|
||||||
foreach my $root (sort keys %info)
|
next unless $vol_info{$sroot};
|
||||||
{
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||||
print "$root\n";
|
|
||||||
foreach my $job (sort { $a->{svol} cmp $b->{svol} } (values %{$info{$root}}))
|
|
||||||
{
|
{
|
||||||
my $sroot = $job->{sroot} || die;
|
my $svol = $config_subvol->{svol} || die;
|
||||||
my $svol = $job->{svol} || die;
|
|
||||||
next unless $vol_info{$job->{sroot}};
|
|
||||||
print "|-- $svol\n";
|
print "|-- $svol\n";
|
||||||
my $sroot_uuid;
|
my $sroot_uuid;
|
||||||
foreach (values $vol_info{$sroot}) {
|
foreach (values $vol_info{$sroot}) {
|
||||||
|
@ -801,17 +942,19 @@ MAIN:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
die unless $sroot_uuid;
|
die unless $sroot_uuid;
|
||||||
foreach (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } (values $vol_info{$sroot})) {
|
foreach (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } (values $vol_info{$sroot}))
|
||||||
|
{
|
||||||
next unless($_->{parent_uuid} eq $sroot_uuid);
|
next unless($_->{parent_uuid} eq $sroot_uuid);
|
||||||
# next unless($_->{SUBVOL_PATH} =~ /^$snapdir/); # don't print non-btrbk snapshots
|
# next unless($_->{SUBVOL_PATH} =~ /^$snapdir/); # don't print non-btrbk snapshots
|
||||||
print "| ^-- $_->{SUBVOL_PATH}\n";
|
print "| ^-- $_->{SUBVOL_PATH}\n";
|
||||||
my $snapshot = $_->{FS_PATH};
|
my $snapshot = $_->{FS_PATH};
|
||||||
$snapshot =~ s/^.*\///;
|
$snapshot =~ s/^.*\///;
|
||||||
foreach (sort { $a->{droot} cmp $b->{droot} } @$jobs) {
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
next unless $vol_info{$_->{droot}};
|
{
|
||||||
next unless(($_->{sroot} eq $sroot) && ($_->{svol} eq $svol));
|
my $droot = $config_target->{droot} || die;
|
||||||
my $match = "$_->{droot}/$snapshot";
|
next unless $vol_info{$droot};
|
||||||
foreach (sort { $a->{FS_PATH} cmp $b->{FS_PATH} } (values $vol_info{$_->{droot}})) {
|
my $match = "$droot/$snapshot";
|
||||||
|
foreach (sort { $a->{FS_PATH} cmp $b->{FS_PATH} } (values $vol_info{$droot})) {
|
||||||
print "| | |== $_->{FS_PATH}\n" if($_->{FS_PATH} eq $match);
|
print "| | |== $_->{FS_PATH}\n" if($_->{FS_PATH} eq $match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -829,113 +972,134 @@ MAIN:
|
||||||
#
|
#
|
||||||
my $timestamp = sprintf("%04d%02d%02d", @today);
|
my $timestamp = sprintf("%04d%02d%02d", @today);
|
||||||
my %snapshot_cache;
|
my %snapshot_cache;
|
||||||
foreach my $job (@$jobs)
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
{
|
{
|
||||||
my $sroot = $job->{sroot} || die;
|
next if($config_vol->{ABORTED});
|
||||||
my $svol = $job->{svol} || die;
|
my $sroot = $config_vol->{sroot} || die;
|
||||||
my $droot = $job->{droot} || die;
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||||
my $type = $job->{type} || die;
|
|
||||||
|
|
||||||
unless(subvol($sroot, $svol)) {
|
|
||||||
WARN "Source subvolume not found, aborting job: $sroot/$svol";
|
|
||||||
$job->{ABORTED} = 1;
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $snapshot;
|
|
||||||
my $snapshot_name;
|
|
||||||
if($snapshot_cache{"$sroot/$svol"})
|
|
||||||
{
|
{
|
||||||
$snapshot = $snapshot_cache{"$sroot/$svol"}->{file};
|
next if($config_subvol->{ABORTED});
|
||||||
$snapshot_name = $snapshot_cache{"$sroot/$svol"}->{name};
|
my $svol = $config_subvol->{svol} || die;
|
||||||
}
|
my $snapshot;
|
||||||
else
|
my $snapshot_name;
|
||||||
{
|
if($snapshot_cache{"$sroot/$svol"})
|
||||||
# find new snapshot name
|
{
|
||||||
my $postfix_counter = -1;
|
$snapshot = $snapshot_cache{"$sroot/$svol"}->{file};
|
||||||
my $postfix;
|
$snapshot_name = $snapshot_cache{"$sroot/$svol"}->{name};
|
||||||
do {
|
|
||||||
$postfix_counter++;
|
|
||||||
$postfix = '.' . $timestamp . ($postfix_counter ? "_$postfix_counter" : "");
|
|
||||||
TRACE "Testing source snapshot name: $snapdir$svol$postfix";
|
|
||||||
} while(subvol($sroot, "$snapdir$svol$postfix")); # NOTE: $snapdir always has trailing slash!
|
|
||||||
$snapshot = "$sroot/$snapdir$svol$postfix";
|
|
||||||
$snapshot_name = "$svol$postfix";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(subvol($droot, $snapshot_name)) {
|
|
||||||
# TODO: this seems not right here: maybe just skip this check, and panic later
|
|
||||||
WARN "Snapshot already exists at destination, aborting job: $droot/$snapshot_name";
|
|
||||||
$job->{ABORTED} = 1;
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
create_snapdir($sroot, $svol, $snapdir);
|
|
||||||
|
|
||||||
# make snapshot of svol, if not already created by another job
|
|
||||||
unless($snapshot_cache{"$sroot/$svol"})
|
|
||||||
{
|
|
||||||
DEBUG "***";
|
|
||||||
DEBUG "*** snapshot";
|
|
||||||
DEBUG "*** source: $sroot/$svol";
|
|
||||||
DEBUG "*** dest : $snapshot";
|
|
||||||
DEBUG "***";
|
|
||||||
INFO "Creating subvolume snapshot for: $sroot/$svol";
|
|
||||||
|
|
||||||
unless(btrfs_snapshot("$sroot/$svol", $snapshot)) {
|
|
||||||
WARN "Failed to create snapshot, aborting job: $sroot/$svol";
|
|
||||||
$job->{ABORTED} = 1;
|
|
||||||
}
|
}
|
||||||
$snapshot_cache{"$sroot/$svol"} = { name => $snapshot_name,
|
else
|
||||||
file => $snapshot };
|
{
|
||||||
|
# find new snapshot name
|
||||||
|
my $postfix_counter = -1;
|
||||||
|
my $postfix;
|
||||||
|
do {
|
||||||
|
$postfix_counter++;
|
||||||
|
$postfix = '.' . $timestamp . ($postfix_counter ? "_$postfix_counter" : "");
|
||||||
|
TRACE "Testing source snapshot name: $snapdir$svol$postfix";
|
||||||
|
} while(subvol($sroot, "$snapdir$svol$postfix")); # NOTE: $snapdir always has trailing slash!
|
||||||
|
$snapshot = "$sroot/$snapdir$svol$postfix";
|
||||||
|
$snapshot_name = "$svol$postfix";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $create_snapshot = config_key($config_subvol, "snapshot_create_always");
|
||||||
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
|
{
|
||||||
|
next if($config_target->{ABORTED});
|
||||||
|
my $droot = $config_target->{droot} || die;
|
||||||
|
if(subvol($droot, $snapshot_name)) {
|
||||||
|
# TODO: this seems not right here: maybe just skip this check, and panic later
|
||||||
|
WARN "Snapshot already exists at destination, skipping target: $droot/$snapshot_name";
|
||||||
|
$config_target->{ABORTED} = 1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
if($config_target->{target_type} eq "send-receive") {
|
||||||
|
$create_snapshot = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unless($create_snapshot) {
|
||||||
|
WARN "No snapshots to be created, skipping subvolume: $sroot/$svol";
|
||||||
|
$config_subvol->{ABORTED} = 1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
create_snapdir($sroot, $svol, $snapdir);
|
||||||
|
|
||||||
|
# make snapshot of svol, if not already created by another job
|
||||||
|
unless($snapshot_cache{"$sroot/$svol"})
|
||||||
|
{
|
||||||
|
INFO "Creating subvolume snapshot for: $sroot/$svol";
|
||||||
|
|
||||||
|
unless(btrfs_snapshot("$sroot/$svol", $snapshot)) {
|
||||||
|
WARN "Failed to create snapshot, skipping subvolume: $sroot/$svol";
|
||||||
|
$config_subvol->{ABORTED} = 1;
|
||||||
|
}
|
||||||
|
$snapshot_cache{"$sroot/$svol"} = { name => $snapshot_name,
|
||||||
|
file => $snapshot };
|
||||||
|
}
|
||||||
|
$config_subvol->{snapshot} = $snapshot;
|
||||||
|
$config_subvol->{snapshot_name} = $snapshot_name;
|
||||||
}
|
}
|
||||||
$job->{snapshot} = $snapshot;
|
|
||||||
$job->{snapshot_name} = $snapshot_name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# create backups
|
# create backups
|
||||||
#
|
#
|
||||||
foreach my $job (@$jobs)
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
{
|
{
|
||||||
next if($job->{ABORTED});
|
next if($config_vol->{ABORTED});
|
||||||
|
my $sroot = $config_vol->{sroot} || die;
|
||||||
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||||
|
{
|
||||||
|
next if($config_subvol->{ABORTED});
|
||||||
|
my $svol = $config_subvol->{svol} || die;
|
||||||
|
my $snapshot = $config_subvol->{snapshot} || die;
|
||||||
|
my $snapshot_name = $config_subvol->{snapshot_name} || die;
|
||||||
|
|
||||||
my $sroot = $job->{sroot} || die;
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
my $svol = $job->{svol} || die;
|
{
|
||||||
my $droot = $job->{droot} || die;
|
next if($config_target->{ABORTED});
|
||||||
my $type = $job->{type} || die;
|
my $droot = $config_target->{droot} || die;
|
||||||
my $snapshot = $job->{snapshot} || die;
|
my $target_type = $config_target->{target_type} || die;
|
||||||
my $snapshot_name = $job->{snapshot_name} || die;
|
|
||||||
my $job_opts = $job->{options} || die;
|
|
||||||
|
|
||||||
DEBUG "***";
|
my $success = 0;
|
||||||
DEBUG "*** $type\[" . join(',', map { "$_=$job_opts->{$_}" } keys(%$job_opts)) . "]";
|
if($target_type eq "send-receive")
|
||||||
DEBUG "*** source: $sroot/$svol";
|
{
|
||||||
DEBUG "*** dest : $droot/";
|
INFO "Creating subvolume backup ($target_type) for: $sroot/$svol";
|
||||||
DEBUG "***";
|
INFO "Using previously created snapshot: $snapshot";
|
||||||
INFO "Creating subvolume backup for: $sroot/$svol";
|
|
||||||
|
|
||||||
my $changelog = "";
|
my $receive_log = config_key($config_target, "receive_log");
|
||||||
if ($job_opts->{log}) {
|
if($receive_log && ($receive_log eq "sidecar")) {
|
||||||
# log defaults to sidecar of destination snapshot
|
# log to sidecar of destination snapshot
|
||||||
$changelog = $job_opts->{logfile} || "$droot/$snapshot_name.btrbk.log";
|
$receive_log = "$droot/$snapshot_name.btrbk.log";
|
||||||
}
|
}
|
||||||
if ($job_opts->{incremental}) {
|
|
||||||
INFO "Using previously created snapshot: $snapshot";
|
my $incremental = config_key($config_target, "incremental");
|
||||||
# INFO "Attempting incremantal backup (option=incremental)";
|
if($incremental)
|
||||||
my ($latest_common_src, $latest_common_dst) = get_latest_common($sroot, $svol, $droot);
|
{
|
||||||
if ($latest_common_src && $latest_common_dst) {
|
my ($latest_common_src, $latest_common_dst) = get_latest_common($sroot, $svol, $droot);
|
||||||
my $parent_snap = $latest_common_src->{FS_PATH};
|
if($latest_common_src && $latest_common_dst) {
|
||||||
INFO "Using parent snapshot: $parent_snap";
|
my $parent_snap = $latest_common_src->{FS_PATH};
|
||||||
btrfs_send_receive($snapshot, $droot, $parent_snap, $changelog);
|
INFO "Incremental from parent snapshot: $parent_snap";
|
||||||
} elsif ($job_opts->{init}) {
|
$success = btrfs_send_receive($snapshot, $droot, $parent_snap, $receive_log);
|
||||||
INFO "No common parent snapshots found, creating initial backup (option=init)";
|
}
|
||||||
btrfs_send_receive($snapshot, $droot, undef, $changelog);
|
elsif($incremental ne "strict") {
|
||||||
} else {
|
INFO "No common parent subvolume present, creating full backup";
|
||||||
WARN "Backup to $droot failed: no common parent subvolume found, and job option \"create\" is not set";
|
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WARN "Backup to $droot failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
INFO "Creating full backup (option \"incremental\" is not set)";
|
||||||
|
$success = btrfs_send_receive($snapshot, $droot, undef, $receive_log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ERROR "Unknown target type \"$target_type\", skipping: $sroot/$svol";
|
||||||
|
}
|
||||||
|
$config_target->{ABORTED} = 1 unless($success);
|
||||||
}
|
}
|
||||||
} elsif ($job_opts->{create}) {
|
|
||||||
INFO "Creating new snapshot copy (option=create))";
|
|
||||||
btrfs_send_receive($snapshot, $droot, undef, $changelog);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -945,98 +1109,95 @@ MAIN:
|
||||||
{
|
{
|
||||||
$dryrun = 1;
|
$dryrun = 1;
|
||||||
# TODO: gather all information first, then delete all backups/snapshots at the end
|
# TODO: gather all information first, then delete all backups/snapshots at the end
|
||||||
# TODO: always keep first/last
|
# TODO: always preserve first/last
|
||||||
|
|
||||||
|
my @last_sunday;
|
||||||
|
if(Day_of_Week(@today) == 7) { # today is sunday
|
||||||
|
@last_sunday = @today;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@last_sunday = Add_Delta_Days(Monday_of_Week(Week_of_Year(@today)), -1);
|
||||||
|
}
|
||||||
|
DEBUG "last sunday: " . join('-', @last_sunday);
|
||||||
|
|
||||||
#
|
#
|
||||||
# remove backups following a keep_days/keep_weekly scheme
|
# remove backups following a preserve_days/preserve_weekly scheme
|
||||||
#
|
#
|
||||||
foreach my $job (@$jobs)
|
foreach my $config_vol (@{$config->{VOLUME}})
|
||||||
{
|
{
|
||||||
next if($job->{ABORTED});
|
next if($config_vol->{ABORTED});
|
||||||
|
my $sroot = $config_vol->{sroot} || die;
|
||||||
my $sroot = $job->{sroot} || die;
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
||||||
my $svol = $job->{svol} || die;
|
|
||||||
my $droot = $job->{droot} || die;
|
|
||||||
my $job_opts = $job->{options} || die;
|
|
||||||
|
|
||||||
unless(ref($job_opts->{preserve})) {
|
|
||||||
INFO "Skip cleaning of subvolume backups (option preserve is not set): $sroot/$svol";
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
|
|
||||||
INFO "Cleaning subvolume backups of job: $sroot/$svol";
|
|
||||||
my $keep_days = $job_opts->{preserve}->{daily};
|
|
||||||
my $keep_weekly = $job_opts->{preserve}->{weekly};
|
|
||||||
|
|
||||||
# calculate weekly_threshold
|
|
||||||
my @last_sunday;
|
|
||||||
if(Day_of_Week(@today) == 7) { # today is sunday
|
|
||||||
@last_sunday = @today;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
@last_sunday = Add_Delta_Days(Monday_of_Week(Week_of_Year(@today)), -1);
|
|
||||||
}
|
|
||||||
my @weekly_threshold = Add_Delta_Days(@last_sunday, (-7 * $keep_weekly));
|
|
||||||
DEBUG "last sunday: " . join('-', @last_sunday);
|
|
||||||
DEBUG "weekly_threshold for keep_weekly=$keep_weekly: " . join('-', @weekly_threshold);
|
|
||||||
|
|
||||||
#
|
|
||||||
# delete backups
|
|
||||||
#
|
|
||||||
my $keep_info = {};
|
|
||||||
my @delete_backups;
|
|
||||||
INFO "Cleaning backups: $droot/$svol.*";
|
|
||||||
foreach my $vol (sort { $b cmp $a } keys %{$vol_info{$droot}})
|
|
||||||
{
|
{
|
||||||
next unless($vol =~ /^$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/);
|
next if($config_subvol->{ABORTED});
|
||||||
my @vol_date = ($1, $2, $3);
|
my $svol = $config_subvol->{svol} || die;
|
||||||
|
INFO "Cleaning subvolume backups for: $sroot/$svol";
|
||||||
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
||||||
|
{
|
||||||
|
next if($config_target->{ABORTED});
|
||||||
|
my $droot = $config_target->{droot} || die;
|
||||||
|
|
||||||
DEBUG "Checking: $vol";
|
#
|
||||||
my $keep = check_backup_scheme(
|
# delete backups
|
||||||
vol_date => \@vol_date,
|
#
|
||||||
today => \@today,
|
my $preserve_info = {};
|
||||||
weekly_threshold => \@weekly_threshold,
|
my @delete_backups;
|
||||||
keep_days => $keep_days,
|
INFO "Cleaning backups: $droot/$svol.*";
|
||||||
keep_info => $keep_info,
|
foreach my $vol (sort { $b cmp $a } keys %{$vol_info{$droot}})
|
||||||
);
|
{
|
||||||
if($keep) {
|
next unless($vol =~ /^$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/);
|
||||||
INFO "$vol: preserved: $keep";
|
my @vol_date = ($1, $2, $3);
|
||||||
|
|
||||||
|
DEBUG "Checking: $vol";
|
||||||
|
my $preserve = check_backup_scheme(
|
||||||
|
vol_date => \@vol_date,
|
||||||
|
today => \@today,
|
||||||
|
week_start => \@last_sunday, # TODO: configurable
|
||||||
|
preserve_days => config_key($config_target, "target_preserve_days"),
|
||||||
|
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
||||||
|
preserve_info => $preserve_info,
|
||||||
|
);
|
||||||
|
if($preserve) {
|
||||||
|
INFO "$vol: preserved: $preserve";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
INFO "$vol: DELETE";
|
||||||
|
push @delete_backups, "$droot/$vol";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btrfs_subvolume_delete(@delete_backups);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
INFO "$vol: DELETE";
|
#
|
||||||
push @delete_backups, "$droot/$vol";
|
# delete snapshots
|
||||||
|
#
|
||||||
|
my $preserve_info = {};
|
||||||
|
my @delete_snapshots;
|
||||||
|
INFO "Cleaning snapshots: $sroot/$snapdir$svol.*";
|
||||||
|
foreach my $vol (sort { $b cmp $a } keys %{$vol_info{$sroot}})
|
||||||
|
{
|
||||||
|
next unless($vol =~ /^$snapdir$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/);
|
||||||
|
my @vol_date = ($1, $2, $3);
|
||||||
|
|
||||||
|
DEBUG "Checking: $vol";
|
||||||
|
my $preserve = check_backup_scheme(
|
||||||
|
vol_date => \@vol_date,
|
||||||
|
today => \@today,
|
||||||
|
week_start => \@last_sunday, # TODO: configurable
|
||||||
|
preserve_days => config_key($config_subvol, "snapshot_preserve_days"),
|
||||||
|
preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"),
|
||||||
|
preserve_info => $preserve_info,
|
||||||
|
);
|
||||||
|
if($preserve) {
|
||||||
|
INFO "$vol: preserved: $preserve";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
INFO "$vol: DELETE";
|
||||||
|
push @delete_snapshots, "$sroot/$vol";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
btrfs_subvolume_delete(@delete_snapshots);
|
||||||
}
|
}
|
||||||
btrfs_subvolume_delete(@delete_backups);
|
|
||||||
|
|
||||||
#
|
|
||||||
# delete snapshots
|
|
||||||
#
|
|
||||||
$keep_info = {};
|
|
||||||
my @delete_snapshots;
|
|
||||||
INFO "Cleaning snapshots: $sroot/$snapdir$svol.*";
|
|
||||||
foreach my $vol (sort { $b cmp $a } keys %{$vol_info{$sroot}})
|
|
||||||
{
|
|
||||||
next unless($vol =~ /^$snapdir$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/);
|
|
||||||
my @vol_date = ($1, $2, $3);
|
|
||||||
|
|
||||||
DEBUG "Checking: $vol";
|
|
||||||
my $keep = check_backup_scheme(
|
|
||||||
vol_date => \@vol_date,
|
|
||||||
today => \@today,
|
|
||||||
weekly_threshold => \@weekly_threshold,
|
|
||||||
keep_days => $keep_days,
|
|
||||||
keep_info => $keep_info,
|
|
||||||
);
|
|
||||||
if($keep) {
|
|
||||||
INFO "$vol: preserved: $keep";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
INFO "$vol: DELETE";
|
|
||||||
push @delete_snapshots, "$sroot/$vol";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btrfs_subvolume_delete(@delete_snapshots);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
59
btrbk.conf
59
btrbk.conf
|
@ -16,24 +16,53 @@
|
||||||
# log=<logfile> append log to specified logfile
|
# log=<logfile> append log to specified logfile
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# old:
|
||||||
|
# /mnt/btr_system root_gentoo /mnt/btr_ext/_btrbk incremental,init,preserve=d14w10
|
||||||
|
|
||||||
/mnt/btr_system root_gentoo /mnt/btr_ext/_btrbk incremental,init,preserve=d14w10
|
snapshot_dir _btrbk_snap
|
||||||
/mnt/btr_system root_gentoo /mnt/btr_backup/_btrbk incremental,init,log
|
snapshot_create_always yes
|
||||||
/mnt/btr_system kvm /mnt/btr_ext/_btrbk incremental,init
|
|
||||||
/mnt/btr_system kvm /mnt/btr_backup/_btrbk incremental,init,log
|
|
||||||
|
|
||||||
/mnt/btr_data home /mnt/btr_backup/_btrbk incremental,init,log
|
# TODO: incremental = {yes|no|strict}
|
||||||
/mnt/btr_data sdms.data /mnt/btr_backup/_btrbk incremental,init,log
|
incremental strict
|
||||||
|
|
||||||
/mnt/btr_ext data /mnt/btr_backup/_btrbk incremental,init,log
|
snapshot_preserve_days 14
|
||||||
# TODO: these monthly
|
snapshot_preserve_weekly 0
|
||||||
#/mnt/btr_ext video /mnt/btr_backup/_btrbk incremental,init,log
|
|
||||||
#/mnt/btr_ext audio /mnt/btr_backup/_btrbk incremental,init,log
|
|
||||||
|
|
||||||
# TODO: these monthly
|
target_preserve_days 28
|
||||||
#/mnt/btr_boot boot /mnt/btr_ext/_btrbk incremental,init,log
|
target_preserve_weekly 10
|
||||||
#/mnt/btr_boot boot /mnt/btr_backup/_btrbk incremental
|
|
||||||
|
|
||||||
|
|
||||||
# non-incremental, create a new snapshot at every invocation!
|
volume /mnt/btr_system
|
||||||
##/mnt/btr_boot boot /mnt/btr_backup/_btrbk create
|
subvolume root_gentoo
|
||||||
|
target send-receive /mnt/btr_ext/_btrbk
|
||||||
|
target send-receive /mnt/btr_backup/_btrbk
|
||||||
|
receive_log sidecar
|
||||||
|
|
||||||
|
subvolume kvm
|
||||||
|
target_preserve_days 7
|
||||||
|
target_preserve_weekly 4
|
||||||
|
|
||||||
|
target send-receive /mnt/btr_ext/_btrbk
|
||||||
|
target_preserve_weekly 0
|
||||||
|
|
||||||
|
target send-receive /mnt/btr_backup/_btrbk
|
||||||
|
receive_log sidecar
|
||||||
|
|
||||||
|
|
||||||
|
volume /mnt/btr_data
|
||||||
|
subvolume home
|
||||||
|
target send-receive /mnt/btr_backup/_btrbk
|
||||||
|
|
||||||
|
|
||||||
|
volume /mnt/btr_ext
|
||||||
|
subvolume data
|
||||||
|
target send-receive /mnt/btr_backup/_btrbk
|
||||||
|
|
||||||
|
|
||||||
|
volume /mnt/btr_boot
|
||||||
|
# schedule weekly
|
||||||
|
incremental yes
|
||||||
|
|
||||||
|
subvolume boot
|
||||||
|
target send-receive /mnt/btr_ext/_btrbk
|
||||||
|
target send-receive /mnt/btr_backup/_btrbk
|
||||||
|
|
Loading…
Reference in New Issue