btrbk: add transaction log (configuration option "transaction_log <file>"

pull/57/head
Axel Burri 2015-10-13 18:24:30 +02:00
parent 282668927d
commit 46c7841e93
4 changed files with 119 additions and 44 deletions

View File

@ -12,6 +12,7 @@ btrbk-current
* Added "target raw", with GnuPG and compression support * Added "target raw", with GnuPG and compression support
(experimental). (experimental).
* Added configuration option "timestamp_format short|long". * Added configuration option "timestamp_format short|long".
* Added transaction log (configuration option "transaction_log").
* Bugfix: correctly handle "incremental no" option. * Bugfix: correctly handle "incremental no" option.
* Bugfix: return exit status 10 instead of 0 if one or more backup * Bugfix: return exit status 10 instead of 0 if one or more backup
tasks aborted. tasks aborted.

152
btrbk
View File

@ -45,6 +45,7 @@ use warnings FATAL => qw( all );
use Carp qw(confess); use Carp qw(confess);
use Date::Calc qw(Today_and_Now Delta_Days Day_of_Week); use Date::Calc qw(Today_and_Now Delta_Days Day_of_Week);
use Getopt::Long qw(GetOptions); use Getopt::Long qw(GetOptions);
use POSIX qw(strftime);
use Data::Dumper; use Data::Dumper;
our $VERSION = "0.21.0-dev"; our $VERSION = "0.21.0-dev";
@ -89,6 +90,7 @@ my %config_options = (
ssh_port => { default => "default", accept => [ "default" ], accept_numeric => 1 }, ssh_port => { default => "default", accept => [ "default" ], accept_numeric => 1 },
ssh_compression => { default => undef, accept => [ "yes", "no" ] }, ssh_compression => { default => undef, accept => [ "yes", "no" ] },
ssh_cipher_spec => { default => "default", accept_regexp => qr/^$ssh_cipher_match(,$ssh_cipher_match)*$/ }, ssh_cipher_spec => { default => "default", accept_regexp => qr/^$ssh_cipher_match(,$ssh_cipher_match)*$/ },
transaction_log => { default => undef, accept_file => { absolute => 1 } },
raw_target_compress => { default => undef, accept => [ "no", "gzip", "bzip2", "xz" ] }, raw_target_compress => { default => undef, accept => [ "no", "gzip", "bzip2", "xz" ] },
raw_target_encrypt => { default => undef, accept => [ "no", "gpg" ] }, raw_target_encrypt => { default => undef, accept => [ "no", "gpg" ] },
@ -146,8 +148,9 @@ my %table_formats = (
}, },
action_log => { table => [ qw( type status target source parent ) ], action_log => { table => [ qw( type status target source parent ) ],
long => [ qw( localtime type status target_host target_path source_host source_path parent_path error_message ) ], long => [ qw( localtime type status duration target_host target_path source_host source_path parent_path message ) ],
raw => [ qw( time localtime type status target_url source_url parent_url error_message ) ], raw => [ qw( time localtime type status duration target_url source_url parent_url message ) ],
tlog => [ qw( localtime type status target_url source_url parent_url message ) ],
}, },
); );
@ -162,7 +165,8 @@ my $dryrun;
my $loglevel = 1; my $loglevel = 1;
my $show_progress = 0; my $show_progress = 0;
my $err = ""; my $err = "";
my $output_format = undef; my $output_format;
my $tlog_fh;
$SIG{__DIE__} = sub { $SIG{__DIE__} = sub {
@ -214,29 +218,57 @@ sub INFO { my $t = shift; print STDERR "$t\n" if($loglevel >= 2); }
sub WARN { my $t = shift; print STDERR "WARNING: $t\n" if($loglevel >= 1); } sub WARN { my $t = shift; print STDERR "WARNING: $t\n" if($loglevel >= 1); }
sub ERROR { my $t = shift; print STDERR "ERROR: $t\n"; } sub ERROR { my $t = shift; print STDERR "ERROR: $t\n"; }
sub action($@)
{
my $type = shift // die;
my %h = @_;
$h{type} = $type;
$h{time} = time;
push @action_log, \%h;
}
sub ABORTED($$) sub ABORTED($$)
{ {
my $config = shift; my $config = shift;
my $t = shift; my $t = shift;
$config->{ABORTED} = $t; $config->{ABORTED} = $t;
unless($t eq "USER_SKIP") { unless($t eq "USER_SKIP") {
$t =~ s/\n/ /g;
action("abort_" . ($config->{CONTEXT} || "undef"), action("abort_" . ($config->{CONTEXT} || "undef"),
status => "ABORT", status => "ABORT",
vinfo_prefixed_keys("target", vinfo($config->{url}, $config)), vinfo_prefixed_keys("target", vinfo($config->{url}, $config)),
error_message => $t, message => $t,
); );
} }
} }
sub init_transaction_log($)
{
my $file = shift // die;
if(open($tlog_fh, ">> $file"))
{
# print headers and startup message
print_formatted("action_log", [ ], output_format => "tlog", outfile => $tlog_fh);
INFO "Using transaction log: $file";
} else {
$tlog_fh = undef;
ERROR "Failed to open transaction log '$file': $!";
}
}
sub close_transaction_log()
{
if($tlog_fh) {
DEBUG "Closing transaction log";
close $tlog_fh || ERROR "Failed to close transaction log: $!";
}
}
sub action($@)
{
my $type = shift // die;
my $h = { @_ };
my $time = time;
$h->{type} = $type;
$h->{time} = $time;
$h->{localtime} = strftime("%FT%T%z", localtime($time));
print_formatted("action_log", [ $h ], output_format => "tlog", no_header => 1, outfile => $tlog_fh);
push @action_log, $h;
}
sub run_cmd(@) sub run_cmd(@)
{ {
my @commands = (ref($_[0]) eq "HASH") ? @_ : { @_ }; my @commands = (ref($_[0]) eq "HASH") ? @_ : { @_ };
@ -1014,11 +1046,13 @@ sub btrfs_subvolume_snapshot($$)
DEBUG "[btrfs] source: $src_path"; DEBUG "[btrfs] source: $src_path";
DEBUG "[btrfs] target: $target_path"; DEBUG "[btrfs] target: $target_path";
INFO ">>> $target_vol->{PRINT}"; INFO ">>> $target_vol->{PRINT}";
my $starttime = time;
my $ret = run_cmd(cmd => [ qw(btrfs subvolume snapshot), '-r', $src_path, $target_path ], my $ret = run_cmd(cmd => [ qw(btrfs subvolume snapshot), '-r', $src_path, $target_path ],
rsh => $svol->{RSH}, rsh => $svol->{RSH},
); );
action("snapshot", action("snapshot",
status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")), status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")),
duration => (time - $starttime),
vinfo_prefixed_keys("target", $target_vol), vinfo_prefixed_keys("target", $target_vol),
vinfo_prefixed_keys("source", $svol), vinfo_prefixed_keys("source", $svol),
); );
@ -1047,11 +1081,13 @@ sub btrfs_subvolume_delete($@)
my @options; my @options;
@options = ("--commit-$commit") if($commit); @options = ("--commit-$commit") if($commit);
my @target_paths = map( { $_->{PATH} } @$targets); my @target_paths = map( { $_->{PATH} } @$targets);
my $starttime = time;
my $ret = run_cmd(cmd => [ qw(btrfs subvolume delete), @options, @target_paths ], my $ret = run_cmd(cmd => [ qw(btrfs subvolume delete), @options, @target_paths ],
rsh => $rsh, rsh => $rsh,
); );
action($opts{type} // "delete", action($opts{type} // "delete",
status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")), status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")),
duration => (time - $starttime),
vinfo_prefixed_keys("target", $_), vinfo_prefixed_keys("target", $_),
) foreach (@$targets); ) foreach (@$targets);
ERROR "Failed to delete btrfs subvolumes: " . join(' ', map( { $_->{PRINT} } @$targets)) unless(defined($ret)); ERROR "Failed to delete btrfs subvolumes: " . join(' ', map( { $_->{PRINT} } @$targets)) unless(defined($ret));
@ -1100,7 +1136,16 @@ sub btrfs_send_receive($$$$)
rsh => $target->{RSH}, rsh => $target->{RSH},
name => "btrfs receive", name => "btrfs receive",
}; };
my $starttime = time;
my $ret = run_cmd(@cmd_pipe); my $ret = run_cmd(@cmd_pipe);
action("send-receive",
status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")),
duration => (time - $starttime),
vinfo_prefixed_keys("target", $vol_received),
vinfo_prefixed_keys("source", $snapshot),
vinfo_prefixed_keys("parent", $parent),
);
unless(defined($ret)) { unless(defined($ret)) {
ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}"; ERROR "Failed to send/receive btrfs subvolume: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $target->{PRINT}";
@ -1194,6 +1239,7 @@ sub btrfs_send_to_file($$$$;@)
DEBUG "[btrfs] parent: $parent->{PRINT}" if($parent); DEBUG "[btrfs] parent: $parent->{PRINT}" if($parent);
DEBUG "[btrfs] target: $target->{PRINT}"; DEBUG "[btrfs] target: $target->{PRINT}";
my $starttime = time;
my $ret = run_cmd(@cmd_pipe); my $ret = run_cmd(@cmd_pipe);
if(defined($ret)) { if(defined($ret)) {
# Test target file for "exists and size > 0" after writing, # Test target file for "exists and size > 0" after writing,
@ -1205,6 +1251,13 @@ sub btrfs_send_to_file($$$$;@)
name => "test", name => "test",
}); });
} }
action("send-to-raw",
status => ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")),
duration => (time - $starttime),
vinfo_prefixed_keys("target", $vol_received),
vinfo_prefixed_keys("source", $snapshot),
vinfo_prefixed_keys("parent", $parent),
);
unless(defined($ret)) { unless(defined($ret)) {
ERROR "Failed to send btrfs subvolume to raw file: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $vol_received->{PRINT}"; ERROR "Failed to send btrfs subvolume to raw file: $snapshot->{PRINT} " . ($parent_path ? "[$parent_path]" : "") . " -> $vol_received->{PRINT}";
return undef; return undef;
@ -1438,12 +1491,6 @@ sub macro_send_receive($@)
$config_target->{SUBVOL_RECEIVED} //= []; $config_target->{SUBVOL_RECEIVED} //= [];
push(@{$config_target->{SUBVOL_RECEIVED}}, \%info); push(@{$config_target->{SUBVOL_RECEIVED}}, \%info);
action("send-receive",
status => ($dryrun ? "DRYRUN" : ($ret ? "success" : "ERROR")),
vinfo_prefixed_keys("target", $vol_received),
vinfo_prefixed_keys("source", $snapshot),
vinfo_prefixed_keys("parent", $parent),
);
unless($ret) { unless($ret) {
$info{ERROR} = 1; $info{ERROR} = 1;
return undef; return undef;
@ -1818,6 +1865,18 @@ sub print_formatted(@)
print join(' ', map { "$_=\"" . ($row->{$_} // "") . "\""; } @$keys) . "\n"; print join(' ', map { "$_=\"" . ($row->{$_} // "") . "\""; } @$keys) . "\n";
} }
} }
elsif($format eq "tlog")
{
# output: value0 value1, ...
if($tlog_fh) {
unless($args{no_header}) {
print $tlog_fh join(' ', @$keys) . "\n";
}
foreach my $row (@$data) {
print $tlog_fh join(' ', map { ($row->{$_} // "-") } @$keys) . "\n";
}
}
}
else else
{ {
# sanitize and calculate maxlen for each column # sanitize and calculate maxlen for each column
@ -2136,6 +2195,15 @@ MAIN:
} }
#
# open transaction log
#
if($action_run && (not $dryrun) && config_key($config, "transaction_log")) {
init_transaction_log(config_key($config, "transaction_log"));
}
action("startup", status => "v$VERSION", message => "$version_info");
# #
# filter subvolumes matching command line arguments # filter subvolumes matching command line arguments
# #
@ -3021,8 +3089,24 @@ MAIN:
} }
} }
my $err_count = 0;
foreach my $config_vol (@{$config->{VOLUME}}) {
if($config_vol->{ABORTED} && ($config_vol->{ABORTED} ne "USER_SKIP")) { $err_count++; next; }
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) {
if($config_subvol->{ABORTED} && ($config_subvol->{ABORTED} ne "USER_SKIP")) { $err_count++; next; }
foreach my $config_target (@{$config_subvol->{TARGET}}) {
if($config_target->{ABORTED} && ($config_target->{ABORTED} ne "USER_SKIP")) { $err_count++; next; }
}
}
}
my $time_elapsed = time - $start_time; my $time_elapsed = time - $start_time;
INFO "Completed within: ${time_elapsed}s (" . localtime(time) . ")"; INFO "Completed within: ${time_elapsed}s (" . localtime(time) . ")";
action("finished",
duration => $time_elapsed,
message => $err_count ? "At least one backup task aborted" : "No errors"
);
close_transaction_log();
unless($quiet) unless($quiet)
@ -3056,8 +3140,6 @@ MAIN:
{ {
my @unrecoverable; my @unrecoverable;
my @out; my @out;
my @raw_data;
my $err_count = 0;
foreach my $config_vol (@{$config->{VOLUME}}) foreach my $config_vol (@{$config->{VOLUME}})
{ {
my $sroot = $config_vol->{sroot} || vinfo($config_vol->{url}, $config_vol); my $sroot = $config_vol->{sroot} || vinfo($config_vol->{url}, $config_vol);
@ -3096,18 +3178,18 @@ MAIN:
if($config_target->{ABORTED} && ($config_target->{ABORTED} ne "USER_SKIP")) { if($config_target->{ABORTED} && ($config_target->{ABORTED} ne "USER_SKIP")) {
push @subvol_out, "!!! Target \"$droot->{PRINT}\" aborted: $config_target->{ABORTED}"; push @subvol_out, "!!! Target \"$droot->{PRINT}\" aborted: $config_target->{ABORTED}";
$err_count++;
} }
push(@unrecoverable, $config_target->{UNRECOVERABLE}) if($config_target->{UNRECOVERABLE}); if($config_target->{UNRECOVERABLE}) {
push(@unrecoverable, $config_target->{UNRECOVERABLE});
}
} }
if($config_vol->{ABORTED} && ($config_vol->{ABORTED} ne "USER_SKIP")) { if($config_vol->{ABORTED} && ($config_vol->{ABORTED} ne "USER_SKIP")) {
# repeat volume errors in subvolume context ($err_count is increased in volume context below) # repeat volume errors in subvolume context
push @subvol_out, "!!! Volume \"$sroot->{PRINT}\" aborted: $config_vol->{ABORTED}"; push @subvol_out, "!!! Volume \"$sroot->{PRINT}\" aborted: $config_vol->{ABORTED}";
} }
if($config_subvol->{ABORTED} && ($config_subvol->{ABORTED} ne "USER_SKIP")) { if($config_subvol->{ABORTED} && ($config_subvol->{ABORTED} ne "USER_SKIP")) {
push @subvol_out, "!!! Aborted: $config_subvol->{ABORTED}"; push @subvol_out, "!!! Aborted: $config_subvol->{ABORTED}";
$err_count++;
} }
if(@subvol_out) { if(@subvol_out) {
@ -3120,15 +3202,6 @@ MAIN:
push @out, "$svol->{PRINT}", "<no_action>", ""; push @out, "$svol->{PRINT}", "<no_action>", "";
} }
} }
if($config_vol->{ABORTED} && ($config_vol->{ABORTED} ne "USER_SKIP")) {
push @raw_data, { type => "btrbk_volume",
status => "ABORT",
target_url => $sroot->{URL},
error_message => $config_vol->{ABORTED},
SORT => 1,
};
$err_count++;
}
} }
print_header(title => "Backup Summary", print_header(title => "Backup Summary",
@ -3164,20 +3237,11 @@ MAIN:
else else
{ {
# print action log # print action log
@action_log = map { $_->{localtime} = localtime($_->{time}); $_; } @action_log;
print_formatted("action_log", \@action_log, title => "BACKUP SUMMARY"); print_formatted("action_log", \@action_log, title => "BACKUP SUMMARY");
} }
} }
foreach my $config_vol (@{$config->{VOLUME}}) { exit 10 if($err_count);
exit 10 if($config_vol->{ABORTED} && ($config_vol->{ABORTED} ne "USER_SKIP"));
foreach my $config_subvol (@{$config_vol->{SUBVOLUME}}) {
exit 10 if($config_subvol->{ABORTED} && ($config_subvol->{ABORTED} ne "USER_SKIP"));
foreach my $config_target (@{$config_subvol->{TARGET}}) {
exit 10 if($config_target->{ABORTED} && ($config_target->{ABORTED} ne "USER_SKIP"));
}
}
}
} }
} }

View File

@ -64,6 +64,9 @@ snapshot_dir _btrbk_snap
# Set this either globally or in a specific "target" section. # Set this either globally or in a specific "target" section.
#btrfs_progs_compat no #btrfs_progs_compat no
# Enable transaction log
#transaction_log /var/log/btrbk_transaction.log
# #
# Volume section: "volume <volume-directory>" # Volume section: "volume <volume-directory>"

View File

@ -258,6 +258,13 @@ can lead to false guesses if the snapshot or target subvolumes are
manipulated by hand (moved, deleted). manipulated by hand (moved, deleted).
.RE .RE
.PP .PP
\fBtransaction_log\fR <file>
.RS 4
If set, all transactions (snapshot create, subvolume send-receive,
subvolume delete) as well as abort messages are logged to <file>, in a
space-separated table format.
.RE
.PP
Lines that contain a hash character (#) in the first column are Lines that contain a hash character (#) in the first column are
treated as comments. treated as comments.
.SH AVAILABILITY .SH AVAILABILITY