btrbk: add new time_format "long-iso", with seconds and timezone offset (iso8601 format); add function timestamp(): remove dependency to POSIX

pull/88/head
Axel Burri 2016-04-21 13:27:54 +02:00
parent e824c21f50
commit c13c99ada5
4 changed files with 114 additions and 33 deletions

View File

@ -18,6 +18,7 @@ btrbk-current
* Added "archive" command (close: #79).
* Changed output format of "origin" command, add table formats.
* Added configuration option "rate_limit" (close: #72).
* Added new timestamp_format "long-iso", having a timezone postfix.
* Added "--print-schedule" command line option.
* Detect interrupted transfers of raw targets (close: #75).
* Always read "readonly" flag (additional call to btrfs-progs).

114
btrbk
View File

@ -44,8 +44,7 @@ use warnings FATAL => qw( all );
use Carp qw(confess);
use Getopt::Long qw(GetOptions);
use POSIX qw(strftime);
use Time::Local qw( timelocal timelocal_nocheck timegm_nocheck );
use Time::Local qw( timelocal timegm timegm_nocheck );
our $VERSION = "0.23.0-dev";
our $AUTHOR = 'Axel Burri <axel@tty0.ch>';
@ -63,7 +62,7 @@ my $host_name_match = qr/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*
my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2}))?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm][_NN]"
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
my $raw_postfix_match = qr/--(?<received_uuid>$uuid_match)(\@(?<parent_uuid>$uuid_match))?\.btrfs?(\.(?<compress>(gz|bz2|xz)))?(\.(?<encrypt>gpg))?(\.(?<incomplete>part))?/; # matches ".btrfs_<received_uuid>[@<parent_uuid>][.gz|bz2|xz][.gpg][.part]"
my $group_match = qr/[a-zA-Z0-9_:-]+/;
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
@ -75,7 +74,7 @@ my %config_options = (
# NOTE: the parser always maps "no" to undef
# NOTE: keys "volume", "subvolume" and "target" are hardcoded
# NOTE: files "." and "no" map to <undef>
timestamp_format => { default => "short", accept => [ "short", "long" ], context => [ "root", "volume", "subvolume" ] },
timestamp_format => { default => "short", accept => [ "short", "long", "long-iso" ], context => [ "root", "volume", "subvolume" ] },
snapshot_dir => { default => undef, accept_file => { relative => 1 } },
snapshot_name => { default => undef, accept_file => { name_only => 1 }, context => [ "subvolume" ], deny_glob_context => 1 }, # NOTE: defaults to the subvolume name (hardcoded)
snapshot_create => { default => "always", accept => [ "no", "always", "ondemand", "onchange" ] },
@ -206,7 +205,7 @@ my $tlog_fh;
my $current_transaction;
my @transaction_log;
my %config_override;
my @lt_now; # ( sec, min, hour, mday, mon, year, wday, yday, isdst )
my @tm_now; # current localtime ( sec, min, hour, mday, mon, year, wday, yday, isdst )
BEGIN {
$do_dumper = eval {
@ -351,7 +350,7 @@ sub action($@)
my $time = $h->{time} // time;
$h->{type} = $type;
$h->{time} = $time;
$h->{localtime} = strftime("%FT%T%z", localtime($time));
$h->{localtime} = timestamp($time, 'debug-iso');
print_formatted("transaction", [ $h ], output_format => "tlog", no_header => 1, outfile => $tlog_fh) if($tlog_fh);
push @transaction_log, $h;
return $h;
@ -1531,7 +1530,7 @@ sub add_btrbk_filename_info($;$)
my $name = $node->{REL_PATH};
return undef unless(defined($name));
# NOTE: we assume localtime for now. this might be configurable in the future.
# NOTE: unless long-iso file format is encountered, the timestamp is interpreted in local timezone.
$name =~ s/^(.*)\///;
if($btrbk_raw_file && ($name =~ /^(?<name>$file_match)$timestamp_postfix_match$raw_postfix_match$/)) {
@ -1548,21 +1547,45 @@ sub add_btrbk_filename_info($;$)
return undef;
}
$name = $+{name} // die;
my ( $mm, $hh, $DD, $MM, $YYYY, $NN ) = ( ($+{mm} // 0), ($+{hh} // 0), ($+{DD} // die), ($+{MM} // die), ($+{YYYY} // die), ($+{NN} // 0) );
my @tm = ( ($+{ss} // 0), ($+{mm} // 0), ($+{hh} // 0), $+{DD}, ($+{MM} - 1), ($+{YYYY} - 1900) );
my $NN = $+{NN} // 0;
my $zz = $+{zz};
my $time;
eval {
local $SIG{'__DIE__'};
$time = timelocal( 0, $mm, $hh, $DD, ($MM - 1), ($YYYY - 1900) );
if(defined($zz)) {
$time = timegm(@tm);
} else {
$time = timelocal(@tm);
}
};
if($@) {
WARN "Illegal timestamp on subvolume \"$node->{REL_PATH}\", ignoring";
# WARN "$@"; # sadly Time::Local croaks, which also prints the line number from here.
return undef;
}
# handle ISO 8601 time offset
if(defined($zz)) {
my $offset;
if($zz eq 'Z') {
$offset = 0; # Zulu time == UTC
}
elsif($zz =~ /^([+-])([0-9][0-9])([0-9][0-9])$/) {
$offset = ( $3 * 60 ) + ( $2 * 60 * 60 );
$offset *= -1 if($1 eq '-');
}
else {
WARN "Failed to parse time offset on subvolume \"$node->{REL_PATH}\", ignoring";
return undef;
}
$time -= $offset;
}
$node->{BTRBK_BASENAME} = $name;
$node->{BTRBK_DATE} = [ $time, $NN ];
return 1;
return $node;
}
@ -2810,23 +2833,27 @@ sub schedule(@)
(($a->{informative_only} ? ($b->{informative_only} ? 0 : 1) : ($b->{informative_only} ? -1 : 0)))
} @$schedule;
DEBUG "Scheduler reference time: " . timestamp(\@tm_now, 'debug-iso');
# first, do our calendar calculations
# - weeks start on $preserve_day_of_week
# - leap hours are taken into account for $delta_hours
my $now_h = timelocal_nocheck( 0, 0, $lt_now[2], $lt_now[3], $lt_now[4], $lt_now[5] );
my $now_d = timegm_nocheck( 0, 0, 0, $lt_now[3], $lt_now[4], $lt_now[5] );
# - leap hours are NOT taken into account for $delta_hours
my $now_h = timegm_nocheck( 0, 0, $tm_now[2], $tm_now[3], $tm_now[4], $tm_now[5] ); # use timelocal() here (and below) if you want to honor leap hours
my $now_d = timegm_nocheck( 0, 0, 0, $tm_now[3], $tm_now[4], $tm_now[5] );
foreach my $href (@sorted_schedule)
{
my @tt = localtime($href->{btrbk_date}->[0]);
my $delta_days_from_eow = $tt[6] - $day_of_week_map{$preserve_day_of_week};
my $time = $href->{btrbk_date}->[0];
my @tm = localtime($time);
my $delta_days_from_eow = $tm[6] - $day_of_week_map{$preserve_day_of_week};
$delta_days_from_eow += 7 if($delta_days_from_eow < 0);
my $delta_days = int(($now_d - timegm_nocheck( 0, 0, 0, $tt[3], $tt[4], $tt[5] ) ) / (60 * 60 * 24));
my $delta_hours = int(($now_h - timelocal_nocheck( 0, 0, $tt[2], $tt[3], $tt[4], $tt[5] ) ) / (60 * 60));
# check timegm: ignores leap hours
my $delta_days = int(($now_d - timegm_nocheck( 0, 0, 0, $tm[3], $tm[4], $tm[5] ) ) / (60 * 60 * 24));
my $delta_hours = int(($now_h - timegm_nocheck( 0, 0, $tm[2], $tm[3], $tm[4], $tm[5] ) ) / (60 * 60));
my $delta_weeks = int(($delta_days + $delta_days_from_eow) / 7); # weeks from beginning of week
my $delta_years = ($lt_now[5] - $tt[5]);
my $delta_months = $delta_years * 12 + ($lt_now[4] - $tt[4]);
my $delta_years = ($tm_now[5] - $tm[5]);
my $delta_months = $delta_years * 12 + ($tm_now[4] - $tm[4]);
$href->{delta_hours} = $delta_hours;
$href->{delta_days} = $delta_days;
@ -2835,8 +2862,8 @@ sub schedule(@)
$href->{delta_years} = $delta_years;
# only for text output
my $year = $tt[5] + 1900;
my $year_month = "${year}-" . ($tt[4] < 9 ? '0' : "") . ($tt[4] + 1);
my $year = $tm[5] + 1900;
my $year_month = "${year}-" . ($tm[4] < 9 ? '0' : "") . ($tm[4] + 1);
$href->{year_month} = $year_month;
$href->{year} = $year;
$href->{err_days} = ($delta_days_from_eow ? "+$delta_days_from_eow days after " : "on ") . "$preserve_day_of_week";
@ -2991,6 +3018,42 @@ sub format_preserve_matrix($@)
}
sub timestamp($$;$)
{
my $time = shift // die; # unixtime, or arrayref from localtime()
my $format = shift;
my $tm_is_utc = shift;
my @tm = ref($time) ? @$time : localtime($time);
my $ts;
# NOTE: can't use POSIX::strftime(), as "%z" always prints offset of local timezone!
if($format eq "short") {
return sprintf('%04u%02u%02u', $tm[5] + 1900, $tm[4] + 1, $tm[3]);
}
elsif($format eq "long") {
return sprintf('%04u%02u%02uT%02u%02u', $tm[5] + 1900, $tm[4] + 1, $tm[3], $tm[2], $tm[1]);
}
elsif($format eq "long-iso") {
$ts = sprintf('%04u%02u%02uT%02u%02u%02u', $tm[5] + 1900, $tm[4] + 1, $tm[3], $tm[2], $tm[1], $tm[0]);
}
elsif($format eq "debug-iso") {
$ts = sprintf('%04u-%02u-%02uT%02u:%02u:%02u', $tm[5] + 1900, $tm[4] + 1, $tm[3], $tm[2], $tm[1], $tm[0]);
}
else { die; }
if($tm_is_utc) {
$ts .= '+0000'; # or 'Z'
} else {
my $offset = timegm(@tm) - timelocal(@tm);
if($offset < 0) { $ts .= '-'; $offset = -$offset; } else { $ts .= '+'; }
$ts .= sprintf('%02u%02u', int($offset / (60 * 60)), int($offset / 60) % 60);
}
return $ts;
return undef;
}
sub print_header(@)
{
my %args = @_;
@ -3242,8 +3305,7 @@ MAIN:
Getopt::Long::Configure qw(gnu_getopt);
my $start_time = time;
@lt_now = localtime($start_time);
my @today_and_now = ( ($lt_now[5] + 1900), ($lt_now[4] + 1), $lt_now[3], $lt_now[2], $lt_now[1], $lt_now[0] );
@tm_now = localtime($start_time);
my %config_override_cmdline;
my ($config_cmdline, $quiet, $verbose, $preserve_backups, $resume_only, $print_schedule);
@ -4581,9 +4643,7 @@ MAIN:
}
# find unique snapshot name
my $timestamp = ((config_key($svol, "timestamp_format") eq "short") ?
sprintf("%04d%02d%02d", @today_and_now[0..2]) :
sprintf("%04d%02d%02dT%02d%02d", @today_and_now[0..4]));
my $timestamp = timestamp(\@tm_now, config_key($svol, "timestamp_format"));
my @unconfirmed_target_name;
my @lookup = map { $_->{SUBVOL_PATH} } @{vinfo_subvol_list($sroot)};
@lookup = grep s/^\Q$snapdir_ts\E// , @lookup;

View File

@ -92,8 +92,10 @@ snapshot_dir _btrbk_snap
#
# Example configuration:
#
snapshot_preserve_min 14d
snapshot_preserve_min no
snapshot_preserve_min 2d
snapshot_preserve 14d
target_preserve_min no
target_preserve 20d 10w *m
# Backup to external disk mounted on /mnt/btr_backup

View File

@ -1,4 +1,4 @@
.TH "btrbk.conf" "5" "2016-04-19" "btrbk v0.23.0-dev" ""
.TH "btrbk.conf" "5" "2016-04-22" "btrbk v0.23.0-dev" ""
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
@ -70,7 +70,7 @@ subvolume delete) as well as abort messages are logged to <file>, in a
space-separated table format.
.RE
.PP
\fBtimestamp_format\fR short|long
\fBtimestamp_format\fR short|long|long-iso
.RS 4
Timestamp format used as postfix for new snapshot subvolume
names. Defaults to \[lq]short\[rq].
@ -79,10 +79,16 @@ names. Defaults to \[lq]short\[rq].
YYYYMMDD[_N] (e.g. "20150825", "20150825_1")
.IP \fBlong\fR
YYYYMMDD<T>hhmm[_N] (e.g. "20150825T1531")
.IP \fBlong-iso\fR
YYYYMMDD<T>hhmmss\[t+-]hhmm[_N] (e.g. "20150825T153123+0200")
.PP
Note that a postfix "_N" is only appended to the timestamp if a
snapshot/backup already exists with the timestamp of current
date/time.
.PP
Use \[lq]long-iso\[rq] if you want to make sure that btrbk never
creates ambiguous time stamps (which can happen if multiple snapshots
are created during a daylight saving time clock change).
.RE
.PP
\fBsnapshot_dir\fR <directory>
@ -241,8 +247,8 @@ With the following semantics:
Defines how many hours back hourly backups should be preserved. The
first backup of an hour is considered an hourly backup. Note that if
you use <hourly> scheduling, make sure to also set
\fItimestamp_format\fR to \[lq]long\[rq], or the scheduler will
interpret the time as "00:00" (midnight).
\fItimestamp_format\fR to \[lq]long\[rq] or \[lq]long-iso\[rq], or the
scheduler will interpret the time as "00:00" (midnight).
.RE
.PP
.B daily
@ -276,6 +282,18 @@ backup.
Use an asterisk for \[lq]all\[rq] (e.g. "target_preserve 60d *m"
states: "preserve daily backups for 60 days back, and all monthly
backups").
.PP
The reference time (which defines the beginning of a day, week, month
or year) for all date/time calculations is the local time of the host
running btrbk.
.PP
Caveats:
.IP \[bu] 2
If "timestamp_format long-iso" is set, running btrbk from different
time zones leads to different interpretation of "first in day, week,
month, or year". So with this setup, make sure to run btrbk with the
same time zone on every host (e.g. by setting the TZ environment
variable).
.SH TARGET TYPES
.PP
\fBsend-receive\fR