btrbk: added "snapshot_name" configuration option

pull/30/head
Axel Burri 2015-04-18 20:18:11 +02:00
parent 0068e078f2
commit 3413425ed9
3 changed files with 111 additions and 85 deletions

View File

@ -1,12 +1,12 @@
btrbk-current
* Bugfix: allow "0" as subvolume name (closes: #10)
* Added configuration option "snapshot_name" (closes: #5).
* Bugfix: allow "0" as subvolume name (closes: #10).
* Bugfix: check source AND targets for determining snapshot postfix
(closes: #11)
(closes: #11).
btrbk-0.16
* Bugfix: correctly check retention policy for missing backups
* Bugfix: correctly check retention policy for missing backups.
btrbk-0.15

173
btrbk
View File

@ -60,7 +60,8 @@ my %day_of_week_map = ( monday => 1, tuesday => 2, wednesday => 3, thursday => 4
my %config_options = (
# NOTE: the parser always maps "no" to undef
# NOTE: keys "volume", "subvolume" and "target" are hardcoded
snapshot_dir => { default => undef, accept_file => { relative => 1 }, append_trailing_slash => 1 },
snapshot_dir => { default => undef, accept_file => { relative => 1 } },
snapshot_name => { default => undef, accept_file => { name_only => 1 }, context => [ "subvolume" ] },
receive_log => { default => undef, accept => [ "sidecar", "no" ], accept_file => { absolute => 1 }, deprecated => "removed" },
incremental => { default => "yes", accept => [ "yes", "no", "strict" ] },
snapshot_create_always => { default => undef, accept => [ "yes", "no" ] },
@ -187,7 +188,12 @@ sub vinfo($;$)
die unless($config);
my %info = ( URL => $url );
my $name = $url;
$name =~ s/^.*\///;
my %info = (
URL => $url,
NAME => $name,
);
if($url =~ /^ssh:\/\/(\S+?)(\/\S+)$/) {
my ($host, $path) = ($1, $2);
@ -256,6 +262,14 @@ sub vinfo_read_detail($)
}
$vol->{$_} = $detail->{$_};
}
if($vol->{RSH_TYPE} && ($vol->{RSH_TYPE} eq "ssh")) {
$vol->{REAL_URL} = "ssh://$vol->{HOST}$vol->{REAL_PATH}";
} else {
$vol->{REAL_URL} = $vol->{REAL_PATH};
}
DEBUG "vinfo updated for: $vol->{URL}";
TRACE(Data::Dumper->Dump([$vol], ["vinfo{$vol->{URL}}"]));
@ -325,6 +339,12 @@ sub check_file($$;$$)
return undef;
}
}
elsif($accept->{name_only}) {
if($file =~ /\//) {
ERROR "Option \"$key\" is not a valid file name in \"$config_file\" line $.: $file";
return undef;
}
}
else {
die("accept_type must contain either 'relative' or 'absolute'");
}
@ -395,7 +415,7 @@ sub parse_config(@)
{
while($cur->{CONTEXT} ne "volume") {
if(($cur->{CONTEXT} eq "root") || (not $cur->{PARENT})) {
ERROR "subvolume keyword outside volume context, in \"$file\" line $.";
ERROR "Subvolume keyword outside volume context, in \"$file\" line $.";
return undef;
}
$cur = $cur->{PARENT} || die;
@ -406,7 +426,7 @@ sub parse_config(@)
$value =~ s/\/+$//; # remove trailing slash
$value =~ s/^\/+//; # remove leading slash
if($value =~ /\//) {
ERROR "subvolume contains slashes: \"$value\" in \"$file\" line $.";
ERROR "Subvolume contains slashes: \"$value\" in \"$file\" line $.";
return undef;
}
@ -427,14 +447,14 @@ sub parse_config(@)
DEBUG "config: context changed to: $cur->{CONTEXT}";
}
if($cur->{CONTEXT} ne "subvolume") {
ERROR "target keyword outside subvolume context, in \"$file\" line $.";
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 $.";
ERROR "Unknown target type \"$target_type\" in \"$file\" line $.";
return undef;
}
# be very strict about file options, for security sake
@ -474,10 +494,6 @@ sub parse_config(@)
TRACE "option \"$key=$value\" is a valid file, accepted";
$value =~ s/\/+$//; # remove trailing slash
$value =~ s/^\/+/\//; # sanitize leading slash
if($config_options{$key}->{append_trailing_slash}) {
TRACE "append_trailing_slash is specified for option \"$key\", adding trailing slash";
$value .= '/';
}
}
elsif($config_options{$key}->{accept_regexp}) {
my $match = $config_options{$key}->{accept_regexp};
@ -494,6 +510,12 @@ sub parse_config(@)
ERROR "Unsupported value \"$value\" for option \"$key\" in \"$file\" line $.";
return undef;
}
if($config_options{$key}->{context} && !grep(/^$cur->{CONTEXT}$/, @{$config_options{$key}->{context}})) {
ERROR "Option \"$key\" is only allowed in " . join(" or ", map("\"$_\"", @{$config_options{$key}->{context}})) . " context, in \"$file\" line $.";
return undef;
}
DEBUG "config: adding option \"$key=$value\" to $cur->{CONTEXT} context";
$value = undef if($value eq "no"); # we don't want to check for "no" all the time
$cur->{$key} = $value;
@ -918,7 +940,6 @@ sub btrfs_send_receive($$$)
my $target_path = $target->{PATH} // die;
my $target_rsh = $target->{RSH} || "";
my $parent_path = $parent ? $parent->{PATH} : undef;
my $now = localtime;
my $snapshot_name = $snapshot_path;
$snapshot_name =~ s/^.*\///;
@ -1504,6 +1525,7 @@ MAIN:
#
# fill vol_info hash, basic checks on configuration
#
my %snapshot_check;
foreach my $config_vol (@{$config->{VOLUME}})
{
next if($config_vol->{ABORTED});
@ -1518,6 +1540,19 @@ MAIN:
{
next if($config_subvol->{ABORTED});
my $svol = vinfo($config_subvol->{url}, $config_vol);
# check for duplicate snapshot locations
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
my $snapshot_basename = config_key($config_subvol, "snapshot_name") // $svol->{NAME} // die;
my $snapshot_target = "$sroot->{REAL_URL}/$snapdir/$snapshot_basename";
if(my $prev = $snapshot_check{$snapshot_target}) {
ERROR "Subvolume \"$prev\" and \"$svol->{PRINT}\" will create same snapshot: $snapshot_target";
ERROR "Please fix \"snapshot_name\" configuration options!";
exit 1;
}
$snapshot_check{$snapshot_target} = $svol->{PRINT};
# read subvolume detail
unless(vinfo_read_detail($svol)) {
$config_subvol->{ABORTED} = "Failed to fetch subvolume detail";
WARN "Skipping subvolume \"$svol->{URL}\": $config_subvol->{ABORTED}";
@ -1529,7 +1564,7 @@ MAIN:
next;
}
unless(subvol($sroot, $config_subvol->{rel_path})) { # !!! TODO: maybe check uuid here?
unless(subvol($sroot, $config_subvol->{rel_path})) { # !!! TODO: check uuid here!
$config_subvol->{ABORTED} = "Subvolume \"$svol->{URL}\" not present in btrfs subvolume list for \"$sroot->{URL}\"";
WARN "Skipping subvolume \"$svol->{URL}\": $config_subvol->{ABORTED}";
next;
@ -1543,6 +1578,15 @@ MAIN:
next;
}
# check for duplicate snapshot locations
my $snapshot_backup_target = "$droot->{REAL_URL}/$snapshot_basename";
if(my $prev = $snapshot_check{$snapshot_backup_target}) {
ERROR "Subvolume \"$prev\" and \"$svol->{PRINT}\" will create same snapshot: $snapshot_target";
ERROR "Please fix \"snapshot_name\" configuration options!";
exit 1;
}
$snapshot_check{$snapshot_backup_target} = $svol->{PRINT};
unless(vinfo_read_subvolumes($droot)) {
$config_target->{ABORTED} = "Failed to fetch subvolume list";
WARN "Skipping target \"$droot->{URL}\": $config_target->{ABORTED}";
@ -1653,7 +1697,6 @@ MAIN:
# create snapshots
#
my $timestamp = sprintf("%04d%02d%02d", @today);
my %snapshot_cache;
foreach my $config_vol (@{$config->{VOLUME}})
{
next if($config_vol->{ABORTED});
@ -1663,44 +1706,13 @@ MAIN:
next if($config_subvol->{ABORTED});
my $svol = vinfo($config_subvol->{url});
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
my $snapshot_name;
my $snapshot_basename = $config_subvol->{rel_path}; # !!! TODO: add configuration option for this
if($svol->{SNAPSHOT}) # !!! TODO: guess we broke this, rethink what happens if same svol is used on different config lines
{
$snapshot_name = $svol->{SNAPSHOT}->{NAME};
}
else
{
# find unique snapshot name
my @lookup = keys %{$sroot->{SUBVOL_INFO}};
@lookup = grep s/^$snapdir// , @lookup;
foreach my $config_target (@{$config_subvol->{TARGET}}) {
my $droot = vinfo($config_target->{url});
push(@lookup, keys %{$droot->{SUBVOL_INFO}});
}
@lookup = grep /^\Q$snapshot_basename.$timestamp\E(_[0-9]+)?$/ ,@lookup;
TRACE "Present snapshot names for \"$svol->{URL}\": " . join(', ', @lookup);
@lookup = map { /_([0-9]+)$/ ? $1 : 0 } @lookup;
@lookup = sort { $b <=> $a } @lookup;
my $postfix_counter = $lookup[0] // -1;
$postfix_counter++;
$snapshot_name = $snapshot_basename . '.' . $timestamp . ($postfix_counter ? "_$postfix_counter" : "");
}
my $snapshot_basename = config_key($config_subvol, "snapshot_name") // $svol->{NAME} // die;
# check if we need to create a snapshot
my $create_snapshot = config_key($config_subvol, "snapshot_create_always");
foreach my $config_target (@{$config_subvol->{TARGET}})
{
foreach my $config_target (@{$config_subvol->{TARGET}}) {
next if($config_target->{ABORTED});
my $droot = vinfo($config_target->{url});
if(subvol($droot, $snapshot_name)) {
$config_target->{ABORTED} = "Subvolume already exists at destination: $droot->{URL}/$snapshot_name";
WARN "Skipping target: $config_target->{ABORTED}";
next;
}
if($config_target->{target_type} eq "send-receive") {
$create_snapshot = 1;
}
$create_snapshot = 1 if($config_target->{target_type} eq "send-receive");
}
unless($create_snapshot) {
$config_subvol->{ABORTED} = "No targets defined for subvolume: $svol->{URL}";
@ -1708,20 +1720,29 @@ MAIN:
next;
}
# make snapshot of svol, if not already created by another job
unless($svol->{SNAPSHOT})
{
INFO "Creating subvolume snapshot for: $svol->{PRINT}";
if(btrfs_snapshot($svol, "$sroot->{PATH}/$snapdir/$snapshot_name")) {
my $snapvol = vinfo("$sroot->{URL}/$snapdir/$snapshot_name", $config_vol);
$snapvol->{SNAP_BASENAME} = $snapshot_basename;
$svol->{SNAPSHOT} = $snapvol;
}
else {
$config_subvol->{ABORTED} = "Failed to create snapshot: $svol->{PRINT} -> $sroot->{PRINT}/$snapdir/$snapshot_name";
WARN "Skipping subvolume section: $config_subvol->{ABORTED}";
$svol->{SNAPSHOT} = { ERROR => $config_subvol->{ABORTED} };
}
# find unique snapshot name
my @lookup = keys %{$sroot->{SUBVOL_INFO}};
@lookup = grep s/^\Q$snapdir\E\/// , @lookup;
foreach my $config_target (@{$config_subvol->{TARGET}}) {
my $droot = vinfo($config_target->{url});
push(@lookup, keys %{$droot->{SUBVOL_INFO}});
}
@lookup = grep /^\Q$snapshot_basename.$timestamp\E(_[0-9]+)?$/ ,@lookup;
TRACE "Present snapshot names for \"$svol->{URL}\": " . join(', ', @lookup);
@lookup = map { /_([0-9]+)$/ ? $1 : 0 } @lookup;
@lookup = sort { $b <=> $a } @lookup;
my $postfix_counter = $lookup[0] // -1;
$postfix_counter++;
my $snapshot_name = $snapshot_basename . '.' . $timestamp . ($postfix_counter ? "_$postfix_counter" : "");
# finally create the snapshot
INFO "Creating subvolume snapshot for: $svol->{PRINT}";
if(btrfs_snapshot($svol, "$sroot->{PATH}/$snapdir/$snapshot_name")) {
$config_subvol->{SNAPSHOT} = vinfo("$sroot->{URL}/$snapdir/$snapshot_name", $config_vol);
}
else {
$config_subvol->{ABORTED} = "Failed to create snapshot: $svol->{PRINT} -> $sroot->{PRINT}/$snapdir/$snapshot_name";
WARN "Skipping subvolume section: $config_subvol->{ABORTED}";
}
}
}
@ -1738,7 +1759,7 @@ MAIN:
next if($config_subvol->{ABORTED});
my $svol = vinfo($config_subvol->{url});
my $snapdir = config_key($config_subvol, "snapshot_dir") || "";
my $snapshot_basename = $config_subvol->{rel_path}; # TODO: add configuration option for this, store into svol
my $snapshot_basename = config_key($config_subvol, "snapshot_name") // $svol->{NAME} // die;
foreach my $config_target (@{$config_subvol->{TARGET}})
{
@ -1770,7 +1791,7 @@ MAIN:
# check if the target would be preserved
my ($date, $date_ext) = get_date_tag($child->{SUBVOL_PATH});
next unless($date && ($child->{SUBVOL_PATH} =~ /^\Q$snapdir$snapshot_basename.\E/));
next unless($date && ($child->{SUBVOL_PATH} =~ /^\Q$snapdir\/$snapshot_basename.\E/));
push(@schedule, { value => $child, date => $date, date_ext => $date_ext }),
}
}
@ -1828,13 +1849,13 @@ MAIN:
# skip creation if resume_missing failed
next if($config_target->{ABORTED});
die unless($svol->{SNAPSHOT});
die unless($config_subvol->{SNAPSHOT});
# finally receive the previously created snapshot
INFO "Creating subvolume backup (send-receive) for: $svol->{URL}";
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot);
macro_send_receive($config_target,
snapshot => $svol->{SNAPSHOT},
snapshot => $config_subvol->{SNAPSHOT},
target => $droot,
parent => $latest_common_src, # this is <undef> if no common found
);
@ -1897,7 +1918,7 @@ MAIN:
my $ret = btrfs_subvolume_delete($config_target, @$delete);
if(defined($ret)) {
INFO "Deleted $ret subvolumes in: $droot->{URL}/$snapshot_basename.*";
$config_target->{subvol_deleted} = $delete;
$config_target->{SUBVOL_DELETED} = $delete;
}
else {
$config_target->{ABORTED} = "btrfs subvolume delete command failed";
@ -1912,11 +1933,11 @@ MAIN:
WARN "Skipping cleanup of snapshots for subvolume \"$svol->{URL}\", as at least one target aborted earlier";
next;
}
INFO "Cleaning snapshots: $sroot->{URL}/$snapdir$snapshot_basename.*";
INFO "Cleaning snapshots: $sroot->{URL}/$snapdir/$snapshot_basename.*";
my @schedule;
foreach my $vol (keys %{$sroot->{SUBVOL_INFO}}) {
my ($date, $date_ext) = get_date_tag($vol);
next unless($date && ($vol =~ /^\Q$snapdir$snapshot_basename.\E/));
next unless($date && ($vol =~ /^\Q$snapdir\/$snapshot_basename.\E/));
push(@schedule, { value => "$sroot->{URL}/$vol", name => $vol, date => $date, date_ext => $date_ext });
}
my (undef, $delete) = schedule(
@ -1930,8 +1951,8 @@ MAIN:
);
my $ret = btrfs_subvolume_delete($config_subvol, @$delete);
if(defined($ret)) {
INFO "Deleted $ret subvolumes in: $sroot->{URL}/$snapdir$snapshot_basename.*";
$config_subvol->{subvol_deleted} = $delete;
INFO "Deleted $ret subvolumes in: $sroot->{URL}/$snapdir/$snapshot_basename.*";
$config_subvol->{SUBVOL_DELETED} = $delete;
}
else {
$config_subvol->{ABORTED} = "btrfs subvolume delete command failed";
@ -1974,9 +1995,9 @@ MAIN:
print "!!! Subvolume \"$config_subvol->{rel_path}\" aborted: $config_subvol->{ABORTED}\n";
$err_count++ unless($config_subvol->{ABORTED_NOERR});
}
print "+++ $svol->{SNAPSHOT}->{PRINT}\n" if($svol->{SNAPSHOT}->{PRINT});
if($config_subvol->{subvol_deleted}) {
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_subvol->{subvol_deleted}});
print "+++ $config_subvol->{SNAPSHOT}->{PRINT}\n" if($config_subvol->{SNAPSHOT});
if($config_subvol->{SUBVOL_DELETED}) {
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_subvol->{SUBVOL_DELETED}});
}
foreach my $config_target (@{$config_subvol->{TARGET}})
{
@ -1988,8 +2009,8 @@ MAIN:
print "$create_mode $_->{received_name}\n";
}
if($config_target->{subvol_deleted}) {
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_target->{subvol_deleted}});
if($config_target->{SUBVOL_DELETED}) {
print "--- $_\n" foreach(sort { $b cmp $a} @{$config_target->{SUBVOL_DELETED}});
}
if($config_target->{ABORTED}) {

View File

@ -13,7 +13,7 @@ generated. The retention policy as well as other options can be
defined for each backup.
.PP
The options specified always apply to the last section encountered,
overriding the same option of the next higher section. This means that
superseding the values set in upper-level sections. This means that
global options must be set before any sections are defined.
.PP
The sections are:
@ -44,14 +44,15 @@ allowed.
The configuration options are:
.TP
\fBsnapshot_dir\fR <directory>
Directory in which the btrfs snapshots are created. Relative to
Directory in which the btrfs snapshots are created, relative to
\fI<volume-directory>\fR of the \fIvolume\fR section. Note that btrbk
does not autmatically create this directory, and the snapshot creation
will fail if it is not present.
.TP
\fBincremental\fR yes|no|strict
Perform incremental backups. Defaults to \(lqyes\(rq. If set to
\(lqstrict\(rq, non-incremental (initial) backups are never created.
\fBsnapshot_name\fR <basename>
Base name of the created snapshot (and backup). Defaults to
\fI<subvolume-name>\fR. This option is only valid in the \fItarget\fR
section.
.TP
\fBsnapshot_create_always\fR yes|no
If set, the snapshots are always created, even if the backup subvolume
@ -62,6 +63,10 @@ is reachable again. Useful for laptop filesystems in order to make
sure the snapshots are created even if you are on the road. Defaults
to \(lqno\(rq.
.TP
\fBincremental\fR yes|no|strict
Perform incremental backups. Defaults to \(lqyes\(rq. If set to
\(lqstrict\(rq, non-incremental (initial) backups are never created.
.TP
\fBresume_missing\fR yes|no
If set, the backups in the target directory are compared to the source
snapshots, and missing backups are created if needed (complying to the