2015-01-13 14:38:44 +01:00
|
|
|
#!/usr/bin/perl -T
|
2014-12-11 18:03:10 +01:00
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
2015-02-08 13:47:31 +01:00
|
|
|
btrbk - backup tool for btrfs volumes
|
2014-12-11 18:03:10 +01:00
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
btrbk --help
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
2015-01-14 14:10:41 +01:00
|
|
|
Backup tool for btrfs subvolumes, taking advantage of btrfs specific
|
|
|
|
send-receive mechanism, allowing incremental backups at file-system
|
|
|
|
level.
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2015-03-19 12:48:09 +01:00
|
|
|
The full btrbk documentation is available at L<http://www.digint.ch/btrbk/>.
|
2014-12-11 18:03:10 +01:00
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
|
|
|
Axel Burri <axel@tty0.ch>
|
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
|
2015-01-13 12:38:01 +01:00
|
|
|
Copyright (c) 2014-2015 Axel Burri. All rights reserved.
|
2014-12-11 18:03:10 +01:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings FATAL => qw( all );
|
|
|
|
|
2015-01-13 14:38:44 +01:00
|
|
|
use Carp qw(confess);
|
|
|
|
use Date::Calc qw(Today Delta_Days Day_of_Week);
|
2014-12-11 18:03:10 +01:00
|
|
|
use Getopt::Std;
|
|
|
|
use Data::Dumper;
|
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
our $VERSION = "0.17.0-dev";
|
2015-01-13 14:38:44 +01:00
|
|
|
our $AUTHOR = 'Axel Burri <axel@tty0.ch>';
|
2015-03-19 12:48:09 +01:00
|
|
|
our $PROJECT_HOME = '<http://www.digint.ch/btrbk/>';
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2015-01-13 14:38:44 +01:00
|
|
|
my $version_info = "btrbk command line client, version $VERSION";
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2015-01-17 13:14:47 +01:00
|
|
|
my @config_src = ("/etc/btrbk.conf", "/etc/btrbk/btrbk.conf");
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2015-01-13 12:38:01 +01:00
|
|
|
my %day_of_week_map = ( monday => 1, tuesday => 2, wednesday => 3, thursday => 4, friday => 5, saturday => 6, sunday => 7 );
|
|
|
|
|
2015-01-12 15:46:24 +01:00
|
|
|
my %config_options = (
|
|
|
|
# NOTE: the parser always maps "no" to undef
|
2015-01-16 17:29:04 +01:00
|
|
|
# NOTE: keys "volume", "subvolume" and "target" are hardcoded
|
|
|
|
snapshot_dir => { default => undef, accept_file => { relative => 1 }, append_trailing_slash => 1 },
|
2015-03-24 18:44:19 +01:00
|
|
|
receive_log => { default => undef, accept => [ "sidecar", "no" ], accept_file => { absolute => 1 }, deprecated => "removed" },
|
2015-01-13 12:50:21 +01:00
|
|
|
incremental => { default => "yes", accept => [ "yes", "no", "strict" ] },
|
|
|
|
snapshot_create_always => { default => undef, accept => [ "yes", "no" ] },
|
2015-04-02 17:08:03 +02:00
|
|
|
resume_missing => { default => "yes", accept => [ "yes", "no" ] },
|
2015-01-13 12:50:21 +01:00
|
|
|
preserve_day_of_week => { default => "sunday", accept => [ (keys %day_of_week_map) ] },
|
|
|
|
snapshot_preserve_daily => { default => "all", accept => [ "all" ], accept_numeric => 1 },
|
|
|
|
snapshot_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1 },
|
|
|
|
snapshot_preserve_monthly => { default => "all", accept => [ "all" ], accept_numeric => 1 },
|
|
|
|
target_preserve_daily => { default => "all", accept => [ "all" ], accept_numeric => 1 },
|
|
|
|
target_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1 },
|
|
|
|
target_preserve_monthly => { default => "all", accept => [ "all" ], accept_numeric => 1 },
|
2015-01-13 18:41:57 +01:00
|
|
|
btrfs_commit_delete => { default => undef, accept => [ "after", "each", "no" ] },
|
2015-01-16 17:29:04 +01:00
|
|
|
ssh_identity => { default => undef, accept_file => { absolute => 1 } },
|
2015-01-14 14:10:41 +01:00
|
|
|
ssh_user => { default => "root", accept_regexp => qr/^[a-z_][a-z0-9_-]*$/ },
|
2015-03-24 13:13:00 +01:00
|
|
|
btrfs_progs_compat => { default => undef, accept => [ "yes", "no" ] },
|
2015-01-09 18:09:32 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
my @config_target_types = qw(send-receive);
|
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
my %vol_detail;
|
|
|
|
my %vol_info; # !!! TODO: rename
|
2015-01-13 14:38:44 +01:00
|
|
|
my %uuid_info;
|
2015-03-13 17:54:08 +01:00
|
|
|
my %uuid_fs_map;
|
2015-03-24 13:13:00 +01:00
|
|
|
my %vol_btrfs_progs_compat; # hacky, maps all subvolumes without received_uuid information
|
2015-01-13 14:38:44 +01:00
|
|
|
|
|
|
|
my $dryrun;
|
|
|
|
my $loglevel = 1;
|
|
|
|
|
2015-02-08 13:46:03 +01:00
|
|
|
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])/;
|
2015-03-19 11:46:59 +01:00
|
|
|
my $file_match = qr/[0-9a-zA-Z_@\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
|
2015-02-28 13:49:36 +01:00
|
|
|
my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
|
|
|
|
|
2015-01-13 14:38:44 +01:00
|
|
|
|
|
|
|
$SIG{__DIE__} = sub {
|
|
|
|
print STDERR "\nERROR: process died unexpectedly (btrbk v$VERSION)";
|
|
|
|
print STDERR "\nPlease contact the author: $AUTHOR\n\n";
|
|
|
|
print STDERR "Stack Trace:\n----------------------------------------\n";
|
|
|
|
Carp::confess @_;
|
|
|
|
};
|
2014-12-14 19:23:02 +01:00
|
|
|
|
2014-12-11 18:03:10 +01:00
|
|
|
sub VERSION_MESSAGE
|
|
|
|
{
|
|
|
|
print STDERR $version_info . "\n\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub HELP_MESSAGE
|
|
|
|
{
|
2015-02-28 11:54:11 +01:00
|
|
|
print STDERR "usage: btrbk [options] <command>\n";
|
2014-12-11 18:03:10 +01:00
|
|
|
print STDERR "\n";
|
|
|
|
print STDERR "options:\n";
|
2014-12-13 15:15:58 +01:00
|
|
|
print STDERR " --help display this help message\n";
|
2014-12-11 18:03:10 +01:00
|
|
|
print STDERR " --version display version information\n";
|
2015-01-17 13:14:47 +01:00
|
|
|
print STDERR " -c FILE specify configuration file\n";
|
2015-02-28 12:02:28 +01:00
|
|
|
print STDERR " -p preserve all backups (do not delete any old targets)\n";
|
2014-12-13 19:34:03 +01:00
|
|
|
print STDERR " -v be verbose (set loglevel=info)\n";
|
2015-02-08 13:47:31 +01:00
|
|
|
print STDERR " -q be quiet (do not print summary at end of \"run\" command)\n";
|
2015-01-13 17:51:24 +01:00
|
|
|
print STDERR " -l LEVEL set loglevel (warn, info, debug, trace)\n";
|
2014-12-13 15:15:58 +01:00
|
|
|
print STDERR "\n";
|
|
|
|
print STDERR "commands:\n";
|
2015-03-19 13:16:58 +01:00
|
|
|
print STDERR " run perform backup operations as defined in the configuration\n";
|
2015-01-13 17:51:24 +01:00
|
|
|
print STDERR " dryrun don't run btrfs commands, just show what would be executed\n";
|
2015-01-20 19:18:38 +01:00
|
|
|
print STDERR " info print useful filesystem information\n";
|
2015-01-13 17:51:24 +01:00
|
|
|
print STDERR " tree shows backup tree\n";
|
2015-01-26 17:31:18 +01:00
|
|
|
print STDERR " origin <subvol> print origin information for subvolume\n";
|
2015-01-13 17:51:24 +01:00
|
|
|
print STDERR " diff <from> <to> shows new files since subvolume <from> for subvolume <to>\n";
|
2014-12-11 18:03:10 +01:00
|
|
|
print STDERR "\n";
|
|
|
|
print STDERR "For additional information, see $PROJECT_HOME\n";
|
|
|
|
}
|
|
|
|
|
2014-12-13 19:34:03 +01:00
|
|
|
sub TRACE { my $t = shift; print STDOUT "... $t\n" if($loglevel >= 4); }
|
|
|
|
sub DEBUG { my $t = shift; print STDOUT "$t\n" if($loglevel >= 3); }
|
|
|
|
sub INFO { my $t = shift; print STDOUT "$t\n" if($loglevel >= 2); }
|
|
|
|
sub WARN { my $t = shift; print STDOUT "WARNING: $t\n" if($loglevel >= 1); }
|
2014-12-13 13:52:43 +01:00
|
|
|
sub ERROR { my $t = shift; print STDOUT "ERROR: $t\n"; }
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2014-12-14 19:23:02 +01:00
|
|
|
|
2014-12-11 18:03:10 +01:00
|
|
|
sub run_cmd($;$)
|
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $cmd = shift || die;
|
2014-12-12 14:05:37 +01:00
|
|
|
my $non_destructive = shift;
|
2014-12-12 16:29:04 +01:00
|
|
|
my $ret = "";
|
2015-01-14 14:10:41 +01:00
|
|
|
$cmd =~ s/^\s+//;
|
|
|
|
$cmd =~ s/\s+$//;
|
2014-12-13 15:15:58 +01:00
|
|
|
if($non_destructive || (not $dryrun)) {
|
2015-01-14 14:10:41 +01:00
|
|
|
DEBUG "### $cmd";
|
2014-12-11 18:03:10 +01:00
|
|
|
$ret = `$cmd`;
|
2014-12-12 10:39:40 +01:00
|
|
|
chomp($ret);
|
2015-01-14 14:10:41 +01:00
|
|
|
TRACE "Command output:\n$ret";
|
2014-12-19 13:31:31 +01:00
|
|
|
if($?) {
|
2015-03-26 18:34:09 +01:00
|
|
|
my $exitcode= $? >> 8;
|
|
|
|
my $signal = $? & 127;
|
|
|
|
WARN "Command execution failed (exitcode=$exitcode" . ($signal ? ", signal=$signal" : "") . "): \"$cmd\"";
|
2014-12-19 13:31:31 +01:00
|
|
|
return undef;
|
|
|
|
}
|
2015-01-14 14:10:41 +01:00
|
|
|
else {
|
|
|
|
DEBUG "Command execution successful";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
DEBUG "### (dryrun) $cmd";
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2014-12-19 14:37:30 +01:00
|
|
|
sub subvol($$)
|
2014-12-11 18:03:10 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $root = shift || die;
|
2015-04-07 11:52:45 +02:00
|
|
|
my $vol = shift // die;
|
2014-12-19 14:37:30 +01:00
|
|
|
if($vol_info{$root} && $vol_info{$root}->{$vol}) {
|
2015-03-13 13:33:40 +01:00
|
|
|
return $vol_info{$root}->{$vol}->{node};
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
2014-12-19 14:37:30 +01:00
|
|
|
return undef;
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
sub vinfo($;$)
|
|
|
|
{
|
|
|
|
my $url = shift // die;
|
|
|
|
my $config = shift;
|
|
|
|
if($vol_detail{$url}) {
|
|
|
|
DEBUG "vinfo cache hit: $url";
|
|
|
|
return $vol_detail{$url};
|
|
|
|
}
|
|
|
|
|
|
|
|
my $detail = btr_subvolume_detail($url, $config);
|
|
|
|
|
|
|
|
unless($detail) {
|
|
|
|
$vol_detail{$url} = { ABORTED => "Failed to fetch subvolume detail for: $url" };
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
$vol_detail{$url} = $detail;
|
|
|
|
DEBUG "vinfo updated for: $url";
|
|
|
|
TRACE(Data::Dumper->Dump([$detail], ["vinfo{$url}"]));
|
|
|
|
|
|
|
|
return $detail;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-14 14:10:41 +01:00
|
|
|
sub get_rsh($$)
|
|
|
|
{
|
2015-01-14 17:14:13 +01:00
|
|
|
my $url = shift // die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2015-01-14 17:14:13 +01:00
|
|
|
if($config && ($url =~ /^ssh:\/\/(\S+?)(\/\S+)$/)) {
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($ssh_host, $path) = ($1, $2);
|
2015-01-20 21:07:28 +01:00
|
|
|
my $ssh_user = config_key($config, "ssh_user");
|
|
|
|
my $ssh_identity = config_key($config, "ssh_identity");
|
|
|
|
my $ssh_options = "";
|
|
|
|
if($ssh_identity) {
|
|
|
|
$ssh_options .= " -i $ssh_identity";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
WARN "No SSH identity provided (option ssh_identity is not set) for: $url";
|
|
|
|
}
|
|
|
|
my $rsh = "/usr/bin/ssh $ssh_options " . $ssh_user . '@' . $ssh_host;
|
2015-04-14 02:40:25 +02:00
|
|
|
return ($rsh, $path);
|
2015-01-14 14:10:41 +01:00
|
|
|
}
|
2015-01-14 17:14:13 +01:00
|
|
|
return ("", $url);
|
2015-01-14 14:10:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-09 18:09:32 +01:00
|
|
|
sub config_key($$)
|
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $node = shift || die;
|
|
|
|
my $key = shift || die;
|
2015-01-13 13:35:58 +01:00
|
|
|
TRACE "config_key: context=$node->{CONTEXT}, key=$key";
|
2015-01-12 15:46:24 +01:00
|
|
|
while(not exists($node->{$key})) {
|
2015-01-09 18:09:32 +01:00
|
|
|
return undef unless($node->{PARENT});
|
|
|
|
$node = $node->{PARENT};
|
|
|
|
}
|
2015-01-13 13:35:58 +01:00
|
|
|
TRACE "config_key: found value=" . ($node->{$key} // "<undef>");
|
2015-01-09 18:09:32 +01:00
|
|
|
return $node->{$key};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
sub check_file($$;$$)
|
2015-01-16 17:29:04 +01:00
|
|
|
{
|
2015-04-14 02:17:17 +02:00
|
|
|
my $file = shift // die;
|
|
|
|
my $accept = shift || die;
|
2015-01-16 17:29:04 +01:00
|
|
|
my $key = shift; # only for error text
|
|
|
|
my $config_file = shift; # only for error text
|
|
|
|
|
|
|
|
if($accept->{ssh} && ($file =~ /^ssh:\/\//)) {
|
2015-02-28 13:49:36 +01:00
|
|
|
unless($file =~ /^$ssh_prefix_match\/$file_match$/) {
|
2015-01-16 17:29:04 +01:00
|
|
|
ERROR "Ambiguous ssh url for option \"$key\" in \"$config_file\" line $.: $file";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elsif($file =~ /^$file_match$/) {
|
|
|
|
if($accept->{absolute}) {
|
|
|
|
unless($file =~ /^\//) {
|
|
|
|
ERROR "Only absolute files allowed for option \"$key\" in \"$config_file\" line $.: $file";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elsif($accept->{relative}) {
|
|
|
|
if($file =~ /^\//) {
|
|
|
|
ERROR "Only relative files allowed for option \"$key\" in \"$config_file\" line $.: $file";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
die("accept_type must contain either 'relative' or 'absolute'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ERROR "Ambiguous file for option \"$key\" in \"$config_file\" line $.: $file";
|
|
|
|
return undef;
|
|
|
|
}
|
2015-04-07 11:52:45 +02:00
|
|
|
return 1;
|
2015-01-16 17:29:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-17 13:14:47 +01:00
|
|
|
sub parse_config(@)
|
2014-12-12 12:32:04 +01:00
|
|
|
{
|
2015-01-17 13:14:47 +01:00
|
|
|
my @config_files = @_;
|
|
|
|
my $file = undef;
|
|
|
|
foreach(@config_files) {
|
|
|
|
TRACE "config: checking for file: $_";
|
|
|
|
if(-r "$_") {
|
|
|
|
$file = $_;
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
unless($file) {
|
|
|
|
ERROR "Configuration file not found: " . join(', ', @config_files);
|
2014-12-13 13:52:43 +01:00
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
|
2015-01-17 14:55:46 +01:00
|
|
|
my $root = { CONTEXT => "root", SRC_FILE => $file };
|
|
|
|
my $cur = $root;
|
|
|
|
# set defaults
|
|
|
|
foreach (keys %config_options) {
|
|
|
|
$root->{$_} = $config_options{$_}->{default};
|
|
|
|
}
|
|
|
|
|
2015-01-09 18:09:32 +01:00
|
|
|
DEBUG "config: parsing file: $file";
|
2014-12-12 16:29:04 +01:00
|
|
|
open(FILE, '<', $file) or die $!;
|
2014-12-12 12:32:04 +01:00
|
|
|
while (<FILE>) {
|
|
|
|
chomp;
|
2014-12-12 14:05:37 +01:00
|
|
|
next if /^\s*#/; # ignore comments
|
2014-12-13 13:52:43 +01:00
|
|
|
next if /^\s*$/; # ignore empty lines
|
2015-01-09 18:09:32 +01:00
|
|
|
TRACE "config: parsing line $. with context=$cur->{CONTEXT}: \"$_\"";
|
|
|
|
if(/^(\s*)([a-zA-Z_]+)\s+(.*)$/)
|
2014-12-12 14:05:37 +01:00
|
|
|
{
|
2015-01-09 18:09:32 +01:00
|
|
|
my ($indent, $key, $value) = (length($1), lc($2), $3);
|
|
|
|
$value =~ s/\s*$//;
|
|
|
|
# NOTE: we do not perform checks on indentation!
|
|
|
|
|
|
|
|
if($key eq "volume")
|
|
|
|
{
|
|
|
|
$cur = $root;
|
|
|
|
DEBUG "config: context forced to: $cur->{CONTEXT}";
|
2015-01-16 17:29:04 +01:00
|
|
|
|
|
|
|
# be very strict about file options, for security sake
|
|
|
|
return undef unless(check_file($value, { absolute => 1, ssh => 1 }, $key, $file));
|
2015-01-09 18:09:32 +01:00
|
|
|
$value =~ s/\/+$//; # remove trailing slash
|
|
|
|
$value =~ s/^\/+/\//; # sanitize leading slash
|
2015-01-16 17:29:04 +01:00
|
|
|
DEBUG "config: adding volume \"$value\" to root context";
|
2015-01-09 18:09:32 +01:00
|
|
|
my $volume = { CONTEXT => "volume",
|
|
|
|
PARENT => $cur,
|
|
|
|
sroot => $value,
|
|
|
|
};
|
|
|
|
$cur->{VOLUME} //= [];
|
|
|
|
push(@{$cur->{VOLUME}}, $volume);
|
|
|
|
$cur = $volume;
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
2015-01-09 18:09:32 +01:00
|
|
|
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}";
|
|
|
|
}
|
2015-01-16 17:29:04 +01:00
|
|
|
# be very strict about file options, for security sake
|
|
|
|
return undef unless(check_file($value, { relative => 1 }, $key, $file));
|
2015-01-09 18:09:32 +01:00
|
|
|
$value =~ s/\/+$//; # remove trailing slash
|
|
|
|
$value =~ s/^\/+//; # remove leading slash
|
|
|
|
if($value =~ /\//) {
|
|
|
|
ERROR "subvolume contains slashes: \"$value\" in \"$file\" line $.";
|
|
|
|
return undef;
|
|
|
|
}
|
2014-12-12 12:32:04 +01:00
|
|
|
|
2015-01-09 18:09:32 +01:00
|
|
|
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;
|
|
|
|
}
|
2015-01-16 17:29:04 +01:00
|
|
|
# be very strict about file options, for security sake
|
|
|
|
return undef unless(check_file($droot, { absolute => 1, ssh => 1 }, $key, $file));
|
|
|
|
|
2015-01-09 18:09:32 +01:00
|
|
|
$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;
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
2015-01-09 18:09:32 +01:00
|
|
|
else
|
|
|
|
{
|
|
|
|
ERROR "Ambiguous target configuration, in \"$file\" line $.";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
2015-01-12 15:46:24 +01:00
|
|
|
elsif(grep(/^$key$/, keys %config_options)) # accept only keys listed in %config_options
|
2015-01-09 18:09:32 +01:00
|
|
|
{
|
2015-01-12 15:46:24 +01:00
|
|
|
if(grep(/^$value$/, @{$config_options{$key}->{accept}})) {
|
|
|
|
TRACE "option \"$key=$value\" found in accept list";
|
|
|
|
}
|
2015-01-13 12:38:01 +01:00
|
|
|
elsif($config_options{$key}->{accept_numeric} && ($value =~ /^[0-9]+$/)) {
|
|
|
|
TRACE "option \"$key=$value\" is numeric, accepted";
|
2015-01-12 15:46:24 +01:00
|
|
|
}
|
|
|
|
elsif($config_options{$key}->{accept_file})
|
|
|
|
{
|
2015-01-16 17:29:04 +01:00
|
|
|
# be very strict about file options, for security sake
|
|
|
|
return undef unless(check_file($value, $config_options{$key}->{accept_file}, $key, $file));
|
|
|
|
|
|
|
|
TRACE "option \"$key=$value\" is a valid file, accepted";
|
2015-01-12 15:46:24 +01:00
|
|
|
$value =~ s/\/+$//; # remove trailing slash
|
|
|
|
$value =~ s/^\/+/\//; # sanitize leading slash
|
2015-01-16 17:29:04 +01:00
|
|
|
if($config_options{$key}->{append_trailing_slash}) {
|
|
|
|
TRACE "append_trailing_slash is specified for option \"$key\", adding trailing slash";
|
2015-01-13 12:50:21 +01:00
|
|
|
$value .= '/';
|
|
|
|
}
|
2015-01-12 15:46:24 +01:00
|
|
|
}
|
2015-01-14 14:10:41 +01:00
|
|
|
elsif($config_options{$key}->{accept_regexp}) {
|
|
|
|
my $match = $config_options{$key}->{accept_regexp};
|
|
|
|
if($value =~ m/$match/) {
|
2015-01-14 17:14:13 +01:00
|
|
|
TRACE "option \"$key=$value\" matched regexp, accepted";
|
2015-01-14 14:10:41 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
ERROR "Value \"$value\" failed input validation for option \"$key\" in \"$file\" line $.";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
2015-01-12 15:46:24 +01:00
|
|
|
else
|
|
|
|
{
|
|
|
|
ERROR "Unsupported value \"$value\" for option \"$key\" in \"$file\" line $.";
|
|
|
|
return undef;
|
|
|
|
}
|
2015-01-09 18:09:32 +01:00
|
|
|
DEBUG "config: adding option \"$key=$value\" to $cur->{CONTEXT} context";
|
2015-01-12 15:46:24 +01:00
|
|
|
$value = undef if($value eq "no"); # we don't want to check for "no" all the time
|
2015-01-09 18:09:32 +01:00
|
|
|
$cur->{$key} = $value;
|
2015-03-24 18:44:19 +01:00
|
|
|
|
|
|
|
if($config_options{$key}->{deprecated}) {
|
|
|
|
WARN "Found deprecated configuration option \"$key\" in \"$file\" line $.";
|
|
|
|
}
|
2015-01-09 18:09:32 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ERROR "Unknown option \"$key\" in \"$file\" line $.";
|
|
|
|
return undef;
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
|
|
|
|
2015-01-09 18:09:32 +01:00
|
|
|
TRACE "line processed: new context=$cur->{CONTEXT}";
|
2014-12-13 13:52:43 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2015-01-09 18:09:32 +01:00
|
|
|
ERROR "Parse error in \"$file\" line $.";
|
|
|
|
return undef;
|
2014-12-12 12:32:04 +01:00
|
|
|
}
|
|
|
|
}
|
2015-01-09 18:09:32 +01:00
|
|
|
|
|
|
|
TRACE(Data::Dumper->Dump([$root], ["config_root"]));
|
|
|
|
return $root;
|
2014-12-12 12:32:04 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-01-20 19:18:38 +01:00
|
|
|
sub btr_filesystem_show_all_local()
|
|
|
|
{
|
|
|
|
return run_cmd("/sbin/btrfs filesystem show", 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub btr_filesystem_show($;$)
|
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-20 19:18:38 +01:00
|
|
|
my $config = shift;
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $path) = get_rsh($url, $config);
|
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs filesystem show $path", 1);
|
2015-01-20 19:18:38 +01:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-25 13:36:07 +01:00
|
|
|
sub btr_filesystem_df($;$)
|
2015-01-20 19:18:38 +01:00
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-20 19:18:38 +01:00
|
|
|
my $config = shift;
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $path) = get_rsh($url, $config);
|
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs filesystem df $path", 1);
|
2015-01-20 19:18:38 +01:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-25 13:36:07 +01:00
|
|
|
sub btr_filesystem_usage($;$)
|
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-25 13:36:07 +01:00
|
|
|
my $config = shift;
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $path) = get_rsh($url, $config);
|
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs filesystem usage $path", 1);
|
2015-01-25 13:36:07 +01:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-26 17:23:37 +01:00
|
|
|
sub btr_subvolume_detail($;$)
|
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-26 17:23:37 +01:00
|
|
|
my $config = shift;
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $path) = get_rsh($url, $config);
|
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs subvolume show $path 2>/dev/null", 1);
|
2015-01-26 17:23:37 +01:00
|
|
|
if($ret)
|
|
|
|
{
|
2015-04-14 02:17:17 +02:00
|
|
|
my $fs_path;
|
|
|
|
if($ret =~ /^($file_match)/) {
|
|
|
|
$fs_path = $1;
|
2015-04-14 02:40:25 +02:00
|
|
|
DEBUG "Real path for subvolume \"$url\" is: $fs_path" if($fs_path ne $path);
|
2015-04-14 02:17:17 +02:00
|
|
|
return undef unless(check_file($fs_path, { absolute => 1 }));
|
|
|
|
}
|
|
|
|
else {
|
2015-04-14 02:40:25 +02:00
|
|
|
$fs_path = $path;
|
|
|
|
WARN "No real path provided by \"btrfs subvolume show\" for subvolume \"$url\", using: $path";
|
2015-04-14 02:17:17 +02:00
|
|
|
}
|
2015-04-14 02:40:25 +02:00
|
|
|
my %detail = ( FS_PATH => $fs_path,
|
|
|
|
URL => $url,
|
2015-04-14 02:17:17 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
if($ret eq "$fs_path is btrfs root") {
|
2015-04-14 02:40:25 +02:00
|
|
|
DEBUG "found btrfs root: $url";
|
2015-04-14 02:17:17 +02:00
|
|
|
$detail{id} = 5;
|
|
|
|
$detail{is_root} = 1;
|
2015-01-26 17:23:37 +01:00
|
|
|
}
|
2015-04-14 02:17:17 +02:00
|
|
|
elsif($ret =~ /^$fs_path/) {
|
2015-04-14 02:40:25 +02:00
|
|
|
TRACE "btr_detail: found btrfs subvolume: $url";
|
2015-01-26 17:23:37 +01:00
|
|
|
my %trans = (
|
|
|
|
name => "Name",
|
|
|
|
uuid => "uuid",
|
|
|
|
parent_uuid => "Parent uuid",
|
|
|
|
creation_time => "Creation time",
|
|
|
|
id => "Object ID",
|
|
|
|
gen => "Generation \\(Gen\\)",
|
|
|
|
cgen => "Gen at creation",
|
|
|
|
parent_id => "Parent",
|
|
|
|
top_level => "Top Level",
|
|
|
|
flags => "Flags",
|
|
|
|
);
|
|
|
|
foreach (keys %trans) {
|
|
|
|
if($ret =~ /^\s+$trans{$_}:\s+(.*)$/m) {
|
|
|
|
$detail{$_} = $1;
|
|
|
|
} else {
|
|
|
|
WARN "Failed to parse subvolume detail \"$trans{$_}\": $ret";
|
|
|
|
}
|
|
|
|
}
|
2015-04-14 02:40:25 +02:00
|
|
|
DEBUG "parsed " . scalar(keys %detail) . " subvolume detail items: $url";
|
|
|
|
TRACE "btr_detail for $url: " . Dumper \%detail;
|
2015-01-26 17:23:37 +01:00
|
|
|
}
|
2015-04-14 02:17:17 +02:00
|
|
|
return \%detail;
|
2015-01-26 17:23:37 +01:00
|
|
|
}
|
2015-04-14 02:40:25 +02:00
|
|
|
WARN "Failed to fetch subvolume detail for: $url";
|
2015-01-26 17:23:37 +01:00
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-14 14:10:41 +01:00
|
|
|
sub btr_subvolume_list($;$@)
|
2014-12-12 10:39:40 +01:00
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2014-12-14 19:23:02 +01:00
|
|
|
my %opts = @_;
|
2015-03-24 13:13:00 +01:00
|
|
|
my $btrfs_progs_compat = config_key($config, "btrfs_progs_compat");
|
2014-12-14 19:23:02 +01:00
|
|
|
my $filter_option = "-a";
|
|
|
|
$filter_option = "-o" if($opts{subvol_only});
|
2015-03-24 13:13:00 +01:00
|
|
|
my $display_options = "-c -u -q";
|
|
|
|
$display_options .= " -R" unless($btrfs_progs_compat);
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $real_vol) = get_rsh($url, $config);
|
2015-03-24 13:13:00 +01:00
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs subvolume list $filter_option $display_options $real_vol", 1);
|
2015-01-14 14:10:41 +01:00
|
|
|
unless(defined($ret)) {
|
2015-04-14 02:40:25 +02:00
|
|
|
WARN "Failed to fetch btrfs subvolume list for: $url";
|
2014-12-19 13:31:31 +01:00
|
|
|
return undef;
|
|
|
|
}
|
2014-12-14 19:23:02 +01:00
|
|
|
my @nodes;
|
2014-12-12 10:39:40 +01:00
|
|
|
foreach (split(/\n/, $ret))
|
|
|
|
{
|
|
|
|
# ID <ID> top level <ID> path <path> where path is the relative path
|
|
|
|
# of the subvolume to the top level subvolume. The subvolume?s ID may
|
|
|
|
# be used by the subvolume set-default command, or at mount time via
|
|
|
|
# the subvolid= option. If -p is given, then parent <ID> is added to
|
|
|
|
# the output between ID and top level. The parent?s ID may be used at
|
|
|
|
# mount time via the subvolrootid= option.
|
2015-03-24 13:13:00 +01:00
|
|
|
|
|
|
|
# NOTE: btrfs-progs prior to v1.17 do not support the -R flag
|
|
|
|
my %node;
|
|
|
|
if($btrfs_progs_compat) {
|
|
|
|
die("Failed to parse line: \"$_\"") unless(/^ID ([0-9]+) gen ([0-9]+) cgen ([0-9]+) top level ([0-9]+) parent_uuid ([0-9a-z-]+) uuid ([0-9a-z-]+) path (.+)$/);
|
|
|
|
%node = (
|
|
|
|
id => $1,
|
|
|
|
gen => $2,
|
|
|
|
cgen => $3,
|
|
|
|
top_level => $4,
|
|
|
|
parent_uuid => $5, # note: parent_uuid="-" if no parent
|
|
|
|
# received_uuid => $6,
|
|
|
|
uuid => $6,
|
|
|
|
path => $7 # btrfs path, NOT filesystem path
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
die("Failed to parse line: \"$_\"") unless(/^ID ([0-9]+) gen ([0-9]+) cgen ([0-9]+) top level ([0-9]+) parent_uuid ([0-9a-z-]+) received_uuid ([0-9a-z-]+) uuid ([0-9a-z-]+) path (.+)$/);
|
|
|
|
%node = (
|
2015-03-13 12:12:37 +01:00
|
|
|
id => $1,
|
|
|
|
gen => $2,
|
|
|
|
cgen => $3,
|
|
|
|
top_level => $4,
|
|
|
|
parent_uuid => $5, # note: parent_uuid="-" if no parent
|
|
|
|
received_uuid => $6,
|
|
|
|
uuid => $7,
|
|
|
|
path => $8 # btrfs path, NOT filesystem path
|
|
|
|
);
|
2015-03-24 13:13:00 +01:00
|
|
|
}
|
2015-03-13 12:12:37 +01:00
|
|
|
|
|
|
|
# NOTE: "btrfs subvolume list <path>" prints <FS_TREE> prefix only if
|
|
|
|
# the subvolume is reachable within <path>. (as of btrfs-progs-3.18.2)
|
|
|
|
#
|
|
|
|
# NOTE: Be prepared for this to change in btrfs-progs!
|
|
|
|
$node{path} =~ s/^<FS_TREE>\///; # remove "<FS_TREE>/" portion from "path".
|
|
|
|
|
|
|
|
push @nodes, \%node;
|
2014-12-14 19:23:02 +01:00
|
|
|
# $node{parent_uuid} = undef if($node{parent_uuid} eq '-');
|
|
|
|
}
|
2015-04-14 02:40:25 +02:00
|
|
|
DEBUG "parsed " . scalar(@nodes) . " total subvolumes for filesystem at: $url";
|
2014-12-19 13:31:31 +01:00
|
|
|
return \@nodes;
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
|
|
|
|
2015-01-14 14:10:41 +01:00
|
|
|
|
|
|
|
sub btr_subvolume_find_new($$;$)
|
2014-12-14 21:29:22 +01:00
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $lastgen = shift // die;
|
|
|
|
my $config = shift;
|
2015-04-14 02:40:25 +02:00
|
|
|
my ($rsh, $real_vol) = get_rsh($url, $config);
|
2015-01-14 14:10:41 +01:00
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs subvolume find-new $real_vol $lastgen");
|
2015-01-03 21:25:46 +01:00
|
|
|
unless(defined($ret)) {
|
2015-04-14 02:40:25 +02:00
|
|
|
ERROR "Failed to fetch modified files for: $url";
|
2015-01-03 21:25:46 +01:00
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
my %files;
|
|
|
|
my $parse_errors = 0;
|
|
|
|
my $transid_marker;
|
|
|
|
foreach (split(/\n/, $ret))
|
|
|
|
{
|
2015-02-08 13:46:03 +01:00
|
|
|
if(/^inode \S+ file offset (\S+) len (\S+) disk start \S+ offset \S+ gen (\S+) flags (\S+) (.+)$/) {
|
2015-01-03 21:25:46 +01:00
|
|
|
my $file_offset = $1;
|
|
|
|
my $len = $2;
|
|
|
|
my $gen = $3;
|
|
|
|
my $flags = $4;
|
|
|
|
my $name = $5;
|
|
|
|
$files{$name}->{len} += $len;
|
|
|
|
$files{$name}->{new} = 1 if($file_offset == 0);
|
|
|
|
$files{$name}->{gen}->{$gen} = 1; # count the generations
|
|
|
|
if($flags eq "COMPRESS") {
|
|
|
|
$files{$name}->{flags}->{compress} = 1;
|
|
|
|
}
|
|
|
|
elsif($flags eq "COMPRESS|INLINE") {
|
|
|
|
$files{$name}->{flags}->{compress} = 1;
|
|
|
|
$files{$name}->{flags}->{inline} = 1;
|
|
|
|
}
|
|
|
|
elsif($flags eq "INLINE") {
|
|
|
|
$files{$name}->{flags}->{inline} = 1;
|
|
|
|
}
|
|
|
|
elsif($flags eq "NONE") {
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
WARN "unparsed flags: $flags";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elsif(/^transid marker was (\S+)$/) {
|
|
|
|
$transid_marker = $1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$parse_errors++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { files => \%files,
|
|
|
|
transid_marker => $transid_marker,
|
|
|
|
parse_errors => $parse_errors,
|
|
|
|
};
|
2014-12-14 21:29:22 +01:00
|
|
|
}
|
|
|
|
|
2014-12-14 19:23:02 +01:00
|
|
|
|
2015-01-14 14:10:41 +01:00
|
|
|
sub btr_tree($;$)
|
2014-12-14 19:23:02 +01:00
|
|
|
{
|
2015-04-14 02:40:25 +02:00
|
|
|
my $url = shift || die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2014-12-14 19:23:02 +01:00
|
|
|
my %tree;
|
|
|
|
my %id;
|
2015-04-14 02:40:25 +02:00
|
|
|
my $subvol_list = btr_subvolume_list($url, $config, subvol_only => 0);
|
2014-12-19 13:31:31 +01:00
|
|
|
return undef unless(ref($subvol_list) eq "ARRAY");
|
2015-03-13 17:54:08 +01:00
|
|
|
|
2015-04-14 02:40:25 +02:00
|
|
|
TRACE "btr_tree: processing subvolume list of: $url";
|
2015-03-13 17:54:08 +01:00
|
|
|
|
2014-12-19 13:31:31 +01:00
|
|
|
foreach my $node (@$subvol_list)
|
2014-12-14 19:23:02 +01:00
|
|
|
{
|
|
|
|
$id{$node->{id}} = $node;
|
|
|
|
$uuid_info{$node->{uuid}} = $node;
|
2014-12-14 15:34:55 +01:00
|
|
|
|
2015-03-13 12:12:37 +01:00
|
|
|
my $rel_path = $node->{path};
|
2014-12-14 22:03:31 +01:00
|
|
|
if($node->{top_level} == 5)
|
2014-12-12 10:39:40 +01:00
|
|
|
{
|
|
|
|
# man btrfs-subvolume:
|
|
|
|
# Also every btrfs filesystem has a default subvolume as its initially
|
|
|
|
# top-level subvolume, whose subvolume id is 5(FS_TREE).
|
|
|
|
|
2014-12-14 22:03:31 +01:00
|
|
|
$tree{$node->{id}} = $node;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2015-03-13 12:12:37 +01:00
|
|
|
# set SUBTREE / PARENT node
|
2014-12-14 19:23:02 +01:00
|
|
|
die unless exists($id{$node->{top_level}});
|
2015-03-13 12:12:37 +01:00
|
|
|
my $parent = $id{$node->{top_level}};
|
2015-03-13 11:20:47 +01:00
|
|
|
|
2015-03-13 12:12:37 +01:00
|
|
|
die if exists($parent->{SUBTREE}->{$node->{id}});
|
|
|
|
$parent->{SUBTREE}->{$node->{id}} = $node;
|
|
|
|
$node->{PARENT} = $parent;
|
2015-03-13 11:20:47 +01:00
|
|
|
|
|
|
|
# "path" always starts with set REL_PATH
|
2015-03-13 12:12:37 +01:00
|
|
|
die unless($rel_path =~ s/^$parent->{path}\///);
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
2015-03-13 12:12:37 +01:00
|
|
|
$node->{REL_PATH} = $rel_path; # relative to {PARENT}->{path}
|
2014-12-14 19:23:02 +01:00
|
|
|
}
|
2015-03-13 11:20:47 +01:00
|
|
|
|
2014-12-14 22:03:31 +01:00
|
|
|
# set PARENT node
|
|
|
|
foreach (values %id){
|
|
|
|
$_->{PARENT} = $uuid_info{$_->{parent_uuid}} if($_->{parent_uuid} ne "-");
|
|
|
|
}
|
2014-12-14 19:23:02 +01:00
|
|
|
return \%tree;
|
|
|
|
}
|
|
|
|
|
2015-03-13 11:44:04 +01:00
|
|
|
|
|
|
|
sub _subtree_list
|
|
|
|
{
|
|
|
|
my $tree = shift;
|
2015-03-13 13:33:40 +01:00
|
|
|
my $list = shift;
|
2015-03-13 11:44:04 +01:00
|
|
|
my $prefix = shift;
|
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
return $list unless $tree; # silent ignore empty subtrees
|
2015-03-13 11:44:04 +01:00
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
foreach(values %$tree) {
|
2015-03-13 11:44:04 +01:00
|
|
|
my $path = $prefix . $_->{REL_PATH};
|
2015-03-13 13:33:40 +01:00
|
|
|
push(@$list, { SUBVOL_PATH => $path,
|
|
|
|
node => $_,
|
|
|
|
});
|
|
|
|
|
|
|
|
# recurse into SUBTREE
|
|
|
|
_subtree_list($_->{SUBTREE}, $list, $path . '/');
|
2015-03-13 11:44:04 +01:00
|
|
|
}
|
2015-03-13 13:33:40 +01:00
|
|
|
return $list;
|
2015-03-13 11:44:04 +01:00
|
|
|
}
|
|
|
|
|
2014-12-14 19:23:02 +01:00
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
|
|
|
|
# returns hash of:
|
2015-04-14 03:24:32 +02:00
|
|
|
# SUBVOL_PATH relative path to URL
|
|
|
|
# URL absolute path
|
2015-03-13 13:33:40 +01:00
|
|
|
# node href to tree node
|
2015-03-20 17:56:36 +01:00
|
|
|
#
|
2015-04-14 03:24:32 +02:00
|
|
|
# returns an empty hash if the subvolume at $url exists, but contains no subvolumes
|
|
|
|
# returns undef if the subvolume at $url does not exists
|
2015-03-13 13:33:40 +01:00
|
|
|
sub btr_fs_info($;$)
|
2014-12-14 19:23:02 +01:00
|
|
|
{
|
2015-04-14 03:24:32 +02:00
|
|
|
my $url = shift || die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2015-04-14 03:24:32 +02:00
|
|
|
my $detail = vinfo($url, $config);
|
2015-03-13 17:54:08 +01:00
|
|
|
return undef unless($detail);
|
2014-12-19 13:31:31 +01:00
|
|
|
|
2015-04-14 03:24:32 +02:00
|
|
|
my $tree = btr_tree($url, $config);
|
2015-03-13 17:54:08 +01:00
|
|
|
my $tree_root;
|
|
|
|
if($detail->{is_root}) {
|
|
|
|
$tree_root = $tree;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
die unless $uuid_info{$detail->{uuid}};
|
2015-04-14 03:24:32 +02:00
|
|
|
$uuid_fs_map{$detail->{uuid}}->{$url} = 1;
|
2015-03-13 17:54:08 +01:00
|
|
|
$tree_root = $uuid_info{$detail->{uuid}}->{SUBTREE};
|
|
|
|
unless($tree_root) {
|
2015-04-14 03:24:32 +02:00
|
|
|
DEBUG "No subvolumes found in: $url";
|
2015-03-20 17:56:36 +01:00
|
|
|
return {};
|
2015-03-13 17:54:08 +01:00
|
|
|
}
|
|
|
|
}
|
2015-03-13 11:44:04 +01:00
|
|
|
|
2015-04-14 03:24:32 +02:00
|
|
|
# recurse into $tree_root, returns list of href: { URL, node }
|
2015-03-13 13:33:40 +01:00
|
|
|
my $list = _subtree_list($tree_root, [], "");
|
2015-03-13 11:44:04 +01:00
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
# return a hash of relative subvolume path
|
|
|
|
my %ret;
|
|
|
|
foreach(@$list) {
|
|
|
|
my $subvol_path = $_->{SUBVOL_PATH};
|
|
|
|
die if exists $ret{$subvol_path};
|
2015-04-14 03:24:32 +02:00
|
|
|
$_->{URL} = $url . '/' . $subvol_path;
|
|
|
|
$uuid_fs_map{$_->{node}->{uuid}}->{$url . '/' . $subvol_path} = 1;
|
2015-03-13 13:33:40 +01:00
|
|
|
$ret{$subvol_path} = $_;
|
2015-03-13 11:44:04 +01:00
|
|
|
}
|
2015-04-14 03:24:32 +02:00
|
|
|
$vol_btrfs_progs_compat{$url} = config_key($config, "btrfs_progs_compat"); # missing received_uuid in node{}
|
2015-03-24 13:13:00 +01:00
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
return \%ret;
|
2014-12-12 10:39:40 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-01-13 12:38:01 +01:00
|
|
|
# returns $target, or undef on error
|
2015-01-14 17:14:13 +01:00
|
|
|
sub btrfs_snapshot($$;$)
|
2014-12-11 18:03:10 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $src = shift || die;
|
|
|
|
my $target = shift || die;
|
2015-01-14 17:14:13 +01:00
|
|
|
my $config = shift;
|
|
|
|
my ($rsh, $real_src) = get_rsh($src, $config);
|
|
|
|
my (undef, $real_target) = get_rsh($target, $config);
|
2014-12-13 19:34:03 +01:00
|
|
|
DEBUG "[btrfs] snapshot (ro):";
|
|
|
|
DEBUG "[btrfs] source: $src";
|
2015-01-13 12:38:01 +01:00
|
|
|
DEBUG "[btrfs] target: $target";
|
|
|
|
INFO ">>> $target";
|
2015-01-14 17:14:13 +01:00
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs subvolume snapshot -r $real_src $real_target");
|
2015-01-13 12:38:01 +01:00
|
|
|
ERROR "Failed to create btrfs subvolume snapshot: $src -> $target" unless(defined($ret));
|
|
|
|
return defined($ret) ? $target : undef;
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-01-13 18:41:57 +01:00
|
|
|
sub btrfs_subvolume_delete($@)
|
2015-01-04 19:30:41 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2015-01-04 19:30:41 +01:00
|
|
|
my @targets = @_;
|
|
|
|
return 0 unless(scalar(@targets));
|
2015-01-14 17:14:13 +01:00
|
|
|
my @real_targets;
|
|
|
|
my $rsh;
|
|
|
|
foreach (@targets) {
|
|
|
|
my ($r, $t) = get_rsh($_, $config);
|
|
|
|
die if($rsh && ($rsh ne $r)); # make sure all targets share same ssh host
|
|
|
|
$rsh = $r;
|
|
|
|
push(@real_targets, $t);
|
|
|
|
}
|
|
|
|
die if(scalar(@targets) != scalar(@real_targets));
|
2015-03-28 15:03:43 +01:00
|
|
|
my $commit_delete = config_key($config, "btrfs_commit_delete") // "";
|
2015-01-14 17:14:13 +01:00
|
|
|
DEBUG "[btrfs] delete" . ($commit_delete ? " (commit-$commit_delete):" : ":");
|
2015-01-13 12:38:01 +01:00
|
|
|
DEBUG "[btrfs] subvolume: $_" foreach(@targets);
|
2015-01-13 18:41:57 +01:00
|
|
|
my $options = "";
|
|
|
|
$options = "--commit-after " if($commit_delete eq "after");
|
|
|
|
$options = "--commit-each " if($commit_delete eq "each");
|
2015-01-14 17:14:13 +01:00
|
|
|
my $ret = run_cmd("$rsh /sbin/btrfs subvolume delete $options" . join(' ', @real_targets));
|
2015-01-04 19:30:41 +01:00
|
|
|
ERROR "Failed to delete btrfs subvolumes: " . join(' ', @targets) unless(defined($ret));
|
|
|
|
return defined($ret) ? scalar(@targets) : undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-31 19:07:33 +02:00
|
|
|
sub btrfs_send_receive($$$;$)
|
2014-12-11 18:03:10 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $src = shift || die;
|
|
|
|
my $target = shift || die;
|
2014-12-12 14:05:37 +01:00
|
|
|
my $parent = shift // "";
|
2015-01-14 14:10:41 +01:00
|
|
|
my $config = shift;
|
2015-01-14 17:14:13 +01:00
|
|
|
my ($rsh_src, $real_src) = get_rsh($src, $config);
|
|
|
|
my ($rsh_target, $real_target) = get_rsh($target, $config);
|
|
|
|
my (undef, $real_parent) = get_rsh($parent, $config);
|
2014-12-12 16:29:04 +01:00
|
|
|
my $now = localtime;
|
2014-12-13 19:34:03 +01:00
|
|
|
|
|
|
|
my $src_name = $src;
|
|
|
|
$src_name =~ s/^.*\///;
|
2015-01-13 12:38:01 +01:00
|
|
|
INFO ">>> $target/$src_name";
|
2014-12-13 19:34:03 +01:00
|
|
|
|
2015-03-24 18:44:19 +01:00
|
|
|
DEBUG "[btrfs] send/receive" . ($parent ? " (incremental)" : " (complete)") . ":";
|
|
|
|
DEBUG "[btrfs] source: $src";
|
|
|
|
DEBUG "[btrfs] parent: $parent" if($parent);
|
|
|
|
DEBUG "[btrfs] target: $target";
|
2014-12-12 16:29:04 +01:00
|
|
|
|
2015-01-14 17:14:13 +01:00
|
|
|
my $parent_option = $real_parent ? "-p $real_parent" : "";
|
2014-12-12 16:29:04 +01:00
|
|
|
my $receive_option = "";
|
2015-03-24 18:44:19 +01:00
|
|
|
$receive_option = "-v" if($loglevel >= 3);
|
2015-03-24 13:13:00 +01:00
|
|
|
|
2015-03-24 18:44:19 +01:00
|
|
|
my $cmd = "$rsh_src /sbin/btrfs send $parent_option $real_src | $rsh_target /sbin/btrfs receive $receive_option $real_target/";
|
2014-12-12 16:29:04 +01:00
|
|
|
my $ret = run_cmd($cmd);
|
2014-12-19 13:31:31 +01:00
|
|
|
unless(defined($ret)) {
|
2015-01-14 17:14:13 +01:00
|
|
|
ERROR "Failed to send/receive btrfs subvolume: $src " . ($real_parent ? "[$real_parent]" : "") . " -> $target";
|
2014-12-19 13:31:31 +01:00
|
|
|
return undef;
|
|
|
|
}
|
2015-03-31 20:36:10 +02:00
|
|
|
return 1;
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
|
|
|
|
2014-12-14 15:34:55 +01:00
|
|
|
|
2015-03-31 19:07:33 +02:00
|
|
|
# sets $config->{ABORTED} on failure
|
|
|
|
# sets $config->{subvol_received}
|
|
|
|
sub macro_send_receive($@)
|
|
|
|
{
|
|
|
|
my $config = shift || die;
|
|
|
|
my %info = @_;
|
|
|
|
my $incremental = config_key($config, "incremental");
|
|
|
|
|
2015-03-31 19:58:24 +02:00
|
|
|
INFO "Receiving from snapshot: $info{src}";
|
2015-03-31 19:07:33 +02:00
|
|
|
|
2015-03-31 20:36:10 +02:00
|
|
|
# add info to $config->{subvol_received}
|
|
|
|
my $src_name = $info{src};
|
|
|
|
$src_name =~ s/^.*\///;
|
|
|
|
$info{received_name} = "$info{target}/$src_name";
|
|
|
|
$config->{subvol_received} //= [];
|
|
|
|
push(@{$config->{subvol_received}}, \%info);
|
|
|
|
|
2015-03-31 19:07:33 +02:00
|
|
|
if($incremental)
|
|
|
|
{
|
|
|
|
# create backup from latest common
|
|
|
|
if($info{parent}) {
|
|
|
|
INFO "Incremental from parent snapshot: $info{parent}";
|
|
|
|
}
|
|
|
|
elsif($incremental ne "strict") {
|
|
|
|
INFO "No common parent subvolume present, creating full backup";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
WARN "Backup to $info{target} failed: no common parent subvolume found, and option \"incremental\" is set to \"strict\"";
|
2015-03-31 20:36:10 +02:00
|
|
|
$info{ERROR} = 1;
|
2015-03-31 19:07:33 +02:00
|
|
|
$config->{ABORTED} = "No common parent subvolume found, and option \"incremental\" is set to \"strict\"";
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
INFO "Option \"incremental\" is not set, creating full backup";
|
|
|
|
delete $info{parent};
|
|
|
|
}
|
|
|
|
|
2015-03-31 20:36:10 +02:00
|
|
|
if(btrfs_send_receive($info{src}, $info{target}, $info{parent}, $config)) {
|
2015-03-31 19:07:33 +02:00
|
|
|
return 1;
|
|
|
|
} else {
|
2015-03-31 20:36:10 +02:00
|
|
|
$info{ERROR} = 1;
|
2015-03-31 19:07:33 +02:00
|
|
|
$config->{ABORTED} = "btrfs send/receive command failed";
|
2015-03-31 20:36:10 +02:00
|
|
|
return undef;
|
2015-03-31 19:07:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-31 16:20:45 +02:00
|
|
|
sub get_date_tag($)
|
|
|
|
{
|
|
|
|
my $name = shift;
|
|
|
|
$name =~ s/_([0-9]+)$//;
|
2015-04-02 16:24:13 +02:00
|
|
|
my $postfix_counter = $1 // 0;
|
2015-03-31 16:20:45 +02:00
|
|
|
my $date = undef;
|
|
|
|
if($name =~ /\.([0-9]{4})([0-9]{2})([0-9]{2})$/) {
|
|
|
|
$date = [ $1, $2, $3 ];
|
|
|
|
}
|
|
|
|
return ($date, $postfix_counter);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
sub get_snapshot_children($$)
|
2014-12-14 15:34:55 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $sroot = shift || die;
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = shift // die;
|
2015-03-13 13:33:40 +01:00
|
|
|
my $svol_node = subvol($sroot, $svol);
|
|
|
|
die("subvolume info not present: $sroot/$svol") unless($svol_node);
|
2014-12-14 15:34:55 +01:00
|
|
|
my @ret;
|
|
|
|
foreach (values %{$vol_info{$sroot}}) {
|
2015-03-13 13:33:40 +01:00
|
|
|
next unless($_->{node}->{parent_uuid} eq $svol_node->{uuid});
|
2015-04-14 03:24:32 +02:00
|
|
|
TRACE "get_snapshot_children: found: $_->{URL}";
|
2014-12-14 15:34:55 +01:00
|
|
|
push(@ret, $_);
|
|
|
|
}
|
2015-04-01 13:26:10 +02:00
|
|
|
DEBUG "Found " . scalar(@ret) . " snapshot children of: $sroot/$svol";
|
2014-12-14 15:34:55 +01:00
|
|
|
return @ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-01 15:05:27 +02:00
|
|
|
sub get_receive_targets($$)
|
2014-12-14 15:34:55 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $droot = shift || die;
|
2015-04-01 15:05:27 +02:00
|
|
|
my $src_href = shift || die;
|
2014-12-19 14:37:30 +01:00
|
|
|
die("root subvolume info not present: $droot") unless($vol_info{$droot});
|
2014-12-14 15:34:55 +01:00
|
|
|
my @ret;
|
2015-04-01 15:05:27 +02:00
|
|
|
|
|
|
|
if($vol_btrfs_progs_compat{$droot})
|
|
|
|
{
|
|
|
|
# guess matches by subvolume name (node->received_uuid is not available if BTRFS_PROGS_COMPAT is set)
|
|
|
|
DEBUG "Fallback to compatibility mode (get_receive_targets)";
|
|
|
|
my $src_name = $src_href->{node}->{REL_PATH};
|
|
|
|
$src_name =~ s/^.*\///; # strip path
|
|
|
|
foreach my $target (values %{$vol_info{$droot}}) {
|
|
|
|
my $target_name = $target->{node}->{REL_PATH};
|
|
|
|
$target_name =~ s/^.*\///; # strip path
|
|
|
|
if($target_name eq $src_name) {
|
|
|
|
TRACE "get_receive_targets: by-name: Found receive target: $target->{SUBVOL_PATH}";
|
|
|
|
push(@ret, $target);
|
|
|
|
}
|
|
|
|
}
|
2014-12-14 15:34:55 +01:00
|
|
|
}
|
2015-04-01 15:05:27 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
# find matches by comparing uuid / received_uuid
|
|
|
|
my $uuid = $src_href->{node}->{uuid};
|
|
|
|
die("subvolume info not present: $uuid") unless($uuid_info{$uuid});
|
|
|
|
foreach (values %{$vol_info{$droot}}) {
|
|
|
|
next unless($_->{node}->{received_uuid} eq $uuid);
|
|
|
|
TRACE "get_receive_targets: by-uuid: Found receive target: $_->{SUBVOL_PATH}";
|
|
|
|
push(@ret, $_);
|
|
|
|
}
|
|
|
|
}
|
2015-04-14 03:24:32 +02:00
|
|
|
DEBUG "Found " . scalar(@ret) . " receive targets in \"$droot/\" for: $src_href->{URL}";
|
2014-12-14 15:34:55 +01:00
|
|
|
return @ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-31 21:45:21 +02:00
|
|
|
sub get_latest_common($$$;$)
|
2014-12-11 18:03:10 +01:00
|
|
|
{
|
2015-01-14 14:10:41 +01:00
|
|
|
my $sroot = shift || die;
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = shift // die;
|
2015-01-14 14:10:41 +01:00
|
|
|
my $droot = shift || die;
|
2015-03-31 21:45:21 +02:00
|
|
|
my $threshold_gen = shift; # skip all snapshot children with generation >= $threshold_gen
|
2014-12-11 18:30:02 +01:00
|
|
|
|
2014-12-19 14:37:30 +01:00
|
|
|
die("source subvolume info not present: $sroot") unless($vol_info{$sroot});
|
|
|
|
die("target subvolume info not present: $droot") unless($vol_info{$droot});
|
2014-12-11 18:30:02 +01:00
|
|
|
|
2015-04-01 13:25:24 +02:00
|
|
|
my $debug_src = "$sroot/$svol";
|
|
|
|
$debug_src .= "@" . $threshold_gen if($threshold_gen);
|
2015-03-31 21:45:21 +02:00
|
|
|
|
2014-12-14 19:23:02 +01:00
|
|
|
# sort children of svol descending by generation
|
2015-03-13 13:33:40 +01:00
|
|
|
foreach my $child (sort { $b->{node}->{gen} <=> $a->{node}->{gen} } get_snapshot_children($sroot, $svol)) {
|
2014-12-14 15:34:55 +01:00
|
|
|
TRACE "get_latest_common: checking source snapshot: $child->{SUBVOL_PATH}";
|
2015-03-31 21:45:21 +02:00
|
|
|
if($threshold_gen && ($child->{node}->{gen} >= $threshold_gen)) {
|
|
|
|
TRACE "get_latest_common: skipped gen=$child->{node}->{gen} >= $threshold_gen: $child->{SUBVOL_PATH}";
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
|
2015-04-01 13:25:24 +02:00
|
|
|
if($child->{RECEIVE_TARGET_PRESENT} && ($child->{RECEIVE_TARGET_PRESENT} eq $droot)) {
|
|
|
|
# little hack to keep track of previously received subvolumes
|
2015-04-14 03:24:32 +02:00
|
|
|
DEBUG("Latest common snapshots for: $debug_src: src=$child->{URL} target=<previously received>");
|
2015-04-01 13:25:24 +02:00
|
|
|
return ($child, undef);
|
|
|
|
}
|
|
|
|
|
2015-04-01 15:05:27 +02:00
|
|
|
foreach (get_receive_targets($droot, $child)) {
|
2015-04-14 03:24:32 +02:00
|
|
|
TRACE "get_latest_common: found receive target: $_->{URL}";
|
|
|
|
DEBUG("Latest common snapshots for: $debug_src: src=$child->{URL} target=$_->{URL}");
|
2015-04-01 15:05:27 +02:00
|
|
|
return ($child, $_);
|
2014-12-11 18:30:02 +01:00
|
|
|
}
|
2015-04-14 03:24:32 +02:00
|
|
|
TRACE "get_latest_common: no matching targets found for: $child->{URL}";
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
2015-04-01 13:25:24 +02:00
|
|
|
DEBUG("No common snapshots for \"$debug_src\" found in src=$sroot/ target=$droot/");
|
2014-12-14 15:34:55 +01:00
|
|
|
return (undef, undef);
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-03-13 17:54:08 +01:00
|
|
|
sub _origin_tree
|
2015-01-26 17:31:18 +01:00
|
|
|
{
|
|
|
|
my $prefix = shift;
|
|
|
|
my $uuid = shift;
|
|
|
|
my $lines = shift;
|
|
|
|
my $node = $uuid_info{$uuid};
|
|
|
|
unless($node) {
|
|
|
|
push(@$lines, ["$prefix<orphaned>", $uuid]);
|
|
|
|
return 0;
|
|
|
|
}
|
2015-03-13 17:54:08 +01:00
|
|
|
if($uuid_fs_map{$uuid}) {
|
|
|
|
foreach(keys %{$uuid_fs_map{$uuid}}) {
|
|
|
|
push(@$lines, ["$prefix$_", $uuid]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
push(@$lines, ["$prefix<BTRFS_ROOT>/$node->{path}", $uuid]);
|
|
|
|
}
|
|
|
|
|
2015-01-26 17:31:18 +01:00
|
|
|
$prefix =~ s/./ /g;
|
2015-03-24 13:13:00 +01:00
|
|
|
if($node->{received_uuid}) {
|
|
|
|
if($node->{received_uuid} ne '-') {
|
|
|
|
_origin_tree("${prefix}^---", $node->{received_uuid}, $lines);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
push(@$lines, ["$prefix^---<missing_received_uuid>", $uuid]); # printed if "btrfs_progs_compat" is set
|
2015-01-26 17:31:18 +01:00
|
|
|
}
|
|
|
|
if($node->{parent_uuid} ne '-') {
|
2015-03-13 17:54:08 +01:00
|
|
|
_origin_tree("${prefix}", $node->{parent_uuid}, $lines);
|
2015-01-26 17:31:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-02 15:53:53 +02:00
|
|
|
sub schedule(@)
|
2015-01-04 21:26:48 +01:00
|
|
|
{
|
|
|
|
my %args = @_;
|
2015-01-13 17:51:24 +01:00
|
|
|
my $schedule = $args{schedule} || die;
|
2015-01-13 12:38:01 +01:00
|
|
|
my @today = @{$args{today}};
|
|
|
|
my $preserve_day_of_week = $args{preserve_day_of_week} || die;
|
|
|
|
my $preserve_daily = $args{preserve_daily} // die;
|
|
|
|
my $preserve_weekly = $args{preserve_weekly} // die;
|
|
|
|
my $preserve_monthly = $args{preserve_monthly} // die;
|
2015-03-31 19:58:24 +02:00
|
|
|
my $log_verbose = $args{log_verbose};
|
2015-01-13 12:38:01 +01:00
|
|
|
|
2015-03-31 19:58:24 +02:00
|
|
|
if($log_verbose) {
|
|
|
|
INFO "Filter scheme: preserving all within $preserve_daily days";
|
|
|
|
INFO "Filter scheme: preserving first in week (starting on $preserve_day_of_week), for $preserve_weekly weeks";
|
|
|
|
INFO "Filter scheme: preserving last weekly of month, for $preserve_monthly months";
|
|
|
|
}
|
2015-01-13 12:38:01 +01:00
|
|
|
|
2015-04-02 16:24:13 +02:00
|
|
|
# sort the schedule, ascending by date
|
|
|
|
my @sorted_schedule = sort { ($a->{date}->[0] <=> $b->{date}->[0]) ||
|
|
|
|
($a->{date}->[1] <=> $b->{date}->[1]) ||
|
|
|
|
($a->{date}->[2] <=> $b->{date}->[2]) ||
|
|
|
|
($a->{date_ext} <=> $a->{date_ext})
|
|
|
|
} @$schedule;
|
|
|
|
|
2015-01-25 18:05:52 +01:00
|
|
|
# first, do our calendar calculations
|
|
|
|
# note: our week starts on $preserve_day_of_week
|
|
|
|
my $delta_days_to_eow_from_today = $day_of_week_map{$preserve_day_of_week} - Day_of_Week(@today) - 1;
|
|
|
|
$delta_days_to_eow_from_today = $delta_days_to_eow_from_today + 7 if($delta_days_to_eow_from_today < 0);
|
2015-04-01 13:26:10 +02:00
|
|
|
TRACE "last day before next $preserve_day_of_week is in $delta_days_to_eow_from_today days";
|
2015-04-02 16:24:13 +02:00
|
|
|
foreach my $href (@sorted_schedule)
|
2015-01-13 12:38:01 +01:00
|
|
|
{
|
|
|
|
my @date = @{$href->{date}};
|
|
|
|
my $delta_days = Delta_Days(@date, @today);
|
2015-01-25 18:05:52 +01:00
|
|
|
my $delta_days_to_eow = $delta_days + $delta_days_to_eow_from_today;
|
2015-01-12 17:56:35 +01:00
|
|
|
{
|
2015-01-13 12:38:01 +01:00
|
|
|
use integer; # do integer arithmetics
|
2015-01-25 18:05:52 +01:00
|
|
|
$href->{delta_days} = $delta_days;
|
|
|
|
$href->{delta_weeks} = $delta_days_to_eow / 7;
|
|
|
|
$href->{err_days} = 6 - ( $delta_days_to_eow % 7 );
|
|
|
|
$href->{delta_months} = ($today[0] - $date[0]) * 12 + ($today[1] - $date[1]);
|
|
|
|
$href->{month} = "$date[0]-$date[1]";
|
2015-01-12 17:56:35 +01:00
|
|
|
}
|
2015-01-04 21:26:48 +01:00
|
|
|
}
|
|
|
|
|
2015-01-25 18:05:52 +01:00
|
|
|
# filter daily, weekly, monthly
|
|
|
|
my %first_in_delta_weeks;
|
|
|
|
my %last_weekly_in_delta_months;
|
2015-04-02 16:24:13 +02:00
|
|
|
foreach my $href (@sorted_schedule) {
|
2015-01-25 18:05:52 +01:00
|
|
|
if($preserve_daily && (($preserve_daily eq "all") || ($href->{delta_days} <= $preserve_daily))) {
|
|
|
|
$href->{preserve} ||= "preserved daily: $href->{delta_days} days ago";
|
|
|
|
}
|
|
|
|
$first_in_delta_weeks{$href->{delta_weeks}} //= $href;
|
|
|
|
}
|
|
|
|
foreach (reverse sort keys %first_in_delta_weeks) {
|
2015-01-20 16:53:35 +01:00
|
|
|
my $href = $first_in_delta_weeks{$_} || die;
|
2015-01-25 18:05:52 +01:00
|
|
|
if($preserve_weekly && (($preserve_weekly eq "all") || ($href->{delta_weeks} <= $preserve_weekly))) {
|
2015-01-20 16:53:35 +01:00
|
|
|
$href->{preserve} ||= "preserved weekly: $href->{delta_weeks} weeks ago, " . ($href->{err_days} ? "+$href->{err_days} days after " : "on ") . "$preserve_day_of_week";
|
2015-01-04 21:26:48 +01:00
|
|
|
}
|
2015-01-25 18:05:52 +01:00
|
|
|
$last_weekly_in_delta_months{$href->{delta_months}} = $href;
|
2015-01-04 21:26:48 +01:00
|
|
|
}
|
2015-01-25 18:05:52 +01:00
|
|
|
foreach (reverse sort keys %last_weekly_in_delta_months) {
|
|
|
|
my $href = $last_weekly_in_delta_months{$_} || die;
|
|
|
|
if($preserve_monthly && (($preserve_monthly eq "all") || ($href->{delta_months} <= $preserve_monthly))) {
|
2015-01-20 16:53:35 +01:00
|
|
|
$href->{preserve} ||= "preserved monthly: " . ($href->{err_days} ? "$href->{err_days} days after " : "") . "last $preserve_day_of_week of month $href->{month} (age: $href->{delta_months} months)";
|
2015-01-13 12:38:01 +01:00
|
|
|
}
|
2015-01-04 21:26:48 +01:00
|
|
|
}
|
|
|
|
|
2015-01-25 18:05:52 +01:00
|
|
|
# assemble results
|
2015-01-13 12:38:01 +01:00
|
|
|
my @delete;
|
2015-04-02 15:53:53 +02:00
|
|
|
my @preserve;
|
2015-04-02 16:24:13 +02:00
|
|
|
foreach my $href (@sorted_schedule)
|
2015-01-13 12:38:01 +01:00
|
|
|
{
|
|
|
|
if($href->{preserve}) {
|
2015-04-02 16:24:13 +02:00
|
|
|
INFO "=== $href->{name}: $href->{preserve}" if($href->{name});
|
|
|
|
push(@preserve, $href->{value});
|
2015-01-13 12:38:01 +01:00
|
|
|
}
|
|
|
|
else {
|
2015-04-02 16:24:13 +02:00
|
|
|
INFO "<<< $href->{name}" if($href->{name});
|
|
|
|
push(@delete, $href->{value});
|
2015-01-13 12:38:01 +01:00
|
|
|
}
|
|
|
|
}
|
2015-04-02 15:53:53 +02:00
|
|
|
DEBUG "Preserving " . @preserve . "/" . @$schedule . " items" unless($log_verbose);
|
|
|
|
return (\@preserve, \@delete);
|
2015-01-04 21:26:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-12-11 18:03:10 +01:00
|
|
|
MAIN:
|
|
|
|
{
|
|
|
|
$ENV{PATH} = '';
|
|
|
|
$Getopt::Std::STANDARD_HELP_VERSION = 1;
|
2014-12-11 18:30:02 +01:00
|
|
|
$Data::Dumper::Sortkeys = 1;
|
2015-01-17 14:55:46 +01:00
|
|
|
my $start_time = time;
|
2015-01-04 19:30:41 +01:00
|
|
|
my @today = Today();
|
2014-12-11 18:03:10 +01:00
|
|
|
|
|
|
|
my %opts;
|
2015-02-28 11:54:11 +01:00
|
|
|
unless(getopts('hc:vql:p', \%opts)) {
|
2015-01-10 16:02:35 +01:00
|
|
|
VERSION_MESSAGE();
|
|
|
|
HELP_MESSAGE(0);
|
|
|
|
exit 1;
|
|
|
|
}
|
2014-12-13 15:15:58 +01:00
|
|
|
my $command = shift @ARGV;
|
2014-12-12 12:32:04 +01:00
|
|
|
|
|
|
|
# assign command line options
|
2014-12-14 22:45:23 +01:00
|
|
|
$loglevel = $opts{l} || "";
|
2014-12-13 19:34:03 +01:00
|
|
|
if (lc($loglevel) eq "warn") { $loglevel = 1; }
|
|
|
|
elsif(lc($loglevel) eq "info") { $loglevel = 2; }
|
|
|
|
elsif(lc($loglevel) eq "debug") { $loglevel = 3; }
|
|
|
|
elsif(lc($loglevel) eq "trace") { $loglevel = 4; }
|
|
|
|
elsif($loglevel =~ /^[0-9]+$/) { ; }
|
|
|
|
else {
|
2015-01-13 17:51:24 +01:00
|
|
|
$loglevel = $opts{v} ? 2 : 1;
|
2014-12-13 19:34:03 +01:00
|
|
|
}
|
2015-01-17 13:14:47 +01:00
|
|
|
@config_src = ( $opts{c} ) if($opts{c});
|
2015-01-13 17:51:24 +01:00
|
|
|
my $quiet = $opts{q};
|
2015-02-28 12:02:28 +01:00
|
|
|
my $preserve_backups = $opts{p};
|
2014-12-11 18:03:10 +01:00
|
|
|
|
2014-12-12 12:32:04 +01:00
|
|
|
# check command line options
|
2014-12-13 15:15:58 +01:00
|
|
|
if($opts{h} || (not $command)) {
|
2014-12-11 18:03:10 +01:00
|
|
|
VERSION_MESSAGE();
|
|
|
|
HELP_MESSAGE(0);
|
|
|
|
exit 0;
|
|
|
|
}
|
2014-12-13 15:15:58 +01:00
|
|
|
|
2015-02-08 13:47:31 +01:00
|
|
|
my ($action_run, $action_info, $action_tree, $action_diff, $action_origin);
|
2015-02-28 13:49:36 +01:00
|
|
|
my @subvol_args;
|
2015-03-01 14:28:26 +01:00
|
|
|
my ($args_expected_min, $args_expected_max) = (0, 0);
|
2015-02-08 13:47:31 +01:00
|
|
|
if(($command eq "run") || ($command eq "dryrun")) {
|
|
|
|
$action_run = 1;
|
2014-12-13 15:15:58 +01:00
|
|
|
$dryrun = 1 if($command eq "dryrun");
|
2015-03-01 14:28:26 +01:00
|
|
|
$args_expected_min = 0;
|
|
|
|
$args_expected_max = 9999;
|
|
|
|
@subvol_args = @ARGV;
|
2014-12-13 15:15:58 +01:00
|
|
|
}
|
2015-01-20 19:18:38 +01:00
|
|
|
elsif ($command eq "info") {
|
|
|
|
$action_info = 1;
|
|
|
|
}
|
2015-01-04 19:30:41 +01:00
|
|
|
elsif ($command eq "tree") {
|
2015-01-03 14:22:38 +01:00
|
|
|
$action_tree = 1;
|
2014-12-13 15:15:58 +01:00
|
|
|
}
|
2015-01-04 19:30:41 +01:00
|
|
|
elsif ($command eq "diff") {
|
2015-01-03 21:25:46 +01:00
|
|
|
$action_diff = 1;
|
2015-03-01 14:28:26 +01:00
|
|
|
$args_expected_min = $args_expected_max = 2;
|
2015-02-28 13:49:36 +01:00
|
|
|
@subvol_args = @ARGV;
|
2014-12-14 21:29:22 +01:00
|
|
|
}
|
2015-01-26 17:31:18 +01:00
|
|
|
elsif ($command eq "origin") {
|
|
|
|
$action_origin = 1;
|
2015-03-01 14:28:26 +01:00
|
|
|
$args_expected_min = $args_expected_max = 1;
|
2015-02-28 13:49:36 +01:00
|
|
|
@subvol_args = @ARGV;
|
2015-01-26 17:31:18 +01:00
|
|
|
}
|
2014-12-13 15:15:58 +01:00
|
|
|
else {
|
|
|
|
ERROR "Unrecognized command: $command";
|
|
|
|
HELP_MESSAGE(0);
|
2014-12-13 13:52:43 +01:00
|
|
|
exit 1;
|
|
|
|
}
|
2015-03-01 14:28:26 +01:00
|
|
|
if(($args_expected_min > scalar(@ARGV)) || ($args_expected_max < scalar(@ARGV))) {
|
2015-02-28 13:49:36 +01:00
|
|
|
ERROR "Incorrect number of arguments";
|
|
|
|
HELP_MESSAGE(0);
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
# input validation
|
|
|
|
foreach (@subvol_args) {
|
|
|
|
s/\/+$//; # remove trailing slash
|
|
|
|
unless(/^(($ssh_prefix_match)?\/$file_match)$/) { # matches ssh statement or absolute file
|
|
|
|
ERROR "Bad argument: not a subvolume declaration: $_";
|
|
|
|
HELP_MESSAGE(0);
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
$_ = $1; # untaint argument
|
|
|
|
}
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-01-20 21:07:28 +01:00
|
|
|
INFO "$version_info (" . localtime($start_time) . ")";
|
2014-12-14 21:29:22 +01:00
|
|
|
|
|
|
|
if($action_diff)
|
|
|
|
{
|
2015-01-04 19:30:41 +01:00
|
|
|
#
|
|
|
|
# print snapshot diff
|
|
|
|
#
|
2015-02-28 13:49:36 +01:00
|
|
|
my $src_vol = $subvol_args[0] || die;
|
|
|
|
my $target_vol = $subvol_args[1] || die;
|
|
|
|
# FIXME: allow ssh:// src/dest (does not work since the configuration is not yet read).
|
2015-01-03 21:25:46 +01:00
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
my $src_detail = vinfo($src_vol);
|
2015-01-03 21:25:46 +01:00
|
|
|
unless($src_detail) { exit 1; }
|
|
|
|
if($src_detail->{is_root}) { ERROR "subvolume at \"$src_vol\" is btrfs root!"; exit 1; }
|
|
|
|
unless($src_detail->{cgen}) { ERROR "subvolume at \"$src_vol\" does not provide cgen"; exit 1; }
|
|
|
|
# if($src_detail->{parent_uuid} eq "-") { ERROR "subvolume at \"$src_vol\" has no parent, aborting."; exit 1; }
|
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
my $target_detail = vinfo($target_vol);
|
2015-01-03 21:25:46 +01:00
|
|
|
unless($target_detail) { exit 1; }
|
|
|
|
unless($src_detail->{cgen}) { ERROR "subvolume at \"$src_vol\" does not provide cgen"; exit 1; }
|
|
|
|
# if($src_detail->{parent_uuid} eq "-") { ERROR "subvolume at \"$src_vol\" has no parent, aborting."; exit 1; }
|
|
|
|
|
|
|
|
my $info = btr_tree($src_vol);
|
|
|
|
my $src = $uuid_info{$src_detail->{uuid}} || die;
|
|
|
|
my $target = $uuid_info{$target_detail->{uuid}};
|
|
|
|
unless($target) { ERROR "target subvolume is not on the same btrfs filesystem!"; exit 1; }
|
|
|
|
|
|
|
|
my $lastgen;
|
|
|
|
|
|
|
|
# check if given src and target share same parent
|
|
|
|
if(ref($src->{PARENT}) && ($src->{PARENT}->{uuid} eq $target->{uuid})) {
|
|
|
|
DEBUG "target subvolume is direct parent of source subvolume";
|
|
|
|
}
|
|
|
|
elsif(ref($src->{PARENT}) && ref($target->{PARENT}) && ($src->{PARENT}->{uuid} eq $target->{PARENT}->{uuid})) {
|
|
|
|
DEBUG "target subvolume and source subvolume share same parent";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
# TODO: this rule only applies to snapshots. find a way to distinguish snapshots from received backups
|
|
|
|
# ERROR "subvolumes \"$target_vol\" and \"$src_vol\" do not share the same parents";
|
|
|
|
# exit 1;
|
2014-12-14 22:03:31 +01:00
|
|
|
}
|
|
|
|
|
2015-01-03 21:25:46 +01:00
|
|
|
# NOTE: in some cases "cgen" differs from "gen", even for read-only snapshots (observed: gen=cgen+1)
|
|
|
|
$lastgen = $src->{cgen} + 1;
|
|
|
|
|
2014-12-14 22:03:31 +01:00
|
|
|
# dump files, sorted and unique
|
2015-01-03 21:25:46 +01:00
|
|
|
my $ret = btr_subvolume_find_new($target_vol, $lastgen);
|
|
|
|
exit 1 unless(ref($ret));
|
|
|
|
|
2014-12-14 22:30:18 +01:00
|
|
|
print "--------------------------------------------------------------------------------\n";
|
2015-01-03 21:25:46 +01:00
|
|
|
print "Showing changed files for subvolume:\n $target->{path} (gen=$target->{gen})\n";
|
|
|
|
print "\nStarting at creation generation from subvolume:\n $src->{path} (cgen=$src->{cgen})\n";
|
|
|
|
print "\nThis will show all files modified within generation range: [$lastgen..$target->{gen}]\n";
|
|
|
|
print "Newest file generation (transid marker) was: $ret->{transid_marker}\n";
|
|
|
|
print "Parse errors: $ret->{parse_errors}\n" if($ret->{parse_errors});
|
2015-02-08 13:46:03 +01:00
|
|
|
print "\nLegend: <flags> <count> <size> <filename>\n";
|
|
|
|
print " +.. file accessed at offset 0 (at least once)\n";
|
|
|
|
print " .c. flags COMPRESS or COMPRESS|INLINE set (at least once)\n";
|
|
|
|
print " ..i flags INLINE or COMPRESS|INLINE set (at least once)\n";
|
|
|
|
print " <count> file was modified in <count> generations\n";
|
|
|
|
print " <size> file was modified for a total of <size> bytes\n";
|
2014-12-14 22:30:18 +01:00
|
|
|
print "--------------------------------------------------------------------------------\n";
|
2015-01-03 21:25:46 +01:00
|
|
|
my $files = $ret->{files};
|
|
|
|
|
|
|
|
# calculate the character offsets
|
|
|
|
my $len_charlen = 0;
|
|
|
|
my $gen_charlen = 0;
|
|
|
|
foreach (values %$files) {
|
|
|
|
my $len = length($_->{len});
|
2015-02-10 13:31:43 +01:00
|
|
|
my $gen = length(scalar(keys(%{$_->{gen}})));
|
2015-01-03 21:25:46 +01:00
|
|
|
$len_charlen = $len if($len > $len_charlen);
|
|
|
|
$gen_charlen = $gen if($gen > $gen_charlen);
|
|
|
|
}
|
|
|
|
|
|
|
|
# finally print the output
|
|
|
|
foreach my $name (sort keys %$files) {
|
|
|
|
print ($files->{$name}->{new} ? '+' : '.');
|
|
|
|
print ($files->{$name}->{flags}->{compress} ? 'c' : '.');
|
|
|
|
print ($files->{$name}->{flags}->{inline} ? 'i' : '.');
|
|
|
|
|
|
|
|
# make nice table
|
2015-02-10 13:31:43 +01:00
|
|
|
my $gens = scalar(keys(%{$files->{$name}->{gen}}));
|
2015-01-03 21:25:46 +01:00
|
|
|
my $len = $files->{$name}->{len};
|
|
|
|
print " " . (' ' x ($gen_charlen - length($gens))) . $gens;
|
|
|
|
print " " . (' ' x ($len_charlen - length($len))) . $len;
|
|
|
|
|
|
|
|
print " $name\n";
|
|
|
|
}
|
|
|
|
|
2014-12-14 21:29:22 +01:00
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-12-13 13:52:43 +01:00
|
|
|
#
|
2015-01-20 19:18:38 +01:00
|
|
|
# parse config file
|
2014-12-13 13:52:43 +01:00
|
|
|
#
|
2015-01-17 13:14:47 +01:00
|
|
|
my $config = parse_config(@config_src);
|
2015-01-10 16:02:35 +01:00
|
|
|
unless($config) {
|
2014-12-13 15:15:58 +01:00
|
|
|
ERROR "Failed to parse configuration file";
|
|
|
|
exit 1;
|
|
|
|
}
|
2015-01-10 16:02:35 +01:00
|
|
|
unless(ref($config->{VOLUME}) eq "ARRAY") {
|
|
|
|
ERROR "No volumes defined in configuration file";
|
|
|
|
exit 1;
|
|
|
|
}
|
2015-01-20 19:18:38 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if($action_info)
|
|
|
|
{
|
|
|
|
#
|
|
|
|
# print filesystem information
|
|
|
|
#
|
|
|
|
print "================================================================================\n";
|
|
|
|
print "Filesystem information ($version_info)\n\n";
|
|
|
|
print " Date: " . localtime($start_time) . "\n";
|
|
|
|
print " Config: $config->{SRC_FILE}\n";
|
|
|
|
print "================================================================================\n";
|
|
|
|
|
2015-01-25 13:36:07 +01:00
|
|
|
# print "\n--------------------------------------------------------------------------------\n";
|
|
|
|
# print "All local btrfs filesystems\n";
|
|
|
|
# print "--------------------------------------------------------------------------------\n";
|
|
|
|
# print (btr_filesystem_show_all_local() // "");
|
|
|
|
# print "\n";
|
2015-01-20 19:18:38 +01:00
|
|
|
|
|
|
|
my %processed;
|
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
|
|
|
{
|
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
unless($processed{$sroot})
|
|
|
|
{
|
|
|
|
print "\n--------------------------------------------------------------------------------\n";
|
|
|
|
print "Source volume: $sroot\n";
|
|
|
|
print "--------------------------------------------------------------------------------\n";
|
2015-01-25 13:36:07 +01:00
|
|
|
# print (btr_filesystem_show($sroot, $config_vol) // "");
|
|
|
|
# print "\n\n";
|
|
|
|
print (btr_filesystem_usage($sroot, $config_vol) // "");
|
2015-01-20 19:18:38 +01:00
|
|
|
print "\n";
|
|
|
|
$processed{$sroot} = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach my $config_vol (@{$config->{VOLUME}}) {
|
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) {
|
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
my $droot = $config_target->{droot} || die;
|
|
|
|
unless($processed{$droot})
|
|
|
|
{
|
|
|
|
print "\n--------------------------------------------------------------------------------\n";
|
|
|
|
print "Target volume: $droot\n";
|
|
|
|
print " ^--- $sroot\n";
|
|
|
|
print "--------------------------------------------------------------------------------\n";
|
2015-01-25 13:36:07 +01:00
|
|
|
print (btr_filesystem_usage($droot, $config_target) // "");
|
2015-01-20 19:18:38 +01:00
|
|
|
print "\n";
|
|
|
|
$processed{$droot} = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
#
|
|
|
|
# fill vol_info hash, basic checks on configuration
|
|
|
|
#
|
2015-03-01 14:28:26 +01:00
|
|
|
my $subvol_filter_count = undef;
|
2015-01-10 16:02:35 +01:00
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
2014-12-13 13:52:43 +01:00
|
|
|
{
|
2015-01-10 16:02:35 +01:00
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
|
|
|
{
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = $config_subvol->{svol} // die;
|
2015-04-14 02:17:17 +02:00
|
|
|
vinfo($sroot, $config_vol);
|
|
|
|
vinfo("$sroot/$svol", $config_vol);
|
2015-03-01 14:28:26 +01:00
|
|
|
|
|
|
|
# filter subvolumes matching command line arguments
|
|
|
|
if($action_run && scalar(@subvol_args)) {
|
|
|
|
$subvol_filter_count //= 0;
|
|
|
|
if(grep(/^$sroot\/$svol$/, @subvol_args)) {
|
|
|
|
$subvol_filter_count++;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
DEBUG "No match on subvolume command line argument, skipping: $sroot/$svol";
|
|
|
|
$config_subvol->{ABORTED} = "No match on subvolume command line arguments";
|
|
|
|
$config_subvol->{ABORTED_NOERR} = 1;
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-13 13:33:40 +01:00
|
|
|
$vol_info{$sroot} //= btr_fs_info($sroot, $config_vol);
|
2015-01-10 16:02:35 +01:00
|
|
|
unless(subvol($sroot, $svol)) {
|
2015-01-13 17:51:24 +01:00
|
|
|
$config_subvol->{ABORTED} = "Subvolume \"$svol\" not present in btrfs subvolume list for \"$sroot\"";
|
|
|
|
WARN "Skipping subvolume section: $config_subvol->{ABORTED}";
|
2015-01-10 16:02:35 +01:00
|
|
|
next;
|
|
|
|
}
|
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
my $droot = $config_target->{droot} || die;
|
2015-04-14 02:17:17 +02:00
|
|
|
vinfo($droot, $config_target);
|
2015-03-13 13:33:40 +01:00
|
|
|
$vol_info{$droot} //= btr_fs_info($droot, $config_target);
|
2015-01-10 16:02:35 +01:00
|
|
|
unless($vol_info{$droot}) {
|
2015-01-13 17:51:24 +01:00
|
|
|
$config_target->{ABORTED} = "Failed to read btrfs subvolume list for \"$droot\"";
|
|
|
|
WARN "Skipping target: $config_target->{ABORTED}";
|
2015-01-10 16:02:35 +01:00
|
|
|
next;
|
|
|
|
}
|
|
|
|
}
|
2014-12-13 20:33:31 +01:00
|
|
|
}
|
2014-12-13 13:52:43 +01:00
|
|
|
}
|
2015-03-01 14:28:26 +01:00
|
|
|
if(defined($subvol_filter_count) && ($subvol_filter_count == 0)) {
|
|
|
|
ERROR "Subvolume command line arguments do not match any volume/subvolume declaration from configuration file, aborting.";
|
|
|
|
exit 1;
|
|
|
|
}
|
2014-12-13 19:34:03 +01:00
|
|
|
TRACE(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
|
2014-12-13 13:52:43 +01:00
|
|
|
|
2015-01-04 19:30:41 +01:00
|
|
|
|
2015-01-26 17:31:18 +01:00
|
|
|
if($action_origin)
|
|
|
|
{
|
|
|
|
#
|
|
|
|
# print origin information
|
|
|
|
#
|
2015-02-28 13:49:36 +01:00
|
|
|
my $subvol = $subvol_args[0] || die;
|
2015-01-26 17:31:18 +01:00
|
|
|
my $dump_uuid = 0;
|
2015-02-28 13:49:36 +01:00
|
|
|
|
2015-04-14 02:17:17 +02:00
|
|
|
my $detail = vinfo($subvol);
|
2015-03-13 17:54:08 +01:00
|
|
|
exit 1 unless($detail);
|
|
|
|
|
|
|
|
if($detail->{is_root}) {
|
|
|
|
ERROR "Subvolume is btrfs root: $subvol\n";
|
2015-01-26 17:31:18 +01:00
|
|
|
exit 1;
|
|
|
|
}
|
2015-03-13 17:54:08 +01:00
|
|
|
my $uuid = $detail->{uuid} || die;
|
|
|
|
my $node = $uuid_info{$uuid};
|
|
|
|
|
|
|
|
unless($node) {
|
|
|
|
DEBUG "Subvolume not parsed yet, fetching info: $subvol";
|
2015-04-14 02:17:17 +02:00
|
|
|
vinfo($subvol);
|
2015-03-13 17:54:08 +01:00
|
|
|
$vol_info{$subvol} //= btr_fs_info($subvol);
|
|
|
|
$node = $uuid_info{$uuid} || die;
|
|
|
|
}
|
|
|
|
|
2015-01-26 17:31:18 +01:00
|
|
|
my $lines = [];
|
2015-03-13 17:54:08 +01:00
|
|
|
_origin_tree("", $uuid, $lines);
|
2015-01-26 17:31:18 +01:00
|
|
|
|
|
|
|
print "--------------------------------------------------------------------------------\n";
|
|
|
|
print "Origin Tree\n\n";
|
|
|
|
print " ^--- : received from subvolume\n";
|
|
|
|
print " newline : parent subvolume\n";
|
|
|
|
print " orphaned: subvolume uuid could not be resolved (probably deleted)\n";
|
|
|
|
print "--------------------------------------------------------------------------------\n";
|
|
|
|
|
|
|
|
my $len = 0;
|
|
|
|
if($dump_uuid) {
|
|
|
|
$len = (length($_->[0]) > $len ? length($_->[0]) : $len) foreach(@$lines);
|
|
|
|
}
|
|
|
|
foreach(@$lines) {
|
|
|
|
print "$_->[0]";
|
|
|
|
print ' ' x ($len - length($_->[0]) + 4) . "$_->[1]" if($dump_uuid);
|
|
|
|
print "\n";
|
|
|
|
}
|
2015-03-13 17:54:08 +01:00
|
|
|
exit 0;
|
2015-01-26 17:31:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-01-03 14:22:38 +01:00
|
|
|
if($action_tree)
|
2014-12-12 12:32:04 +01:00
|
|
|
{
|
2014-12-13 16:51:30 +01:00
|
|
|
#
|
|
|
|
# print snapshot tree
|
|
|
|
#
|
2015-03-13 15:32:00 +01:00
|
|
|
# TODO: reverse tree: print all backups from $droot and their corresponding source snapshots
|
2015-01-10 16:03:47 +01:00
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
2014-12-13 16:51:30 +01:00
|
|
|
{
|
2015-03-24 13:13:00 +01:00
|
|
|
my %droot_compat;
|
2015-01-10 16:03:47 +01:00
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
print "$sroot\n";
|
|
|
|
next unless $vol_info{$sroot};
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
2014-12-13 16:51:30 +01:00
|
|
|
{
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = $config_subvol->{svol} // die;
|
2014-12-13 16:51:30 +01:00
|
|
|
print "|-- $svol\n";
|
2015-03-13 15:32:00 +01:00
|
|
|
unless($vol_info{$sroot}->{$svol}) {
|
|
|
|
print " !!! error: no subvolume \"$svol\" found in \"$sroot\"\n";
|
|
|
|
next;
|
2014-12-13 16:51:30 +01:00
|
|
|
}
|
2015-03-13 15:32:00 +01:00
|
|
|
|
|
|
|
my $sroot_uuid = $vol_info{$sroot}->{$svol}->{node}->{uuid} || die;
|
2015-04-01 15:06:11 +02:00
|
|
|
foreach my $snapshot (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } (values %{$vol_info{$sroot}}))
|
2015-01-10 16:03:47 +01:00
|
|
|
{
|
2015-04-01 15:06:11 +02:00
|
|
|
next unless($snapshot->{node}->{parent_uuid} eq $sroot_uuid);
|
|
|
|
# next unless($snapshot->{SUBVOL_PATH} =~ /^$snapdir/); # don't print non-btrbk snapshots
|
|
|
|
print "| ^-- $snapshot->{SUBVOL_PATH}\n";
|
2015-01-10 16:03:47 +01:00
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
my $droot = $config_target->{droot} || die;
|
|
|
|
next unless $vol_info{$droot};
|
2015-04-01 15:06:11 +02:00
|
|
|
$droot_compat{$droot} = 1 if($vol_btrfs_progs_compat{$droot});
|
|
|
|
foreach (sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } get_receive_targets($droot, $snapshot)) {
|
2015-04-14 03:24:32 +02:00
|
|
|
print "| | ^== $_->{URL}\n";
|
2014-12-13 16:51:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-03-24 13:13:00 +01:00
|
|
|
if(keys %droot_compat) {
|
2015-04-01 15:06:11 +02:00
|
|
|
print "\nNOTE: Received subvolumes (backups) are guessed by subvolume name for targets:\n";
|
2015-03-24 13:13:00 +01:00
|
|
|
print " - " . join("\n - ", (sort keys %droot_compat));
|
|
|
|
}
|
2014-12-13 16:51:30 +01:00
|
|
|
print "\n";
|
2014-12-13 13:52:43 +01:00
|
|
|
}
|
2015-03-13 17:54:08 +01:00
|
|
|
exit 0;
|
2014-12-13 13:52:43 +01:00
|
|
|
}
|
|
|
|
|
2015-01-03 21:25:46 +01:00
|
|
|
|
2015-02-08 13:47:31 +01:00
|
|
|
if($action_run)
|
2014-12-13 13:52:43 +01:00
|
|
|
{
|
2015-01-04 19:30:41 +01:00
|
|
|
#
|
|
|
|
# create snapshots
|
|
|
|
#
|
|
|
|
my $timestamp = sprintf("%04d%02d%02d", @today);
|
2014-12-14 20:35:15 +01:00
|
|
|
my %snapshot_cache;
|
2015-01-10 16:33:01 +01:00
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
2014-12-13 13:52:43 +01:00
|
|
|
{
|
2015-01-10 16:33:01 +01:00
|
|
|
next if($config_vol->{ABORTED});
|
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
|
|
|
{
|
|
|
|
next if($config_subvol->{ABORTED});
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = $config_subvol->{svol} // die;
|
2015-01-14 17:14:13 +01:00
|
|
|
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
|
2015-01-10 16:33:01 +01:00
|
|
|
my $snapshot;
|
|
|
|
my $snapshot_name;
|
|
|
|
if($snapshot_cache{"$sroot/$svol"})
|
|
|
|
{
|
|
|
|
$snapshot = $snapshot_cache{"$sroot/$svol"}->{file};
|
|
|
|
$snapshot_name = $snapshot_cache{"$sroot/$svol"}->{name};
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2015-04-07 12:57:11 +02:00
|
|
|
# find unique snapshot name
|
|
|
|
my @lookup = keys %{$vol_info{$sroot}};
|
|
|
|
@lookup = grep s/^$snapdir// , @lookup;
|
|
|
|
foreach (@{$config_subvol->{TARGET}}){
|
|
|
|
push(@lookup, keys %{$vol_info{$_->{droot}}});
|
|
|
|
}
|
|
|
|
@lookup = grep /^$svol\.$timestamp(_[0-9]+)?$/ ,@lookup;
|
|
|
|
TRACE "Present snapshot names for \"$sroot/$svol\": " . join(', ', @lookup);
|
|
|
|
@lookup = map { /_([0-9]+)$/ ? $1 : 0 } @lookup;
|
|
|
|
@lookup = sort { $b <=> $a } @lookup;
|
|
|
|
my $postfix_counter = $lookup[0] // -1;
|
|
|
|
$postfix_counter++;
|
|
|
|
|
|
|
|
$snapshot_name = $svol . '.' . $timestamp . ($postfix_counter ? "_$postfix_counter" : "");
|
|
|
|
$snapshot = "$sroot/$snapdir$snapshot_name";
|
2015-01-10 16:33:01 +01:00
|
|
|
}
|
2014-12-13 15:15:58 +01:00
|
|
|
|
2015-01-12 15:46:24 +01:00
|
|
|
my $create_snapshot = config_key($config_subvol, "snapshot_create_always");
|
2015-01-10 16:33:01 +01:00
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
next if($config_target->{ABORTED});
|
|
|
|
my $droot = $config_target->{droot} || die;
|
|
|
|
if(subvol($droot, $snapshot_name)) {
|
2015-03-11 19:19:32 +01:00
|
|
|
$config_target->{ABORTED} = "Subvolume already exists at destination: $droot/$snapshot_name";
|
2015-01-13 17:51:24 +01:00
|
|
|
WARN "Skipping target: $config_target->{ABORTED}";
|
2015-01-10 16:33:01 +01:00
|
|
|
next;
|
|
|
|
}
|
|
|
|
if($config_target->{target_type} eq "send-receive") {
|
|
|
|
$create_snapshot = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
unless($create_snapshot) {
|
2015-01-13 17:51:24 +01:00
|
|
|
$config_subvol->{ABORTED} = "No targets defined for subvolume: $sroot/$svol";
|
|
|
|
WARN "Skipping subvolume section: $config_subvol->{ABORTED}";
|
2015-01-10 16:33:01 +01:00
|
|
|
next;
|
|
|
|
}
|
2014-12-14 20:35:15 +01:00
|
|
|
|
2015-01-10 16:33:01 +01:00
|
|
|
# make snapshot of svol, if not already created by another job
|
|
|
|
unless($snapshot_cache{"$sroot/$svol"})
|
|
|
|
{
|
|
|
|
INFO "Creating subvolume snapshot for: $sroot/$svol";
|
2014-12-13 15:15:58 +01:00
|
|
|
|
2015-01-14 17:14:13 +01:00
|
|
|
unless(btrfs_snapshot("$sroot/$svol", $snapshot, $config_subvol)) {
|
2015-01-13 17:51:24 +01:00
|
|
|
$config_subvol->{ABORTED} = "Failed to create snapshot, skipping subvolume: $sroot/$svol";
|
|
|
|
WARN "Skipping subvolume section: $config_subvol->{ABORTED}";
|
2015-01-10 16:33:01 +01:00
|
|
|
}
|
|
|
|
$snapshot_cache{"$sroot/$svol"} = { name => $snapshot_name,
|
|
|
|
file => $snapshot };
|
2014-12-19 13:31:31 +01:00
|
|
|
}
|
2015-01-10 16:33:01 +01:00
|
|
|
$config_subvol->{snapshot} = $snapshot;
|
|
|
|
$config_subvol->{snapshot_name} = $snapshot_name;
|
2014-12-13 15:15:58 +01:00
|
|
|
}
|
2014-12-13 13:52:43 +01:00
|
|
|
}
|
2014-12-13 15:15:58 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# create backups
|
|
|
|
#
|
2015-01-12 14:04:07 +01:00
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
2014-12-13 13:52:43 +01:00
|
|
|
{
|
2015-01-12 14:04:07 +01:00
|
|
|
next if($config_vol->{ABORTED});
|
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
|
|
|
{
|
|
|
|
next if($config_subvol->{ABORTED});
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = $config_subvol->{svol} // die;
|
2015-01-12 14:04:07 +01:00
|
|
|
my $snapshot = $config_subvol->{snapshot} || die;
|
|
|
|
my $snapshot_name = $config_subvol->{snapshot_name} || die;
|
|
|
|
|
2015-04-02 15:53:53 +02:00
|
|
|
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
|
|
|
|
|
2015-01-12 14:04:07 +01:00
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
next if($config_target->{ABORTED});
|
|
|
|
my $droot = $config_target->{droot} || die;
|
|
|
|
my $target_type = $config_target->{target_type} || die;
|
|
|
|
|
|
|
|
if($target_type eq "send-receive")
|
|
|
|
{
|
2015-03-24 18:44:19 +01:00
|
|
|
if(config_key($config_target, "receive_log")) {
|
|
|
|
WARN "Ignoring deprecated option \"receive_log\" for target: $droot"
|
2015-01-12 14:04:07 +01:00
|
|
|
}
|
2015-01-12 15:46:24 +01:00
|
|
|
|
2015-03-31 19:07:33 +02:00
|
|
|
# resume missing backups (resume_missing)
|
|
|
|
if(config_key($config_target, "resume_missing"))
|
2015-01-12 14:04:07 +01:00
|
|
|
{
|
2015-03-31 20:36:10 +02:00
|
|
|
INFO "Checking for missing backups of subvolume \"$sroot/$svol\" in: $droot/";
|
2015-04-02 16:24:13 +02:00
|
|
|
my @schedule;
|
|
|
|
my $found_missing = 0;
|
2015-04-02 15:53:53 +02:00
|
|
|
|
2015-03-31 21:45:21 +02:00
|
|
|
# sort children of svol ascending by generation
|
2015-04-02 15:53:53 +02:00
|
|
|
foreach my $child (get_snapshot_children($sroot, $svol))
|
2015-03-31 20:36:10 +02:00
|
|
|
{
|
2015-04-01 15:05:27 +02:00
|
|
|
if(scalar get_receive_targets($droot, $child)) {
|
2015-04-14 03:24:32 +02:00
|
|
|
DEBUG "Found matching receive target, skipping: $child->{URL}";
|
2015-03-31 19:07:33 +02:00
|
|
|
}
|
|
|
|
else {
|
2015-04-14 03:24:32 +02:00
|
|
|
DEBUG "No matching receive targets found, adding resume candidate: $child->{URL}";
|
2015-03-31 19:07:33 +02:00
|
|
|
|
|
|
|
# check if the target would be preserved
|
2015-04-02 16:24:13 +02:00
|
|
|
my ($date, $date_ext) = get_date_tag($child->{SUBVOL_PATH});
|
|
|
|
next unless($date && ($child->{SUBVOL_PATH} =~ /^$snapdir$svol\./));
|
|
|
|
push(@schedule, { value => $child, date => $date, date_ext => $date_ext }),
|
2015-04-02 15:53:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(scalar @schedule)
|
|
|
|
{
|
2015-04-02 16:24:13 +02:00
|
|
|
DEBUG "Checking schedule for resume candidates";
|
|
|
|
# add all present backups to schedule, with no value
|
|
|
|
# these are needed for correct results of schedule()
|
2015-04-02 15:53:53 +02:00
|
|
|
foreach my $vol (keys %{$vol_info{$droot}}) {
|
2015-04-02 16:24:13 +02:00
|
|
|
my ($date, $date_ext) = get_date_tag($vol);
|
2015-04-02 15:53:53 +02:00
|
|
|
next unless($date && ($vol =~ s/^$svol\.//)); # use only the date suffix for sorting
|
2015-04-02 16:24:13 +02:00
|
|
|
push(@schedule, { value => undef, date => $date, date_ext => $date_ext });
|
2015-04-02 15:53:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
my ($preserve, undef) = schedule(
|
|
|
|
schedule => \@schedule,
|
|
|
|
today => \@today,
|
|
|
|
preserve_day_of_week => config_key($config_target, "preserve_day_of_week"),
|
|
|
|
preserve_daily => config_key($config_target, "target_preserve_daily"),
|
|
|
|
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
|
|
|
preserve_monthly => config_key($config_target, "target_preserve_monthly"),
|
|
|
|
);
|
2015-04-02 16:24:13 +02:00
|
|
|
my @resume = grep defined, @$preserve; # remove entries with no value from list (target subvolumes)
|
2015-04-02 15:53:53 +02:00
|
|
|
|
|
|
|
foreach my $child (sort { $a->{node}->{gen} <=> $b->{node}->{gen} } @resume) {
|
2015-04-14 03:24:32 +02:00
|
|
|
INFO "Resuming subvolume backup (send-receive) for: $child->{URL}";
|
2015-04-02 16:24:13 +02:00
|
|
|
$found_missing++;
|
|
|
|
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, $child->{node}->{gen});
|
2015-04-02 15:53:53 +02:00
|
|
|
if(macro_send_receive($config_target,
|
2015-04-14 03:24:32 +02:00
|
|
|
src => $child->{URL},
|
2015-04-02 15:53:53 +02:00
|
|
|
target => $droot,
|
2015-04-14 03:24:32 +02:00
|
|
|
parent => $latest_common_src ? $latest_common_src->{URL} : undef,
|
2015-04-02 15:53:53 +02:00
|
|
|
resume => 1, # propagated to $config_target->{subvol_received}
|
|
|
|
))
|
2015-03-31 19:07:33 +02:00
|
|
|
{
|
2015-04-02 15:53:53 +02:00
|
|
|
# tag the source snapshot, so that get_latest_common() above can make use of the newly received subvolume
|
|
|
|
$child->{RECEIVE_TARGET_PRESENT} = $droot;
|
2015-03-31 19:58:24 +02:00
|
|
|
}
|
2015-04-02 15:53:53 +02:00
|
|
|
else {
|
|
|
|
# note: ABORTED flag is already set by macro_send_receive()
|
|
|
|
ERROR("Error while resuming backups, aborting");
|
|
|
|
last;
|
2015-03-31 13:37:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-04-02 16:24:13 +02:00
|
|
|
|
|
|
|
if($found_missing) {
|
|
|
|
INFO "Resumed $found_missing backups";
|
|
|
|
} else {
|
2015-03-31 19:07:33 +02:00
|
|
|
INFO "No missing backups found";
|
2015-01-12 14:04:07 +01:00
|
|
|
}
|
2015-01-12 15:46:24 +01:00
|
|
|
}
|
2015-03-31 13:37:56 +02:00
|
|
|
|
2015-03-31 20:36:10 +02:00
|
|
|
# skip creation if resume_missing failed
|
|
|
|
next if($config_target->{ABORTED});
|
|
|
|
|
2015-04-01 13:25:24 +02:00
|
|
|
# finally receive the previously created snapshot
|
2015-03-31 19:07:33 +02:00
|
|
|
INFO "Creating subvolume backup (send-receive) for: $sroot/$svol";
|
2015-04-01 13:25:24 +02:00
|
|
|
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot);
|
2015-03-31 19:07:33 +02:00
|
|
|
macro_send_receive($config_target,
|
|
|
|
src => $snapshot,
|
|
|
|
target => $droot,
|
2015-04-14 03:24:32 +02:00
|
|
|
parent => $latest_common_src ? $latest_common_src->{URL} : undef,
|
2015-03-31 19:07:33 +02:00
|
|
|
);
|
2015-01-12 14:04:07 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
ERROR "Unknown target type \"$target_type\", skipping: $sroot/$svol";
|
2015-03-31 19:07:33 +02:00
|
|
|
$config_target->{ABORTED} = "Unknown target type \"$target_type\"";
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
2014-12-12 14:05:37 +01:00
|
|
|
}
|
2014-12-12 12:32:04 +01:00
|
|
|
}
|
|
|
|
}
|
2015-01-12 17:56:35 +01:00
|
|
|
|
2015-01-04 21:26:48 +01:00
|
|
|
|
2015-01-04 19:30:41 +01:00
|
|
|
#
|
2015-01-13 12:38:01 +01:00
|
|
|
# remove backups following a preserve daily/weekly/monthly scheme
|
2015-01-04 19:30:41 +01:00
|
|
|
#
|
2015-02-28 12:02:28 +01:00
|
|
|
if($preserve_backups) {
|
|
|
|
INFO "Preserving all backups (option \"-p\" present)";
|
|
|
|
}
|
|
|
|
else
|
2015-01-04 19:30:41 +01:00
|
|
|
{
|
2015-02-28 12:02:28 +01:00
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
2015-01-04 21:26:48 +01:00
|
|
|
{
|
2015-02-28 12:02:28 +01:00
|
|
|
next if($config_vol->{ABORTED});
|
|
|
|
my $sroot = $config_vol->{sroot} || die;
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
2015-01-12 17:56:35 +01:00
|
|
|
{
|
2015-02-28 12:02:28 +01:00
|
|
|
next if($config_subvol->{ABORTED});
|
2015-04-07 11:52:45 +02:00
|
|
|
my $svol = $config_subvol->{svol} // die;
|
2015-02-28 12:02:28 +01:00
|
|
|
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
|
|
|
|
my $target_aborted = 0;
|
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
|
|
|
if($config_target->{ABORTED}) {
|
|
|
|
$target_aborted = 1;
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
my $droot = $config_target->{droot} || die;
|
|
|
|
|
|
|
|
#
|
|
|
|
# delete backups
|
|
|
|
#
|
|
|
|
INFO "Cleaning backups of subvolume \"$sroot/$svol\": $droot/$svol.*";
|
|
|
|
my @schedule;
|
|
|
|
foreach my $vol (keys %{$vol_info{$droot}}) {
|
2015-04-02 16:24:13 +02:00
|
|
|
my ($date, $date_ext) = get_date_tag($vol);
|
2015-04-02 15:53:53 +02:00
|
|
|
next unless($date && ($vol =~ /^$svol\./));
|
2015-04-02 16:24:13 +02:00
|
|
|
push(@schedule, { value => "$droot/$vol", name => $vol, date => $date, date_ext => $date_ext });
|
2015-02-28 12:02:28 +01:00
|
|
|
}
|
2015-04-02 15:53:53 +02:00
|
|
|
my (undef, $delete) = schedule(
|
2015-02-28 12:02:28 +01:00
|
|
|
schedule => \@schedule,
|
|
|
|
today => \@today,
|
|
|
|
preserve_day_of_week => config_key($config_target, "preserve_day_of_week"),
|
|
|
|
preserve_daily => config_key($config_target, "target_preserve_daily"),
|
|
|
|
preserve_weekly => config_key($config_target, "target_preserve_weekly"),
|
|
|
|
preserve_monthly => config_key($config_target, "target_preserve_monthly"),
|
2015-03-31 19:58:24 +02:00
|
|
|
log_verbose => 1,
|
2015-02-28 12:02:28 +01:00
|
|
|
);
|
2015-04-02 15:53:53 +02:00
|
|
|
my $ret = btrfs_subvolume_delete($config_target, @$delete);
|
2015-02-28 12:02:28 +01:00
|
|
|
if(defined($ret)) {
|
|
|
|
INFO "Deleted $ret subvolumes in: $droot/$svol.*";
|
2015-04-02 15:53:53 +02:00
|
|
|
$config_target->{subvol_deleted} = $delete;
|
2015-02-28 12:02:28 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$config_target->{ABORTED} = "btrfs subvolume delete command failed";
|
|
|
|
$target_aborted = 1;
|
|
|
|
}
|
2015-01-16 17:41:57 +01:00
|
|
|
}
|
2015-01-12 17:56:35 +01:00
|
|
|
|
|
|
|
#
|
2015-02-28 12:02:28 +01:00
|
|
|
# delete snapshots
|
2015-01-12 17:56:35 +01:00
|
|
|
#
|
2015-02-28 12:02:28 +01:00
|
|
|
if($target_aborted) {
|
|
|
|
WARN "Skipping cleanup of snapshots for subvolume \"$sroot/$svol\", as at least one target aborted earlier";
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
INFO "Cleaning snapshots: $sroot/$snapdir$svol.*";
|
2015-01-13 17:51:24 +01:00
|
|
|
my @schedule;
|
2015-02-28 12:02:28 +01:00
|
|
|
foreach my $vol (keys %{$vol_info{$sroot}}) {
|
2015-04-02 16:24:13 +02:00
|
|
|
my ($date, $date_ext) = get_date_tag($vol);
|
2015-03-31 16:20:45 +02:00
|
|
|
next unless($date && ($vol =~ /^$snapdir$svol\./));
|
2015-04-02 16:24:13 +02:00
|
|
|
push(@schedule, { value => "$sroot/$vol", name => $vol, date => $date, date_ext => $date_ext });
|
2015-01-12 17:56:35 +01:00
|
|
|
}
|
2015-04-02 15:53:53 +02:00
|
|
|
my (undef, $delete) = schedule(
|
2015-01-13 17:51:24 +01:00
|
|
|
schedule => \@schedule,
|
2015-01-13 12:38:01 +01:00
|
|
|
today => \@today,
|
2015-02-28 12:02:28 +01:00
|
|
|
preserve_day_of_week => config_key($config_subvol, "preserve_day_of_week"),
|
|
|
|
preserve_daily => config_key($config_subvol, "snapshot_preserve_daily"),
|
|
|
|
preserve_weekly => config_key($config_subvol, "snapshot_preserve_weekly"),
|
|
|
|
preserve_monthly => config_key($config_subvol, "snapshot_preserve_monthly"),
|
2015-03-31 19:58:24 +02:00
|
|
|
log_verbose => 1,
|
2015-01-13 12:38:01 +01:00
|
|
|
);
|
2015-04-02 15:53:53 +02:00
|
|
|
my $ret = btrfs_subvolume_delete($config_subvol, @$delete);
|
2015-01-13 18:41:57 +01:00
|
|
|
if(defined($ret)) {
|
2015-02-28 12:02:28 +01:00
|
|
|
INFO "Deleted $ret subvolumes in: $sroot/$snapdir$svol.*";
|
2015-04-02 15:53:53 +02:00
|
|
|
$config_subvol->{subvol_deleted} = $delete;
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
|
|
|
else {
|
2015-02-28 12:02:28 +01:00
|
|
|
$config_subvol->{ABORTED} = "btrfs subvolume delete command failed";
|
2015-01-12 17:56:35 +01:00
|
|
|
}
|
2015-01-04 19:30:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-13 14:38:44 +01:00
|
|
|
|
2015-01-17 14:55:46 +01:00
|
|
|
my $time_elapsed = time - $start_time;
|
2015-01-20 21:07:28 +01:00
|
|
|
INFO "Completed within: ${time_elapsed}s (" . localtime(time) . ")";
|
2015-01-13 17:51:24 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# print summary
|
|
|
|
#
|
|
|
|
unless($quiet)
|
|
|
|
{
|
|
|
|
my $err_count = 0;
|
|
|
|
print "--------------------------------------------------------------------------------\n";
|
2015-01-17 14:55:46 +01:00
|
|
|
print "Backup Summary ($version_info)\n\n";
|
|
|
|
print " Date: " . localtime($start_time) . "\n";
|
|
|
|
print " Config: $config->{SRC_FILE}\n";
|
2015-03-31 13:37:56 +02:00
|
|
|
print "\nLegend:\n";
|
2015-04-01 13:26:10 +02:00
|
|
|
print " +++ created subvolume (source snapshot)\n";
|
2015-04-04 14:55:11 +02:00
|
|
|
print " --- deleted subvolume\n";
|
2015-03-31 13:37:56 +02:00
|
|
|
print " *** received subvolume (non-incremental)\n";
|
|
|
|
print " >>> received subvolume (incremental)\n";
|
2015-04-01 13:26:10 +02:00
|
|
|
# print " %>> received subvolume (incremental, resume_missing)\n";
|
2015-01-13 17:51:24 +01:00
|
|
|
print "--------------------------------------------------------------------------------";
|
|
|
|
foreach my $config_vol (@{$config->{VOLUME}})
|
|
|
|
{
|
|
|
|
if($config_vol->{ABORTED}) {
|
|
|
|
print "!!! $config_vol->{sroot}: ABORTED: $config_vol->{ABORTED}\n";
|
2015-03-01 14:28:26 +01:00
|
|
|
$err_count++ unless($config_vol->{ABORTED_NOERR});
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
|
|
|
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}})
|
|
|
|
{
|
|
|
|
print "\n$config_vol->{sroot}/$config_subvol->{svol}\n";
|
|
|
|
if($config_subvol->{ABORTED}) {
|
|
|
|
print "!!! Subvolume \"$config_subvol->{svol}\" aborted: $config_subvol->{ABORTED}\n";
|
2015-03-01 14:28:26 +01:00
|
|
|
$err_count++ unless($config_subvol->{ABORTED_NOERR});
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
2015-01-17 14:55:46 +01:00
|
|
|
print "+++ $config_subvol->{snapshot}\n" if($config_subvol->{snapshot});
|
2015-01-13 17:51:24 +01:00
|
|
|
if($config_subvol->{subvol_deleted}) {
|
2015-01-17 14:55:46 +01:00
|
|
|
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_subvol->{subvol_deleted}});
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
|
|
|
foreach my $config_target (@{$config_subvol->{TARGET}})
|
|
|
|
{
|
2015-03-31 19:07:33 +02:00
|
|
|
foreach(@{$config_target->{subvol_received} // []}) {
|
2015-03-31 20:36:10 +02:00
|
|
|
my $create_mode = "***";
|
|
|
|
$create_mode = ">>>" if($_->{parent});
|
2015-04-01 13:26:10 +02:00
|
|
|
# substr($create_mode, 0, 1, '%') if($_->{resume});
|
2015-03-31 20:36:10 +02:00
|
|
|
$create_mode = "!!!" if($_->{ERROR});
|
2015-03-31 19:07:33 +02:00
|
|
|
print "$create_mode $_->{received_name}\n";
|
|
|
|
}
|
2015-03-31 13:37:56 +02:00
|
|
|
|
2015-01-13 17:51:24 +01:00
|
|
|
if($config_target->{subvol_deleted}) {
|
2015-01-17 14:55:46 +01:00
|
|
|
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_target->{subvol_deleted}});
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
2015-03-31 19:07:33 +02:00
|
|
|
|
|
|
|
if($config_target->{ABORTED}) {
|
|
|
|
print "!!! Target \"$config_target->{droot}\" aborted: $config_target->{ABORTED}\n";
|
|
|
|
$err_count++ unless($config_target->{ABORTED_NOERR});
|
|
|
|
}
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if($err_count) {
|
|
|
|
print "\nNOTE: Some errors occurred, which may result in missing backups!\n";
|
|
|
|
print "Please check warning and error messages above.\n";
|
|
|
|
}
|
2015-03-01 14:28:26 +01:00
|
|
|
if($preserve_backups) {
|
|
|
|
print "\nNOTE: Preserved all backups (option -p present)\n";
|
|
|
|
}
|
2015-01-16 17:41:57 +01:00
|
|
|
if($dryrun) {
|
2015-01-17 14:55:46 +01:00
|
|
|
print "\nNOTE: Dryrun was active, none of the operations above were actually executed!\n";
|
2015-01-16 17:41:57 +01:00
|
|
|
}
|
2015-01-13 17:51:24 +01:00
|
|
|
}
|
2015-01-04 19:30:41 +01:00
|
|
|
}
|
2014-12-11 18:03:10 +01:00
|
|
|
}
|
2014-12-14 22:45:23 +01:00
|
|
|
|
|
|
|
|
2014-12-11 18:03:10 +01:00
|
|
|
1;
|