diff --git a/ChangeLog b/ChangeLog index 3198002..2067b53 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 diff --git a/btrbk b/btrbk index 23a511f..45229a3 100755 --- a/btrbk +++ b/btrbk @@ -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 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}) { diff --git a/doc/btrbk.conf.5 b/doc/btrbk.conf.5 index 186eb6a..292c207 100644 --- a/doc/btrbk.conf.5 +++ b/doc/btrbk.conf.5 @@ -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 in which the btrfs snapshots are created. Relative to +Directory in which the btrfs snapshots are created, relative to \fI\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 +Base name of the created snapshot (and backup). Defaults to +\fI\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