mirror of https://github.com/digint/btrbk
btrbk: create all snapshots before starting any send_receive operation; cleanup
parent
adaabb599a
commit
a5ad796aeb
232
btrbk
232
btrbk
|
@ -57,7 +57,7 @@ my $time_format = "%Y%m%d_%H%M%S";
|
||||||
my $src_snapshot_dir = "_btrbk_snap";
|
my $src_snapshot_dir = "_btrbk_snap";
|
||||||
|
|
||||||
my %vol_info;
|
my %vol_info;
|
||||||
my $dryrun;
|
my $pretend;
|
||||||
my $verbose = 0;
|
my $verbose = 0;
|
||||||
my $debug = 0;
|
my $debug = 0;
|
||||||
|
|
||||||
|
@ -82,26 +82,28 @@ sub HELP_MESSAGE
|
||||||
print STDERR "For additional information, see $PROJECT_HOME\n";
|
print STDERR "For additional information, see $PROJECT_HOME\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub DEBUG { my $t = shift; print STDOUT "DEBUG: $t\n" if($debug); }
|
sub DEBUG { my $t = shift; print STDOUT "... $t\n" if($debug); }
|
||||||
sub INFO { my $t = shift; print STDOUT "$t\n" if($verbose); }
|
sub INFO { my $t = shift; print STDOUT "$t\n" if($verbose); }
|
||||||
sub WARN { my $t = shift; print STDOUT "WARN: $t\n"; }
|
sub WARN { my $t = shift; print STDOUT "!!! $t\n"; }
|
||||||
|
sub ERROR { my $t = shift; print STDOUT "ERROR: $t\n"; }
|
||||||
|
|
||||||
sub run_cmd($;$)
|
sub run_cmd($;$)
|
||||||
{
|
{
|
||||||
my $cmd = shift;
|
my $cmd = shift;
|
||||||
my $non_destructive = shift;
|
my $non_destructive = shift;
|
||||||
my $ret = "";
|
my $ret = "";
|
||||||
INFO ">>> $cmd" unless($non_destructive);
|
INFO "### $cmd" unless($non_destructive);
|
||||||
if($non_destructive || (not $dryrun)) {
|
if($non_destructive || (not $pretend)) {
|
||||||
DEBUG "CMD: $cmd";
|
DEBUG "### $cmd";
|
||||||
$ret = `$cmd`;
|
$ret = `$cmd`;
|
||||||
chomp($ret);
|
chomp($ret);
|
||||||
DEBUG "RET: $ret";
|
DEBUG "command output:\n$ret";
|
||||||
die("command execution failed: \"$cmd\"") if($?);
|
die("command execution failed: \"$cmd\"") if($?);
|
||||||
}
|
}
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub check_vol($$)
|
sub check_vol($$)
|
||||||
{
|
{
|
||||||
my $root = shift;
|
my $root = shift;
|
||||||
|
@ -113,21 +115,24 @@ sub check_vol($$)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub check_src($$)
|
sub check_src($$)
|
||||||
{
|
{
|
||||||
my $root = shift;
|
my $root = shift;
|
||||||
my $vol = shift;
|
my $vol = shift;
|
||||||
die("subvolume not found: ${root}/${vol}") unless(check_vol($root, $vol));
|
return 0 unless(check_vol($root, $vol));
|
||||||
unless($dryrun)
|
unless($pretend)
|
||||||
{
|
{
|
||||||
my $dir = "${root}/${src_snapshot_dir}";
|
my $dir = "${root}/${src_snapshot_dir}";
|
||||||
unless(-d $dir) {
|
unless(-d $dir) {
|
||||||
print "creating directory: $dir\n";
|
INFO "--- creating directory: $dir\n";
|
||||||
make_path("${root}/${src_snapshot_dir}");
|
make_path("${root}/${src_snapshot_dir}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub check_rootvol($)
|
sub check_rootvol($)
|
||||||
{
|
{
|
||||||
my $vol = shift;
|
my $vol = shift;
|
||||||
|
@ -140,15 +145,22 @@ sub check_rootvol($)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub parse_config($)
|
sub parse_config($)
|
||||||
{
|
{
|
||||||
my $file = shift;
|
my $file = shift;
|
||||||
|
my @jobs;
|
||||||
|
unless(-r "$file") {
|
||||||
|
WARN "Configuration file not found: $file";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG "parsing config file: $file";
|
DEBUG "parsing config file: $file";
|
||||||
tie my %cfg, "Tie::IxHash";
|
|
||||||
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
|
||||||
DEBUG "parse_config: parsing line: $_";
|
DEBUG "parse_config: parsing line: $_";
|
||||||
if(/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/)
|
if(/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/)
|
||||||
{
|
{
|
||||||
|
@ -159,7 +171,6 @@ sub parse_config($)
|
||||||
dvol => $4,
|
dvol => $4,
|
||||||
options => [ split(/,/, $5) ],
|
options => [ split(/,/, $5) ],
|
||||||
);
|
);
|
||||||
DEBUG(Dumper \%job);
|
|
||||||
$job{sroot} =~ s/\/+$//; # remove trailing slash
|
$job{sroot} =~ s/\/+$//; # remove trailing slash
|
||||||
$job{sroot} =~ s/^\/+/\//; # sanitize leading slash
|
$job{sroot} =~ s/^\/+/\//; # sanitize leading slash
|
||||||
$job{svol} =~ s/\/+$//; # remove trailing slash
|
$job{svol} =~ s/\/+$//; # remove trailing slash
|
||||||
|
@ -175,14 +186,22 @@ sub parse_config($)
|
||||||
$job{mountpoint} = $job{sroot}; # TODO: honor this, automount
|
$job{mountpoint} = $job{sroot}; # TODO: honor this, automount
|
||||||
|
|
||||||
DEBUG "parse_config: adding job \"$job{type}\": $job{sroot}/$job{svol} -> $job{droot}/$job{dvol}";
|
DEBUG "parse_config: adding job \"$job{type}\": $job{sroot}/$job{svol} -> $job{droot}/$job{dvol}";
|
||||||
$cfg{"$job{sroot}/$job{svol}"} //= [];
|
# $cfg{"$job{sroot}/$job{svol}"} //= [];
|
||||||
push @{$cfg{"$job{sroot}/$job{svol}"}}, \%job;
|
# push @{$cfg{"$job{sroot}/$job{svol}"}}, \%job;
|
||||||
|
push @jobs, \%job;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WARN "Ambiguous configuration: $file line $.";
|
||||||
|
return undef; # be very strict here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close FILE;
|
close FILE;
|
||||||
return \%cfg;
|
DEBUG "jobs: " . Dumper(\@jobs);
|
||||||
|
return \@jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btr_tree($)
|
sub btr_tree($)
|
||||||
{
|
{
|
||||||
my $vol = shift;
|
my $vol = shift;
|
||||||
|
@ -239,16 +258,19 @@ sub btr_tree($)
|
||||||
return \%tree;
|
return \%tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btrfs_snapshot($$)
|
sub btrfs_snapshot($$)
|
||||||
{
|
{
|
||||||
my $src = shift;
|
my $src = shift;
|
||||||
my $dst = shift;
|
my $dst = shift;
|
||||||
INFO "[btrfs] snapshot (ro):";
|
# INFO "[btrfs] snapshot (ro):";
|
||||||
INFO "[btrfs] source: $src";
|
# INFO "[btrfs] source: $src";
|
||||||
INFO "[btrfs] dest : $dst";
|
# INFO "[btrfs] dest : $dst";
|
||||||
|
INFO ">>> $dst <-- $src";
|
||||||
run_cmd("/sbin/btrfs subvolume snapshot -r $src $dst");
|
run_cmd("/sbin/btrfs subvolume snapshot -r $src $dst");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btrfs_send_receive($$;$$)
|
sub btrfs_send_receive($$;$$)
|
||||||
{
|
{
|
||||||
my $src = shift;
|
my $src = shift;
|
||||||
|
@ -271,7 +293,7 @@ sub btrfs_send_receive($$;$$)
|
||||||
my $cmd = "/sbin/btrfs send $parent_option $src | /sbin/btrfs receive $receive_option $dst/ 2>&1";
|
my $cmd = "/sbin/btrfs send $parent_option $src | /sbin/btrfs receive $receive_option $dst/ 2>&1";
|
||||||
my $ret = run_cmd($cmd);
|
my $ret = run_cmd($cmd);
|
||||||
# run_cmd("/bin/sync");
|
# run_cmd("/bin/sync");
|
||||||
if($changelog && (not $dryrun))
|
if($changelog && (not $pretend))
|
||||||
{
|
{
|
||||||
INFO "--- writing changelog: $changelog";
|
INFO "--- writing changelog: $changelog";
|
||||||
if(open(LOGFILE, '>>', $changelog)) {
|
if(open(LOGFILE, '>>', $changelog)) {
|
||||||
|
@ -324,6 +346,7 @@ sub get_latest_common($$$$)
|
||||||
return $latest;
|
return $latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
MAIN:
|
MAIN:
|
||||||
{
|
{
|
||||||
$ENV{PATH} = '';
|
$ENV{PATH} = '';
|
||||||
|
@ -338,7 +361,7 @@ MAIN:
|
||||||
# my $dvol = shift @ARGV;
|
# my $dvol = shift @ARGV;
|
||||||
|
|
||||||
# assign command line options
|
# assign command line options
|
||||||
$dryrun = $opts{p}; # TODO: rename to $pretend
|
$pretend = $opts{p};
|
||||||
$debug = $opts{d};
|
$debug = $opts{d};
|
||||||
$verbose = $opts{v} || $debug;
|
$verbose = $opts{v} || $debug;
|
||||||
# my $incremental = $opts{i};
|
# my $incremental = $opts{i};
|
||||||
|
@ -351,92 +374,123 @@ MAIN:
|
||||||
exit 0;
|
exit 0;
|
||||||
}
|
}
|
||||||
my $jobs = parse_config($config);
|
my $jobs = parse_config($config);
|
||||||
|
unless($jobs) {
|
||||||
|
ERROR "Failed to parse configuration file";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
my $postfix = '.' . strftime($time_format, localtime);
|
my $postfix = '.' . strftime($time_format, localtime);
|
||||||
my %snapshots_created;
|
|
||||||
|
|
||||||
foreach my $job_key (keys %$jobs)
|
#
|
||||||
|
# check jobs, fill vol_info hash
|
||||||
|
#
|
||||||
|
foreach my $job (@$jobs)
|
||||||
{
|
{
|
||||||
# INFO "========================================";
|
my $sroot = $job->{sroot} || die;
|
||||||
# INFO "job_key: $job_key";
|
my $droot = $job->{droot} || die;
|
||||||
# INFO "========================================";
|
$vol_info{$sroot} //= btr_tree($sroot);
|
||||||
foreach (@{$jobs->{$job_key}})
|
$vol_info{$droot} //= btr_tree($droot);
|
||||||
|
}
|
||||||
|
DEBUG(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
|
||||||
|
|
||||||
|
#
|
||||||
|
# create snapshots
|
||||||
|
#
|
||||||
|
my %snapshots;
|
||||||
|
foreach my $job (@$jobs)
|
||||||
|
{
|
||||||
|
my $sroot = $job->{sroot} || die;
|
||||||
|
my $svol = $job->{svol} || die;
|
||||||
|
my $droot = $job->{droot} || die;
|
||||||
|
my $dvol = $job->{dvol} || die;
|
||||||
|
my $type = $job->{type} || die;
|
||||||
|
my @job_opts = @{$job->{options}} || die;
|
||||||
|
my $ssnap = "$src_snapshot_dir/$svol$postfix";
|
||||||
|
|
||||||
|
if(check_vol($droot, "$dvol/$svol$postfix")) {
|
||||||
|
$job->{ABORTED} = 1;
|
||||||
|
WARN "snapshot already exists at destination, aborting job: $droot/$dvol/$svol$postfix";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
unless(check_src($sroot, $svol)) {
|
||||||
|
$job->{ABORTED} = 1;
|
||||||
|
WARN "source subvolume not found, aborting job: ${sroot}/${svol}";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
unless($snapshots{"$sroot/$svol"})
|
||||||
{
|
{
|
||||||
my $sroot = $_->{sroot};
|
# make snapshot of svol, if not already created by another job
|
||||||
my $svol = $_->{svol};;
|
die("snapshot source does not exists: $sroot/$svol") unless check_vol($sroot, $svol);
|
||||||
my $droot = $_->{droot};
|
die("snapshot destination already exists: $sroot/$ssnap") if check_vol($sroot, $ssnap); # TODO: better
|
||||||
my $dvol = $_->{dvol};
|
btrfs_snapshot("$sroot/$svol", "$sroot/$ssnap");
|
||||||
my $type = $_->{type};
|
$snapshots{"$sroot/$svol"} = "$sroot/$ssnap";
|
||||||
my @job_opts = @{$_->{options}};
|
}
|
||||||
|
$job->{snapshot} = $snapshots{"$sroot/$svol"};
|
||||||
|
}
|
||||||
|
|
||||||
$vol_info{$sroot} //= btr_tree($sroot);
|
#
|
||||||
$vol_info{$droot} //= btr_tree($droot);
|
# create backups
|
||||||
|
#
|
||||||
|
foreach my $job (@$jobs)
|
||||||
|
{
|
||||||
|
my $sroot = $job->{sroot} || die;
|
||||||
|
my $svol = $job->{svol} || die;
|
||||||
|
my $droot = $job->{droot} || die;
|
||||||
|
my $dvol = $job->{dvol} || die;
|
||||||
|
my $type = $job->{type} || die;
|
||||||
|
my $snapshot = $job->{snapshot} || die;
|
||||||
|
my @job_opts = @{$job->{options}};
|
||||||
|
|
||||||
INFO "***";
|
INFO "***";
|
||||||
INFO "*** $type\[" . join(',', @job_opts) . "]";
|
INFO "*** $type\[" . join(',', @job_opts) . "]";
|
||||||
INFO "*** source: $sroot/$svol";
|
INFO "*** source: $sroot/$svol";
|
||||||
INFO "*** dest : $droot/$dvol";
|
INFO "*** dest : $droot/$dvol";
|
||||||
INFO "***";
|
INFO "***";
|
||||||
DEBUG(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
|
|
||||||
|
|
||||||
my $ssnap = "${src_snapshot_dir}/${svol}${postfix}";
|
my $changelog = "";
|
||||||
check_src($sroot, $svol);
|
if(grep(/^log/, @job_opts))
|
||||||
|
{
|
||||||
unless($snapshots_created{"${sroot}/${svol}"})
|
if(my @res = grep(/^log=\S+$/, @job_opts)) {
|
||||||
{
|
die if(scalar(@res) != 1);
|
||||||
# make snapshot of svol, if not already created by another job
|
$changelog = $res[0];
|
||||||
die("snapshot source does not exists: $sroot/$svol") unless check_vol($sroot, $svol);
|
$changelog =~ s/^log=//;
|
||||||
die("snapshot destination already exists: $sroot/$ssnap") if check_vol($sroot, $ssnap);
|
|
||||||
btrfs_snapshot("$sroot/$svol", "$sroot/$ssnap");
|
|
||||||
$snapshots_created{"$sroot/$svol"} = "$sroot/$ssnap";
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
INFO "--- reusing snapshot: $ssnap";
|
# log defaults to sidecar of destination snapshot
|
||||||
|
$changelog = "$droot/$dvol/${svol}${postfix}.btrbk.log";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
die("snapshot already exists at destination: $droot") if(check_vol($droot, "${svol}${postfix}"));
|
if(grep(/incremental/, @job_opts))
|
||||||
my $changelog = "";
|
{
|
||||||
if(grep(/^log/, @job_opts))
|
INFO "--- processing option=incremental";
|
||||||
|
my $latest_common = get_latest_common($sroot, $svol, $droot, $dvol);
|
||||||
|
if($latest_common)
|
||||||
{
|
{
|
||||||
if(my @res = grep(/^log=\S+$/, @job_opts)) {
|
INFO "--- found common parent: $latest_common";
|
||||||
die if(scalar(@res) != 1);
|
my $parent_snap = "$src_snapshot_dir/$latest_common";
|
||||||
$changelog = $res[0];
|
die("snapshot parent source does not exists: $sroot/$parent_snap") unless check_vol($sroot, $parent_snap);
|
||||||
$changelog =~ s/^log=//;
|
btrfs_send_receive($snapshot, "$droot/$dvol", "$sroot/$parent_snap", $changelog);
|
||||||
|
}
|
||||||
|
elsif(grep(/init/, @job_opts)) {
|
||||||
|
if(check_vol($droot, $dvol)) {
|
||||||
|
INFO "--- no common parent subvolume found, making new snapshot copy (option=init)";
|
||||||
|
btrfs_send_receive($snapshot, "$droot/$dvol", undef, $changelog);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# log defaults to sidecar of destination snapshot
|
WARN "backup to $droot failed: target subvolume not found: $droot/$dvol";
|
||||||
$changelog = "$droot/$dvol/${svol}${postfix}.btrbk.log";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(grep(/incremental/, @job_opts))
|
else {
|
||||||
{
|
WARN "backup to $droot failed: no common parent subvolume found, and job option \"create\" is not set";
|
||||||
INFO "--- processing option=incremental";
|
|
||||||
my $latest_common = get_latest_common($sroot, $svol, $droot, $dvol);
|
|
||||||
if($latest_common)
|
|
||||||
{
|
|
||||||
INFO "--- found common parent: $latest_common";
|
|
||||||
my $parent_snap = "$src_snapshot_dir/$latest_common";
|
|
||||||
die("snapshot parent source does not exists: $sroot/$parent_snap") unless check_vol($sroot, $parent_snap);
|
|
||||||
btrfs_send_receive("$sroot/$ssnap", "$droot/$dvol", "$sroot/$parent_snap", $changelog);
|
|
||||||
}
|
|
||||||
elsif(grep(/init/, @job_opts)) {
|
|
||||||
if(check_vol($droot, $dvol)) {
|
|
||||||
INFO "--- no common parent subvolume found, making new snapshot copy (option=init)";
|
|
||||||
btrfs_send_receive("$sroot/$ssnap", "$droot/$dvol", undef, $changelog);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
WARN "backup to $droot failed: target subvolume not found: $droot/$dvol";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
WARN "backup to $droot failed: no common parent subvolume found, and job option \"create\" is not set";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elsif(grep(/create/, @job_opts))
|
|
||||||
{
|
|
||||||
INFO "<$type> making new snapshot copy (option=create))";
|
|
||||||
btrfs_send_receive("${sroot}/${ssnap}", "${droot}/${dvol}", undef, $changelog);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
elsif(grep(/create/, @job_opts))
|
||||||
|
{
|
||||||
|
INFO "<$type> making new snapshot copy (option=create))";
|
||||||
|
btrfs_send_receive($snapshot, "${droot}/${dvol}", undef, $changelog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue