btrbk: create all snapshots before starting any send_receive operation; cleanup

pull/30/head
Axel Burri 2014-12-13 13:52:43 +01:00
parent adaabb599a
commit a5ad796aeb
1 changed files with 143 additions and 89 deletions

164
btrbk
View File

@ -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,49 +374,81 @@ 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 "========================================";
foreach (@{$jobs->{$job_key}})
{
my $sroot = $_->{sroot};
my $svol = $_->{svol};;
my $droot = $_->{droot};
my $dvol = $_->{dvol};
my $type = $_->{type};
my @job_opts = @{$_->{options}};
$vol_info{$sroot} //= btr_tree($sroot); $vol_info{$sroot} //= btr_tree($sroot);
$vol_info{$droot} //= btr_tree($droot); $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"})
{
# make snapshot of svol, if not already created by another job
die("snapshot source does not exists: $sroot/$svol") unless check_vol($sroot, $svol);
die("snapshot destination already exists: $sroot/$ssnap") if check_vol($sroot, $ssnap); # TODO: better
btrfs_snapshot("$sroot/$svol", "$sroot/$ssnap");
$snapshots{"$sroot/$svol"} = "$sroot/$ssnap";
}
$job->{snapshot} = $snapshots{"$sroot/$svol"};
}
#
# 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}";
check_src($sroot, $svol);
unless($snapshots_created{"${sroot}/${svol}"})
{
# make snapshot of svol, if not already created by another job
die("snapshot source does not exists: $sroot/$svol") unless check_vol($sroot, $svol);
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 {
INFO "--- reusing snapshot: $ssnap";
}
die("snapshot already exists at destination: $droot") if(check_vol($droot, "${svol}${postfix}"));
my $changelog = ""; my $changelog = "";
if(grep(/^log/, @job_opts)) if(grep(/^log/, @job_opts))
{ {
@ -416,12 +471,12 @@ MAIN:
INFO "--- found common parent: $latest_common"; INFO "--- found common parent: $latest_common";
my $parent_snap = "$src_snapshot_dir/$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); 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); btrfs_send_receive($snapshot, "$droot/$dvol", "$sroot/$parent_snap", $changelog);
} }
elsif(grep(/init/, @job_opts)) { elsif(grep(/init/, @job_opts)) {
if(check_vol($droot, $dvol)) { if(check_vol($droot, $dvol)) {
INFO "--- no common parent subvolume found, making new snapshot copy (option=init)"; INFO "--- no common parent subvolume found, making new snapshot copy (option=init)";
btrfs_send_receive("$sroot/$ssnap", "$droot/$dvol", undef, $changelog); btrfs_send_receive($snapshot, "$droot/$dvol", undef, $changelog);
} }
else { else {
WARN "backup to $droot failed: target subvolume not found: $droot/$dvol"; WARN "backup to $droot failed: target subvolume not found: $droot/$dvol";
@ -434,8 +489,7 @@ MAIN:
elsif(grep(/create/, @job_opts)) elsif(grep(/create/, @job_opts))
{ {
INFO "<$type> making new snapshot copy (option=create))"; INFO "<$type> making new snapshot copy (option=create))";
btrfs_send_receive("${sroot}/${ssnap}", "${droot}/${dvol}", undef, $changelog); btrfs_send_receive($snapshot, "${droot}/${dvol}", undef, $changelog);
}
} }
} }
} }