2017-09-25 16:05:42 +02:00
#!/usr/bin/perl
2017-08-28 20:33:00 +02:00
#
# btrbk - Create snapshots and remote backups of btrfs subvolumes
#
2023-04-10 16:03:32 +02:00
# Copyright (C) 2014-2023 Axel Burri
2017-08-28 20:33:00 +02: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/>.
#
# ---------------------------------------------------------------------
# The official btrbk website is located at:
2017-10-11 18:14:26 +02:00
# https://digint.ch/btrbk/
2017-08-28 20:33:00 +02:00
#
# Author:
# Axel Burri <axel@tty0.ch>
# ---------------------------------------------------------------------
2014-12-11 18:03:10 +01:00
use strict;
2017-08-29 16:42:09 +02:00
use warnings FATAL => qw( all ), NONFATAL => qw( deprecated );
2014-12-11 18:03:10 +01:00
2015-01-13 14:38:44 +01:00
use Carp qw(confess);
2015-08-15 17:51:00 +02:00
use Getopt::Long qw(GetOptions);
2016-04-21 13:27:54 +02:00
use Time::Local qw( timelocal timegm timegm_nocheck );
2019-08-18 11:46:31 +02:00
use IPC::Open3 qw(open3);
use Symbol qw(gensym);
2022-02-26 18:09:42 +01:00
use Cwd qw(abs_path);
2014-12-11 18:03:10 +01:00
2023-03-25 18:30:48 +01:00
our $VERSION = '0.33.0-dev';
2016-04-15 01:22:19 +02:00
our $AUTHOR = 'Axel Burri <axel@tty0.ch>';
2017-10-11 18:14:26 +02:00
our $PROJECT_HOME = '<https://digint.ch/btrbk/>';
2016-04-15 01:22:19 +02:00
2018-12-25 22:12:23 +01:00
our $BTRFS_PROGS_MIN = '4.12'; # required since btrbk-v0.27.0
2016-04-15 01:22:19 +02:00
2016-05-03 13:19:42 +02: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
2016-08-18 14:02:51 +02:00
my %compression = (
2016-08-18 17:41:26 +02:00
# NOTE: also adapt "compress_list" in ssh_filter_btrbk.sh if you change this
2016-08-18 14:02:51 +02:00
gzip => { name => 'gzip', format => 'gz', compress_cmd => [ 'gzip', '-c' ], decompress_cmd => [ 'gzip', '-d', '-c' ], level_min => 1, level_max => 9 },
pigz => { name => 'pigz', format => 'gz', compress_cmd => [ 'pigz', '-c' ], decompress_cmd => [ 'pigz', '-d', '-c' ], level_min => 1, level_max => 9, threads => '-p' },
bzip2 => { name => 'bzip2', format => 'bz2', compress_cmd => [ 'bzip2', '-c' ], decompress_cmd => [ 'bzip2', '-d', '-c' ], level_min => 1, level_max => 9 },
pbzip2 => { name => 'pbzip2', format => 'bz2', compress_cmd => [ 'pbzip2', '-c' ], decompress_cmd => [ 'pbzip2', '-d', '-c' ], level_min => 1, level_max => 9, threads => '-p' },
2022-11-15 03:20:24 +01:00
bzip3 => { name => 'bzip3', format => 'bz3', compress_cmd => [ 'bzip3', '-c' ], decompress_cmd => [ 'bzip3', '-d', '-c' ], threads => '-j' },
2016-08-18 14:02:51 +02:00
xz => { name => 'xz', format => 'xz', compress_cmd => [ 'xz', '-c' ], decompress_cmd => [ 'xz', '-d', '-c' ], level_min => 0, level_max => 9, threads => '-T' },
lzo => { name => 'lzo', format => 'lzo', compress_cmd => [ 'lzop', '-c' ], decompress_cmd => [ 'lzop', '-d', '-c' ], level_min => 1, level_max => 9 },
2016-08-18 14:09:08 +02:00
lz4 => { name => 'lz4', format => 'lz4', compress_cmd => [ 'lz4', '-c' ], decompress_cmd => [ 'lz4', '-d', '-c' ], level_min => 1, level_max => 9 },
2021-06-07 07:03:47 +02:00
zstd => { name => 'zstd', format => 'zst', compress_cmd => [ 'zstd', '-c' ], decompress_cmd => [ 'zstd', '-d', '-c' ], level_min => 1, level_max => 19, threads => '-T', long => '--long=', adapt => '--adapt' },
2016-08-18 14:02:51 +02:00
);
my $compress_format_alt = join '|', map { $_->{format} } values %compression; # note: this contains duplicate alternations
2020-08-02 19:07:55 +02:00
my $ipv4_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 $ipv6_addr_match = qr/[a-fA-F0-9]*:[a-fA-F0-9]*:[a-fA-F0-9:]+/; # simplified (contains at least two colons), matches "::1", "2001:db8::7"
2015-09-02 11:04:22 +02:00
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-09-29 14:07:58 +02:00
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
2019-08-05 14:59:22 +02:00
my $btrbk_timestamp_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]"
2022-11-16 00:34:58 +01:00
my $raw_postfix_match = qr/\.btrfs(\.($compress_format_alt))?(\.(gpg|encrypted))?/; # matches ".btrfs[.gz|.bz2|.xz|...][.gpg|.encrypted]"
2021-11-06 16:10:26 +01:00
my $safe_file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
2016-08-18 14:02:51 +02:00
2015-09-02 11:04:22 +02:00
my $group_match = qr/[a-zA-Z0-9_:-]+/;
2022-05-28 21:12:39 +02:00
my $config_split_match = qr/\s*[,\s]\s*/;
2015-09-02 11:04:22 +02:00
2016-04-20 22:45:11 +02:00
my %day_of_week_map = ( sunday => 0, monday => 1, tuesday => 2, wednesday => 3, thursday => 4, friday => 5, saturday => 6 );
2016-04-25 18:36:15 +02:00
my @syslog_facilities = qw( user mail daemon auth lpr news cron authpriv local0 local1 local2 local3 local4 local5 local6 local7 );
2015-01-13 12:38:01 +01:00
2021-08-19 16:20:16 +02:00
my @incremental_prefs_avail = qw(sro srn sao san aro arn);
2021-08-27 16:44:05 +02:00
my @incremental_prefs_default = qw(sro:1 srn:1 sao:1 san:1 aro:1 arn:1);
2021-08-19 16:20:16 +02:00
my $incremental_prefs_match = "(defaults|(" . join("|", @incremental_prefs_avail) . ")(:[0-9]+)?)";
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
2015-10-23 21:28:58 +02:00
# NOTE: files "." and "no" map to <undef>
2022-05-28 21:26:09 +02:00
timestamp_format => { default => "long", accept => [qw( short long long-iso )], context => [qw( global volume subvolume )] },
snapshot_dir => { default => undef, accept_file => { relative => 1, absolute => 1 }, context => [qw( global volume subvolume )] },
snapshot_name => { c_default => 1, accept_file => { name_only => 1 }, context => [qw( subvolume )], deny_glob_context => 1 }, # NOTE: defaults to the subvolume name (hardcoded)
snapshot_create => { default => "always", accept => [qw( no always ondemand onchange )], context => [qw( global volume subvolume )] },
incremental => { default => "yes", accept => [qw( yes no strict )] },
2022-05-28 21:12:39 +02:00
incremental_prefs => { default => \@incremental_prefs_default, accept => [ qr/$incremental_prefs_match/ ], split => 1 },
2022-05-28 21:26:09 +02:00
incremental_clones => { default => "yes", accept => [qw( yes no )] },
incremental_resolve => { default => "mountpoint", accept => [qw( mountpoint directory _all_accessible )] },
preserve_day_of_week => { default => "sunday", accept => [ (keys %day_of_week_map) ] },
preserve_hour_of_day => { default => 0, accept => [ (0..23) ] },
snapshot_preserve => { default => undef, accept => [qw( no )], accept_preserve_matrix => 1, context => [qw( global volume subvolume )], },
snapshot_preserve_min => { default => "all", accept => [qw( all latest ), qr/[1-9][0-9]*[hdwmy]/ ], context => [qw( global volume subvolume )], },
target_preserve => { default => undef, accept => [qw( no )], accept_preserve_matrix => 1 },
target_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ] },
2023-04-11 00:48:55 +02:00
# target_create_dir => { default => undef, accept => [qw( yes no )] },
2022-05-28 21:26:09 +02:00
archive_preserve => { default => undef, accept => [qw( no )], accept_preserve_matrix => 1, context => [qw( global )] },
archive_preserve_min => { default => "all", accept => [qw( all latest no ), qr/[0-9]+[hdwmy]/ ], context => [qw( global )] },
2022-02-22 20:34:51 +01:00
ssh_identity => { default => undef, accept => [qw( no ) ], accept_file => { absolute => 1 } },
2022-02-22 22:19:15 +01:00
ssh_user => { default => "root", accept => [qw( no ), qr/[a-z_][a-z0-9_-]*/ ] },
2022-05-28 21:26:09 +02:00
ssh_compression => { default => undef, accept => [qw( yes no )] },
2022-05-28 21:21:52 +02:00
ssh_cipher_spec => { default => [ "default" ], accept => [qw( default ), qr/[a-z0-9][a-z0-9@.-]+/ ], split => 1 },
2022-05-28 21:26:09 +02:00
transaction_log => { default => undef, accept => [qw( no )], accept_file => { absolute => 1 }, context => [qw( global )] },
transaction_syslog => { default => undef, accept => [qw( no ), @syslog_facilities ], context => [qw( global )] },
lockfile => { default => undef, accept => [qw( no )], accept_file => { absolute => 1 }, context => [qw( global )] },
rate_limit => { default => undef, accept => [qw( no ), qr/[0-9]+[kmgtKMGT]?/ ], require_bin => 'mbuffer' },
rate_limit_remote => { default => undef, accept => [qw( no ), qr/[0-9]+[kmgtKMGT]?/ ] }, # NOTE: requires 'mbuffer' command on remote hosts
stream_buffer => { default => undef, accept => [qw( no ), qr/[0-9]+[kmgKMG%]?/ ], require_bin => 'mbuffer' },
stream_buffer_remote => { default => undef, accept => [qw( no ), qr/[0-9]+[kmgKMG%]?/ ] }, # NOTE: requires 'mbuffer' command on remote hosts
stream_compress => { default => undef, accept => [qw( no ), (keys %compression) ] },
stream_compress_level => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
stream_compress_long => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
stream_compress_threads => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
stream_compress_adapt => { default => undef, accept => [qw( yes no )] },
raw_target_compress => { default => undef, accept => [qw( no ), (keys %compression) ] },
raw_target_compress_level => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
raw_target_compress_long => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
raw_target_compress_threads => { default => "default", accept => [qw( default ), qr/[0-9]+/ ] },
raw_target_encrypt => { default => undef, accept => [qw( no gpg openssl_enc )] },
raw_target_block_size => { default => "128K", accept => [ qr/[0-9]+[kmgKMG]?/ ] },
raw_target_split => { default => undef, accept => [qw( no ), qr/[0-9]+([kmgtpezyKMGTPEZY][bB]?)?/ ] },
gpg_keyring => { default => undef, accept_file => { absolute => 1 } },
2022-05-06 21:06:19 +02:00
gpg_recipient => { default => undef, accept => [ qr/[0-9a-zA-Z_@\+\-\.]+/ ], split => 1 },
2022-05-28 17:56:00 +02:00
openssl_ciphername => { default => "aes-256-cbc", accept => [ qr/[0-9a-zA-Z\-]+/ ] },
2022-05-28 21:26:09 +02:00
openssl_iv_size => { default => undef, accept => [qw( no ), qr/[0-9]+/ ] },
openssl_keyfile => { default => undef, accept_file => { absolute => 1 } },
2016-01-14 18:02:53 +01:00
2022-05-28 21:26:09 +02:00
kdf_backend => { default => undef, accept_file => { absolute => 1 } },
kdf_keysize => { default => "32", accept => [ qr/[0-9]+/ ] },
kdf_keygen => { default => "once", accept => [qw( once each )] },
2017-06-30 14:35:20 +02:00
2022-05-28 21:26:09 +02:00
group => { default => undef, accept => [ qr/$group_match/ ], allow_multiple => 1, split => 1 },
noauto => { default => undef, accept => [qw( yes no )] },
2015-05-20 18:20:16 +02:00
2022-05-28 20:31:35 +02:00
backend => { default => "btrfs-progs", accept => [qw( btrfs-progs btrfs-progs-btrbk btrfs-progs-sudo btrfs-progs-doas )] },
backend_local => { default => undef, accept => [qw( no btrfs-progs btrfs-progs-btrbk btrfs-progs-sudo btrfs-progs-doas )] },
backend_remote => { default => undef, accept => [qw( no btrfs-progs btrfs-progs-btrbk btrfs-progs-sudo btrfs-progs-doas )] },
backend_local_user => { default => undef, accept => [qw( no btrfs-progs btrfs-progs-btrbk btrfs-progs-sudo btrfs-progs-doas )] },
2016-08-27 17:35:47 +02:00
2022-02-06 20:41:10 +01:00
compat => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 },
compat_local => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 },
compat_remote => { default => undef, accept => [qw( no busybox ignore_receive_errors )], split => 1 },
2022-05-28 21:26:09 +02:00
safe_commands => { default => undef, accept => [qw( yes no )], context => [qw( global )] },
2022-07-27 20:35:58 +02:00
btrfs_commit_delete => { default => undef, accept => [qw( yes no after each )],
deprecated => { MATCH => { regex => qr/^(?:after|each)$/, warn => 'Please use "btrfs_commit_delete yes|no"', replace_key => "btrfs_commit_delete", replace_value => "yes" } } },
2022-10-30 16:57:00 +01:00
send_protocol => { default => undef, accept => [qw( no 1 2 )] }, # NOTE: requires btrfs-progs-5.19
send_compressed_data => { default => undef, accept => [qw( yes no )] }, # NOTE: implies send_protocol=2
2020-05-24 00:13:10 +02:00
2022-05-28 21:26:09 +02:00
snapshot_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global volume subvolume )] },
target_qgroup_destroy => { default => undef, accept => [qw( yes no )] },
archive_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global )] },
2017-10-02 14:00:09 +02:00
2022-05-28 21:26:09 +02:00
archive_exclude => { default => undef, accept_file => { wildcards => 1 }, allow_multiple => 1, context => [qw( global )] },
archive_exclude_older => { default => undef, accept => [qw( yes no )] },
2018-02-13 21:36:21 +01:00
2022-05-28 21:26:09 +02:00
cache_dir => { default => undef, accept_file => { absolute => 1 }, allow_multiple => 1, context => [qw( global )] },
ignore_extent_data_inline => { default => "yes", accept => [qw( yes no )] },
2019-08-07 00:26:12 +02:00
2022-05-28 21:26:09 +02:00
warn_unknown_targets => { default => undef, accept => [qw( yes no )] },
2021-07-15 13:21:40 +02:00
2015-05-20 18:20:16 +02:00
# deprecated options
2022-05-28 20:31:35 +02:00
ssh_port => { default => "default", accept => [qw( default ), qr/[0-9]+/ ],
2019-03-31 23:31:55 +02:00
deprecated => { DEFAULT => { warn => 'Please use "ssh://hostname[:port]" notation in the "volume" and "target" configuration lines.' } } },
2022-05-28 20:31:35 +02:00
btrfs_progs_compat => { default => undef, accept => [qw( yes no )],
2016-04-15 01:22:19 +02:00
deprecated => { DEFAULT => { ABORT => 1, warn => 'This feature has been dropped in btrbk-v0.23.0. Please update to newest btrfs-progs, AT LEAST >= $BTRFS_PROGS_MIN' } } },
2022-05-28 20:31:35 +02:00
snapshot_preserve_daily => { default => 'all', accept => [qw( all ), qr/[0-9]+/ ], context => [qw( global volume subvolume )],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
snapshot_preserve_weekly => { default => 0, accept => [qw( all ), qr/[0-9]+/ ], context => [qw( global volume subvolume )],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
snapshot_preserve_monthly => { default => 'all', accept => [qw( all ), qr/[0-9]+/ ], context => [qw( global volume subvolume )],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
target_preserve_daily => { default => 'all', accept => [qw( all ), qr/[0-9]+/ ],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "target_preserve" and/or "target_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
target_preserve_weekly => { default => 0, accept => [qw( all ), qr/[0-9]+/ ],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "target_preserve" and/or "target_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
target_preserve_monthly => { default => 'all', accept => [qw( all ), qr/[0-9]+/ ],
2016-04-14 13:01:28 +02:00
deprecated => { DEFAULT => { FAILSAFE_PRESERVE => 1, warn => 'Please use "target_preserve" and/or "target_preserve_min"' } } },
2022-05-28 20:31:35 +02:00
resume_missing => { default => "yes", accept => [qw( yes no )],
2016-04-13 17:13:03 +02:00
deprecated => { yes => { warn => 'ignoring (missing backups are always resumed since btrbk v0.23.0)' },
2022-07-27 18:36:20 +02:00
no => { FAILSAFE_PRESERVE => 1, warn => 'Please use "target_preserve_min latest" and "target_preserve no" if you want to keep only the latest backup', },
DEFAULT => {} } },
2022-05-28 20:31:35 +02:00
snapshot_create_always => { default => undef, accept => [qw( yes no )],
2016-01-14 18:02:53 +01:00
deprecated => { yes => { warn => "Please use \"snapshot_create always\"",
replace_key => "snapshot_create",
replace_value => "always",
},
no => { warn => "Please use \"snapshot_create no\" or \"snapshot_create ondemand\"",
replace_key => "snapshot_create",
replace_value => "ondemand",
2022-07-27 18:36:20 +02:00
},
DEFAULT => {},
2016-01-14 18:02:53 +01:00
},
},
2022-05-28 20:31:35 +02:00
receive_log => { default => undef, accept => [qw( sidecar no )], accept_file => { absolute => 1 },
2016-01-14 18:02:53 +01:00
deprecated => { DEFAULT => { warn => "ignoring" } },
}
2015-01-09 18:09:32 +01:00
);
2022-06-06 18:00:51 +02:00
my @config_target_types = qw(send-receive raw); # first in list is default
2015-01-09 18:09:32 +01:00
2015-10-13 01:10:06 +02:00
my %table_formats = (
2021-04-16 21:31:17 +02:00
config_volume => {
2021-04-16 21:09:47 +02:00
table => [ qw( -volume_host -volume_port volume_path ) ],
long => [ qw( volume_host -volume_port volume_path -volume_rsh ) ],
raw => [ qw( volume_url volume_host volume_port volume_path volume_rsh ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( volume_url ) ],
2021-04-16 21:09:47 +02:00
},
2021-04-16 21:31:17 +02:00
config_source => {
2021-04-16 21:09:47 +02:00
table => [ qw( -source_host -source_port source_subvolume snapshot_path snapshot_name ) ],
long => [ qw( source_host -source_port source_subvolume snapshot_path snapshot_name -source_rsh ) ],
2021-08-08 16:22:05 +02:00
raw => [ qw( source_url source_host source_port source_subvolume snapshot_path snapshot_name source_rsh ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( source_url ) ],
2021-04-16 21:09:47 +02:00
},
2021-04-16 21:31:17 +02:00
config_target => {
2021-04-16 21:09:47 +02:00
table => [ qw( -target_host -target_port target_path ) ],
long => [ qw( target_host -target_port target_path -target_rsh ) ],
raw => [ qw( target_url target_host target_port target_path target_rsh ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( target_url ) ],
2021-04-16 21:09:47 +02:00
},
2021-04-16 21:31:17 +02:00
config => {
2021-04-16 21:09:47 +02:00
table => [ qw( -source_host -source_port source_subvolume snapshot_path snapshot_name -target_host -target_port target_path ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_path snapshot_name -target_host -target_port target_path target_type snapshot_preserve target_preserve ) ],
raw => [ qw( source_url source_host source_port source_subvolume snapshot_path snapshot_name target_url target_host target_port target_path target_type snapshot_preserve target_preserve source_rsh target_rsh ) ],
},
resolved => {
table => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port target_subvolume ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port target_subvolume target_type ) ],
2021-08-08 16:32:15 +02:00
raw => [ qw( type source_url source_host source_port source_subvolume snapshot_subvolume snapshot_name status target_url target_host target_port target_subvolume target_type source_rsh target_rsh ) ],
2021-04-16 21:09:47 +02:00
},
snapshots => {
table => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status ) ],
2021-08-08 16:22:05 +02:00
raw => [ qw( source_url source_host source_port source_subvolume snapshot_subvolume snapshot_name status source_rsh ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( snapshot_url ) ],
2021-04-16 21:09:47 +02:00
},
2021-04-16 15:35:48 +02:00
backups => { # same as resolved, except for single_column
2021-04-16 22:10:50 +02:00
table => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port target_subvolume ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port target_subvolume target_type ) ],
2021-08-08 16:32:15 +02:00
raw => [ qw( type source_url source_host source_port source_subvolume snapshot_subvolume snapshot_name status target_url target_host target_port target_subvolume target_type source_rsh target_rsh ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( target_url ) ],
2021-04-16 22:10:50 +02:00
},
2021-04-16 22:08:39 +02:00
latest => { # same as resolved, except hiding target if not present
table => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port -target_subvolume ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_subvolume status -target_host -target_port -target_subvolume -target_type ) ],
2021-08-08 16:32:15 +02:00
raw => [ qw( type source_url source_host source_port source_subvolume snapshot_subvolume snapshot_name status target_url target_host target_port target_subvolume target_type source_rsh target_rsh ) ],
2021-04-16 22:08:39 +02:00
},
2021-04-16 21:09:47 +02:00
stats => {
table => [ qw( -source_host -source_port source_subvolume snapshot_subvolume -target_host -target_port -target_subvolume snapshots -backups ) ],
long => [ qw( -source_host -source_port source_subvolume snapshot_subvolume -target_host -target_port -target_subvolume snapshot_status backup_status snapshots -backups -correlated -orphaned -incomplete ) ],
raw => [ qw( source_url source_host source_port source_subvolume snapshot_subvolume snapshot_name target_url target_host target_port target_subvolume snapshot_status backup_status snapshots backups correlated orphaned incomplete ) ],
RALIGN => { snapshots=>1, backups=>1, correlated=>1, orphaned=>1, incomplete=>1 },
},
schedule => {
table => [ qw( action -host -port subvolume scheme reason ) ],
2021-07-22 17:42:11 +02:00
long => [ qw( action -host -port subvolume scheme reason ) ],
2021-04-16 21:09:47 +02:00
raw => [ qw( topic action url host port path hod dow min h d w m y) ],
},
usage => {
2022-05-29 14:04:07 +02:00
table => [ qw( -host -port mount_source path size used free ) ],
2021-07-25 16:36:18 +02:00
long => [ qw( type -host -port mount_source path size used device_size device_allocated device_unallocated device_missing device_used free free_min data_ratio metadata_ratio global_reserve global_reserve_used ) ],
raw => [ qw( type host port mount_source path size used device_size device_allocated device_unallocated device_missing device_used free free_min data_ratio metadata_ratio global_reserve global_reserve_used ) ],
2021-04-16 21:09:47 +02:00
RALIGN => { size=>1, used=>1, device_size=>1, device_allocated=>1, device_unallocated=>1, device_missing=>1, device_used=>1, free=>1, free_min=>1, data_ratio=>1, metadata_ratio=>1, global_reserve=>1, global_reserve_used=>1 },
},
transaction => {
table => [ qw( type status -target_host -target_port target_subvolume -source_host -source_port source_subvolume parent_subvolume ) ],
long => [ qw( localtime type status duration target_host -target_port target_subvolume source_host -source_port source_subvolume parent_subvolume message ) ],
tlog => [ qw( localtime type status target_url source_url parent_url message ) ],
syslog => [ qw( type status target_url source_url parent_url message ) ],
raw => [ qw( time localtime type status duration target_url source_url parent_url message ) ],
},
origin_tree => {
table => [ qw( tree uuid parent_uuid received_uuid ) ],
long => [ qw( tree uuid parent_uuid received_uuid recursion ) ],
raw => [ qw( tree uuid parent_uuid received_uuid recursion ) ],
},
diff => {
table => [ qw( flags count size file ) ],
long => [ qw( flags count size file ) ],
raw => [ qw( flags count size file ) ],
RALIGN => { count=>1, size=>1 },
},
fs_list => {
table => [ qw( -host mount_source mount_subvol mount_point id flags subvolume_path path ) ],
short => [ qw( -host mount_source id flags path ) ],
long => [ qw( -host mount_source id top cgen gen uuid parent_uuid received_uuid flags path ) ],
raw => [ qw( host mount_source mount_subvol mount_point mount_subvolid id top_level cgen gen uuid parent_uuid received_uuid readonly path subvolume_path subvolume_rel_path url ) ],
2021-04-16 15:35:48 +02:00
single_column => [ qw( url ) ],
2021-08-17 12:42:36 +02:00
RALIGN => { id=>1, top=>1, cgen=>1, gen=>1 },
2021-04-16 21:09:47 +02:00
},
extent_diff => {
table => [ qw( total exclusive -diff -set subvol ) ],
long => [ qw( id cgen gen total exclusive -diff -set subvol ) ],
raw => [ qw( id cgen gen total exclusive -diff -set subvol ) ],
2021-08-17 12:42:36 +02:00
RALIGN => { id=>1, cgen=>1, gen=>1, total=>1, exclusive=>1, diff=>1, set=>1 },
2021-04-16 21:09:47 +02:00
},
2015-10-13 01:10:06 +02:00
);
2022-02-08 01:02:27 +01:00
my @btrfs_cmd = (
"btrfs subvolume list",
"btrfs subvolume show",
"btrfs subvolume snapshot",
"btrfs subvolume delete",
"btrfs send",
"btrfs receive",
"btrfs filesystem usage",
"btrfs qgroup destroy",
);
my @system_cmd = (
"readlink",
"test",
);
2016-08-27 17:35:47 +02:00
my %backend_cmd_map = (
2022-02-08 01:02:27 +01:00
"btrfs-progs-btrbk" => { map +( $_ => [ s/ /-/gr ] ), @btrfs_cmd },
"btrfs-progs-sudo" => { map +( $_ => [ qw( sudo -n ), split(" ", $_) ] ), @btrfs_cmd, @system_cmd },
2022-02-08 01:03:32 +01:00
"btrfs-progs-doas" => { map +( $_ => [ qw( doas -n ), split(" ", $_) ] ), @btrfs_cmd, @system_cmd },
2016-08-27 17:35:47 +02:00
);
2017-06-16 17:43:17 +02:00
# keys used in raw target sidecar files (.info):
my %raw_info_sort = (
TYPE => 1,
2022-11-20 11:11:50 +01:00
FILE => 2, # informative only (as of btrbk-0.32.6)
2017-06-16 17:43:17 +02:00
RECEIVED_UUID => 3,
RECEIVED_PARENT_UUID => 4,
2022-10-19 11:24:11 +02:00
compress => 10,
split => 11,
encrypt => 12,
cipher => 13,
iv => 14,
2017-06-30 14:35:20 +02:00
# kdf_* (generated by kdf_backend)
2022-10-19 11:24:11 +02:00
INCOMPLETE => 100,
);
2017-06-16 17:43:17 +02:00
2022-11-20 11:20:25 +01:00
my $raw_info_value_match = qr/[0-9a-zA-Z_-]*/;
2018-02-07 20:17:23 +01:00
my %raw_url_cache; # map URL to (fake) btr_tree node
2019-04-01 00:32:24 +02:00
my %mountinfo_cache; # map MACHINE_ID to mount points (sorted descending by file length)
my %mount_source_cache; # map URL_PREFIX:mount_source (aka device) to btr_tree node
2016-03-30 15:32:28 +02:00
my %uuid_cache; # map UUID to btr_tree node
2023-03-26 13:19:11 +02:00
my %realpath_cache; # map URL to realpath (symlink target). undef denotes an error.
2023-03-26 14:03:09 +02:00
my %mkdir_cache; # map URL to mkdir status: 1=created, undef=error
2016-04-13 22:04:53 +02:00
my $tree_inject_id = 0; # fake subvolume id for injected nodes (negative)
2016-04-15 22:00:10 +02:00
my $fake_uuid_prefix = 'XXXXXXXX-XXXX-XXXX-XXXX-'; # plus 0-padded inject_id: XXXXXXXX-XXXX-XXXX-XXXX-000000000000
2016-03-10 05:26:43 +01:00
2020-02-29 17:25:35 +01:00
my $program_name; # "btrbk" or "lsbtr", default to "btrbk"
2021-11-06 16:10:26 +01:00
my $safe_commands;
2015-01-13 14:38:44 +01:00
my $dryrun;
my $loglevel = 1;
2019-03-29 20:42:40 +01:00
my $quiet;
2019-08-04 21:37:17 +02:00
my @exclude_vf;
2016-04-18 16:40:49 +02:00
my $do_dumper;
2020-08-28 17:15:39 +02:00
my $do_trace;
2015-08-15 18:23:48 +02:00
my $show_progress = 0;
2015-10-13 18:24:30 +02:00
my $output_format;
2019-08-06 17:28:05 +02:00
my $output_pretty = 0;
2020-10-30 18:35:52 +01:00
my @output_unit;
2016-06-07 16:17:02 +02:00
my $lockfile;
2015-10-13 18:24:30 +02:00
my $tlog_fh;
2016-04-25 19:40:11 +02:00
my $syslog_enabled = 0;
2015-10-20 18:23:54 +02:00
my $current_transaction;
my @transaction_log;
2015-10-23 14:43:36 +02:00
my %config_override;
2016-04-21 13:27:54 +02:00
my @tm_now; # current localtime ( sec, min, hour, mday, mon, year, wday, yday, isdst )
2019-08-19 13:04:44 +02:00
my @stderr; # stderr of last run_cmd
2016-05-10 15:50:33 +02:00
my %warn_once;
2017-06-30 14:35:20 +02:00
my %kdf_vars;
my $kdf_session_key;
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
2015-10-20 20:17:31 +02:00
$SIG{INT} = sub {
2020-11-07 14:36:36 +01:00
print STDERR "\nERROR: Caught SIGINT, dumping transaction log:\n";
2015-10-20 20:17:31 +02:00
action("signal", status => "SIGINT");
print_formatted("transaction", \@transaction_log, output_format => "tlog", outfile => *STDERR);
exit 1;
};
2014-12-11 18:03:10 +01:00
sub VERSION_MESSAGE
{
2022-03-25 15:20:20 +01:00
print $VERSION_INFO . "\n";
2014-12-11 18:03:10 +01:00
}
2022-03-25 15:20:14 +01:00
sub ERROR_HELP_MESSAGE
{
return if($quiet);
print STDERR "See '$program_name --help'.\n";
}
2014-12-11 18:03:10 +01:00
sub HELP_MESSAGE
{
2019-03-29 20:42:40 +01:00
return if($quiet);
2019-03-29 20:46:40 +01:00
#80-----------------------------------------------------------------------------
2020-02-29 17:25:35 +01:00
if($program_name eq "lsbtr") {
2022-03-25 15:20:20 +01:00
print <<"END_HELP_LSBTR";
2021-08-18 03:32:15 +02:00
usage: lsbtr [<options>] [[--] <path>...]
2020-02-29 17:25:35 +01:00
options:
-h, --help display this help message
--version display version information
-l, --long use long listing format
-u, --uuid print uuid table (parent/received relations)
2021-04-16 15:35:48 +02:00
-1, --single-column Print path column only
2020-02-29 17:25:35 +01:00
--raw print raw table format
-v, --verbose increase output verbosity
-c, --config=FILE specify btrbk configuration file
--override=KEY=VALUE globally override a configuration option
For additional information, see $PROJECT_HOME
END_HELP_LSBTR
}
else {
2022-03-25 15:20:20 +01:00
print <<"END_HELP_BTRBK";
2021-08-18 03:32:15 +02:00
usage: btrbk [<options>] <command> [[--] <filter>...]
2018-03-28 22:16:54 +02:00
options:
-h, --help display this help message
--version display version information
-c, --config=FILE specify configuration file
-n, --dry-run perform a trial run with no changes made
2019-04-17 17:12:35 +02:00
--exclude=FILTER exclude configured sections
2018-03-28 22:16:54 +02:00
-p, --preserve preserve all (do not delete anything)
--preserve-snapshots preserve snapshots (do not delete snapshots)
--preserve-backups preserve backups (do not delete backups)
--wipe delete all but latest snapshots
2020-08-21 16:14:17 +02:00
-v, --verbose be more verbose (increase logging level)
2018-03-28 22:16:54 +02:00
-q, --quiet be quiet (do not print backup summary)
2020-08-21 16:14:17 +02:00
-l, --loglevel=LEVEL set logging level (error, warn, info, debug, trace)
2018-03-28 22:16:54 +02:00
-t, --table change output to table format
2019-08-02 22:39:35 +02:00
-L, --long change output to long format
2018-03-28 22:16:54 +02:00
--format=FORMAT change output format, FORMAT=table|long|raw
2018-10-15 16:25:07 +02:00
-S, --print-schedule print scheduler details (for the "run" command)
2018-03-28 22:16:54 +02:00
--progress show progress bar on send-receive operation
2019-04-17 17:07:54 +02:00
--lockfile=FILE create and check lockfile
--override=KEY=VALUE globally override a configuration option
2018-03-28 22:16:54 +02:00
commands:
run run snapshot and backup operations
dryrun don't run btrfs commands; show what would be executed
snapshot run snapshot operations only
resume run backup operations, and delete snapshots
prune only delete snapshots and backups
2019-04-24 18:29:05 +02:00
archive <src> <dst> recursively copy all subvolumes
2018-03-28 22:16:54 +02:00
clean delete incomplete (garbled) backups
stats print snapshot/backup statistics
2020-12-20 18:32:41 +01:00
list <subcommand> available subcommands are (default "all"):
2020-12-13 23:54:25 +01:00
all snapshots and backups
snapshots snapshots
backups backups and correlated snapshots
2018-03-28 22:16:54 +02:00
latest most recent snapshots and backups
config configured source/snapshot/target relations
source configured source/snapshot relations
volume configured volume sections
target configured targets
usage print filesystem usage
2019-07-28 18:54:37 +02:00
ls <path> list all btrfs subvolumes below path
2020-12-12 17:44:57 +01:00
origin <subvol> print origin information for subvolume
diff <from> <to> list file changes between related subvolumes
extents [diff] <path> calculate accurate disk space usage
2018-03-28 22:16:54 +02:00
For additional information, see $PROJECT_HOME
2020-02-29 17:25:35 +01:00
END_HELP_BTRBK
}
2019-03-29 20:46:40 +01:00
#80-----------------------------------------------------------------------------
2014-12-11 18:03:10 +01:00
}
2022-07-27 18:19:46 +02:00
sub _log_cont { my $p = shift; print STDERR $p . join("\n${p}... ", grep defined, @_) . "\n"; }
2019-12-15 18:01:16 +01:00
sub TRACE { print STDERR map { "___ $_\n" } @_ if($loglevel >= 4) }
sub DEBUG { _log_cont("", @_) if($loglevel >= 3) }
sub INFO { _log_cont("", @_) if($loglevel >= 2) }
sub WARN { _log_cont("WARNING: ", @_) if($loglevel >= 1) }
sub ERROR { _log_cont("ERROR: ", @_) }
2014-12-11 18:03:10 +01:00
2021-07-15 13:39:24 +02:00
sub INFO_ONCE {
my $t = shift;
2021-08-15 17:37:11 +02:00
if($warn_once{INFO}{$t}) { TRACE("INFO(again): $t", @_) if($do_trace); return 0; }
2021-07-15 13:39:24 +02:00
else { $warn_once{INFO}{$t} = 1; INFO($t, @_); return 1; }
}
2016-05-10 15:50:33 +02:00
sub WARN_ONCE {
my $t = shift;
2021-08-15 17:37:11 +02:00
if($warn_once{WARN}{$t}) { TRACE("WARNING(again): $t", @_) if($do_trace); return 0; }
2021-07-15 13:39:24 +02:00
else { $warn_once{WARN}{$t} = 1; WARN($t, @_); return 1; }
2016-05-10 15:50:33 +02:00
}
2016-03-10 18:29:21 +01:00
sub VINFO {
2016-04-28 13:03:15 +02:00
return undef unless($do_dumper);
2016-03-17 14:02:22 +01:00
my $vinfo = shift; my $t = shift || "vinfo"; my $maxdepth = shift // 2;
print STDERR Data::Dumper->new([$vinfo], [$t])->Maxdepth($maxdepth)->Dump();
2016-03-10 18:29:21 +01:00
}
sub SUBVOL_LIST {
2016-04-28 13:03:15 +02:00
return undef unless($do_dumper);
2016-03-10 18:29:21 +01:00
my $vol = shift; my $t = shift // "SUBVOL_LIST"; my $svl = vinfo_subvol_list($vol);
2016-04-12 17:50:12 +02:00
print STDERR "$t:\n " . join("\n ", map { "$vol->{PRINT}/./$_->{SUBVOL_PATH}\t$_->{node}{id}" } @$svl) . "\n";
2016-03-10 18:29:21 +01:00
}
2016-03-10 19:26:17 +01:00
2016-03-10 05:26:43 +01:00
2019-04-17 15:20:18 +02:00
sub ABORTED($$;$)
2015-10-12 22:26:36 +02:00
{
my $config = shift;
2019-04-17 15:20:18 +02:00
my $abrt_key = shift // die;
my $abrt = shift;
2016-03-07 23:53:47 +01:00
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
2019-04-17 15:20:18 +02:00
unless(defined($abrt)) {
# no key (only text) set: switch arguments, use default key
$abrt = $abrt_key;
$abrt_key = "abort_" . $config->{CONTEXT};
}
unless($abrt_key =~ /^skip_/) {
# keys starting with "skip_" are not actions
2016-03-07 19:17:33 +01:00
$abrt =~ s/\n/\\\\/g;
$abrt =~ s/\r//g;
2019-04-17 15:20:18 +02:00
action($abrt_key,
2015-10-12 22:56:52 +02:00
status => "ABORT",
2015-10-12 23:58:38 +02:00
vinfo_prefixed_keys("target", vinfo($config->{url}, $config)),
2016-03-07 19:17:33 +01:00
message => $abrt,
2015-10-12 22:56:52 +02:00
);
}
2019-04-17 15:20:18 +02:00
$config->{ABORTED} = { key => $abrt_key, text => $abrt };
}
sub IS_ABORTED($;$)
{
my $config = shift;
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
return undef unless(defined($config->{ABORTED}));
my $abrt_key = $config->{ABORTED}->{key};
return undef unless(defined($abrt_key));
my $filter_prefix = shift;
2021-08-28 14:41:01 +02:00
return undef if($filter_prefix && ($abrt_key !~ /^$filter_prefix/));
2019-04-17 15:20:18 +02:00
return $abrt_key;
2015-10-12 22:26:36 +02:00
}
2014-12-14 19:23:02 +01:00
2019-04-17 15:20:18 +02:00
sub ABORTED_TEXT($)
{
my $config = shift;
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
return "" unless(defined($config->{ABORTED}));
return $config->{ABORTED}->{text} // "";
}
2019-07-15 18:19:33 +02:00
sub FIX_MANUALLY($$)
{
# treated as error, but does not abort config section
my $config = shift;
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
my $msg = shift // die;
$config->{FIX_MANUALLY} //= [];
push(@{$config->{FIX_MANUALLY}}, $msg);
}
2019-04-17 15:20:18 +02:00
2016-04-25 21:05:46 +02:00
sub eval_quiet(&)
{
local $SIG{__DIE__};
return eval { $_[0]->() }
}
2015-10-13 18:24:30 +02:00
2016-04-28 13:03:15 +02:00
sub require_data_dumper
{
if(eval_quiet { require Data::Dumper; }) {
Data::Dumper->import("Dumper");
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;
$do_dumper = 1;
# silence perl warning: Name "Data::Dumper::Sortkeys" used only once: possible typo at...
2020-08-28 17:15:39 +02:00
TRACE "Successfully loaded Dumper module: sortkeys=$Data::Dumper::Sortkeys, quotekeys=$Data::Dumper::Quotekeys" if($do_trace);
2016-04-28 13:03:15 +02:00
} else {
2020-08-28 17:15:39 +02:00
WARN "Perl module \"Data::Dumper\" not found: data trace dumps disabled!" if($do_trace);
2016-04-28 13:03:15 +02:00
}
}
btrbk: add transaction logging to syslog
Add configuration option transaction_syslog, which can be set to a short
name of a syslog facility, like user or local5. Most of the ones besides
localX do not really make sense, but whatever, let the user decide.
The only logging that is relevant for logging to syslog is the logging
generated inside sub action, so it's easy to hijack all messages in
there and also send them to syslog if needed.
All output is done via print_formatted, which expects a file handle.
So, abuse a file handle to a string to be able to change as less code as
needed for this feature.
Since syslog already adds the timestamps for us, I added a syslog
formatting pattern, which is very similar to tlog, omitting the
timestap.
2016-04-22 23:11:00 +02:00
sub init_transaction_log($$)
2015-10-13 18:24:30 +02:00
{
2016-04-06 20:19:12 +02:00
my $file = shift;
2016-04-25 18:36:15 +02:00
my $config_syslog_facility = shift;
2016-04-06 20:19:12 +02:00
if(defined($file) && (not $dryrun)) {
2021-08-16 16:58:51 +02:00
if(open($tlog_fh, '>>', $file)) {
2016-04-28 12:48:58 +02:00
# print headers (disabled)
# print_formatted("transaction", [ ], output_format => "tlog", outfile => $tlog_fh);
2016-04-06 20:19:12 +02:00
INFO "Using transaction log: $file";
} else {
$tlog_fh = undef;
ERROR "Failed to open transaction log '$file': $!";
}
2015-10-13 18:24:30 +02:00
}
btrbk: add transaction logging to syslog
Add configuration option transaction_syslog, which can be set to a short
name of a syslog facility, like user or local5. Most of the ones besides
localX do not really make sense, but whatever, let the user decide.
The only logging that is relevant for logging to syslog is the logging
generated inside sub action, so it's easy to hijack all messages in
there and also send them to syslog if needed.
All output is done via print_formatted, which expects a file handle.
So, abuse a file handle to a string to be able to change as less code as
needed for this feature.
Since syslog already adds the timestamps for us, I added a syslog
formatting pattern, which is very similar to tlog, omitting the
timestap.
2016-04-22 23:11:00 +02:00
if(defined($config_syslog_facility) && (not $dryrun)) {
2016-04-25 21:05:46 +02:00
DEBUG "Opening syslog";
if(eval_quiet { require Sys::Syslog; }) {
2016-04-25 19:40:11 +02:00
$syslog_enabled = 1;
Sys::Syslog::openlog("btrbk", "", $config_syslog_facility);
2016-04-25 21:05:46 +02:00
DEBUG "Syslog enabled";
}
else {
WARN "Syslog disabled: $@";
2016-04-25 18:36:15 +02:00
}
btrbk: add transaction logging to syslog
Add configuration option transaction_syslog, which can be set to a short
name of a syslog facility, like user or local5. Most of the ones besides
localX do not really make sense, but whatever, let the user decide.
The only logging that is relevant for logging to syslog is the logging
generated inside sub action, so it's easy to hijack all messages in
there and also send them to syslog if needed.
All output is done via print_formatted, which expects a file handle.
So, abuse a file handle to a string to be able to change as less code as
needed for this feature.
Since syslog already adds the timestamps for us, I added a syslog
formatting pattern, which is very similar to tlog, omitting the
timestap.
2016-04-22 23:11:00 +02:00
}
2019-04-18 16:28:53 +02:00
action("DEFERRED", %$_) foreach (@transaction_log);
2015-10-13 18:24:30 +02:00
}
sub close_transaction_log()
{
if($tlog_fh) {
DEBUG "Closing transaction log";
close $tlog_fh || ERROR "Failed to close transaction log: $!";
}
2016-04-25 19:40:11 +02:00
if($syslog_enabled) {
2016-04-25 21:05:46 +02:00
DEBUG "Closing syslog";
eval_quiet { Sys::Syslog::closelog(); };
2016-04-25 18:36:15 +02:00
}
2015-10-13 18:24:30 +02:00
}
sub action($@)
{
my $type = shift // die;
my $h = { @_ };
2019-04-18 16:28:53 +02:00
unless($type eq "DEFERRED") {
my $time = $h->{time} // time;
$h->{type} = $type;
$h->{time} = $time;
$h->{localtime} = timestamp($time, 'debug-iso');
push @transaction_log, $h;
}
2015-10-20 20:16:34 +02:00
print_formatted("transaction", [ $h ], output_format => "tlog", no_header => 1, outfile => $tlog_fh) if($tlog_fh);
2016-04-25 19:40:11 +02:00
print_formatted("transaction", [ $h ], output_format => "syslog", no_header => 1) if($syslog_enabled); # dirty hack, this calls syslog()
2015-10-20 18:23:54 +02:00
return $h;
}
sub start_transaction($@)
{
my $type = shift // die;
my $time = time;
die("start_transaction() while transaction is running") if($current_transaction);
my @actions = (ref($_[0]) eq "HASH") ? @_ : { @_ }; # single action is not hashref
$current_transaction = [];
foreach (@actions) {
2017-09-27 19:35:43 +02:00
push @$current_transaction, action($type, %$_, status => ($dryrun ? "dryrun_starting" : "starting"), time => $time);
2015-10-20 18:23:54 +02:00
}
}
sub end_transaction($$)
{
my $type = shift // die;
2017-09-27 19:35:43 +02:00
my $success = shift; # scalar or coderef: if scalar, status is set for all current transitions
2015-10-20 18:23:54 +02:00
my $time = time;
die("end_transaction() while no transaction is running") unless($current_transaction);
foreach (@$current_transaction) {
die("end_transaction() has different type") unless($_->{type} eq $type);
2017-09-27 19:35:43 +02:00
my $status = (ref($success) ? &{$success} ($_) : $success) ? "success" : "ERROR";
$status = "dryrun_" . $status if($dryrun);
2017-08-28 17:54:17 +02:00
action($type, %$_, status => $status, time => $time, duration => ($dryrun ? undef : ($time - $_->{time})));
2015-10-20 18:23:54 +02:00
}
$current_transaction = undef;
2015-10-13 18:24:30 +02:00
}
2016-04-25 19:40:11 +02:00
sub syslog($)
{
return undef unless($syslog_enabled);
2016-04-25 21:05:46 +02:00
my $line = shift;
eval_quiet { Sys::Syslog::syslog("info", $line); };
2016-04-25 19:40:11 +02:00
}
2016-05-03 14:34:04 +02:00
sub check_exe($)
{
my $cmd = shift // die;
foreach my $path (split(":", $ENV{PATH})) {
return 1 if( -x "$path/$cmd" );
}
return 0;
}
2019-07-29 21:59:03 +02:00
sub stream_buffer_cmd_text($)
2017-03-17 20:39:51 +01:00
{
2019-07-29 21:59:03 +02:00
my $opts = shift;
my $rl_in = $opts->{rate_limit_in} // $opts->{rate_limit}; # maximum read rate: b,k,M,G
my $rl_out = $opts->{rate_limit_out}; # maximum write rate: b,k,M,G
my $bufsize = $opts->{stream_buffer}; # b,k,M,G,% (default: 2%)
my $blocksize = $opts->{blocksize}; # defaults to 10k
my $progress = $opts->{show_progress};
2019-07-28 15:04:23 +02:00
2019-07-29 21:59:03 +02:00
# return empty array if mbuffer is not needed
return () unless($rl_in || $rl_out || $bufsize || $progress);
2019-07-28 15:04:23 +02:00
# NOTE: mbuffer takes defaults from /etc/mbuffer.rc
my @cmd = ( "mbuffer" );
2020-11-07 14:36:36 +01:00
push @cmd, ( "-v", "1" ); # disable warnings (they arrive asynchronously and cant be caught)
2019-07-28 15:04:23 +02:00
push @cmd, "-q" unless($progress);
2019-07-29 21:59:03 +02:00
push @cmd, ( "-s", $blocksize ) if($blocksize);
push @cmd, ( "-m", lc($bufsize) ) if($bufsize);
push @cmd, ( "-r", lc($rl_in) ) if($rl_in);
push @cmd, ( "-R", lc($rl_out) ) if($rl_out);
return { cmd_text => join(' ', @cmd) };
2017-03-17 20:39:51 +01:00
}
2019-07-27 18:56:19 +02:00
sub compress_cmd_text($;$)
2016-05-11 20:15:46 +02:00
{
2019-07-29 21:59:03 +02:00
my $def = shift // die;
2016-05-11 20:15:46 +02:00
my $decompress = shift;
my $cc = $compression{$def->{key}};
my @cmd = $decompress ? @{$cc->{decompress_cmd}} : @{$cc->{compress_cmd}};
if((not $decompress) && defined($def->{level}) && ($def->{level} ne "default")) {
my $level = $def->{level};
2022-11-15 23:06:21 +01:00
if(!defined($cc->{level_min})) {
WARN_ONCE "Compression level is not supported for '$cc->{name}', ignoring";
$level = undef;
} elsif($level < $cc->{level_min}) {
2016-05-11 20:15:46 +02:00
WARN_ONCE "Compression level capped to minimum for '$cc->{name}': $cc->{level_min}";
$level = $cc->{level_min};
2022-11-15 23:06:21 +01:00
} elsif($level > $cc->{level_max}) {
2016-05-11 20:15:46 +02:00
WARN_ONCE "Compression level capped to maximum for '$cc->{name}': $cc->{level_max}";
$level = $cc->{level_max};
}
2022-11-15 23:06:21 +01:00
push @cmd, '-' . $level if(defined($level));
2016-05-11 20:15:46 +02:00
}
if(defined($def->{threads}) && ($def->{threads} ne "default")) {
my $thread_opt = $cc->{threads};
if($thread_opt) {
push @cmd, $thread_opt . $def->{threads};
}
else {
WARN_ONCE "Threading is not supported for '$cc->{name}', ignoring";
}
}
2020-12-24 00:10:33 +01:00
if(defined($def->{long}) && ($def->{long} ne "default")) {
my $long_opt = $cc->{long};
if($long_opt) {
push @cmd, $long_opt . $def->{long};
}
else {
WARN_ONCE "Long distance matching is not supported for '$cc->{name}', ignoring";
}
}
2021-06-07 07:03:47 +02:00
if(defined($def->{adapt})) {
my $adapt_opt = $cc->{adapt};
if($adapt_opt) {
push @cmd, $adapt_opt;
}
else {
WARN_ONCE "Adaptive compression is not supported for '$cc->{name}', ignoring";
}
}
2019-07-29 21:59:03 +02:00
return { cmd_text => join(' ', @cmd) };
2016-05-11 20:15:46 +02:00
}
2019-07-27 18:56:19 +02:00
sub decompress_cmd_text($)
2016-05-11 20:15:46 +02:00
{
2019-07-27 18:56:19 +02:00
return compress_cmd_text($_[0], 1);
2016-04-02 14:13:16 +02:00
}
2015-10-13 18:24:30 +02:00
2021-07-14 15:42:57 +02:00
sub _piped_cmd_txt($)
2016-08-21 12:57:15 +02:00
{
my $cmd_pipe = shift;
my $cmd = "";
my $pipe = "";
2021-07-14 15:42:57 +02:00
my $last;
foreach (map $_->{cmd_text}, @$cmd_pipe) {
die if($last);
if(/^>/) {
# can't be first, must be last
2016-08-21 12:57:15 +02:00
die unless($pipe);
2021-07-14 15:42:57 +02:00
$last = 1;
$pipe = ' ';
2016-08-21 12:57:15 +02:00
}
2021-07-14 15:42:57 +02:00
$cmd .= $pipe . $_;
$pipe = ' | ';
2016-08-21 12:57:15 +02:00
}
return $cmd;
}
2021-08-15 12:27:54 +02:00
sub quoteshell(@) {
# replace ' -> '\''
join ' ', map { "'" . s/'/'\\''/gr . "'" } @_
}
2021-09-04 15:46:09 +02:00
sub _safe_cmd($;$)
2016-08-24 15:15:46 +02:00
{
2021-09-04 15:46:09 +02:00
# hashes of form: "{ unsafe => 'string' }" get translated to "'string'"
2016-08-24 15:15:46 +02:00
my $aref = shift;
my $offending = shift;
2021-09-04 15:46:09 +02:00
return join ' ', map {
if(ref($_)) {
2021-08-28 13:29:07 +02:00
my $prefix = $_->{prefix} // "";
my $postfix = $_->{postfix} // "";
2021-09-04 15:46:09 +02:00
$_ = $_->{unsafe};
die "cannot quote leading dash for command: $_" if(/^-/);
# NOTE: all files must be absolute
2022-12-02 22:32:17 +01:00
my $file = check_file($_, { absolute => 1 }, sanitize => 1 );
unless(defined($file)) {
die "uncaught unsafe file: $_" unless($offending);
push @$offending, $_;
2016-08-24 15:15:46 +02:00
}
2022-12-02 22:32:17 +01:00
$_ = $prefix . quoteshell($file // $_) . $postfix;
2016-08-24 15:15:46 +02:00
}
2021-09-04 15:46:09 +02:00
$_
} @$aref;
2016-08-24 15:15:46 +02:00
}
2015-08-07 15:31:05 +02:00
sub run_cmd(@)
2014-12-11 18:03:10 +01:00
{
2019-08-18 11:46:31 +02:00
# IPC::Open3 based implementation.
2016-01-19 17:52:27 +01:00
# NOTE: multiple filters are not supported!
2016-08-21 12:57:15 +02:00
my @cmd_pipe_in = (ref($_[0]) eq "HASH") ? @_ : { @_ };
die unless(scalar(@cmd_pipe_in));
2019-08-19 13:04:44 +02:00
@stderr = ();
2015-08-07 15:31:05 +02:00
my $destructive = 0;
2016-08-21 12:57:15 +02:00
my @cmd_pipe;
2016-08-24 15:15:46 +02:00
my @unsafe_cmd;
2016-05-11 20:15:46 +02:00
my $compressed = undef;
2019-05-23 15:36:34 +02:00
my $large_output;
2019-07-29 21:59:03 +02:00
my $stream_options = $cmd_pipe_in[0]->{stream_options} // {};
2019-08-19 13:04:44 +02:00
my @filter_stderr;
my $fatal_stderr;
2019-08-17 20:27:05 +02:00
my $has_rsh;
2019-07-29 21:59:03 +02:00
$cmd_pipe_in[0]->{stream_source} = 1;
$cmd_pipe_in[-1]->{stream_sink} = 1;
2016-08-21 12:57:15 +02:00
foreach my $href (@cmd_pipe_in)
2016-05-11 20:15:46 +02:00
{
2016-08-24 15:54:05 +02:00
die if(defined($href->{cmd_text}));
2016-08-21 12:57:15 +02:00
2019-08-19 13:04:44 +02:00
push @filter_stderr, ((ref($href->{filter_stderr}) eq "ARRAY") ? @{$href->{filter_stderr}} : $href->{filter_stderr}) if($href->{filter_stderr});
$fatal_stderr = $href->{fatal_stderr} if($href->{fatal_stderr});
2016-05-11 20:15:46 +02:00
$destructive = 1 unless($href->{non_destructive});
2022-07-28 13:36:20 +02:00
$has_rsh = $href->{rsh} if($href->{rsh});
2019-05-23 15:36:34 +02:00
$large_output = 1 if($href->{large_output});
2016-05-11 20:15:46 +02:00
2019-07-29 21:59:03 +02:00
if($href->{redirect_to_file}) {
die unless($href->{stream_sink});
$href->{cmd_text} = _safe_cmd([ '>', $href->{redirect_to_file} ], \@unsafe_cmd);
}
2022-10-19 11:03:43 +02:00
elsif($href->{append_to_file}) {
die unless($href->{stream_sink});
$href->{cmd_text} = _safe_cmd([ '>>', $href->{append_to_file} ], \@unsafe_cmd);
}
2019-07-29 21:59:03 +02:00
elsif($href->{compress_stdin}) {
# does nothing if already compressed correctly by stream_compress
if($compressed && ($compression{$compressed->{key}}->{format} ne $compression{$href->{compress_stdin}->{key}}->{format})) {
# re-compress with different algorithm
push @cmd_pipe, decompress_cmd_text($compressed);
2016-05-11 20:15:46 +02:00
$compressed = undef;
}
unless($compressed) {
2019-07-29 21:59:03 +02:00
push @cmd_pipe, compress_cmd_text($href->{compress_stdin});
$compressed = $href->{compress_stdin};
2016-05-11 20:15:46 +02:00
}
2019-07-29 21:59:03 +02:00
next;
2015-10-20 22:05:02 +02:00
}
2019-07-29 21:59:03 +02:00
elsif($href->{cmd}) {
$href->{cmd_text} = _safe_cmd($href->{cmd}, \@unsafe_cmd);
}
return undef unless(defined($href->{cmd_text}));
2016-05-11 20:15:46 +02:00
2019-07-29 21:59:03 +02:00
my @rsh_compress_in;
my @rsh_compress_out;
my @decompress_in;
2016-05-11 20:15:46 +02:00
2019-07-29 21:59:03 +02:00
# input stream compression: local, in front of rsh_cmd_pipe
if($href->{rsh} && $stream_options->{stream_compress} && (not $href->{stream_source})) {
if($compressed && ($compression{$compressed->{key}}->{format} ne $compression{$stream_options->{stream_compress}->{key}}->{format})) {
# re-compress with different algorithm, should be avoided!
push @rsh_compress_in, decompress_cmd_text($compressed);
$compressed = undef;
}
if(not $compressed) {
$compressed = $stream_options->{stream_compress};
push @rsh_compress_in, compress_cmd_text($compressed);
}
}
2016-08-21 12:57:15 +02:00
2019-07-29 21:59:03 +02:00
if($compressed && (not ($href->{compressed_ok}))) {
push @decompress_in, decompress_cmd_text($compressed);
$compressed = undef;
}
2016-08-21 12:57:15 +02:00
2019-07-29 21:59:03 +02:00
# output stream compression: remote, at end of rsh_cmd_pipe
if($href->{rsh} && $stream_options->{stream_compress} && (not $href->{stream_sink}) && (not $compressed)) {
$compressed = $stream_options->{stream_compress};
push @rsh_compress_out, compress_cmd_text($compressed);
}
2019-07-28 15:04:23 +02:00
2019-07-29 21:59:03 +02:00
if($href->{rsh}) {
# honor stream_buffer_remote, rate_limit_remote for stream source / sink
my @rsh_stream_buffer_in = $href->{stream_sink} ? stream_buffer_cmd_text($stream_options->{rsh_sink}) : ();
my @rsh_stream_buffer_out = $href->{stream_source} ? stream_buffer_cmd_text($stream_options->{rsh_source}) : ();
2016-08-21 12:57:15 +02:00
2019-07-29 21:59:03 +02:00
my @rsh_cmd_pipe = (
@decompress_in,
@rsh_stream_buffer_in,
$href,
@rsh_stream_buffer_out,
@rsh_compress_out,
);
@decompress_in = ();
2016-08-21 12:57:15 +02:00
2019-07-29 21:59:03 +02:00
# fixup redirect_to_file
2022-10-19 11:03:43 +02:00
if((scalar(@rsh_cmd_pipe) == 1) && ($rsh_cmd_pipe[0]->{redirect_to_file} || $rsh_cmd_pipe[0]->{append_to_file})) {
2019-07-29 21:59:03 +02:00
# NOTE: direct redirection in ssh command does not work: "ssh '> outfile'"
# we need to assemble: "ssh 'cat > outfile'"
unshift @rsh_cmd_pipe, { cmd_text => 'cat' };
2016-01-19 17:52:27 +01:00
}
2019-07-28 15:04:23 +02:00
2019-07-29 21:59:03 +02:00
my $rsh_text = _safe_cmd($href->{rsh}, \@unsafe_cmd);
return undef unless(defined($rsh_text));
2021-08-15 12:27:54 +02:00
$href->{cmd_text} = $rsh_text . ' ' . quoteshell(_piped_cmd_txt(\@rsh_cmd_pipe));
2016-05-11 20:15:46 +02:00
}
2019-07-29 21:59:03 +02:00
# local stream_buffer, rate_limit and show_progress in front of stream sink
my @stream_buffer_in = $href->{stream_sink} ? stream_buffer_cmd_text($stream_options->{local_sink}) : ();
push @cmd_pipe, (
@decompress_in, # empty if rsh
@stream_buffer_in,
@rsh_compress_in, # empty if not rsh
$href, # command or rsh_cmd_pipe
);
2016-05-11 20:15:46 +02:00
}
2021-07-14 15:42:57 +02:00
my $cmd = _piped_cmd_txt(\@cmd_pipe);
2016-01-19 17:52:27 +01:00
2016-08-24 15:15:46 +02:00
if(scalar(@unsafe_cmd)) {
2021-11-06 16:09:51 +01:00
ERROR "Unsafe command `$cmd`", map "Offending string: \"$_\"", @unsafe_cmd;
2016-08-24 15:15:46 +02:00
return undef;
}
2016-01-19 17:52:27 +01:00
if($dryrun && $destructive) {
2019-08-18 11:46:31 +02:00
DEBUG "### (dryrun) $cmd";
2019-08-18 12:59:12 +02:00
return [];
2015-10-20 22:05:02 +02:00
}
2019-08-18 11:46:31 +02:00
DEBUG "### $cmd";
# execute command
2019-08-19 13:04:44 +02:00
my ($pid, $out_fh, $err_fh, @stdout);
2019-08-18 11:46:31 +02:00
$err_fh = gensym;
if(eval_quiet { $pid = open3(undef, $out_fh, $err_fh, $cmd); }) {
2019-08-18 12:59:12 +02:00
chomp(@stdout = readline($out_fh));
chomp(@stderr = readline($err_fh));
2019-08-18 11:46:31 +02:00
waitpid($pid, 0);
2020-08-28 17:15:39 +02:00
if($do_trace) {
2019-05-23 15:36:34 +02:00
if($large_output) {
TRACE "Command output lines=" . scalar(@stdout) . " (large_output, not dumped)";
} else {
TRACE map("[stdout] $_", @stdout);
}
2019-08-19 13:04:44 +02:00
TRACE map("[stderr] $_", @stderr);
2019-08-18 12:59:12 +02:00
}
2019-08-18 11:46:31 +02:00
}
else {
ERROR "Command execution failed ($!): `$cmd`";
return undef;
2017-08-29 16:52:58 +02:00
}
2019-08-16 01:22:49 +02:00
# fatal errors
2017-08-29 16:52:58 +02:00
if($? == -1) {
2019-08-18 11:46:31 +02:00
ERROR "Command execution failed ($!): `$cmd`";
2017-08-29 16:52:58 +02:00
return undef;
}
elsif ($? & 127) {
2015-08-07 15:31:05 +02:00
my $signal = $? & 127;
2019-08-18 11:46:31 +02:00
ERROR "Command execution failed (child died with signal $signal): `$cmd`";
2017-08-29 16:52:58 +02:00
return undef;
}
2019-08-16 01:22:49 +02:00
my $exitcode = $? >> 8;
2015-08-07 15:31:05 +02:00
2019-08-19 13:04:44 +02:00
# call hooks: fatal_stderr, filter_stderr
if(($exitcode == 0) && $fatal_stderr) {
$exitcode = -1 if(grep &{$fatal_stderr}(), @stderr);
2019-08-16 01:22:49 +02:00
}
2019-08-19 13:04:44 +02:00
foreach my $filter_fn (@filter_stderr) {
@stderr = map { &{$filter_fn} ($exitcode); $_ // () } @stderr;
2019-08-17 20:27:05 +02:00
}
2019-08-19 13:04:44 +02:00
if($exitcode) {
2019-12-14 17:06:26 +01:00
unshift @stderr, "sh: $cmd";
if($has_rsh && ($exitcode == 255)) {
# SSH returns exit status 255 if an error occurred (including
# network errors, dns failures).
2022-07-28 13:36:20 +02:00
unshift @stderr, "(note: option \"ssh_identity\" is not set, using ssh defaults)" unless(grep /^-i$/, @$has_rsh);
2019-12-14 17:06:26 +01:00
unshift @stderr, "SSH command failed (exitcode=$exitcode)";
2019-08-19 13:04:44 +02:00
} else {
2019-12-14 17:06:26 +01:00
unshift @stderr, "Command execution failed (exitcode=$exitcode)";
2019-08-16 01:22:49 +02:00
}
2019-12-14 17:06:26 +01:00
DEBUG @stderr;
2015-08-07 15:31:05 +02:00
return undef;
2015-01-14 14:10:41 +01:00
}
else {
2015-08-07 15:31:05 +02:00
DEBUG "Command execution successful";
2014-12-11 18:03:10 +01:00
}
2019-08-18 12:59:12 +02:00
return \@stdout;
2014-12-11 18:03:10 +01:00
}
2014-12-13 13:52:43 +01:00
2019-08-19 13:04:44 +02:00
sub _btrfs_filter_stderr
{
if(/^usage: / || /(unrecognized|invalid) option/) {
WARN_ONCE "Using unsupported btrfs-progs < v$BTRFS_PROGS_MIN";
}
# strip error prefix (we print our own)
# note that this also affects ssh_filter_btrbk.sh error strings
s/^ERROR: //;
}
2016-03-15 11:21:59 +01:00
sub btrfs_filesystem_show($)
{
my $vol = shift || die;
my $path = $vol->{PATH} // die;
2016-08-27 17:35:47 +02:00
return run_cmd( cmd => vinfo_cmd($vol, "btrfs filesystem show", { unsafe => $path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2019-08-19 13:04:44 +02:00
non_destructive => 1,
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
}
2016-03-09 19:52:45 +01:00
2016-03-15 11:21:59 +01:00
sub btrfs_filesystem_df($)
{
my $vol = shift || die;
my $path = $vol->{PATH} // die;
2016-08-27 17:35:47 +02:00
return run_cmd( cmd => vinfo_cmd($vol, "btrfs filesystem df", { unsafe => $path }),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2019-08-19 13:04:44 +02:00
non_destructive => 1,
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
}
sub btrfs_filesystem_usage($)
{
my $vol = shift || die;
my $path = $vol->{PATH} // die;
2016-08-27 17:35:47 +02:00
my $ret = run_cmd( cmd => vinfo_cmd($vol, "btrfs filesystem usage", { unsafe => $path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2019-08-19 13:04:44 +02:00
non_destructive => 1,
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
2019-08-19 13:04:44 +02:00
unless(defined($ret)) {
2019-12-15 18:01:16 +01:00
ERROR "Failed to fetch btrfs filesystem usage for: $vol->{PRINT}", @stderr;
2019-08-19 13:04:44 +02:00
return undef;
}
2016-03-15 11:21:59 +01:00
return undef unless(defined($ret));
my %detail;
2019-08-18 12:59:12 +02:00
foreach(@$ret) {
2022-05-29 14:04:07 +02:00
$detail{device_size} = $1, next if(/^\s+Device size:\s+(\S+)/);
$detail{device_allocated} = $1, next if(/^\s+Device allocated:\s+(\S+)/);
$detail{device_unallocated} = $1, next if(/^\s+Device unallocated:\s+(\S+)/);
$detail{device_missing} = $1, next if(/^\s+Device missing:\s+(\S+)/);
$detail{device_used} = $1, next if(/^\s+Used:\s+(\S+)/);
@detail{qw(free free_min)} = ($1,$2), next if(/^\s+Free \(estimated\):\s+(\S+)\s+\(min: (\S+)\)/);
$detail{data_ratio} = $1, next if(/^\s+Data ratio:\s+([0-9]+\.[0-9]+)/);
$detail{metadata_ratio} = $1, next if(/^\s+Metadata ratio:\s+([0-9]+\.[0-9]+)/);
$detail{used} = $1, next if(/^\s+Used:\s+(\S+)/);
@detail{qw(global_reserve global_reserve_used)} = ($1,$2), next if(/^\s+Global reserve:\s+(\S+)\s+\(used: (\S+)\)/);
TRACE "Failed to parse filesystem usage line \"$_\" for: $vol->{PRINT}" if($do_trace);
2016-03-15 11:21:59 +01:00
}
DEBUG "Parsed " . scalar(keys %detail) . " filesystem usage detail items: $vol->{PRINT}";
2016-12-11 15:35:00 +01:00
2022-05-29 14:04:07 +02:00
foreach (qw(device_size device_used data_ratio)) {
unless(defined($detail{$_})) {
ERROR "Failed to parse filesystem usage detail (unsupported btrfs-progs) for: $vol->{PRINT}";
return undef;
2016-12-11 15:35:00 +01:00
}
}
2022-05-29 14:04:07 +02:00
# calculate aggregate size / usage
if($detail{device_size} =~ /^([0-9]+\.[0-9]+)(.*)/) {
$detail{size} = sprintf('%.2f%s', $1 / $detail{data_ratio}, $2);
}
if($detail{device_used} =~ /^([0-9]+\.[0-9]+)(.*)/) {
$detail{used} = sprintf('%.2f%s', $1 / $detail{data_ratio}, $2);
}
2020-08-28 17:15:39 +02:00
TRACE(Data::Dumper->Dump([\%detail], ["btrfs_filesystem_usage($vol->{URL})"])) if($do_trace && $do_dumper);
2016-03-15 11:21:59 +01:00
return \%detail;
}
2018-06-29 16:56:59 +02:00
# returns hashref with keys: (uuid parent_uuid id gen cgen top_level)
# for btrfs root, returns at least: (id is_root)
2016-03-15 11:21:59 +01:00
# for btrfs-progs >= 4.1, also returns key: "received_uuid"
2018-06-29 16:56:59 +02:00
# if present, also returns (unvalidated) keys: (name creation_time flags)
2018-06-29 18:47:47 +02:00
sub btrfs_subvolume_show($;@)
2016-03-15 11:21:59 +01:00
{
my $vol = shift || die;
2018-06-29 18:47:47 +02:00
my %opts = @_;
my @cmd_options;
push(@cmd_options, '--rootid=' . $opts{rootid}) if($opts{rootid}); # btrfs-progs >= 4.12
2016-03-15 11:21:59 +01:00
my $path = $vol->{PATH} // die;
2018-06-29 18:47:47 +02:00
my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume show", @cmd_options, { unsafe => $path }),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-03-15 11:21:59 +01:00
non_destructive => 1,
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
return undef unless(defined($ret));
2019-08-18 12:59:12 +02:00
unless(scalar(@$ret)) {
2017-07-30 15:25:32 +02:00
ERROR "Failed to parse subvolume detail (unsupported btrfs-progs) for: $vol->{PRINT}";
return undef;
2016-03-15 11:21:59 +01:00
}
2017-07-30 15:25:32 +02:00
2018-06-29 18:44:06 +02:00
# NOTE: the first line starts with a path:
# - btrfs-progs < 4.12 prints the full (absolute, resolved) path
# - btrfs-progs >= 4.12 prints the relative path to btrfs root (or "/" if it is the root)
2016-03-15 11:21:59 +01:00
2016-11-16 15:02:49 +01:00
my %detail;
2019-08-18 12:59:12 +02:00
if($ret->[0] =~ / is (btrfs root|toplevel subvolume)$/) {
2016-03-15 11:21:59 +01:00
# btrfs-progs < 4.4 prints: "<subvol> is btrfs root"
# btrfs-progs >= 4.4 prints: "<subvol> is toplevel subvolume"
2016-11-16 15:02:49 +01:00
# btrfs-progs >= 4.8.3 does not enter here, as output shares format with regular subvolumes
$detail{id} = 5;
2016-03-15 11:21:59 +01:00
}
2017-07-30 15:25:32 +02:00
else {
2016-03-15 11:21:59 +01:00
my %trans = (
"Name" => "name",
"uuid" => "uuid",
"UUID" => "uuid", # btrfs-progs >= 4.1
"Parent uuid" => "parent_uuid",
"Parent UUID" => "parent_uuid", # btrfs-progs >= 4.1
"Received UUID" => "received_uuid", # btrfs-progs >= 4.1
"Creation time" => "creation_time",
"Object ID" => "id",
"Subvolume ID" => "id", # btrfs-progs >= 4.1
"Generation (Gen)" => "gen",
"Generation" => "gen", # btrfs-progs >= 4.1
"Gen at creation" => "cgen",
"Parent" => "parent_id",
"Parent ID" => "parent_id", # btrfs-progs >= 4.1
"Top Level" => "top_level",
"Top level ID" => "top_level", # btrfs-progs >= 4.1
"Flags" => "flags",
2021-10-23 11:13:08 +02:00
"Send transid" => "send_transid", # btrfs-progs >= 5.14.2
"Send time" => "send_time", # btrfs-progs >= 5.14.2
"Receive transid" => "receive_transid", # btrfs-progs >= 5.14.2
"Receive time" => "receive_time", # btrfs-progs >= 5.14.2
2015-04-16 12:00:04 +02:00
);
2019-08-18 12:59:12 +02:00
foreach(@$ret) {
2016-03-15 11:21:59 +01:00
next unless /^\s+(.+):\s+(.*)$/;
my ($key, $value) = ($1, $2);
if($trans{$key}) {
$detail{$trans{$key}} = $value;
2016-03-09 19:52:45 +01:00
} else {
2019-08-04 13:25:01 +02:00
DEBUG "Ignoring subvolume detail \"$key: $value\" for: $vol->{PRINT}";
2016-03-15 11:21:59 +01:00
}
}
DEBUG "Parsed " . scalar(keys %detail) . " subvolume detail items: $vol->{PRINT}";
2016-08-19 16:33:30 +02:00
2019-08-04 13:49:19 +02:00
# NOTE: as of btrfs-progs v4.6.1, flags are either "-" or "readonly"
$detail{readonly} = ($detail{flags} =~ /readonly/) ? 1 : 0 if($detail{flags});
2018-06-29 16:56:59 +02:00
# validate required keys
unless((defined($detail{parent_uuid}) && (($detail{parent_uuid} eq '-') || ($detail{parent_uuid} =~ /^$uuid_match$/))) &&
(defined($detail{id}) && ($detail{id} =~ /^\d+$/) && ($detail{id} >= 5)) &&
(defined($detail{gen}) && ($detail{gen} =~ /^\d+$/)) &&
(defined($detail{cgen}) && ($detail{cgen} =~ /^\d+$/)) &&
2019-08-04 13:49:19 +02:00
(defined($detail{top_level}) && ($detail{top_level} =~ /^\d+$/)) &&
(defined($detail{readonly})))
2018-06-29 16:56:59 +02:00
{
ERROR "Failed to parse subvolume detail (unsupported btrfs-progs) for: $vol->{PRINT}";
return undef;
}
2016-08-19 16:33:30 +02:00
2018-06-29 16:56:59 +02:00
# NOTE: filesystems created with btrfs-progs < 4.16 have no UUID for subvolid=5,
# assert {uuid} is either valid or undef
if(defined($detail{uuid}) && ($detail{uuid} !~ /^$uuid_match$/)) {
if($detail{id} == 5) {
DEBUG "No UUID on btrfs root (id=5): $vol->{PRINT}";
} else {
2016-03-15 11:21:59 +01:00
ERROR "Failed to parse subvolume detail (unsupported btrfs-progs) for: $vol->{PRINT}";
return undef;
2016-03-09 19:52:45 +01:00
}
2018-06-29 16:56:59 +02:00
delete $detail{uuid};
}
# NOTE: received_uuid is not required here, as btrfs-progs < 4.1 does not give us that information.
# no worries, we get this from btrfs_subvolume_list() for all subvols.
if(defined($detail{received_uuid}) && ($detail{received_uuid} ne '-') && ($detail{received_uuid} !~ /^$uuid_match$/)) {
ERROR "Failed to parse subvolume detail (unsupported btrfs-progs) for: $vol->{PRINT}";
return undef;
2016-03-09 19:52:45 +01:00
}
2018-06-29 16:56:59 +02:00
VINFO(\%detail, "detail") if($loglevel >=4);
2015-04-14 02:17:17 +02:00
}
2016-11-16 15:02:49 +01:00
2018-06-29 18:47:47 +02:00
if($opts{rootid} && ($detail{id} != $opts{rootid})) {
ERROR "Failed to parse subvolume detail (rootid mismatch) for: $vol->{PRINT}";
return undef;
}
2016-11-16 15:02:49 +01:00
if($detail{id} == 5) {
2020-08-28 18:38:06 +02:00
DEBUG "Found btrfs root: $vol->{PRINT}";
2018-06-29 16:56:59 +02:00
$detail{is_root} = 1;
2016-11-16 15:02:49 +01:00
}
2016-03-15 11:21:59 +01:00
return \%detail;
}
2015-04-14 16:03:31 +02:00
2016-03-15 11:21:59 +01:00
sub btrfs_subvolume_list_readonly_flag($)
{
my $vol = shift || die;
my $path = $vol->{PATH} // die;
2016-08-27 17:35:47 +02:00
my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume list", '-a', '-r', { unsafe => $path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-03-15 11:21:59 +01:00
non_destructive => 1,
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
return undef unless(defined($ret));
my %ro;
2019-08-18 13:11:33 +02:00
foreach(@$ret) {
unless(/^ID\s+([0-9]+)\s+gen\s+[0-9]+\s+top level\s+[0-9]+\s+path\s/) {
ERROR "Failed to parse subvolume list (unsupported btrfs-progs) for: $vol->{PRINT}";
DEBUG "Offending line: $_";
return undef;
}
2016-03-15 11:21:59 +01:00
$ro{$1} = 1;
}
DEBUG "Parsed " . scalar(keys %ro) . " readonly subvolumes for filesystem at: $vol->{PRINT}";
return \%ro;
2015-04-14 02:17:17 +02:00
}
2016-03-15 11:21:59 +01:00
sub btrfs_subvolume_list($;@)
{
my $vol = shift || die;
my %opts = @_;
2017-07-30 15:25:32 +02:00
my $path = $vol->{PATH} // die;
2016-03-15 11:21:59 +01:00
my @filter_options = ('-a');
push(@filter_options, '-o') if($opts{subvol_only});
2021-08-16 14:26:57 +02:00
push(@filter_options, '-d') if($opts{deleted_only});
2016-04-15 01:22:19 +02:00
# NOTE: btrfs-progs <= 3.17 do NOT support the '-R' flag.
# NOTE: Support for btrfs-progs <= 3.17 has been dropped in
# btrbk-0.23, the received_uuid flag very essential!
my @display_options = ('-c', '-u', '-q', '-R');
2016-08-27 17:35:47 +02:00
my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume list", @filter_options, @display_options, { unsafe => $path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-03-15 11:21:59 +01:00
non_destructive => 1,
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
return undef unless(defined($ret));
my @nodes;
2019-08-18 12:59:12 +02:00
foreach(@$ret)
2016-03-15 11:21:59 +01:00
{
my %node;
2017-10-09 23:04:07 +02:00
# NOTE: btrfs-progs >= 4.13.2 pads uuid's with 36 whitespaces
2018-06-03 18:16:26 +02:00
unless(/^ID \s+ ([0-9]+) \s+
gen \s+ ([0-9]+) \s+
cgen \s+ ([0-9]+) \s+
top\ level \s+ ([0-9]+) \s+
parent_uuid \s+ ([0-9a-f-]+) \s+
received_uuid \s+ ([0-9a-f-]+) \s+
uuid \s+ ([0-9a-f-]+) \s+
path \s+ (.+) $/x) {
2016-04-15 01:22:19 +02:00
ERROR "Failed to parse subvolume list (unsupported btrfs-progs) for: $vol->{PRINT}";
DEBUG "Offending line: $_";
return undef;
}
%node = (
2016-03-15 11:21:59 +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
);
# 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;
}
DEBUG "Parsed " . scalar(@nodes) . " total subvolumes for filesystem at: $vol->{PRINT}";
2018-07-09 14:29:28 +02:00
return \@nodes;
}
sub btrfs_subvolume_list_complete($)
{
my $vol = shift || die;
# fetch subvolume list
my $nodes = btrfs_subvolume_list($vol);
return undef unless($nodes);
2016-03-15 11:21:59 +01:00
# fetch readonly flag
# NOTE: the only way to get "readonly" flag is via a second call to "btrfs subvol list" with the "-r" option (as of btrfs-progs v4.3.1)
my $ro = btrfs_subvolume_list_readonly_flag($vol);
return undef unless(defined($ro));
2018-07-09 14:29:28 +02:00
foreach (@$nodes) {
2016-03-15 11:21:59 +01:00
$_->{readonly} = $ro->{$_->{id}} // 0;
}
2016-03-07 17:35:17 +01:00
2018-07-09 14:29:28 +02:00
# btrfs root (id=5) is not provided by btrfs_subvolume_list above, read it separately (best-efford)
my $tree_root = btrfs_subvolume_show($vol, rootid => 5);
unless($tree_root) {
# this is not an error:
# - btrfs-progs < 4.12 does not support rootid lookup
# - UUID can be missing if filesystem was created with btrfs-progs < 4.16
DEBUG "Failed to fetch subvolume detail (old btrfs-progs?) for btrfs root (id=5) on: $vol->{PRINT}";
$tree_root = { id => 5, is_root => 1 };
}
unshift(@$nodes, $tree_root);
return $nodes;
2016-03-07 17:35:17 +01:00
}
2016-03-15 11:21:59 +01:00
sub btrfs_subvolume_find_new($$;$)
2015-04-16 12:00:04 +02:00
{
2016-03-15 11:21:59 +01:00
my $vol = shift || die;
my $path = $vol->{PATH} // die;
my $lastgen = shift // die;
2016-08-27 17:35:47 +02:00
my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume find-new", { unsafe => $path }, $lastgen ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-03-15 11:21:59 +01:00
non_destructive => 1,
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2019-05-23 15:36:34 +02:00
large_output => 1,
2016-03-15 11:21:59 +01:00
);
unless(defined($ret)) {
2019-12-15 18:01:16 +01:00
ERROR "Failed to fetch modified files for: $vol->{PRINT}", @stderr;
2016-03-15 11:21:59 +01:00
return undef;
}
2015-04-16 12:00:04 +02:00
2016-03-15 11:21:59 +01:00
my %files;
my $parse_errors = 0;
my $transid_marker;
2019-08-18 12:59:12 +02:00
foreach(@$ret)
2015-04-19 11:36:40 +02:00
{
2016-03-15 11:21:59 +01:00
if(/^inode \S+ file offset (\S+) len (\S+) disk start \S+ offset \S+ gen (\S+) flags (\S+) (.+)$/) {
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
2020-12-21 00:20:02 +01:00
if($flags ne "NONE") {
$files{$name}->{flags}{$_} = 1 foreach split(/\|/, $flags);
2016-03-15 11:21:59 +01:00
}
}
elsif(/^transid marker was (\S+)$/) {
$transid_marker = $1;
}
else {
2020-12-21 00:20:02 +01:00
ERROR "Failed to parse output from `btrfs subvolume find-new`:", $_;
2016-03-15 11:21:59 +01:00
$parse_errors++;
}
2015-04-16 12:00:04 +02:00
}
2020-12-21 00:20:02 +01:00
ERROR "Failed to parse $parse_errors lines from `btrfs subvolume find-new`" if($parse_errors);
2015-04-16 12:00:04 +02:00
2016-03-15 11:21:59 +01:00
return { files => \%files,
transid_marker => $transid_marker,
parse_errors => $parse_errors,
};
2015-04-19 11:36:40 +02:00
}
2015-04-16 12:00:04 +02:00
2015-04-19 11:36:40 +02:00
2016-03-15 11:21:59 +01:00
# returns $target, or undef on error
sub btrfs_subvolume_snapshot($$)
2015-10-12 23:58:38 +02:00
{
2016-03-15 11:21:59 +01:00
my $svol = shift || die;
my $target_vol = shift // die;
my $target_path = $target_vol->{PATH} // die;
my $src_path = $svol->{PATH} // die;
2016-04-16 21:08:07 +02:00
INFO "[snapshot] source: $svol->{PRINT}";
INFO "[snapshot] target: $target_vol->{PRINT}";
2016-03-15 11:21:59 +01:00
start_transaction("snapshot",
vinfo_prefixed_keys("target", $target_vol),
vinfo_prefixed_keys("source", $svol),
);
2016-08-27 17:35:47 +02:00
my $ret = run_cmd(cmd => vinfo_cmd($svol, "btrfs subvolume snapshot", '-r', { unsafe => $src_path }, { unsafe => $target_path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($svol),
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2016-03-15 11:21:59 +01:00
);
2017-09-27 19:35:43 +02:00
end_transaction("snapshot", defined($ret));
2016-04-12 17:50:12 +02:00
unless(defined($ret)) {
2019-12-15 18:01:16 +01:00
ERROR "Failed to create snapshot: $svol->{PRINT} -> $target_path", @stderr;
2016-04-12 17:50:12 +02:00
return undef;
}
return $target_vol;
2015-10-12 23:58:38 +02:00
}
2016-03-15 11:21:59 +01:00
sub btrfs_subvolume_delete($@)
2015-01-09 18:09:32 +01:00
{
2022-07-27 20:35:58 +02:00
my $vol = shift // die;
2015-09-10 11:22:19 +02:00
my %opts = @_;
2022-07-27 20:35:58 +02:00
my $target_type = $vol->{node}{TARGET_TYPE} || "";
2017-06-30 18:49:17 +02:00
my $ret;
2022-07-27 20:35:58 +02:00
INFO "[delete] target: $vol->{PRINT}";
start_transaction($opts{type} // "delete", vinfo_prefixed_keys("target", $vol));
2017-06-30 18:49:17 +02:00
if($target_type eq "raw") {
2022-07-27 20:35:58 +02:00
$ret = run_cmd(cmd => [ 'rm', '-f',
{ unsafe => $vol->{PATH}, postfix => ($vol->{node}{BTRBK_RAW}{split} && ".split_??") },
{ unsafe => $vol->{PATH}, postfix => ".info" },
],
rsh => vinfo_rsh($vol),
2017-06-30 18:49:17 +02:00
);
}
else {
my @options;
2022-07-27 20:35:58 +02:00
push @options, "--commit-each" if($opts{commit});
$ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs subvolume delete", @options, { unsafe => $vol->{PATH} } ),
rsh => vinfo_rsh($vol),
2019-08-19 13:04:44 +02:00
fatal_stderr => sub { m/^ERROR: /; }, # probably not needed, "btrfs sub delete" returns correct exit status
filter_stderr => \&_btrfs_filter_stderr,
2017-06-30 18:49:17 +02:00
);
}
2022-07-27 20:35:58 +02:00
end_transaction($opts{type} // "delete", defined($ret));
2017-09-27 20:23:08 +02:00
2022-07-27 20:35:58 +02:00
unless(defined($ret)) {
ERROR "Failed to delete subvolume: $vol->{PRINT}", @stderr;
return undef;
2017-09-27 20:23:08 +02:00
}
2022-07-27 20:35:58 +02:00
return $vol;
2015-01-09 18:09:32 +01:00
}
2017-10-02 14:00:09 +02:00
sub btrfs_qgroup_destroy($@)
{
my $vol = shift // die;
my %opts = @_;
my $vol_id = $vol->{node}{id};
unless($vol_id) {
ERROR "Unknown subvolume_id for: $vol->{PRINT}";
return undef;
}
my $path = $vol->{PATH} // die;
my $qgroup_id = "0/$vol_id";
INFO "[qgroup-destroy] qgroup_id: $qgroup_id";
INFO "[qgroup-destroy] subvolume: $vol->{PRINT}";
start_transaction($opts{type} // "qgroup_destroy",
vinfo_prefixed_keys("target", $vol));
my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs qgroup destroy", $qgroup_id, { unsafe => $path }),
rsh => vinfo_rsh($vol),
2019-08-19 13:04:44 +02:00
filter_stderr => \&_btrfs_filter_stderr,
2017-10-02 14:00:09 +02:00
);
end_transaction($opts{type} // "qgroup_destroy", defined($ret));
unless(defined($ret)) {
2019-12-15 18:01:16 +01:00
ERROR "Failed to destroy qgroup \"$qgroup_id\" for subvolume: $vol->{PRINT}", @stderr;
2017-10-02 14:00:09 +02:00
return undef;
}
return $vol;
}
2022-10-30 16:40:53 +01:00
sub _btrfs_send_options($$;$$)
{
my $snapshot = shift;
my $target = shift;
my $parent = shift;
my $clone_src = shift // [];
2022-10-30 16:57:00 +01:00
my $send_protocol = config_key($target, "send_protocol");
my $send_compressed_data = config_key($target, "send_compressed_data");
2022-10-30 16:40:53 +01:00
my @send_options;
push(@send_options, '-p', { unsafe => $parent->{PATH} } ) if($parent);
push(@send_options, '-c', { unsafe => $_ } ) foreach(map { $_->{PATH} } @$clone_src);
2022-10-30 16:57:00 +01:00
push(@send_options, '--proto', $send_protocol ) if($send_protocol);
push(@send_options, '--compressed-data' ) if($send_compressed_data);
2022-10-30 16:40:53 +01:00
#push(@send_options, '-v') if($loglevel >= 3);
return \@send_options;
}
2017-10-02 14:00:09 +02:00
2019-04-05 12:06:41 +02:00
sub btrfs_send_receive($$;$$$)
2016-03-08 15:25:35 +01:00
{
2016-03-15 11:21:59 +01:00
my $snapshot = shift || die;
my $target = shift || die;
my $parent = shift;
2019-04-09 22:09:12 +02:00
my $clone_src = shift // [];
2016-03-15 11:21:59 +01:00
my $ret_vol_received = shift;
my $vol_received = vinfo_child($target, $snapshot->{NAME});
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
2016-04-23 14:58:08 +02:00
print STDOUT "Creating backup: $vol_received->{PRINT}\n" if($show_progress && (not $dryrun));
2016-03-15 11:21:59 +01:00
2019-04-09 22:09:12 +02:00
INFO "[send/receive] target: $vol_received->{PRINT}";
2016-04-16 21:08:07 +02:00
INFO "[send/receive] source: $snapshot->{PRINT}";
INFO "[send/receive] parent: $parent->{PRINT}" if($parent);
2019-04-09 22:09:12 +02:00
INFO "[send/receive] clone-src: $_->{PRINT}" foreach(@$clone_src);
2016-03-15 11:21:59 +01:00
2019-07-29 21:59:03 +02:00
my $stream_options = config_stream_hash($snapshot, $target);
2022-02-06 20:41:10 +01:00
my $compat_ignore_err = config_key_lru($target, "compat", "ignore_receive_errors");
2019-07-29 21:59:03 +02:00
2022-10-30 16:40:53 +01:00
my $send_options = _btrfs_send_options($snapshot, $target, $parent, $clone_src);
2016-03-15 11:21:59 +01:00
my @receive_options;
2022-02-06 20:41:10 +01:00
push(@receive_options, '--max-errors=0') if($compat_ignore_err);
2016-03-15 11:21:59 +01:00
my @cmd_pipe;
push @cmd_pipe, {
2022-10-30 16:40:53 +01:00
cmd => vinfo_cmd($snapshot, "btrfs send", @$send_options, { unsafe => $snapshot->{PATH} }),
2019-07-29 21:59:03 +02:00
rsh => vinfo_rsh($snapshot, disable_compression => $stream_options->{stream_compress}),
stream_options => $stream_options,
2019-08-19 13:04:44 +02:00
filter_stderr => [ \&_btrfs_filter_stderr, sub { $_ = undef if(/^At subvol/) } ],
2016-03-15 11:21:59 +01:00
};
2019-07-29 21:59:03 +02:00
2016-03-15 11:21:59 +01:00
push @cmd_pipe, {
2022-10-30 16:40:53 +01:00
cmd => vinfo_cmd($target, "btrfs receive", @receive_options, { unsafe => $target->{PATH} . '/' } ),
2019-07-29 21:59:03 +02:00
rsh => vinfo_rsh($target, disable_compression => $stream_options->{stream_compress}),
2022-02-06 20:41:10 +01:00
fatal_stderr => sub {
# NOTE: btrfs-progs < 4.11: if "btrfs send" fails, "btrfs receive" returns 0!
if($compat_ignore_err && s/^ERROR: (.*)//) {
WARN "Ignoring btrfs receive error (compat=ignore_receive_errors): $1";
}
m/^ERROR: /;
},
2016-03-15 11:21:59 +01:00
};
my $send_receive_error = 0;
start_transaction("send-receive",
vinfo_prefixed_keys("target", $vol_received),
vinfo_prefixed_keys("source", $snapshot),
vinfo_prefixed_keys("parent", $parent),
);
my $ret = run_cmd(@cmd_pipe);
2019-08-19 13:04:44 +02:00
my @cmd_err;
2016-03-15 11:21:59 +01:00
unless(defined($ret)) {
2019-08-19 13:04:44 +02:00
@cmd_err = @stderr; # save for later
2016-03-15 11:21:59 +01:00
$send_receive_error = 1;
}
2016-03-08 15:25:35 +01:00
2018-07-09 19:24:52 +02:00
# Read in target subvolume metadata (btrfs subvolume show):
# Double checking the output increases robustness against exotic
# revisions of external commands (btrfs-progs, pv, xz, lz4, ...).
#
# NOTE: we cannot rely on the underlying shell to have
# "pipefail" functionality.
#
# NOTE: btrfs-progs < 4.11:
# "cat /dev/null | btrfs receive" returns with exitcode=0 and no
# error message, having the effect that silently no subvolume is
# created if any command in @cmd_pipe fail.
my $is_garbled;
if($dryrun) {
INFO "[send/receive] (dryrun, skip) checking target metadata: $vol_received->{PRINT}";
}
else {
INFO "[send/receive] checking target metadata: $vol_received->{PRINT}";
my $detail = btrfs_subvolume_show($vol_received);
if(defined($detail)) {
unless($send_receive_error) {
2016-08-29 13:08:45 +02:00
# plausibility checks on target detail
unless($detail->{readonly}) {
2019-08-19 13:04:44 +02:00
push @cmd_err, "target is not readonly: $vol_received->{PRINT}";
2016-08-29 13:08:45 +02:00
$send_receive_error = 1;
}
if($detail->{received_uuid} && ($detail->{received_uuid} eq '-')) {
# NOTE: received_uuid is not in @required_keys (needs btrfs-progs >= 4.1 (BTRFS_PROGS_MIN))
# so we only check it if it's really present
2019-08-19 13:04:44 +02:00
push @cmd_err, "received_uuid is not set on target: $vol_received->{PRINT}";
2016-08-29 13:08:45 +02:00
$send_receive_error = 1;
}
if($parent && ($detail->{parent_uuid} eq '-')) {
2019-08-19 13:04:44 +02:00
push @cmd_err, "parent_uuid is not set on target: $vol_received->{PRINT}";
2016-08-29 13:08:45 +02:00
$send_receive_error = 1;
}
if((not $parent) && ($detail->{parent_uuid} ne '-')) {
2019-08-19 13:04:44 +02:00
push @cmd_err, "parent_uuid is set on target: $vol_received->{PRINT}";
2016-08-29 13:08:45 +02:00
$send_receive_error = 1;
}
2016-08-19 16:33:30 +02:00
}
2018-07-09 19:24:52 +02:00
# incomplete received (garbled) subvolumes are not readonly and have no received_uuid
$is_garbled = ((not $detail->{readonly}) && defined($detail->{received_uuid}) && ($detail->{received_uuid} eq '-'));
}
else {
2019-08-19 13:04:44 +02:00
push @cmd_err, "failed to check target subvolume: $vol_received->{PRINT}", @stderr;
2018-07-09 19:24:52 +02:00
$send_receive_error = 1;
2016-08-19 16:33:30 +02:00
}
}
2016-03-08 15:25:35 +01:00
2017-09-27 19:35:43 +02:00
end_transaction("send-receive", not $send_receive_error);
2015-10-10 21:26:59 +02:00
2016-03-15 11:21:59 +01:00
if($send_receive_error) {
2022-10-30 16:40:53 +01:00
ERROR "Failed to send/receive subvolume: $snapshot->{PRINT}" . ($parent ? " [$parent->{PATH}]" : "") . " -> $vol_received->{PRINT}", @cmd_err;
2018-07-09 19:24:52 +02:00
}
2016-03-15 11:21:59 +01:00
2018-07-09 19:24:52 +02:00
if($is_garbled) {
# NOTE: btrfs-progs does not delete incomplete received (garbled) subvolumes,
2016-03-15 11:21:59 +01:00
# we need to do this by hand.
# TODO: remove this as soon as btrfs-progs handle receive errors correctly.
2022-07-27 20:35:58 +02:00
if(btrfs_subvolume_delete($vol_received, commit => "after", type => "delete_garbled")) {
2016-03-15 11:21:59 +01:00
WARN "Deleted partially received (garbled) subvolume: $vol_received->{PRINT}";
2022-07-27 20:35:58 +02:00
} else {
2016-03-15 11:21:59 +01:00
WARN "Deletion of partially received (garbled) subvolume failed, assuming clean environment: $vol_received->{PRINT}";
2015-10-10 21:26:59 +02:00
}
}
2018-07-09 19:24:52 +02:00
return $send_receive_error ? undef : 1;
2015-10-10 21:26:59 +02:00
}
2017-06-16 17:43:17 +02:00
sub btrfs_send_to_file($$$;$$)
2015-01-16 17:29:04 +01:00
{
2016-03-15 11:21:59 +01:00
my $source = shift || die;
my $target = shift || die;
my $parent = shift;
my $ret_vol_received = shift;
2017-06-16 17:43:17 +02:00
my $ret_raw_info = shift;
2016-03-15 11:21:59 +01:00
my $target_path = $target->{PATH} // die;
my $parent_uuid = $parent ? $parent->{node}{uuid} : undef ;
my $received_uuid = $source->{node}{uuid};
die unless($received_uuid);
die if($parent && !$parent_uuid);
2015-01-16 17:29:04 +01:00
2017-06-16 17:43:17 +02:00
# prepare raw_info (for vinfo_inject_child)
my %raw_info = (
TYPE => 'raw',
RECEIVED_UUID => $received_uuid,
INCOMPLETE => 1,
);
2016-03-15 11:21:59 +01:00
my $target_filename = $source->{NAME} || die;
$target_filename .= ".btrfs";
2017-03-18 15:06:48 +01:00
my $compress = config_compress_hash($target, "raw_target_compress");
my $encrypt = config_encrypt_hash($target, "raw_target_encrypt");
my $split = config_key($target, "raw_target_split");
2019-07-29 21:59:03 +02:00
my $stream_options = config_stream_hash($source, $target);
# make sure we dont re-compress, override "stream_compress" with "raw_target_compress"
$stream_options->{stream_compress} = $compress if($compress);
2017-03-18 15:06:48 +01:00
2022-10-30 16:40:53 +01:00
my $send_options = _btrfs_send_options($source, $target, $parent);
2016-03-15 11:21:59 +01:00
my @cmd_pipe;
push @cmd_pipe, {
2022-10-30 16:40:53 +01:00
cmd => vinfo_cmd($source, "btrfs send", @$send_options, { unsafe => $source->{PATH} } ),
2019-07-29 21:59:03 +02:00
rsh => vinfo_rsh($source, disable_compression => $stream_options->{stream_compress}),
stream_options => $stream_options,
2019-08-19 13:04:44 +02:00
filter_stderr => [ \&_btrfs_filter_stderr, sub { $_ = undef if(/^At subvol/) } ],
fatal_stderr => sub { m/^ERROR: /; },
2016-03-15 11:21:59 +01:00
};
2019-07-29 21:59:03 +02:00
2017-03-18 15:06:48 +01:00
if($compress) {
2019-07-29 21:59:03 +02:00
$raw_info{compress} = $compression{$compress->{key}}->{format};
2017-03-18 15:06:48 +01:00
$target_filename .= '.' . $compression{$compress->{key}}->{format};
2019-07-29 21:59:03 +02:00
push @cmd_pipe, { compress_stdin => $compress }; # does nothing if already compressed by stream_compress
2015-01-16 17:29:04 +01:00
}
2017-06-16 17:04:18 +02:00
if($encrypt) {
$target_filename .= ($encrypt->{type} eq "gpg") ? '.gpg' : '.encrypted';
}
# NOTE: $ret_vol_received must always be set when function returns!
my $vol_received = vinfo_child($target, $target_filename);
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
2017-03-18 15:06:48 +01:00
if($encrypt) {
2017-06-16 17:43:17 +02:00
$raw_info{encrypt} = $encrypt->{type};
if($encrypt->{type} eq "gpg") {
# NOTE: We set "--no-random-seed-file" since one of the btrbk
# design principles is to never create any files unasked. Enabling
# "--no-random-seed-file" creates ~/.gnupg/random_seed, and as
# such depends on $HOME to be set correctly (which e.g. is set to
# "/" by some cron daemons). From gpg2(1) man page:
# --no-random-seed-file GnuPG uses a file to store its
# internal random pool over invocations This makes random
# generation faster; however sometimes write operations are not
# desired. This option can be used to achieve that with the cost
# of slower random generation.
my @gpg_options = ( '--batch', '--no-tty', '--no-random-seed-file', '--trust-model', 'always' );
push @gpg_options, ( '--compress-algo', 'none' ) if($compress); # NOTE: if --compress-algo is not set, gpg might still compress according to OpenPGP standard.
2021-08-16 16:58:51 +02:00
push(@gpg_options, ( '--no-default-keyring', '--keyring', { unsafe => $encrypt->{keyring} } )) if($encrypt->{keyring});
2022-05-29 16:57:52 +02:00
if($encrypt->{recipient}) {
push(@gpg_options, '--no-default-recipient');
push(@gpg_options, map +( '--recipient', $_ ), @{$encrypt->{recipient}});
2022-05-06 21:06:19 +02:00
}
2017-06-16 17:43:17 +02:00
push @cmd_pipe, {
cmd => [ 'gpg', @gpg_options, '--encrypt' ],
compressed_ok => ($compress ? 1 : 0),
};
}
2017-06-16 17:04:18 +02:00
elsif($encrypt->{type} eq "openssl_enc") {
# encrypt using "openssl enc"
$raw_info{cipher} = $encrypt->{ciphername};
# NOTE: iv is always generated locally!
my $iv_size = $encrypt->{iv_size};
my $iv;
if($iv_size) {
INFO "Generating iv for openssl encryption (cipher=$encrypt->{ciphername})";
$iv = system_urandom($iv_size, 'hex');
unless($iv) {
ERROR "Failed generate IV for openssl_enc: $source->{PRINT}";
return undef;
}
$raw_info{iv} = $iv;
}
2017-06-30 14:35:20 +02:00
my $encrypt_key;
if($encrypt->{keyfile}) {
if($encrypt->{kdf_backend}) {
WARN "Both openssl_keyfile and kdf_backend are configured, ignoring kdf_backend!";
}
2021-08-16 16:58:51 +02:00
$encrypt_key = '$(cat ' . quoteshell($encrypt->{keyfile}) . ')';
2017-06-30 14:35:20 +02:00
}
elsif($encrypt->{kdf_backend}) {
if($encrypt->{kdf_keygen_each}) {
$kdf_session_key = undef;
%kdf_vars = ();
}
if($kdf_session_key) {
INFO "Reusing session key for: $vol_received->{PRINT}";
}
else {
# run kdf backend, set session key and vars
DEBUG "Generating session key for: $vol_received->{PRINT}";
2019-08-19 13:04:44 +02:00
my $key_target_text = $encrypt->{kdf_keygen_each} ? "\"$vol_received->{PRINT}\"" : "all raw backups";
2017-06-30 14:35:20 +02:00
2019-08-19 13:04:44 +02:00
print STDOUT "\nGenerate session key for $key_target_text:\n";
2021-08-16 16:58:51 +02:00
my $kdf_values = run_cmd(cmd => [ { unsafe => $encrypt->{kdf_backend} }, $encrypt->{kdf_keysize} ],
2017-06-30 14:35:20 +02:00
non_destructive => 1,
);
2019-08-19 13:04:44 +02:00
unless(defined($kdf_values)) {
2019-12-15 18:01:16 +01:00
ERROR "Failed to generate session key for $key_target_text", @stderr;
2019-08-19 13:04:44 +02:00
return undef;
}
2017-06-30 14:35:20 +02:00
return undef unless(defined($kdf_values));
2019-08-18 12:59:12 +02:00
foreach(@$kdf_values) {
2017-06-30 14:35:20 +02:00
chomp;
next if /^\s*$/; # ignore empty lines
if(/^KEY=([0-9a-fA-f]+)/) {
$kdf_session_key = $1;
}
2022-11-20 15:49:31 +01:00
elsif(/^([a-z_]+)=($raw_info_value_match)/) {
2017-06-30 14:35:20 +02:00
my $info_key = 'kdf_' . $1;
my $info_val = $2;
DEBUG "Adding raw_info from kdf_backend: $info_key=$info_val";
$kdf_vars{$info_key} = $info_val;
}
else {
ERROR "Ambiguous line from kdf_backend: $encrypt->{kdf_backend}";
return undef;
}
}
unless($kdf_session_key && (length($kdf_session_key) == ($encrypt->{kdf_keysize} * 2))) {
ERROR "Ambiguous key value from kdf_backend: $encrypt->{kdf_backend}";
return undef;
}
INFO "Generated session key for: $vol_received->{PRINT}";
}
$encrypt_key = $kdf_session_key;
%raw_info = ( %kdf_vars, %raw_info );
}
2017-06-16 17:04:18 +02:00
my @openssl_options = (
'-' . $encrypt->{ciphername},
2017-06-30 14:35:20 +02:00
'-K', $encrypt_key,
2017-06-16 17:04:18 +02:00
);
push @openssl_options, ('-iv', $iv) if($iv);
push @cmd_pipe, {
cmd => [ 'openssl', 'enc', '-e', @openssl_options ],
compressed_ok => ($compress ? 1 : 0),
};
}
2017-06-16 17:43:17 +02:00
else {
die "Usupported encryption type (raw_target_encrypt)";
}
2015-01-16 17:29:04 +01:00
}
2017-03-18 14:47:43 +01:00
if($split) {
2017-06-16 17:43:17 +02:00
# NOTE: we do not append a ".split" suffix on $target_filename here, as this propagates to ".info" file
$raw_info{split} = $split;
2017-03-18 14:47:43 +01:00
push @cmd_pipe, {
2021-08-15 12:28:56 +02:00
cmd => [ 'split', '-b', uc($split), '-', { unsafe => "${target_path}/${target_filename}.split_" } ],
2019-07-29 21:59:03 +02:00
rsh => vinfo_rsh($target, disable_compression => $stream_options->{stream_compress}),
2017-03-18 15:06:48 +01:00
compressed_ok => ($compress ? 1 : 0),
2017-03-18 14:47:43 +01:00
}
}
else {
push @cmd_pipe, {
# NOTE: We use "dd" instead of shell redirections here, as it is
# common to have special filesystems (like NFS, SMB, FUSE) mounted
# on $target_path. By using "dd" we make sure to write in
# reasonably large blocks (default=128K), which is not always the
# case when using redirections (e.g. "gpg > outfile" writes in 8K
# blocks).
# Another approach would be to always pipe through "cat", which
# uses st_blksize from fstat(2) (with a minimum of 128K) to
# determine the block size.
2021-08-15 12:28:56 +02:00
cmd => [ 'dd', 'status=none', 'bs=' . config_key($target, "raw_target_block_size"), { prefix => "of=", unsafe => "${target_path}/${target_filename}" } ],
2017-06-16 17:43:17 +02:00
#redirect_to_file => { unsafe => "${target_path}/${target_filename}" }, # alternative (use shell redirection), less overhead on local filesystems (barely measurable):
2019-07-29 21:59:03 +02:00
rsh => vinfo_rsh($target, disable_compression => $stream_options->{stream_compress}),
2017-03-18 15:06:48 +01:00
compressed_ok => ($compress ? 1 : 0),
2017-03-18 14:47:43 +01:00
};
}
2016-03-15 11:21:59 +01:00
2017-06-16 17:43:17 +02:00
$raw_info{FILE} = $target_filename;
$raw_info{RECEIVED_PARENT_UUID} = $parent_uuid if($parent_uuid);
# disabled for now, as its not very useful and might leak information:
# $raw_info{parent_url} = $parent->{URL} if($parent);
# $raw_info{target_url} = $vol_received->{URL};
$$ret_raw_info = \%raw_info if($ret_raw_info);
2016-04-23 14:58:08 +02:00
print STDOUT "Creating raw backup: $vol_received->{PRINT}\n" if($show_progress && (not $dryrun));
2016-03-15 11:21:59 +01:00
2020-08-02 14:03:38 +02:00
INFO "[send-to-raw] target: $vol_received->{PRINT}";
2016-04-16 21:08:07 +02:00
INFO "[send-to-raw] source: $source->{PRINT}";
INFO "[send-to-raw] parent: $parent->{PRINT}" if($parent);
2016-03-15 11:21:59 +01:00
start_transaction("send-to-raw",
vinfo_prefixed_keys("target", $vol_received),
vinfo_prefixed_keys("source", $source),
vinfo_prefixed_keys("parent", $parent),
);
2017-06-16 17:43:17 +02:00
my $ret;
$ret = system_write_raw_info($vol_received, \%raw_info);
2019-08-19 13:04:44 +02:00
my @cmd_err;
2017-03-18 14:47:43 +01:00
if(defined($ret)) {
$ret = run_cmd(@cmd_pipe);
2019-08-19 13:04:44 +02:00
@cmd_err = @stderr unless(defined($ret)); # save for later
}
else {
push @cmd_err, "failed to write raw .info file: $vol_received->{PATH}.info", @stderr;
2017-03-18 14:47:43 +01:00
}
2016-03-15 11:21:59 +01:00
if(defined($ret)) {
2017-03-18 14:47:43 +01:00
# Test target file for "exists and size > 0" after writing, as we
# can not rely on the exit status of the command pipe, and a shell
# redirection as well as "dd" always creates the target file.
# Note that "split" does not create empty files.
2017-06-16 17:43:17 +02:00
my $test_postfix = ($split ? ".split_aa" : "");
2019-08-19 13:04:44 +02:00
my $check_file = "${target_path}/${target_filename}${test_postfix}";
DEBUG "Testing target data file (non-zero size): $check_file";
2021-08-15 13:10:57 +02:00
$ret = run_cmd(cmd => [ 'test', '-s', { unsafe => $check_file } ],
rsh => vinfo_rsh($target),
);
2016-03-22 19:05:12 +01:00
if(defined($ret)) {
2017-06-16 17:43:17 +02:00
delete $raw_info{INCOMPLETE};
2022-10-19 11:24:24 +02:00
$ret = system_write_raw_info($vol_received, { INCOMPLETE => 0 }, append => 1);
2022-10-19 11:24:11 +02:00
} else {
2019-08-19 13:04:44 +02:00
push @cmd_err, "failed to check target file (not present or zero length): $check_file";
}
2016-03-15 11:21:59 +01:00
}
2017-09-27 19:35:43 +02:00
end_transaction("send-to-raw", defined($ret));
2016-03-15 11:21:59 +01:00
unless(defined($ret)) {
2022-10-30 16:40:53 +01:00
ERROR "Failed to send btrfs subvolume to raw file: $source->{PRINT}" . ($parent ? " [$parent->{PATH}]" : "") . " -> $vol_received->{PRINT}", @cmd_err;
2015-10-23 21:28:58 +02:00
return undef;
}
2015-04-07 11:52:45 +02:00
return 1;
2015-01-16 17:29:04 +01:00
}
2018-08-27 14:52:28 +02:00
sub system_list_mountinfo($)
{
my $vol = shift // die;
2018-08-27 14:54:32 +02:00
my $file = '/proc/self/mountinfo'; # NOTE: /proc/self/mounts is deprecated
2021-08-15 13:10:57 +02:00
my $ret = run_cmd(cmd => [ 'cat', $file ],
2018-08-27 14:52:28 +02:00
rsh => vinfo_rsh($vol),
non_destructive => 1,
);
return undef unless(defined($ret));
2021-08-17 18:39:59 +02:00
unless(@$ret) {
ERROR "Failed to parse \"$vol->{URL_PREFIX}$file\": no output";
return undef;
}
2018-08-27 14:52:28 +02:00
my @mountinfo;
2019-08-18 12:59:12 +02:00
foreach(@$ret)
2018-08-27 14:52:28 +02:00
{
# https://www.kernel.org/doc/Documentation/filesystems/proc.txt
2021-07-15 13:47:24 +02:00
unless(/^(?<mount_id>[0-9]+) # mount ID: unique identifier of the mount (may be reused after umount)
\s(?<parent_id>[0-9]+) # parent ID: ID of parent (or of self for the top of the mount tree)
\s(?<st_dev>[0-9]+:[0-9]+) # major:minor: value of st_dev for files on filesystem
\s(?<fs_root>\S+) # root: root of the mount within the filesystem
\s(?<mount_point>\S+) # mount point: mount point relative to the process's root
\s(?<mount_options>\S+) # mount options: per mount options
2018-08-27 14:52:28 +02:00
(\s\S+)* # optional fields: zero or more fields of the form "tag[:value]"
2021-07-15 13:47:24 +02:00
\s- # separator: marks the end of the optional fields
2018-08-27 14:52:28 +02:00
\s(?<fs_type>\S+) # filesystem type: name of filesystem of the form "type[.subtype]"
2021-07-15 13:47:24 +02:00
\s(?<mount_source>\S+) # mount source: filesystem specific information or "none"
\s(?<super_options>\S+)$ # super options: per super block options
2018-08-27 14:52:28 +02:00
/x)
{
2019-04-01 00:32:24 +02:00
ERROR "Failed to parse \"$vol->{URL_PREFIX}$file\"";
2018-08-27 14:52:28 +02:00
DEBUG "Offending line: $_";
return undef;
}
my %line = %+;
btrbk: fix mountinfo parsing (octal encoded chars)
Making sure this is done after splitting, as encoded value could be a
comma.
After some testing it shows that the kernel [1] produces ambigous
output in "super options" if a subvolume containing a comma is mounted
using "-o subvolid=" (tried hard to mount with "-o subvol=", seems not
possible via shell):
# btrfs sub create /tmp/btrbk_unittest/mnt_source/svol\,comma
# mount /dev/loop0 -o subvolid=282 '/tmp/btrbk_unittest/mount,comma'
# cat /proc/self/mountinfo
[...]
48 40 0:319 /svol,comma /tmp/btrbk_unittest/mount,comma rw,relatime - btrfs /dev/loop0 rw,ssd,noacl,space_cache,subvolid=282,subvol=/svol,comma
^^^^^^^^^^^^^^^^^^
[1] sys-kernel/gentoo-sources-5.10.45
2021-07-14 21:47:09 +02:00
2021-08-17 18:39:59 +02:00
unless(defined(check_file($line{mount_point}, { absolute => 1 }))) {
ERROR "Ambiguous mount point in \"$vol->{URL_PREFIX}$file\": $line{mount_point}";
return undef;
}
2019-09-08 18:06:31 +02:00
# merge super_options and mount_options to MNTOPS.
btrbk: fix mountinfo parsing (octal encoded chars)
Making sure this is done after splitting, as encoded value could be a
comma.
After some testing it shows that the kernel [1] produces ambigous
output in "super options" if a subvolume containing a comma is mounted
using "-o subvolid=" (tried hard to mount with "-o subvol=", seems not
possible via shell):
# btrfs sub create /tmp/btrbk_unittest/mnt_source/svol\,comma
# mount /dev/loop0 -o subvolid=282 '/tmp/btrbk_unittest/mount,comma'
# cat /proc/self/mountinfo
[...]
48 40 0:319 /svol,comma /tmp/btrbk_unittest/mount,comma rw,relatime - btrfs /dev/loop0 rw,ssd,noacl,space_cache,subvolid=282,subvol=/svol,comma
^^^^^^^^^^^^^^^^^^
[1] sys-kernel/gentoo-sources-5.10.45
2021-07-14 21:47:09 +02:00
my %mntops;
foreach (split(',', delete($line{super_options})),
split(',', delete($line{mount_options})))
{
2018-08-27 14:52:28 +02:00
if(/^(.+?)=(.+)$/) {
btrbk: fix mountinfo parsing (octal encoded chars)
Making sure this is done after splitting, as encoded value could be a
comma.
After some testing it shows that the kernel [1] produces ambigous
output in "super options" if a subvolume containing a comma is mounted
using "-o subvolid=" (tried hard to mount with "-o subvol=", seems not
possible via shell):
# btrfs sub create /tmp/btrbk_unittest/mnt_source/svol\,comma
# mount /dev/loop0 -o subvolid=282 '/tmp/btrbk_unittest/mount,comma'
# cat /proc/self/mountinfo
[...]
48 40 0:319 /svol,comma /tmp/btrbk_unittest/mount,comma rw,relatime - btrfs /dev/loop0 rw,ssd,noacl,space_cache,subvolid=282,subvol=/svol,comma
^^^^^^^^^^^^^^^^^^
[1] sys-kernel/gentoo-sources-5.10.45
2021-07-14 21:47:09 +02:00
$mntops{$1} = $2;
2018-08-27 14:52:28 +02:00
} else {
btrbk: fix mountinfo parsing (octal encoded chars)
Making sure this is done after splitting, as encoded value could be a
comma.
After some testing it shows that the kernel [1] produces ambigous
output in "super options" if a subvolume containing a comma is mounted
using "-o subvolid=" (tried hard to mount with "-o subvol=", seems not
possible via shell):
# btrfs sub create /tmp/btrbk_unittest/mnt_source/svol\,comma
# mount /dev/loop0 -o subvolid=282 '/tmp/btrbk_unittest/mount,comma'
# cat /proc/self/mountinfo
[...]
48 40 0:319 /svol,comma /tmp/btrbk_unittest/mount,comma rw,relatime - btrfs /dev/loop0 rw,ssd,noacl,space_cache,subvolid=282,subvol=/svol,comma
^^^^^^^^^^^^^^^^^^
[1] sys-kernel/gentoo-sources-5.10.45
2021-07-14 21:47:09 +02:00
$mntops{$_} = 1;
2018-08-27 14:52:28 +02:00
}
}
btrbk: fix mountinfo parsing (octal encoded chars)
Making sure this is done after splitting, as encoded value could be a
comma.
After some testing it shows that the kernel [1] produces ambigous
output in "super options" if a subvolume containing a comma is mounted
using "-o subvolid=" (tried hard to mount with "-o subvol=", seems not
possible via shell):
# btrfs sub create /tmp/btrbk_unittest/mnt_source/svol\,comma
# mount /dev/loop0 -o subvolid=282 '/tmp/btrbk_unittest/mount,comma'
# cat /proc/self/mountinfo
[...]
48 40 0:319 /svol,comma /tmp/btrbk_unittest/mount,comma rw,relatime - btrfs /dev/loop0 rw,ssd,noacl,space_cache,subvolid=282,subvol=/svol,comma
^^^^^^^^^^^^^^^^^^
[1] sys-kernel/gentoo-sources-5.10.45
2021-07-14 21:47:09 +02:00
$mntops{rw} = 0 if($mntops{ro}); # e.g. mount_options="ro", super_options="rw"
# decode values (octal, e.g. "\040" = whitespace)
s/\\([0-7]{3})/chr(oct($1))/eg foreach(values %line, values %mntops);
$line{MNTOPS} = \%mntops;
2018-08-27 14:52:28 +02:00
push @mountinfo, \%line;
}
2020-08-28 17:15:39 +02:00
# TRACE(Data::Dumper->Dump([\@mountinfo], ["mountinfo"])) if($do_trace && $do_dumper);
2018-08-27 14:52:28 +02:00
return \@mountinfo;
}
2020-05-24 00:13:10 +02:00
sub system_testdir($)
2016-03-30 15:32:28 +02:00
{
my $vol = shift // die;
2020-05-24 00:13:10 +02:00
my $path = $vol->{PATH} // die;
my $ret = run_cmd(cmd => vinfo_cmd($vol, "test", '-d', { unsafe => $path } ),
rsh => vinfo_rsh($vol),
non_destructive => 1,
);
return undef unless(defined($ret));
DEBUG "Directory exists: $vol->{PRINT}";
return 1;
}
2016-03-30 15:32:28 +02:00
2020-05-24 00:13:10 +02:00
sub system_realpath($)
{
my $vol = shift // die;
my $path = $vol->{PATH} // die;
2022-02-06 16:26:29 +01:00
my $compat = config_key_lru($vol, "compat", "busybox");
2020-05-24 00:13:10 +02:00
my @options = ("-v"); # report error messages
push @options, "-e" unless($compat); # all components must exist (not available in busybox!)
push @options, "-f" if($compat); # all but the last component must exist.
my $ret = run_cmd(cmd => vinfo_cmd($vol, "readlink", @options, { unsafe => $path } ),
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-03-30 15:32:28 +02:00
non_destructive => 1,
);
return undef unless(defined($ret));
2020-05-24 00:13:10 +02:00
my $realpath = scalar(@$ret) ? (check_file($ret->[0], { absolute => 1 }) // "") : "";
unless($realpath) {
2019-08-18 12:59:12 +02:00
ERROR "Failed to parse output of `realpath` for \"$vol->{PRINT}\": \"$ret->[0]\"";
2016-03-30 15:32:28 +02:00
return undef;
}
2016-04-02 14:10:35 +02:00
DEBUG "Real path for \"$vol->{PRINT}\" is: $realpath";
2020-05-24 00:13:10 +02:00
return undef if($compat && !system_testdir($vol));
2016-04-02 14:10:35 +02:00
return $realpath;
2016-03-30 15:32:28 +02:00
}
2016-04-14 18:46:35 +02:00
sub system_mkdir($)
{
my $vol = shift // die;
my $path = $vol->{PATH} // die;;
INFO "Creating directory: $vol->{PRINT}/";
2019-04-18 17:28:12 +02:00
start_transaction("mkdir", vinfo_prefixed_keys("target", $vol));
2021-08-15 13:10:57 +02:00
my $ret = run_cmd(cmd => [ 'mkdir', '-p', { unsafe => $path } ],
2016-05-10 15:51:44 +02:00
rsh => vinfo_rsh($vol),
2016-04-14 18:46:35 +02:00
);
2019-04-18 17:28:12 +02:00
end_transaction("mkdir", defined($ret));
2016-04-14 18:46:35 +02:00
return undef unless(defined($ret));
return 1;
}
2017-06-16 17:43:17 +02:00
sub system_read_raw_info_dir($)
{
my $droot = shift // die;
my $ret = run_cmd(
# NOTE: we cannot simply "cat" all files here, as it will fail if no files found
cmd => [ 'find', { unsafe => $droot->{PATH} },
'-maxdepth', '1',
'-type', 'f',
2022-10-14 06:30:12 +02:00
'!', '-size', '0',
2022-11-16 00:34:58 +01:00
'-name', '\*.btrfs.\*info', # match ".btrfs[.gz|.bz2|.xz|...][.gpg].info"
2022-11-19 18:29:04 +01:00
'-print0',
'-exec', 'cat \{\} \;',
'-exec', 'printf "\0\0" \;',
2017-06-16 17:43:17 +02:00
],
rsh => vinfo_rsh($droot),
non_destructive => 1,
);
unless(defined($ret)) {
2018-02-07 20:17:23 +01:00
ERROR("Failed to read *.btrfs.*.info files in: $droot->{PATH}");
2017-06-16 17:43:17 +02:00
return undef;
}
my @raw_targets;
2022-11-20 11:20:25 +01:00
foreach my $info_text (split "\000\000", join "\n", @$ret) {
unless($info_text =~ s/^(.*?)\000//s) {
2018-02-07 20:17:23 +01:00
ERROR("Error while parsing command output for: $droot->{PATH}");
2017-06-16 17:43:17 +02:00
return undef;
}
2022-11-19 18:29:04 +01:00
my $info_file = check_file($1, { absolute => 1 }, error_statement => 'for raw info file')
// return undef;
2022-11-20 11:11:50 +01:00
my $name = ($info_file =~ s/^.*\///r);
$name =~ s/\.info$//;
2022-11-20 11:20:25 +01:00
2022-11-20 11:11:50 +01:00
my $raw_info = {
INFO_FILE => $info_file,
NAME => $name,
};
2022-11-20 11:20:25 +01:00
foreach my $line (split "\n", $info_text) {
my ($key, $value) = ($line =~ /^([a-zA-Z_]+)=(.*)/);
next unless $key;
if($key eq "FILE") {
WARN("Ignoring ambiguous \"FILE=$value\" from raw info file, using \"$name\": $info_file") if($value ne $name);
next;
}
unless($value =~ /^$raw_info_value_match$/) {
ERROR("Failed to parse \"$key=$value\" in raw info file: $info_file");
return undef;
}
$raw_info->{$key} = $value;
2022-11-19 18:29:04 +01:00
}
# input validation (we need to abort here, or the backups will be resumed)
2017-06-16 17:43:17 +02:00
unless($raw_info->{TYPE} && ($raw_info->{TYPE} eq 'raw')) {
2022-11-19 18:29:04 +01:00
ERROR("Unsupported \"type\" in raw info file: $info_file");
2017-06-16 17:43:17 +02:00
return undef;
}
unless($raw_info->{RECEIVED_UUID} && ($raw_info->{RECEIVED_UUID} =~ /^$uuid_match$/)) {
2022-11-19 18:29:04 +01:00
ERROR("Missing/Illegal \"received_uuid\" in raw info file: $info_file");
2017-06-16 17:43:17 +02:00
return undef;
}
if(defined $raw_info->{RECEIVED_PARENT_UUID}) {
unless(($raw_info->{RECEIVED_PARENT_UUID} eq '-') || ($raw_info->{RECEIVED_PARENT_UUID} =~ /^$uuid_match$/)) {
2022-11-19 18:29:04 +01:00
ERROR("Illegal \"RECEIVED_PARENT_UUID\" in raw info file: $info_file");
2017-06-16 17:43:17 +02:00
return undef;
}
}
else {
$raw_info->{RECEIVED_PARENT_UUID} = '-';
}
2022-11-19 18:29:04 +01:00
push @raw_targets, $raw_info;
2017-06-16 17:43:17 +02:00
}
DEBUG("Parsed " . @raw_targets . " raw info files in path: $droot->{PATH}");
2020-08-28 17:15:39 +02:00
TRACE(Data::Dumper->Dump([\@raw_targets], ["system_read_raw_info_dir($droot->{URL})"])) if($do_trace && $do_dumper);
2017-06-16 17:43:17 +02:00
return \@raw_targets;
}
2022-10-19 11:18:40 +02:00
sub system_write_raw_info($$;@)
2017-06-16 17:43:17 +02:00
{
my $vol = shift // die;
my $raw_info = shift // die;
2022-10-19 11:18:40 +02:00
my %opts = @_;
my $append = $opts{append};
2017-06-16 17:43:17 +02:00
my $info_file = $vol->{PATH} . '.info';
# sort by %raw_info_sort, then by key
2022-10-19 11:18:40 +02:00
my @line = $append ? () : ("#btrbk-v$VERSION", "# Do not edit this file");
2022-11-20 15:57:23 +01:00
my @subst;
2022-10-19 11:16:57 +02:00
push @line, '#t=' . time;
2022-10-19 11:18:40 +02:00
foreach(sort { (($raw_info_sort{$a} // 99) <=> ($raw_info_sort{$b} // 99)) || ($a cmp $b) } keys %$raw_info) {
2022-11-20 15:57:23 +01:00
push @line, ($_ . '=%s');
push @subst, $raw_info->{$_};
2017-06-16 17:43:17 +02:00
}
2022-10-19 11:18:40 +02:00
DEBUG "Writing (" . ($append ? "append:" . join(",", keys %$raw_info) : "create") . ") raw info file: $info_file";
2017-06-16 17:43:17 +02:00
my $ret = run_cmd(
2022-11-20 15:57:23 +01:00
{ cmd => [ 'printf', quoteshell(join('\n', @line, "")), map quoteshell($_), @subst ] },
2022-10-19 11:18:40 +02:00
{ ($append ? "append_to_file" : "redirect_to_file") => { unsafe => $info_file },
2017-06-16 17:43:17 +02:00
rsh => vinfo_rsh($vol),
});
return undef unless(defined($ret));
return $info_file;
}
2017-06-16 17:04:18 +02:00
sub system_urandom($;$) {
my $size = shift;
my $format = shift || 'hex';
die unless(($size > 0) && ($size <= 256)); # sanity check
unless(open(URANDOM, '<', '/dev/urandom')) {
ERROR "Failed to open /dev/urandom: $!";
return undef;
}
binmode URANDOM;
my $rand;
my $rlen = read(URANDOM, $rand, $size);
close(FILE);
unless(defined($rand) && ($rlen == $size)) {
ERROR "Failed to read from /dev/urandom: $!";
return undef;
}
if($format eq 'hex') {
my $hex = unpack('H*', $rand);
die unless(length($hex) == ($size * 2)); # paranoia check
return $hex;
}
elsif($format eq 'bin') {
return $rand;
}
die "unsupported format";
}
2019-08-07 00:26:12 +02:00
sub read_extentmap_cache($)
{
my $vol = shift;
my $cache_dir = config_key($vol, 'cache_dir');
return undef unless($cache_dir);
my $uuid = $vol->{node}{uuid} // die;
foreach (@$cache_dir) {
my $file = "$_/${uuid}.extentmap.bin";
next unless (-f $file);
DEBUG "Reading extentmap cache: $file";
if(open(my $fh, '<:raw', $file)) {
my @range;
my $buf;
2020-11-08 13:33:23 +01:00
read($fh, $buf, 24 + 8 * 2); # read header
my ($v, $gen, $time) = unpack('a24Q<Q<', $buf);
unless(($v =~ /^btrbk_extentmap_v1/) && $gen && $time) {
2019-08-07 00:26:12 +02:00
ERROR "Ambigous cache file: $file";
next;
}
if($gen != $vol->{node}{gen}) {
2020-12-12 17:05:14 +01:00
WARN "Subvolume generation has changed (cache=$gen, subvol=$vol->{node}{gen}), ignoring cache: $file";
2019-08-07 00:26:12 +02:00
next;
}
while(read $fh, $buf, 8 * 2) { # read / unpack two words
push @range, [ unpack('Q<Q<', $buf) ];
#TRACE "read_extentmap_cache: range " . join("..", @{$range[-1]});
};
2020-11-08 13:33:23 +01:00
DEBUG "Read " . scalar(@range) . " regions (gen=$gen, timestamp='" . localtime($time) . "') from: $file";
return \@range;
2019-08-07 00:26:12 +02:00
} else {
ERROR "Failed to open '$file': $!";
}
}
return undef;
}
sub write_extentmap_cache($)
{
my $vol = shift;
my $extmap = $vol->{EXTENTMAP};
my $cache_dir = config_key($vol, 'cache_dir');
return undef unless($extmap && $cache_dir);
my $uuid = $vol->{node}{uuid} // die;
foreach (@$cache_dir) {
unless(-d $_) {
WARN_ONCE "Ignoring cache_dir (not a directory): $_";
next;
}
my $file = "$_/${uuid}.extentmap.bin";
INFO "Writing extentmap cache: $file";
if(open(my $fh, '>:raw', $file)) {
# pack Q: unsigned quad (64bit, Documentation/filesystems/fiemap.txt)
2020-11-08 13:33:23 +01:00
print $fh pack('a24Q<Q<', "btrbk_extentmap_v1", $vol->{node}{gen}, time);
print $fh pack('Q<*', map(@{$_}, @$extmap));
2019-08-07 00:26:12 +02:00
close($fh);
} else {
ERROR "Failed to create '$file': $!";
}
}
}
2017-06-16 17:04:18 +02:00
2019-05-23 15:36:34 +02:00
# returns extents range (sorted array of [start,end], inclusive) from FIEMAP ioctl
sub filefrag_extentmap($)
{
my $vol = shift || die;
my $starttime = time;
2020-11-08 22:59:30 +01:00
INFO("Fetching extent map (filefrag): $vol->{PRINT}");
2019-05-23 15:36:34 +02:00
# NOTE: this returns exitstatus=0 if file is not found, or no files found
2021-08-15 13:10:57 +02:00
my $ret = run_cmd(cmd => [ 'find', { unsafe => $vol->{PATH} }, '-xdev', '-type', 'f',
'-exec', 'filefrag -b1 -v \{\} +' ],
large_output => 1);
2019-05-23 15:36:34 +02:00
unless(defined($ret)) {
2020-11-08 22:59:30 +01:00
ERROR "Failed to fetch extent map: $vol->{PRINT}", @stderr;
2019-05-23 15:36:34 +02:00
return undef;
}
2020-11-08 20:59:11 +01:00
WARN_ONCE "Configuration option \"ignore_extent_data_inline=no\" not available for filefrag (please install \"IO::AIO\" perl module)" unless(config_key($vol, "ignore_extent_data_inline"));
2019-05-23 15:36:34 +02:00
my @range; # array of [start,end]
foreach (@$ret) {
2020-11-08 20:59:11 +01:00
#my $file = $1 if(/^File size of (.*?) is/);
2019-05-23 15:36:34 +02:00
if(/^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):/) {
2020-11-08 20:59:11 +01:00
# NOTE: filefrag (v1.45.5) returns wrong (?) physical_offset for
# "inline" regions unless run with `-b1` (blocksize=1) option.
#
# For btrfs file systems it does not make much sense to consider
# the "inline" extents anyways: these are stored in metadata
# section and are not really part of the used disk space.
#
# # filefrag -v MYFILE
# File size of MYFILE is 2307 (1 block of 4096 bytes)
# ext: logical_offset: physical_offset: length: expected: flags:
# 0: 0.. 4095: 0.. 4095: 4096: last,not_aligned,inline,eof
# # filefrag -v -b1 MYFILE
# File size of MYFILE is 2307 (4096 block of 1 bytes)
# ext: logical_offset: physical_offset: length: expected: flags:
# 0: 0.. 4095: 0.. 4095: 4096: last,not_aligned,inline,eof
next if(/inline/);
2019-05-23 15:36:34 +02:00
push @range, [ $1, $2 ];
}
}
2020-11-08 13:33:23 +01:00
DEBUG("Parsed " . scalar(@range) . " regions in " . (time - $starttime) . "s for: $vol->{PRINT}");
return extentmap_merge(\@range);
2019-05-23 15:36:34 +02:00
}
2019-05-28 20:45:50 +02:00
# returns extents range (sorted array of [start,end], inclusive) from FIEMAP ioctl
sub aio_extentmap($)
{
my $vol = shift || die;
2020-11-08 17:10:48 +01:00
my $starttime = time;
2020-11-08 20:59:11 +01:00
my $ignore_inline = config_key($vol, "ignore_extent_data_inline");
2019-05-28 20:45:50 +02:00
2020-11-08 22:59:30 +01:00
INFO("Fetching extent map: $vol->{PRINT}");
2020-11-08 17:10:48 +01:00
2019-05-28 20:45:50 +02:00
# NOTE: this returns exitstatus=0 if file is not found, or no files found
2021-08-15 13:10:57 +02:00
my $ret = run_cmd(cmd => [ 'find', { unsafe => $vol->{PATH} }, '-xdev', '-type', 'f' ],
large_output => 1 );
2019-05-28 20:45:50 +02:00
unless(defined($ret)) {
ERROR "Failed to find files in: $vol->{PRINT}", @stderr;
return undef;
}
2020-11-08 17:10:48 +01:00
DEBUG("Reading ioctl FIEMAP of " . scalar(@$ret) . " files");
2019-05-28 20:45:50 +02:00
2020-11-08 17:10:48 +01:00
IO::AIO::max_outstanding(128); # < 1024 (max file descriptors)
IO::AIO::max_poll_reqs(32);
2019-05-28 20:45:50 +02:00
my @range;
my $count = 0;
2020-11-08 20:59:11 +01:00
my $inline_count = 0;
2019-05-28 20:45:50 +02:00
foreach my $file (@$ret) {
IO::AIO::aio_open($file, IO::AIO::O_RDONLY(), 0, sub {
2020-11-08 22:59:30 +01:00
# graceful abort on file open errors (check $count below)
return unless($_[0]); # [ $fh ]
# note: aio_fiemap returns byte range (not blocks)
# see: Documentation/filesystems/fiemap.rst
2020-11-08 17:10:48 +01:00
IO::AIO::aio_fiemap($_[0], 0, undef, 0, undef, sub {
2019-05-28 20:45:50 +02:00
$count++;
2020-11-08 22:59:30 +01:00
foreach(@{$_[0]}) { # [ $logical, $physical, $length, $flags ]
2020-11-08 20:59:11 +01:00
if($_->[3] & IO::AIO::FIEMAP_EXTENT_DATA_INLINE()) {
$inline_count++;
next if($ignore_inline);
2020-11-08 22:59:30 +01:00
WARN_ONCE "Ambigous inline region [$_->[1] .. $_->[1] + $_->[2] - 1] for $file" if((($_->[1] != 0) || ($_->[2] != 4096)));
2020-11-08 17:10:48 +01:00
}
push @range, [ $_->[1], $_->[1] + $_->[2] - 1 ];
}
2019-05-28 20:45:50 +02:00
});
});
2020-11-08 17:10:48 +01:00
# poll, or the above eats up all our filedescriptors
IO::AIO::poll_cb(); # takes "max_outstanding" and "max_poll_reqs" settings
2019-05-28 20:45:50 +02:00
}
IO::AIO::flush();
2020-11-08 17:10:48 +01:00
WARN "Failed to open $count / " . scalar(@$ret) . " files" if($count != scalar(@$ret));
2020-11-08 20:59:11 +01:00
DEBUG("Parsed " . scalar(@range) . " regions (" . ($ignore_inline ? "ignored " : "") . "$inline_count \"inline\") for $count files in " . (time - $starttime) . "s for: $vol->{PRINT}");
2019-05-28 20:45:50 +02:00
return extentmap_merge(\@range);
}
2019-05-23 15:36:34 +02:00
sub extentmap_total_blocks($)
{
my $extmap = shift;
my $count = 0;
foreach(@{$extmap->{rmap}}) {
$count += ($_->[1] - $_->[0] + 1);
}
return $count;
}
sub extentmap_size($)
{
2020-11-08 13:33:23 +01:00
my $extmap = shift; # merged ranges
return undef unless($extmap);
my $size = 0;
foreach(@$extmap) {
$size += $_->[1] - $_->[0] + 1;
}
return $size;
2019-05-23 15:36:34 +02:00
}
sub extentmap_merge(@) {
return undef unless(scalar(@_));
2020-11-08 13:33:23 +01:00
my @range = sort { $a->[0] <=> $b->[0] } map @$_, @_;
2019-05-23 15:36:34 +02:00
my @merged;
my $start = -1;
my $end = -2;
2020-11-08 13:33:23 +01:00
foreach (@range) {
2019-05-23 15:36:34 +02:00
if($_->[0] <= $end + 1) {
# range overlaps the preceeding one, or is adjacent to it
$end = $_->[1] if($_->[1] > $end);
}
else {
push @merged, [ $start, $end ] if($start >= 0);
$start = $_->[0];
$end = $_->[1];
}
}
push @merged, [ $start, $end ] if($start >= 0);
DEBUG "extentmap: merged " . scalar(@range) . " regions into " . scalar(@merged) . " regions";
2020-11-08 13:33:23 +01:00
return \@merged;
2019-05-23 15:36:34 +02:00
}
# ( A \ B ) : data in A that is not in B (relative complement of B in A)
sub extentmap_diff($$)
{
2020-11-08 13:33:23 +01:00
my $l = shift // die; # A, sorted
my $r = shift; # B, sorted
return $l unless($r); # A \ 0 = A
2019-05-23 15:36:34 +02:00
my $i = 0;
my $rn = scalar(@$r);
my @diff;
foreach(@$l) {
my $l_start = $_->[0];
my $l_end = $_->[1];
while(($i < $rn) && ($r->[$i][1] < $l_start)) { # r_end < l_start
# advance r to next overlapping
$i++;
}
while(($i < $rn) && ($r->[$i][0] <= $l_end)) { # r_start <= l_end
# while overlapping, advance l_start
my $r_start = $r->[$i][0];
my $r_end = $r->[$i][1];
push @diff, [ $l_start, $r_start - 1 ] if($l_start < $r_start);
$l_start = $r_end + 1;
last if($l_start > $l_end);
$i++;
}
push @diff, [ $l_start, $l_end ] if($l_start <= $l_end);
}
DEBUG "extentmap: relative complement ( B=" . scalar(@$r) . ' \ A=' . scalar(@$l) . " ) = " . scalar(@diff) . " regions";
2020-11-08 13:33:23 +01:00
return \@diff;
2019-05-23 15:36:34 +02:00
}
2018-02-05 18:03:20 +01:00
sub btr_tree($$$$)
2015-10-23 14:43:36 +02:00
{
2016-03-15 11:21:59 +01:00
my $vol = shift;
my $vol_root_id = shift || die;
2018-08-27 14:54:32 +02:00
my $mount_source = shift || die; # aka device
2020-08-22 13:37:03 +02:00
my $mountpoints = shift || die; # all known mountpoints for this filesystem: arrayref of mountinfo
2016-04-14 18:21:00 +02:00
die unless($vol_root_id >= 5);
2018-02-14 22:09:45 +01:00
2018-08-27 14:54:32 +02:00
# return parsed tree from %mount_source_cache if present
2020-08-22 13:37:03 +02:00
my $host_mount_source = $vol->{URL_PREFIX} . $mount_source;
2018-08-27 14:54:32 +02:00
my $cached_tree = $mount_source_cache{$host_mount_source};
2020-08-28 17:15:39 +02:00
TRACE "mount_source_cache " . ($cached_tree ? "HIT" : "MISS") . ": $host_mount_source" if($do_trace);
2018-02-14 22:09:45 +01:00
if($cached_tree) {
2020-08-28 17:15:39 +02:00
TRACE "btr_tree: returning cached tree at id=$vol_root_id" if($do_trace);
2018-02-14 22:09:45 +01:00
my $node = $cached_tree->{ID_HASH}{$vol_root_id};
2018-08-27 14:54:32 +02:00
ERROR "Unknown subvolid=$vol_root_id in btrfs tree of $host_mount_source" unless($node);
2018-02-14 22:09:45 +01:00
return $node;
}
2018-07-09 14:29:28 +02:00
my $node_list = btrfs_subvolume_list_complete($vol);
2016-03-15 11:21:59 +01:00
return undef unless(ref($node_list) eq "ARRAY");
2016-04-05 16:37:23 +02:00
my $vol_root;
2016-02-29 23:19:55 +01:00
2020-08-28 17:15:39 +02:00
TRACE "btr_tree: processing subvolume list of: $vol->{PRINT}" if($do_trace);
2016-02-29 23:19:55 +01:00
2019-04-01 00:32:24 +02:00
# return a reference to the cached root if we already know the tree,
# making sure every tree is only stored once, which is essential
# e.g. when injecting nodes. die if duplicate UUID exist on
# different file systems (no matter if local or remote).
#
2018-01-30 13:20:44 +01:00
# note: this relies on subvolume UUID's to be "universally unique"
# (which is why cloning btrfs filesystems using "dd" is a bad idea)
2019-04-01 00:32:24 +02:00
#
# note: a better way would be to always compare the UUID of
# subvolid=5. unfortunately this is not possible for filesystems
# created with btrfs-progs < 4.16 (no UUID for subvolid=5).
2018-07-09 14:29:28 +02:00
foreach(@$node_list) {
my $node_uuid = $_->{uuid};
next unless($node_uuid);
if($uuid_cache{$node_uuid}) {
# at least one uuid of $node_list is already known
2020-08-28 17:15:39 +02:00
TRACE "uuid_cache HIT: $node_uuid" if($do_trace);
2018-07-09 14:29:28 +02:00
$vol_root = $uuid_cache{$node_uuid}->{TREE_ROOT}->{ID_HASH}->{$vol_root_id};
2021-08-16 14:26:57 +02:00
unless($vol_root) {
# check for deleted subvolumes: e.g. still mounted, but deleted elsewhere
my $deleted_nodes = btrfs_subvolume_list($vol, deleted_only => 1);
return undef unless(ref($deleted_nodes) eq "ARRAY");
if(grep ($_->{id} eq $vol_root_id), @$deleted_nodes) {
ERROR "Subvolume is deleted: id=$vol_root_id mounted on: $vol->{PRINT}";
return undef;
}
ERROR "Subvolume id=$vol_root_id is not present on known btrfs tree: $vol->{PRINT}",
"Possible causes:",
" - Mismatch in mountinfo",
" - Subvolume was deleted while btrbk is running",
" - Duplicate UUID present on multiple filesystems: $node_uuid";
ERROR "Refusing to run on unstable environment; exiting";
exit 1;
}
2019-04-01 00:32:24 +02:00
INFO "Assuming same filesystem: \"$vol_root->{TREE_ROOT}->{host_mount_source}\", \"$host_mount_source\"";
2020-08-28 17:15:39 +02:00
TRACE "btr_tree: returning already parsed tree at id=$vol_root->{id}" if($do_trace);
2019-04-01 00:32:24 +02:00
$mount_source_cache{$host_mount_source} = $vol_root->{TREE_ROOT};
2018-07-09 14:29:28 +02:00
return $vol_root;
}
last; # check only first UUID (for performance)
2016-04-05 16:37:23 +02:00
}
2018-02-15 17:42:41 +01:00
# fill our hashes and uuid_cache
2018-07-09 14:29:28 +02:00
my %id;
my %uuid_hash;
my %received_uuid_hash;
2018-10-18 17:52:01 +02:00
my %parent_uuid_hash;
2016-04-12 17:50:12 +02:00
my $gen_max = 0;
2018-07-09 14:29:28 +02:00
foreach my $node (@$node_list) {
my $node_id = $node->{id};
2018-07-09 18:34:57 +02:00
my $node_uuid = $node->{uuid};
2018-07-09 14:29:28 +02:00
die unless($node_id >= 5);
die "duplicate node id" if(exists($id{$node_id}));
$id{$node_id} = $node;
2018-07-09 18:34:57 +02:00
if($node_uuid) {
# NOTE: uuid on btrfs root (id=5) is not always present
$uuid_hash{$node_uuid} = $node;
$uuid_cache{$node_uuid} = $node;
# hacky: if root node has no "uuid", it also has no "received_uuid" and no "gen"
push(@{$received_uuid_hash{$node->{received_uuid}}}, $node) if($node->{received_uuid} ne '-');
2018-10-18 17:52:01 +02:00
push(@{$parent_uuid_hash{$node->{parent_uuid}}}, $node) if($node->{parent_uuid} ne '-');
2018-07-09 18:34:57 +02:00
$gen_max = $node->{gen} if($node->{gen} > $gen_max);
2018-07-09 14:29:28 +02:00
}
elsif(not $node->{is_root}) {
die "missing uuid on subvolume";
}
$node->{SUBTREE} = [];
2015-10-23 14:43:36 +02:00
}
2018-07-09 14:29:28 +02:00
my $tree_root = $id{5} // die "missing btrfs root";
$tree_root->{ID_HASH} = \%id;
$tree_root->{UUID_HASH} = \%uuid_hash;
$tree_root->{RECEIVED_UUID_HASH} = \%received_uuid_hash;
2018-10-18 17:52:01 +02:00
$tree_root->{PARENT_UUID_HASH} = \%parent_uuid_hash;
2018-06-29 19:00:12 +02:00
$tree_root->{GEN_MAX} = $gen_max;
2020-08-22 13:37:03 +02:00
$tree_root->{URL_PREFIX} = $vol->{URL_PREFIX}; # hacky, first url prefix for logging
2019-04-01 00:32:24 +02:00
# NOTE: host_mount_source is NOT dependent on MACHINE_ID:
# if we return already present tree (see above), the value of
# host_mount_source will still point to the mount_source of the
# first machine.
2020-05-23 18:00:31 +02:00
$tree_root->{mount_source} = $mount_source;
2020-08-22 13:37:03 +02:00
$tree_root->{host_mount_source} = $host_mount_source; # unique identifier, e.g. "/dev/sda1" or "ssh://hostname[:port]/dev/sda1"
2018-06-29 19:00:12 +02:00
$vol_root = $id{$vol_root_id};
unless($vol_root) {
ERROR "Failed to resolve tree root for subvolid=$vol_root_id: " . ($vol->{PRINT} // $vol->{id});
return undef;
}
2016-03-15 11:21:59 +01:00
2018-07-09 14:29:28 +02:00
# set REL_PATH and tree references (TREE_ROOT, SUBTREE, TOP_LEVEL)
foreach my $node (@$node_list) {
unless($node->{is_root}) {
# note: it is possible that id < top_level, e.g. after restoring
my $top_level = $id{$node->{top_level}};
die "missing top_level reference" unless(defined($top_level));
2015-10-23 14:43:36 +02:00
2018-07-09 14:29:28 +02:00
push(@{$top_level->{SUBTREE}}, $node);
$node->{TOP_LEVEL} = $top_level;
2016-03-15 11:21:59 +01:00
2018-07-09 14:29:28 +02:00
# "path" always starts with set REL_PATH
my $rel_path = $node->{path};
unless($top_level->{is_root}) {
2019-07-31 13:44:26 +02:00
die unless($rel_path =~ s/^\Q$top_level->{path}\E\///);
2018-07-09 14:29:28 +02:00
}
$node->{REL_PATH} = $rel_path; # relative to {TOP_LEVEL}->{path}
2016-03-15 11:21:59 +01:00
}
2018-07-09 14:29:28 +02:00
$node->{TREE_ROOT} = $tree_root;
2016-04-19 13:06:31 +02:00
add_btrbk_filename_info($node);
2015-10-23 14:43:36 +02:00
}
2018-02-14 22:17:32 +01:00
# add known mountpoints to nodes
2020-08-22 13:37:03 +02:00
my %mountpoints_hash;
2018-02-14 22:17:32 +01:00
foreach(@$mountpoints) {
2020-08-22 13:37:03 +02:00
my $node_id = $_->{MNTOPS}{subvolid};
my $node = $id{$node_id};
2018-02-14 22:17:32 +01:00
unless($node) {
2020-10-19 00:34:11 +02:00
WARN "Unknown subvolid=$node_id (in btrfs tree of $host_mount_source) for mountpoint: $vol->{URL_PREFIX}$_->{mount_point}";
2018-02-14 22:17:32 +01:00
next;
}
2020-08-22 13:37:03 +02:00
$mountpoints_hash{$node_id} = $node;
push @{$node->{MOUNTINFO}}, $_; # if present, node is mounted at MOUNTINFO
2018-02-14 22:17:32 +01:00
}
2020-08-22 13:37:03 +02:00
$tree_root->{MOUNTED_NODES} = [ (values %mountpoints_hash) ]; # list of mounted nodes
2018-02-14 22:17:32 +01:00
2020-08-28 17:15:39 +02:00
TRACE "btr_tree: returning tree at id=$vol_root->{id}" if($do_trace);
2016-04-28 13:03:15 +02:00
VINFO($vol_root, "node") if($loglevel >=4);
2015-10-23 14:43:36 +02:00
2018-08-27 14:54:32 +02:00
$mount_source_cache{$host_mount_source} = $tree_root;
2016-03-15 11:21:59 +01:00
return $vol_root;
2015-10-23 14:43:36 +02:00
}
2018-10-18 17:52:01 +02:00
sub btr_tree_inject_node($$$)
2016-04-12 17:50:12 +02:00
{
my $top_node = shift;
my $detail = shift;
my $rel_path = shift;
my $subtree = $top_node->{SUBTREE} // die;
my $tree_root = $top_node->{TREE_ROOT};
2018-02-15 17:42:41 +01:00
die unless($detail->{parent_uuid} && $detail->{received_uuid} && exists($detail->{readonly}));
2016-04-12 17:50:12 +02:00
$tree_inject_id -= 1;
$tree_root->{GEN_MAX} += 1;
2016-04-13 22:04:53 +02:00
my $uuid = sprintf("${fake_uuid_prefix}%012u", -($tree_inject_id));
2016-04-12 17:50:12 +02:00
my $node = {
%$detail, # make a copy
2018-02-15 17:42:41 +01:00
TREE_ROOT => $tree_root,
2016-04-12 17:50:12 +02:00
SUBTREE => [],
TOP_LEVEL => $top_node,
REL_PATH => $rel_path,
INJECTED => 1,
id => $tree_inject_id,
uuid => $uuid,
gen => $tree_root->{GEN_MAX},
cgen => $tree_root->{GEN_MAX},
};
push(@$subtree, $node);
$uuid_cache{$uuid} = $node;
$tree_root->{ID_HASH}->{$tree_inject_id} = $node;
2018-02-15 17:42:41 +01:00
$tree_root->{UUID_HASH}->{$uuid} = $node;
push( @{$tree_root->{RECEIVED_UUID_HASH}->{$node->{received_uuid}}}, $node ) if($node->{received_uuid} ne '-');
2018-10-18 17:52:01 +02:00
push( @{$tree_root->{PARENT_UUID_HASH}->{$node->{parent_uuid}}}, $node ) if($node->{parent_uuid} ne '-');
2016-04-12 17:50:12 +02:00
return $node;
}
2020-08-22 13:37:03 +02:00
# returns array of { path, mountinfo }
# NOTE: includes subvolumes hidden by other mountpoint
sub __fs_info
{
my $node = shift;
my $url_prefix = shift;
my @ret = $node->{MOUNTINFO} ? map +{ path => $url_prefix . $_->{mount_point}, mountinfo => $_ }, @{$node->{MOUNTINFO}} : ();
return @ret if($node->{is_root});
return ((map +{ path => $_->{path} . '/' . $node->{REL_PATH}, mountinfo => $_->{mountinfo} }, __fs_info($node->{TOP_LEVEL}, $url_prefix)), @ret);
}
sub _fs_info
2016-04-07 14:34:51 +02:00
{
my $node = shift // die;
2020-08-22 13:37:03 +02:00
my $url_prefix = shift // $node->{TREE_ROOT}{URL_PREFIX};
my @ret = __fs_info($node, $url_prefix);
2020-12-12 20:09:57 +01:00
@ret = ({ path => "$url_prefix<$node->{TREE_ROOT}{mount_source}>/$node->{path}",
mountinfo => undef }) unless(scalar(@ret));
return @ret;
2020-08-22 13:37:03 +02:00
}
sub _fs_path
{
2020-12-12 20:09:57 +01:00
my @ret = map $_->{path}, _fs_info(@_);
return wantarray ? @ret : $ret[0];
2016-04-07 14:34:51 +02:00
}
2018-02-07 16:23:46 +01:00
sub _is_correlated($$)
{
my $a = shift; # node a
my $b = shift; # node b
return 0 if($a->{is_root} || $b->{is_root});
return 0 unless($a->{readonly} && $b->{readonly});
return (($a->{uuid} eq $b->{received_uuid}) ||
($b->{uuid} eq $a->{received_uuid}) ||
(($a->{received_uuid} ne '-') && ($a->{received_uuid} eq $b->{received_uuid})));
}
2018-02-15 00:17:01 +01:00
sub _is_same_fs_tree($$)
{
2018-08-27 14:54:32 +02:00
return ($_[0]->{TREE_ROOT}{host_mount_source} eq $_[1]->{TREE_ROOT}{host_mount_source});
2018-02-15 00:17:01 +01:00
}
2016-03-15 11:21:59 +01:00
sub _is_child_of
2015-10-20 19:07:08 +02:00
{
2016-03-15 11:21:59 +01:00
my $node = shift;
my $uuid = shift;
foreach(@{$node->{SUBTREE}}) {
return 1 if($_->{uuid} eq $uuid);
return 1 if(_is_child_of($_, $uuid));
}
return 0;
}
2015-10-20 19:07:08 +02:00
2016-03-30 21:55:02 +02:00
sub _get_longest_match
{
my $node = shift;
my $path = shift;
my $check_path = shift; # MUST have a trailing slash
$path .= '/' unless($path =~ /\/$/); # correctly handle root path="/"
return undef unless($check_path =~ /^\Q$path\E/);
foreach(@{$node->{SUBTREE}}) {
my $ret = _get_longest_match($_, $path . $_->{REL_PATH}, $check_path);
return $ret if($ret);
}
return { node => $node,
path => $path };
}
2019-08-07 21:30:59 +02:00
sub vinfo($$)
2014-12-12 12:32:04 +01:00
{
2016-03-15 11:21:59 +01:00
my $url = shift // die;
my $config = shift;
2014-12-13 13:52:43 +01:00
2016-04-25 15:10:00 +02:00
my ($url_prefix, $path) = check_url($url);
die "invalid url: $url" unless(defined($path));
2016-05-10 15:51:44 +02:00
my $print = $path;
2016-04-25 15:10:00 +02:00
my $name = $path;
2016-03-15 11:21:59 +01:00
$name =~ s/^.*\///;
2016-04-25 15:10:00 +02:00
$name = '/' if($name eq "");
2015-01-17 14:55:46 +01:00
2016-05-10 15:51:44 +02:00
my $host = undef;
2019-03-31 23:31:55 +02:00
my $port = undef;
2016-04-25 15:10:00 +02:00
if($url_prefix) {
2016-05-10 15:51:44 +02:00
$host = $url_prefix;
2016-04-25 15:10:00 +02:00
die unless($host =~ s/^ssh:\/\///);
2020-08-02 19:07:55 +02:00
$port = $1 if($host =~ s/:([1-9][0-9]*)$//);
$print = $host . (defined($port) ? "[$port]:" : ":") . $path;
$host =~ s/^\[//; # remove brackets from ipv6_addr
$host =~ s/\]$//; # remove brackets from ipv6_addr
2014-12-12 12:32:04 +01:00
}
2020-03-07 00:24:45 +01:00
# Note that PATH and URL have no trailing slash, except if "/".
2020-08-02 19:07:55 +02:00
# Note that URL and URL_PREFIX can contain ipv6 address in brackets (e.g. "[::1]").
2016-05-10 15:51:44 +02:00
return {
2020-08-02 19:07:55 +02:00
HOST => $host, # hostname|ipv4_address|ipv6_address|<undef>
2020-03-07 00:24:45 +01:00
PORT => $port, # port|<undef>
2016-05-10 15:51:44 +02:00
NAME => $name,
PATH => $path,
2020-08-02 19:07:55 +02:00
PRINT => $print, # "hostname:/path" or "hostname[port]:/path"
2019-04-01 00:32:24 +02:00
URL => $url_prefix . $path, # ssh://hostname[:port]/path
URL_PREFIX => $url_prefix, # ssh://hostname[:port] (or "" if local)
MACHINE_ID => $url_prefix || "LOCAL:", # unique: "LOCAL:" or hostname and port
2016-05-10 15:51:44 +02:00
CONFIG => $config,
2021-07-24 11:20:03 +02:00
# These are added in vinfo_init_root
#NODE_SUBDIR => undef,
#VINFO_MOUNTPOINT => undef,
2016-03-30 21:55:02 +02:00
}
}
2016-05-10 15:51:44 +02:00
sub vinfo_child($$;$)
2015-01-20 19:18:38 +01:00
{
2016-03-15 11:21:59 +01:00
my $parent = shift || die;
my $rel_path = shift // die;
2016-05-10 15:51:44 +02:00
my $config = shift; # override parent config
2016-03-15 11:21:59 +01:00
my $name = $rel_path;
2016-04-03 20:46:29 +02:00
my $subvol_dir = "";
$subvol_dir = $1 if($name =~ s/^(.*)\///);
2018-02-03 12:55:21 +01:00
# Note that PATH and URL intentionally contain "//" if $parent->{PATH} = "/".
2016-03-30 21:55:02 +02:00
my $vinfo = {
2016-05-10 15:51:44 +02:00
HOST => $parent->{HOST},
2019-03-31 23:31:55 +02:00
PORT => $parent->{PORT},
2016-03-15 11:21:59 +01:00
NAME => $name,
PATH => "$parent->{PATH}/$rel_path",
2020-03-07 00:24:45 +01:00
PRINT => "$parent->{PRINT}" . ($parent->{PRINT} =~ /\/$/ ? "" : "/") . $rel_path,
2016-05-10 15:51:44 +02:00
URL => "$parent->{URL}/$rel_path",
2016-03-30 21:55:02 +02:00
URL_PREFIX => $parent->{URL_PREFIX},
2019-04-01 00:32:24 +02:00
MACHINE_ID => $parent->{MACHINE_ID},
2016-05-10 15:51:44 +02:00
CONFIG => $config // $parent->{CONFIG},
2018-10-18 17:52:01 +02:00
VINFO_MOUNTPOINT => $parent->{VINFO_MOUNTPOINT},
2021-07-24 11:20:03 +02:00
# NOTE: these are NOT present in non-child vinfo, and should be used
# only for printing and comparing results of vinfo_subvol_list.
SUBVOL_PATH => $rel_path,
SUBVOL_DIR => $subvol_dir, # SUBVOL_PATH=SUBVOL_DIR/NAME
2016-03-30 21:55:02 +02:00
};
2015-01-20 19:18:38 +01:00
2020-08-28 17:15:39 +02:00
# TRACE "vinfo_child: created from \"$parent->{PRINT}\": $info{PRINT}" if($do_trace);
2016-04-19 13:06:31 +02:00
return $vinfo;
}
2016-05-11 20:15:46 +02:00
sub vinfo_rsh($;@)
2016-05-10 15:51:44 +02:00
{
my $vinfo = shift || die;
2016-05-11 20:15:46 +02:00
my %opts = @_;
2016-05-10 15:51:44 +02:00
my $host = $vinfo->{HOST};
return undef unless(defined($host));
my $config = $vinfo->{CONFIG};
die unless($config);
2019-03-31 23:31:55 +02:00
# as of btrbk-0.28.0, ssh port is a property of a "vinfo", set with
# "ssh://hostname[:port]" in 'volume' and 'target' sections. Note
# that the port number is also used for the MACHINE_ID to
# distinguish virtual machines on same host with different ports.
my $ssh_port = $vinfo->{PORT};
unless($ssh_port) {
# PORT defaults to ssh_port (DEPRECATED)
$ssh_port = config_key($config, "ssh_port") // "default";
$ssh_port = undef if($ssh_port eq "default");
}
2016-05-10 15:51:44 +02:00
my $ssh_user = config_key($config, "ssh_user");
my $ssh_identity = config_key($config, "ssh_identity");
2016-08-21 17:36:36 +02:00
my $ssh_compression = config_key($config, "ssh_compression");
2022-05-28 21:21:52 +02:00
my $ssh_cipher_spec = join(",", @{config_key($config, "ssh_cipher_spec")});
2019-12-14 17:06:26 +01:00
my @ssh_options; # as of btrbk-0.29.0, we run ssh without -q (catching @stderr)
2019-03-31 23:31:55 +02:00
push(@ssh_options, '-p', $ssh_port) if($ssh_port);
2016-05-10 15:51:44 +02:00
push(@ssh_options, '-c', $ssh_cipher_spec) if($ssh_cipher_spec ne "default");
2022-07-28 13:36:20 +02:00
push(@ssh_options, '-i', { unsafe => $ssh_identity }) if($ssh_identity); # NOTE: hackily used in run_cmd on errors
2016-08-21 17:36:36 +02:00
if($opts{disable_compression}) {
push(@ssh_options, '-o', 'compression=no'); # force ssh compression=no (in case it is defined in ssh_config)
} elsif($ssh_compression) {
push(@ssh_options, '-C');
}
2022-02-22 22:19:15 +01:00
my $ssh_dest = $ssh_user ? $ssh_user . '@' . $host : $host;
return ['ssh', @ssh_options, $ssh_dest ];
2016-05-10 15:51:44 +02:00
}
2016-08-27 17:35:47 +02:00
sub vinfo_cmd($$@)
{
my $vinfo = shift || die;
my $cmd = shift || die;
my @cmd_args = @_;
2022-02-06 16:26:29 +01:00
my $backend = config_key_lru($vinfo, "backend") // die;
2022-02-08 01:02:27 +01:00
my $cmd_mapped = $backend_cmd_map{$backend}{$cmd} // [ split(" ", $cmd) ];
return [ @$cmd_mapped, @cmd_args ];
2016-08-27 17:35:47 +02:00
}
2019-08-05 14:59:41 +02:00
sub _get_btrbk_date(@)
2016-04-19 13:06:31 +02:00
{
2022-06-18 15:51:10 +02:00
my %a = @_; # named capture buffers (%+) from $btrbk_timestamp_match
2016-04-20 22:45:11 +02:00
2022-06-18 15:51:10 +02:00
my @tm = ( ($a{ss} // 0), ($a{mm} // 0), ($a{hh} // 0), $a{DD}, ($a{MM} - 1), ($a{YYYY} - 1900) );
my $NN = $a{NN} // 0;
my $zz = $a{zz};
my $has_exact_time = defined($a{hh}); # false if timestamp_format=short
2016-04-20 22:45:11 +02:00
my $time;
2016-04-25 21:05:46 +02:00
if(defined($zz)) {
eval_quiet { $time = timegm(@tm); };
} else {
eval_quiet { $time = timelocal(@tm); };
}
unless(defined($time)) {
2016-04-21 13:27:54 +02:00
# WARN "$@"; # sadly Time::Local croaks, which also prints the line number from here.
2016-04-20 22:45:11 +02:00
return undef;
}
2016-04-21 13:27:54 +02:00
# 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 {
return undef;
}
$time -= $offset;
}
2019-08-05 14:59:41 +02:00
return [ $time, $NN, $has_exact_time ];
}
sub add_btrbk_filename_info($;$)
{
my $node = shift;
my $raw_info = shift;
my $name = $node->{REL_PATH};
return undef unless(defined($name));
# NOTE: unless long-iso file format is encountered, the timestamp is interpreted in local timezone.
$name =~ s/^(.*)\///;
2021-08-17 11:40:54 +02:00
if($raw_info && ($name =~ /^(?<name>.+)\.$btrbk_timestamp_match$raw_postfix_match$/)) { ; }
elsif((not $raw_info) && ($name =~ /^(?<name>.+)\.$btrbk_timestamp_match$/)) { ; }
2019-08-05 14:59:41 +02:00
else {
return undef;
}
$name = $+{name} // die;
my $btrbk_date = _get_btrbk_date(%+); # use named capture buffers of previous match
unless($btrbk_date) {
WARN "Illegal timestamp on subvolume \"$node->{REL_PATH}\", ignoring";
return undef;
}
2016-04-20 22:45:11 +02:00
$node->{BTRBK_BASENAME} = $name;
2019-08-05 14:59:41 +02:00
$node->{BTRBK_DATE} = $btrbk_date;
2017-06-16 17:43:17 +02:00
$node->{BTRBK_RAW} = $raw_info if($raw_info);
2016-04-21 13:27:54 +02:00
return $node;
2015-01-20 19:18:38 +01:00
}
2021-08-17 16:50:22 +02:00
sub _find_mountpoint($$)
{
my $root = shift;
my $path = shift;
$path .= '/' unless($path =~ /\/$/); # append trailing slash
while (my $tree = $root->{SUBTREE}) {
my $m = undef;
foreach (@$tree) {
$m = $_, last if($path =~ /^\Q$_->{mount_point}\E\//);
}
last unless defined $m;
$root = $m;
}
TRACE "resolved mount point for \"$path\": $root->{mount_point} (mount_source=$root->{mount_source}, subvolid=" . ($root->{MNTOPS}->{subvolid} // '<undef>') . ")" if($do_trace);
return $root;
}
sub mountinfo_tree($)
{
my $vol = shift;
my $mountinfo = $mountinfo_cache{$vol->{MACHINE_ID}};
TRACE "mountinfo_cache " . ($mountinfo ? "HIT" : "MISS") . ": $vol->{MACHINE_ID}" if($do_trace);
unless($mountinfo) {
$mountinfo = system_list_mountinfo($vol);
return undef unless($mountinfo);
$mountinfo_cache{$vol->{MACHINE_ID}} = $mountinfo;
}
return $mountinfo->[0]->{TREE_ROOT} if($mountinfo->[0]->{TREE_ROOT});
my %id = map +( $_->{mount_id} => $_ ), @$mountinfo;
my $tree_root;
foreach my $node (@$mountinfo) {
2022-02-06 11:55:04 +01:00
my $parent = $id{$node->{parent_id}};
if($parent && ($node->{mount_id} != $node->{parent_id})) {
2021-08-17 16:50:22 +02:00
$node->{PARENT} = $parent;
push @{$parent->{SUBTREE}}, $node;
} else {
die "multiple root mount points" if($tree_root);
$tree_root = $node;
}
# populate cache (mount points are always real paths)
$realpath_cache{$vol->{URL_PREFIX} . $node->{mount_point}} = $node->{mount_point};
}
die "no root mount point" unless($tree_root);
$_->{TREE_ROOT} = $tree_root foreach (@$mountinfo);
$tree_root->{MOUNTINFO_LIST} = $mountinfo;
return $tree_root;
}
2023-03-26 13:19:11 +02:00
sub vinfo_realpath($@)
{
my $vol = shift // die;
my $url = $vol->{URL} // die;
return $realpath_cache{$url} if(exists($realpath_cache{$url}));
return $realpath_cache{$url} = system_realpath($vol);
}
2023-03-26 14:03:09 +02:00
sub vinfo_mkdir($)
{
my $vol = shift // die;
my $url = $vol->{URL} // die;
return $mkdir_cache{$url} if(exists($mkdir_cache{$url}));
return -1 if(vinfo_realpath($vol));
return undef unless($mkdir_cache{$url} = system_mkdir($vol));
$vol->{SUBDIR_CREATED} = 1;
delete $realpath_cache{$url}; # clear realpath cache (allow retry)
return 1;
}
2021-08-17 16:50:22 +02:00
sub vinfo_mountpoint
{
my $vol = shift // die;
my %args = @_;
DEBUG "Resolving mount point for: $vol->{PRINT}";
2023-03-26 13:21:35 +02:00
my $mountinfo_root = mountinfo_tree($vol)
or return undef;
2021-08-17 16:50:22 +02:00
2023-03-26 13:21:35 +02:00
my $realpath = vinfo_realpath($vol)
or return undef;
2021-08-17 16:50:22 +02:00
my $mountpoint = _find_mountpoint($mountinfo_root, $realpath);
# handle autofs
if($mountpoint->{fs_type} eq 'autofs') {
if($args{autofs_retry}) {
DEBUG "Non-btrfs autofs mount point for: $vol->{PRINT}";
return undef;
}
DEBUG "Found autofs mount point, triggering automount on $mountpoint->{mount_point} for: $vol->{PRINT}";
btrfs_subvolume_show(vinfo($vol->{URL_PREFIX} . $mountpoint->{mount_point}, $vol->{CONFIG}));
$mountinfo_cache{$vol->{MACHINE_ID}} = undef;
return vinfo_mountpoint($vol, %args, autofs_retry => 1);
}
if($args{fs_type} && ($mountpoint->{fs_type} ne $args{fs_type})) {
ERROR "Not a btrfs filesystem (mountpoint=\"$mountpoint->{mount_point}\", fs_type=\"$mountpoint->{fs_type}\"): $vol->{PRINT}";
return undef;
}
DEBUG "Mount point for \"$vol->{PRINT}\": $mountpoint->{mount_point} (mount_source=$mountpoint->{mount_source}, fs_type=$mountpoint->{fs_type})";
return ($realpath, $mountpoint);
}
2019-04-24 23:46:44 +02:00
sub vinfo_init_root($)
2015-01-25 13:36:07 +01:00
{
2015-04-16 12:00:04 +02:00
my $vol = shift || die;
2015-04-14 02:17:17 +02:00
2019-12-15 18:47:27 +01:00
@stderr = (); # clear @stderr (propagated for logging)
2021-08-17 16:50:22 +02:00
my ($real_path, $mountpoint) = vinfo_mountpoint($vol, fs_type => 'btrfs');
return undef unless($mountpoint);
my @same_source_mounts = grep { $_->{mount_source} eq $mountpoint->{mount_source} } @{$mountpoint->{TREE_ROOT}{MOUNTINFO_LIST}};
foreach my $mnt (grep { !defined($_->{MNTOPS}{subvolid}) } @same_source_mounts) {
# kernel <= 4.2 does not have subvolid=NN in /proc/self/mounts, read it with btrfs-progs
DEBUG "No subvolid provided in mounts for: $mnt->{mount_point}";
my $detail = btrfs_subvolume_show(vinfo($vol->{URL_PREFIX} . $mnt->{mount_point}, $vol->{CONFIG}));
return undef unless($detail);
$mnt->{MNTOPS}{subvolid} = $detail->{id} || die; # also affects %mountinfo_cache
}
2018-02-12 21:58:18 +01:00
2018-02-14 22:09:45 +01:00
# read btrfs tree for the mount point
2019-12-15 18:47:27 +01:00
@stderr = (); # clear @stderr (propagated for logging)
2021-08-17 16:50:22 +02:00
my $mnt_path = $mountpoint->{mount_point};
2018-02-14 22:09:45 +01:00
my $mnt_vol = vinfo($vol->{URL_PREFIX} . $mnt_path, $vol->{CONFIG});
2021-08-17 16:50:22 +02:00
my $mnt_tree_root = btr_tree($mnt_vol, $mountpoint->{MNTOPS}{subvolid}, $mountpoint->{mount_source}, \@same_source_mounts);
2018-02-14 22:09:45 +01:00
return undef unless($mnt_tree_root);
2016-03-30 21:55:02 +02:00
2018-02-14 22:09:45 +01:00
# find longest match in btrfs tree
2021-08-17 16:50:22 +02:00
$real_path .= '/' unless($real_path =~ /\/$/); # correctly handle root path="/"
2018-02-14 22:09:45 +01:00
my $ret = _get_longest_match($mnt_tree_root, $mnt_path, $real_path) // die;
my $tree_root = $ret->{node};
2016-03-15 11:21:59 +01:00
return undef unless($tree_root);
2015-06-07 11:52:39 +02:00
2018-02-14 22:09:45 +01:00
# set NODE_SUBDIR if $vol->{PATH} points to a regular (non-subvolume) directory.
# in other words, "PATH=<path_to_subvolume>/NODE_SUBDIR"
my $node_subdir = $real_path;
die unless($node_subdir =~ s/^\Q$ret->{path}\E//); # NOTE: $ret->{path} has trailing slash!
$node_subdir =~ s/\/+$//;
$vol->{NODE_SUBDIR} = $node_subdir if($node_subdir ne '');
2016-03-15 11:21:59 +01:00
$vol->{node} = $tree_root;
2018-10-18 17:52:01 +02:00
$vol->{VINFO_MOUNTPOINT} = vinfo($vol->{URL_PREFIX} . $mnt_path, $vol->{CONFIG});
$vol->{VINFO_MOUNTPOINT}{node} = $mnt_tree_root;
2018-02-14 22:09:45 +01:00
2016-03-15 11:21:59 +01:00
return $tree_root;
2015-01-26 17:23:37 +01:00
}
2018-02-07 20:17:23 +01:00
sub vinfo_init_raw_root($;@)
{
my $droot = shift || die;
my $tree_root = $raw_url_cache{$droot->{URL}};
2020-08-28 17:15:39 +02:00
TRACE "raw_url_cache " . ($tree_root ? "HIT" : "MISS") . ": URL=$droot->{URL}" if($do_trace);
2018-02-07 20:17:23 +01:00
unless($tree_root) {
2023-03-26 13:21:35 +02:00
if(my $real_path = vinfo_realpath($droot)) {
2018-02-07 20:17:23 +01:00
my $real_url = $droot->{URL_PREFIX} . $real_path;
$tree_root = $raw_url_cache{$real_url};
2020-08-28 17:15:39 +02:00
TRACE "raw_url_cache " . ($tree_root ? "HIT" : "MISS") . ": REAL_URL=$real_url" if($do_trace);
2018-02-07 20:17:23 +01:00
}
}
unless($tree_root) {
DEBUG "Creating raw subvolume list: $droot->{PRINT}";
# create fake btr_tree
$tree_root = { id => 5,
is_root => 1,
2020-08-22 13:37:03 +02:00
mount_source => '@raw_tree', # for _fs_path (logging)
2019-04-01 00:32:24 +02:00
host_mount_source => $droot->{URL} . '@raw_tree', # for completeness (this is never used)
2018-02-07 20:17:23 +01:00
GEN_MAX => 1,
SUBTREE => [],
UUID_HASH => {},
RECEIVED_UUID_HASH => {},
2022-11-20 12:21:12 +01:00
PARENT_UUID_HASH => {},
2020-08-22 13:37:03 +02:00
URL_PREFIX => $droot->{URL_PREFIX}, # for _fs_path (logging)
MOUNTINFO => [ { mount_point => $droot->{PATH} } ], # for _fs_path (logging)
2018-02-07 20:17:23 +01:00
};
$tree_root->{TREE_ROOT} = $tree_root;
# list and parse *.info
my $raw_info_ary = system_read_raw_info_dir($droot);
return undef unless($raw_info_ary);
# inject nodes to fake btr_tree
$droot->{node} = $tree_root;
2022-11-20 12:21:12 +01:00
foreach my $raw_info (sort { $a->{NAME} cmp $b->{NAME} } @$raw_info_ary)
2018-02-07 20:17:23 +01:00
{
2022-11-20 12:21:12 +01:00
# Set btrfs subvolume information from filename info.
2018-02-07 20:17:23 +01:00
#
2022-11-20 12:21:12 +01:00
# Important notes:
# - Raw targets have a fake uuid and parent_uuid.
# - RECEIVED_PARENT_UUID in BTRBK_RAW is the "parent of the
# source subvolume", NOT the "parent of the received subvolume".
#
# If restoring a backup from raw btrfs images (using "incremental yes|strict"):
# "btrfs send -p parent source > svol.btrfs", the backups
# on the target will get corrupted (unusable!) as soon as
# an any files in the chain gets deleted.
#
# We need to make sure btrbk will NEVER delete those (see _raw_depends):
# - svol.<timestamp>--<received_uuid_0>.btrfs : root (full) image
# - svol.<timestamp>--<received_uuid-n>[@<received_uuid_n-1>].btrfs : incremental image
2022-11-20 11:11:50 +01:00
my $subvol = vinfo_child($droot, $raw_info->{NAME});
2018-02-07 20:17:23 +01:00
unless(vinfo_inject_child($droot, $subvol, {
TARGET_TYPE => $raw_info->{TYPE},
parent_uuid => '-', # NOTE: correct value gets inserted below
# Incomplete raw fakes get same semantics as real subvolumes (readonly=0, received_uuid='-')
received_uuid => ($raw_info->{INCOMPLETE} ? '-' : $raw_info->{RECEIVED_UUID}),
readonly => ($raw_info->{INCOMPLETE} ? 0 : 1),
}, $raw_info))
{
2022-11-20 11:11:50 +01:00
ERROR("Failed create raw node \"$raw_info->{NAME}\" from raw info file: \"$raw_info->{INFO_FILE}\"");
2018-02-07 20:17:23 +01:00
return undef;
}
}
my @subvol_list = @{vinfo_subvol_list($droot, sort => 'path')};
DEBUG "Found " . scalar(@subvol_list) . " raw subvolume backups in: $droot->{PRINT}";
2022-11-20 12:21:12 +01:00
# set parent_uuid based on RECEIVED_PARENT_UUID
foreach my $node (@{$tree_root->{SUBTREE}}) {
my $parents = $tree_root->{RECEIVED_UUID_HASH}{$node->{BTRBK_RAW}{RECEIVED_PARENT_UUID}} // [];
my $parent = (grep { $_->{BTRBK_RAW}{RECEIVED_PARENT_UUID} eq '-' } @$parents)[0] // $parents->[0]; # if multiple candidates, prefer non-incremental
TRACE "vinfo_init_raw_root: $node->{BTRBK_RAW}{NAME} parent=" . ($parent ? $parent->{BTRBK_RAW}{NAME} : "") if($do_trace);
next unless $parent;
$node->{parent_uuid} = $parent->{uuid};
push @{$tree_root->{PARENT_UUID_HASH}{$node->{parent_uuid}}}, $node;
2018-02-07 20:17:23 +01:00
}
}
$droot->{node} = $tree_root;
2019-05-19 15:20:13 +02:00
$droot->{VINFO_MOUNTPOINT} = $droot; # fake mountpoint
2018-02-07 20:17:23 +01:00
$raw_url_cache{$droot->{URL}} = $tree_root;
return $tree_root;
}
2016-03-15 11:21:59 +01:00
sub _vinfo_subtree_list
2016-01-13 14:29:44 +01:00
{
2016-03-15 11:21:59 +01:00
my $tree = shift;
my $vinfo_parent = shift;
2018-02-15 16:53:29 +01:00
my $filter_readonly = shift; # if set, return only read-only
my $filter_btrbk_direct_leaf = shift; # if set, return only read-only direct leafs matching btrbk_basename
2016-03-15 11:21:59 +01:00
my $list = shift // [];
my $path_prefix = shift // "";
2016-04-03 20:46:29 +02:00
my $depth = shift // 0;
2016-01-13 14:29:44 +01:00
2018-02-05 18:03:20 +01:00
# if $vinfo_parent->{NODE_SUBDIR} is set, vinfo_parent->{PATH} does
# not point to a subvolume directly, but to "<path_to_subvolume>/NODE_SUBDIR".
# skip nodes wich are not in NODE_SUBDIR, or strip NODE_SUBDIR from from rel_path.
my $node_subdir_filter = ($depth == 0) ? $vinfo_parent->{NODE_SUBDIR} : undef;
2016-04-03 20:46:29 +02:00
foreach my $node (@{$tree->{SUBTREE}}) {
my $rel_path = $node->{REL_PATH};
2016-04-03 16:24:38 +02:00
if(defined($node_subdir_filter)) {
next unless($rel_path =~ s/^\Q$node_subdir_filter\E\///);
2016-03-30 21:55:02 +02:00
}
2018-02-05 18:03:20 +01:00
my $path = $path_prefix . $rel_path; # always points to a subvolume
2016-04-03 20:46:29 +02:00
2018-02-15 16:53:29 +01:00
# filter direct leafs (SUBVOL_DIR="") matching btrbk_basename
next unless(!defined($filter_btrbk_direct_leaf) ||
(exists($node->{BTRBK_BASENAME}) && ($node->{BTRBK_BASENAME} eq $filter_btrbk_direct_leaf) &&
($rel_path !~ /\//))); # note: depth is always 0 if $filter_btrbk_direct_leaf
# filter readonly, push vinfo_child
if(!$filter_readonly || $node->{readonly}) {
my $vinfo = vinfo_child($vinfo_parent, $path);
$vinfo->{node} = $node;
# add some additional information to vinfo
$vinfo->{subtree_depth} = $depth;
push(@$list, $vinfo);
2016-04-03 20:46:29 +02:00
}
2016-01-13 14:29:44 +01:00
2018-02-15 16:53:29 +01:00
unless(defined($filter_btrbk_direct_leaf)) {
_vinfo_subtree_list($node, $vinfo_parent, $filter_readonly, undef, $list, $path . '/', $depth + 1);
}
2016-01-13 14:29:44 +01:00
}
2016-03-15 11:21:59 +01:00
return $list;
2016-01-13 14:29:44 +01:00
}
2016-04-03 20:46:29 +02:00
sub vinfo_subvol_list($;@)
2014-12-12 10:39:40 +01:00
{
2015-04-16 12:00:04 +02:00
my $vol = shift || die;
2016-04-03 20:46:29 +02:00
my %opts = @_;
2016-03-15 14:46:25 +01:00
2020-08-28 17:15:39 +02:00
TRACE "Creating subvolume list for: $vol->{PRINT}" if($do_trace);
2018-02-05 18:03:20 +01:00
2018-02-07 19:21:35 +01:00
# recurse into tree from $vol->{node}, returns arrayref of vinfo
2018-02-15 16:53:29 +01:00
my $subvol_list = _vinfo_subtree_list($vol->{node}, $vol, $opts{readonly}, $opts{btrbk_direct_leaf});
2016-04-03 20:46:29 +02:00
if($opts{sort}) {
if($opts{sort} eq 'path') {
2016-04-12 17:50:12 +02:00
my @sorted = sort { $a->{SUBVOL_PATH} cmp $b->{SUBVOL_PATH} } @$subvol_list;
2018-02-05 18:03:20 +01:00
return \@sorted;
2016-04-03 20:46:29 +02:00
}
2016-04-12 17:50:12 +02:00
else { die; }
2016-04-03 20:46:29 +02:00
}
return $subvol_list;
2016-03-15 11:21:59 +01:00
}
2015-03-13 12:12:37 +01:00
2016-01-13 14:29:44 +01:00
2019-05-22 23:02:36 +02:00
# returns vinfo_child if $node is in tree below $vol (or equal if allow_equal), or undef
2019-04-11 14:38:41 +02:00
sub vinfo_resolved($$;@)
2018-02-15 17:42:41 +01:00
{
my $node = shift || die;
my $vol = shift || die; # root vinfo node
2019-04-11 14:38:41 +02:00
my %opts = @_;
2018-02-15 17:42:41 +01:00
my $top_id = $vol->{node}{id};
my @path;
my $nn = $node;
while(($nn->{id} != $top_id) && (!$nn->{is_root})) {
unshift(@path, $nn->{REL_PATH});
$nn = $nn->{TOP_LEVEL};
}
2019-05-22 23:02:36 +02:00
if(scalar(@path) == 0) {
return $vol if($opts{allow_equal} && not defined($vol->{NODE_SUBDIR}));
return undef;
}
2018-02-15 17:42:41 +01:00
return undef if($nn->{is_root} && (!$vol->{node}{is_root}));
my $jpath = join('/', @path);
2019-05-22 23:02:36 +02:00
if(defined($vol->{NODE_SUBDIR})) {
2018-02-15 17:42:41 +01:00
return undef unless($jpath =~ s/^\Q$vol->{NODE_SUBDIR}\E\///);
}
2019-04-11 14:38:41 +02:00
if(defined($opts{btrbk_direct_leaf})) {
return undef if($jpath =~ /\//);
return undef unless(exists($node->{BTRBK_BASENAME}) && ($node->{BTRBK_BASENAME} eq $opts{btrbk_direct_leaf}))
}
2018-10-15 16:19:52 +02:00
my $vinfo = vinfo_child($vol, $jpath);
$vinfo->{node} = $node;
return $vinfo;
2018-02-15 17:42:41 +01:00
}
2018-10-18 17:54:46 +02:00
# returns vinfo if $node is below any mountpoint of $vol
sub vinfo_resolved_all_mountpoints($$)
{
my $node = shift || die;
my $vol = shift || die;
my $tree_root = $vol->{node}{TREE_ROOT};
2020-08-22 13:37:03 +02:00
foreach my $mnt_node (@{$tree_root->{MOUNTED_NODES}}) {
foreach my $mountinfo (@{$mnt_node->{MOUNTINFO}}) {
my $mnt_vol = vinfo($vol->{URL_PREFIX} . $mountinfo->{mount_point}, $vol->{CONFIG});
$mnt_vol->{node} = $mnt_node;
2020-08-28 17:15:39 +02:00
TRACE "vinfo_resolved_all_mountpoints: trying mountpoint: $mnt_vol->{PRINT}" if($do_trace);
2020-08-22 13:37:03 +02:00
my $vinfo = vinfo_resolved($node, $mnt_vol, allow_equal => 1);
return $vinfo if($vinfo);
}
2018-10-18 17:54:46 +02:00
}
return undef;
}
2016-03-15 11:21:59 +01:00
sub vinfo_subvol($$)
{
my $vol = shift || die;
my $subvol_path = shift // die;
foreach (@{vinfo_subvol_list($vol)}) {
return $_ if($_->{SUBVOL_PATH} eq $subvol_path);
2016-01-13 14:29:44 +01:00
}
2016-03-15 11:21:59 +01:00
return undef;
2014-12-14 19:23:02 +01:00
}
2015-01-14 14:10:41 +01:00
2017-06-16 17:43:17 +02:00
sub vinfo_inject_child($$$;$)
2016-04-12 17:50:12 +02:00
{
my $vinfo = shift;
my $vinfo_child = shift;
my $detail = shift;
2017-06-16 17:43:17 +02:00
my $raw_info = shift;
2016-04-13 22:04:53 +02:00
my $node;
2016-04-19 13:06:31 +02:00
my $node_subdir = defined($vinfo->{NODE_SUBDIR}) ? $vinfo->{NODE_SUBDIR} . '/' : "";
my $rel_path = $node_subdir . $vinfo_child->{SUBVOL_PATH};
2018-02-07 19:21:35 +01:00
$node = btr_tree_inject_node($vinfo->{node}, $detail, $rel_path);
return undef unless(add_btrbk_filename_info($node, $raw_info));
2016-04-19 13:06:31 +02:00
2016-04-12 17:50:12 +02:00
$vinfo_child->{node} = $node;
2020-08-28 17:15:39 +02:00
TRACE "vinfo_inject_child: injected child id=$node->{id} to $vinfo->{PRINT}" if($do_trace);
2016-04-12 17:50:12 +02:00
return $vinfo_child;
}
2016-03-15 11:21:59 +01:00
# returns hash: ( $prefix_{url,path,host,name,subvol_path,rsh} => value, ... )
sub vinfo_prefixed_keys($$)
2014-12-14 21:29:22 +01:00
{
2016-03-15 11:21:59 +01:00
my $prefix = shift // die;
my $vinfo = shift;
return () unless($vinfo);
my %ret;
if($prefix) {
$ret{$prefix} = $vinfo->{PRINT};
$prefix .= '_';
2015-01-03 21:25:46 +01:00
}
2019-09-08 16:29:05 +02:00
foreach (qw( URL PATH HOST PORT NAME )) {
2016-03-15 11:21:59 +01:00
$ret{$prefix . lc($_)} = $vinfo->{$_};
2015-01-03 21:25:46 +01:00
}
2019-09-08 16:29:05 +02:00
$ret{$prefix . "subvolume"} = $vinfo->{PATH};
2016-05-10 15:51:44 +02:00
my $rsh = vinfo_rsh($vinfo);
2021-09-04 15:46:57 +02:00
$ret{$prefix . "rsh"} = $rsh ? _safe_cmd($rsh) : undef,
2016-03-15 11:21:59 +01:00
return %ret;
2014-12-14 21:29:22 +01:00
}
2014-12-14 19:23:02 +01:00
2018-02-15 00:17:01 +01:00
sub vinfo_assign_config($;$)
2015-04-23 16:19:34 +02:00
{
2016-03-15 11:21:59 +01:00
my $vinfo = shift || die;
2018-02-15 00:17:01 +01:00
my $vinfo_snapshot_root = shift;
2016-05-10 15:51:44 +02:00
my $config = $vinfo->{CONFIG} || die;
2016-03-15 11:21:59 +01:00
die if($config->{VINFO});
$config->{VINFO} = $vinfo;
2018-02-15 00:17:01 +01:00
$config->{VINFO_SNAPROOT} = $vinfo_snapshot_root;
}
sub vinfo_snapshot_root($)
{
my $vinfo = shift;
return $vinfo->{CONFIG}{VINFO_SNAPROOT};
2015-04-23 16:19:34 +02:00
}
2021-07-24 20:11:32 +02:00
sub config_subsection($$;$)
{
my $config = shift || die;
my $context = shift || die;
die if grep($_->{CONTEXT} ne $context, @{$config->{SUBSECTION}});
return @{$config->{SUBSECTION}};
}
2016-03-15 11:21:59 +01:00
sub vinfo_subsection($$;$)
2015-04-23 16:19:34 +02:00
{
2016-03-15 11:21:59 +01:00
# if config: must have SUBSECTION key
# if vinfo: must have CONFIG key
my $config_or_vinfo = shift || die;
my $context = shift || die;
my $include_aborted = shift;
2021-07-24 20:11:32 +02:00
my @config_list;
2016-03-15 11:21:59 +01:00
my $vinfo_check;
if(exists($config_or_vinfo->{SUBSECTION})) {
2021-07-24 20:11:32 +02:00
@config_list = config_subsection($config_or_vinfo, $context);
2015-04-23 16:19:34 +02:00
}
2016-03-15 11:21:59 +01:00
else {
2021-07-24 20:11:32 +02:00
@config_list = config_subsection($config_or_vinfo->{CONFIG}, $context);
2016-03-15 11:21:59 +01:00
die unless($config_or_vinfo->{CONFIG}->{VINFO} == $config_or_vinfo); # check back reference
}
2021-07-24 20:11:32 +02:00
return map {
2016-03-15 11:21:59 +01:00
die unless($_->{VINFO} == $_->{VINFO}->{CONFIG}->{VINFO}); # check all back references
2021-07-24 20:11:32 +02:00
($include_aborted || !$_->{ABORTED}) ? $_->{VINFO} : ()
} @config_list;
2015-04-23 16:19:34 +02:00
}
2019-04-17 15:56:35 +02:00
# allow (absolute) path / url with wildcards
# allow group (exact match)
# allow host[:port] (exact match)
sub vinfo_filter_statement($) {
my $filter = shift;
my %ret = ( unparsed => $filter );
my ($url_prefix, $path) = check_url($filter, accept_wildcards => 1);
unless($path) {
# allow relative path with wildcards
$url_prefix = "";
$path = check_file($filter, { relative => 1, wildcards => 1 }, sanitize => 1);
}
if($path) {
# support "*some*file*", "*/*"
2020-08-20 15:42:34 +02:00
my $regex = join('[^\/]*', map(quotemeta($_), split(/\*+/, lc($url_prefix) . $path, -1)));
2019-04-17 15:56:35 +02:00
if($path =~ /^\//) {
$ret{url_regex} = qr/^$regex$/; # absolute path, match full string
} else {
$ret{url_regex} = qr/\/$regex$/; # match end of string
}
}
$ret{group_eq} = $filter if($filter =~ /^$group_match$/);
2020-08-02 19:07:55 +02:00
if($filter =~ /^(?<host>$host_name_match|$ipv4_addr_match|\[$ipv6_addr_match\])(:(?<port>[1-9][0-9]*))?$/) {
my ($host, $port) = ( $+{host}, $+{port} );
$host =~ s/^\[//; # remove brackets from ipv6_addr
$host =~ s/\]$//; # remove brackets from ipv6_addr
$ret{host_port_eq} = { host => $host, port => $port };
}
elsif($filter =~ /^$ipv6_addr_match$/) {
$ret{host_port_eq} = { host => $filter } ;
}
2020-08-28 17:15:39 +02:00
TRACE 'vinfo_filter_statement: filter="' . $filter . '" url_regex="' . ($ret{url_regex} // "<undef>") . '" group_eq="' . ($ret{group_eq} // "<undef>") . '" host_port_eq="' . ($ret{host_port_eq} ? $ret{host_port_eq}{host} . ":" . ($ret{host_port_eq}{port} // "<undef>") : "<undef>") . '"' if($do_trace);
2019-04-17 15:56:35 +02:00
return undef unless(exists($ret{url_regex}) || exists($ret{group_eq}) || exists($ret{host_port_eq}));
return \%ret;
}
sub vinfo_match($$;@)
{
my $filter = shift;
my $vinfo = shift;
my %opts = @_;
my $flag_matched = $opts{flag_matched};
2019-12-14 17:06:15 +01:00
2021-07-24 20:55:50 +02:00
# never match dummy volume section
return 0 if($vinfo->{CONFIG}{DUMMY});
2019-12-14 17:06:15 +01:00
# match URL against sane path (can contain "//", see vinfo_child),
# no wildcards
my ($url_prefix, $path) = check_url($vinfo->{URL});
2020-08-20 15:42:34 +02:00
my $url = defined($path) ? lc($url_prefix) . $path : undef;
2019-04-17 15:56:35 +02:00
my $count = 0;
foreach my $ff (@$filter) {
if(defined($ff->{group_eq}) && (grep { $ff->{group_eq} eq $_ } @{$vinfo->{CONFIG}{group}})) {
2020-08-28 17:15:39 +02:00
TRACE "filter \"$ff->{unparsed}\" equals $vinfo->{CONFIG}{CONTEXT} group: $vinfo->{PRINT}" if($do_trace);
2019-04-17 15:56:35 +02:00
return $ff unless($flag_matched);
#push @{$ff->{$flag_matched}}, 'group=' . $ff->{group_eq};
$ff->{$flag_matched} = 1;
$count++;
}
2019-12-14 17:06:15 +01:00
if(defined($ff->{url_regex}) && defined($url) && ($url =~ /$ff->{url_regex}/)) {
2020-08-28 17:15:39 +02:00
TRACE "filter \"$ff->{unparsed}\" matches $vinfo->{CONFIG}{CONTEXT} url: $vinfo->{PRINT}" if($do_trace);
2019-04-17 15:56:35 +02:00
return $ff unless($flag_matched);
#push @{$ff->{$flag_matched}}, $vinfo->{CONFIG}{CONTEXT} . '=' . $vinfo->{PRINT};
$ff->{$flag_matched} = 1;
$count++;
}
2020-08-02 19:07:55 +02:00
if(defined($ff->{host_port_eq}) && defined($vinfo->{HOST})) {
my $host = $ff->{host_port_eq}{host};
my $port = $ff->{host_port_eq}{port};
2020-08-20 15:42:34 +02:00
if((lc($host) eq lc($vinfo->{HOST})) &&
2020-08-02 19:07:55 +02:00
(!defined($port) || (defined($vinfo->{PORT}) && ($port == $vinfo->{PORT}))))
{
2020-08-28 17:15:39 +02:00
TRACE "filter \"$ff->{unparsed}\" matches $vinfo->{CONFIG}{CONTEXT} host: $vinfo->{PRINT}" if($do_trace);
2020-08-02 19:07:55 +02:00
return $ff unless($flag_matched);
#push @{$ff->{$flag_matched}}, $vinfo->{CONFIG}{CONTEXT} . '=' . $vinfo->{PRINT};
$ff->{$flag_matched} = 1;
$count++;
2019-04-17 15:56:35 +02:00
}
}
}
return $count;
}
2019-04-11 15:56:37 +02:00
sub get_related_snapshots($$;$)
2015-04-23 16:19:34 +02:00
{
2018-02-15 02:51:22 +01:00
my $snaproot = shift || die;
2016-03-15 11:21:59 +01:00
my $svol = shift // die;
2018-02-15 16:53:29 +01:00
my $btrbk_basename = shift; # if set, also filter by direct_leaf
btrbk: treat all related readonly subvolumes within snapdir as "snapshots"
With this, previous snapshots (far relations) are still listed when
restoring a snapshot.
Example (S = source subvolume, readwrite):
After 3 snapshots:
A->S, B->S, C->S
Restore B: `btrfs subvol delete S; btrfs subvol snapshot B S'`
A->S, B->S, C->S, S'->B
Previous implementation would show now snapshots for S', as no
snapshot has parent_uuid=S'.
New implementation shows A, B, C as snapshots for S', as orphaned
siblings (A, B, C pointing to deleted S) are also related.
2019-04-11 14:41:08 +02:00
my @ret = map( { vinfo_resolved($_, $snaproot, btrbk_direct_leaf => $btrbk_basename) // () }
2022-11-20 14:36:16 +01:00
@{_related_nodes($svol->{node}, readonly => 1, omit_self => 1)} );
2015-04-23 16:19:34 +02:00
2020-08-28 17:15:39 +02:00
if($do_trace) { TRACE "get_related_snapshots: found: $_->{PRINT}" foreach(@ret); }
btrbk: treat all related readonly subvolumes within snapdir as "snapshots"
With this, previous snapshots (far relations) are still listed when
restoring a snapshot.
Example (S = source subvolume, readwrite):
After 3 snapshots:
A->S, B->S, C->S
Restore B: `btrfs subvol delete S; btrfs subvol snapshot B S'`
A->S, B->S, C->S, S'->B
Previous implementation would show now snapshots for S', as no
snapshot has parent_uuid=S'.
New implementation shows A, B, C as snapshots for S', as orphaned
siblings (A, B, C pointing to deleted S) are also related.
2019-04-11 14:41:08 +02:00
DEBUG "Found " . scalar(@ret) . " related snapshots of \"$svol->{PRINT}\" in: $snaproot->{PRINT}" . (defined($btrbk_basename) ? "/$btrbk_basename.*" : "");
2016-03-15 11:21:59 +01:00
return @ret;
}
2015-04-23 16:19:34 +02:00
2019-04-04 15:55:17 +02:00
sub _correlated_nodes($$)
2016-03-15 11:21:59 +01:00
{
2020-12-30 12:25:32 +01:00
my $dnode = shift || die; # any node on target filesystem
my $snode = shift || die;
2016-04-16 16:05:57 +02:00
my @ret;
2015-10-20 22:05:02 +02:00
2020-12-30 12:25:32 +01:00
if($snode->{is_root}) {
TRACE "Skip search for correlated targets: source subvolume is btrfs root: " . _fs_path($snode) if($do_trace);
2016-04-16 16:05:57 +02:00
return @ret;
2016-04-05 16:11:46 +02:00
}
2020-12-30 12:25:32 +01:00
unless($snode->{readonly}) {
TRACE "Skip search for correlated targets: source subvolume is not read-only: " . _fs_path($snode) if($do_trace);
2016-04-16 16:05:57 +02:00
return @ret;
2016-04-05 16:11:46 +02:00
}
2016-04-15 01:22:19 +02:00
# find matches by comparing uuid / received_uuid
2020-12-30 12:25:32 +01:00
my $uuid = $snode->{uuid};
my $received_uuid = $snode->{received_uuid};
2016-04-16 16:05:57 +02:00
$received_uuid = undef if($received_uuid eq '-');
2016-11-20 00:25:55 +01:00
2020-12-30 12:25:32 +01:00
my $received_uuid_hash = $dnode->{TREE_ROOT}{RECEIVED_UUID_HASH};
my $uuid_hash = $dnode->{TREE_ROOT}{UUID_HASH};
2016-04-16 16:05:57 +02:00
2018-02-15 17:42:41 +01:00
# match uuid/received_uuid combinations
my @match;
2018-06-25 13:30:14 +02:00
push(@match, @{ $received_uuid_hash->{$uuid} // [] }); # match src.uuid == target.received_uuid
2018-02-15 17:42:41 +01:00
if($received_uuid) {
2018-06-25 13:30:14 +02:00
push(@match, $uuid_hash->{$received_uuid} ); # match src.received_uuid == target.uuid
push(@match, @{ $received_uuid_hash->{$received_uuid} // [] }); # match src.received_uuid == target.received_uuid
2016-04-16 16:05:57 +02:00
}
2018-02-15 17:42:41 +01:00
@ret = grep($_->{readonly}, @match);
2020-12-30 12:25:32 +01:00
TRACE "correlated_nodes: dst=\"" . _fs_path($dnode) . "\", src=\"" . _fs_path($snode) . "\": [" . join(", ", map _fs_path($_),@ret) . "]" if($do_trace);
2016-04-16 16:05:57 +02:00
return @ret;
}
2018-02-15 17:42:41 +01:00
# returns array of vinfo of receive targets matching btrbk name
sub get_receive_targets($$;@)
2016-04-16 16:05:57 +02:00
{
2018-02-15 17:42:41 +01:00
my $droot = shift || die;
my $src_vol = shift || die;
2016-04-16 16:05:57 +02:00
my %opts = @_;
2018-02-15 17:42:41 +01:00
my @ret;
2016-04-16 16:05:57 +02:00
2020-12-30 12:25:32 +01:00
my @correlated = _correlated_nodes($droot->{node}, $src_vol->{node});
2022-06-05 17:35:20 +02:00
my @unexpected;
2019-04-04 15:55:17 +02:00
foreach (@correlated) {
2018-02-15 17:42:41 +01:00
my $vinfo = vinfo_resolved($_, $droot); # returns undef if not below $droot
if(exists($_->{BTRBK_RAW})) {
2020-08-28 17:15:39 +02:00
TRACE "get_receive_targets: found raw receive target: " . _fs_path($_) if($do_trace);
2018-02-15 17:42:41 +01:00
}
2018-05-14 23:43:13 +02:00
elsif($vinfo && ($vinfo->{SUBVOL_PATH} eq $src_vol->{NAME})) { # direct leaf, (SUBVOL_DIR = "", matching NAME)
2020-08-28 17:15:39 +02:00
TRACE "get_receive_targets: found receive target (exact-match): $vinfo->{PRINT}" if($do_trace);
2018-05-14 23:43:13 +02:00
}
elsif($vinfo && (not $opts{exact})) {
2020-08-28 17:15:39 +02:00
TRACE "get_receive_targets: found receive target (non-exact-match): $vinfo->{PRINT}" if($do_trace);
2018-02-15 17:42:41 +01:00
}
else {
2020-08-28 17:15:39 +02:00
TRACE "get_receive_targets: skip unexpected match: " . _fs_path($_) if($do_trace);
2022-06-05 17:35:20 +02:00
push @unexpected, { src_vol => $src_vol, target_node => $_ };
2021-07-15 13:21:40 +02:00
if($opts{warn} && config_key($droot, "warn_unknown_targets")) {
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unknown location: " . ($vinfo ? $vinfo->{PRINT} : _fs_path($_));
2016-04-07 14:34:51 +02:00
}
2018-02-15 17:42:41 +01:00
next;
2016-04-07 14:34:51 +02:00
}
2018-02-15 17:42:41 +01:00
push(@ret, $vinfo);
2016-03-15 11:21:59 +01:00
}
2022-06-05 17:35:20 +02:00
push(@{$opts{ret_unexpected_only}}, @unexpected) if($opts{ret_unexpected_only} && scalar(@unexpected) && !scalar(@ret));
2018-02-15 17:42:41 +01:00
return @ret;
2016-03-15 11:21:59 +01:00
}
2015-10-20 22:05:02 +02:00
2018-10-18 17:54:46 +02:00
# returns best correlated receive target within droot (independent of btrbk name)
2019-04-09 22:09:12 +02:00
sub get_best_correlated($$;@)
2018-10-18 17:54:46 +02:00
{
my $droot = shift || die;
my $src_vol = shift || die;
my %opts = @_;
2019-04-09 22:09:12 +02:00
my $inaccessible_nodes = $opts{push_inaccessible_nodes};
2018-10-18 17:54:46 +02:00
2020-12-30 12:25:32 +01:00
my @correlated = _correlated_nodes($droot->{node}, $src_vol->{node}); # all matching src_vol, from droot->TREE_ROOT
2018-10-18 17:54:46 +02:00
foreach (@correlated) {
2019-04-09 22:09:12 +02:00
my $vinfo = vinfo_resolved($_, $droot); # $vinfo is within $droot
return [ $src_vol, $vinfo ] if($vinfo);
2018-10-18 17:54:46 +02:00
}
if($opts{fallback_all_mountpoints}) {
foreach (@correlated) {
my $vinfo = vinfo_resolved_all_mountpoints($_, $droot); # $vinfo is within any mountpoint of filesystem at $droot
2019-04-09 22:09:12 +02:00
return [ $src_vol, $vinfo ] if($vinfo);
2018-10-18 17:54:46 +02:00
}
}
2019-04-09 22:09:12 +02:00
push @$inaccessible_nodes, @correlated if($inaccessible_nodes);
2018-10-18 17:54:46 +02:00
return undef;
}
2019-05-02 18:41:15 +02:00
# returns all related readonly nodes (by parent_uuid relationship), unsorted.
2020-12-30 12:25:32 +01:00
sub _related_nodes($;@)
2018-10-18 17:52:01 +02:00
{
2020-12-30 12:25:32 +01:00
my $snode = shift // die;
2019-05-02 18:41:15 +02:00
my %opts = @_;
2020-12-30 12:25:32 +01:00
TRACE "related_nodes: resolving related subvolumes of: " . _fs_path($snode) if($do_trace);
2018-10-18 17:52:01 +02:00
# iterate parent chain
my @related_nodes;
2020-12-30 12:25:32 +01:00
my $uuid_hash = $snode->{TREE_ROOT}{UUID_HASH};
my $parent_uuid_hash = $snode->{TREE_ROOT}{PARENT_UUID_HASH};
my $node = $snode;
2019-04-09 21:58:16 +02:00
my $uuid = $node->{uuid};
2019-05-03 15:35:25 +02:00
my $abort_distance = 4096;
2019-05-02 18:41:15 +02:00
# climb up parent chain
my $distance = 0; # parent distance
while(($distance < $abort_distance) && defined($node) && ($node->{parent_uuid} ne "-")) {
2019-04-09 21:58:16 +02:00
$uuid = $node->{parent_uuid};
$node = $uuid_hash->{$uuid};
2020-08-28 17:15:39 +02:00
TRACE "related_nodes: d=$distance uuid=$uuid : parent: " . ($node ? _fs_path($node) : "<deleted>") if($do_trace);
2018-10-18 17:52:01 +02:00
$distance++;
}
2019-05-02 18:41:15 +02:00
if($distance >= $abort_distance) {
2022-11-20 14:37:12 +01:00
my $logmsg = "Parent UUID chain exceeds depth=$abort_distance" .
($opts{fatal} ? " for: " : ", ignoring related parents of uuid=$uuid for: ") . _fs_path($snode);
2019-05-02 18:41:15 +02:00
DEBUG $logmsg;
WARN_ONCE $logmsg unless($opts{nowarn});
2022-11-20 14:37:12 +01:00
return undef if($opts{fatal});
2019-05-02 18:41:15 +02:00
}
2020-08-28 17:15:39 +02:00
TRACE "related_nodes: d=$distance uuid=$uuid : top of parent chain" if($do_trace);
2019-05-02 18:41:15 +02:00
# push related children (even if parent node is missing -> siblings)
my @nn;
$abort_distance = $abort_distance;
$distance = $distance * (-1); # child distance (from top parent)
while($uuid) {
2019-05-22 15:33:48 +02:00
push @related_nodes, $node if($node && (!$opts{readonly} || $node->{readonly}));
2019-05-02 18:41:15 +02:00
my $children = $parent_uuid_hash->{$uuid};
if($children) {
if($distance >= $abort_distance) {
2022-11-20 14:37:12 +01:00
my $logmsg = "Parent/child relations exceed depth=$abort_distance" .
($opts{fatal} ? " for: " : ", ignoring related children of uuid=$uuid for: ") . _fs_path($snode);
2019-05-02 18:41:15 +02:00
DEBUG $logmsg;
WARN_ONCE $logmsg unless($opts{nowarn});
2022-11-20 14:37:12 +01:00
return undef if($opts{fatal});
2019-05-02 18:41:15 +02:00
} else {
push @nn, { MARK_UUID => $uuid, MARK_DISTANCE => ($distance + 1) }, @$children;
}
}
2020-08-28 17:15:39 +02:00
if($do_trace) {
2019-05-02 18:41:15 +02:00
if($node) {
if($node->{readonly}) {
TRACE "related_nodes: d=$distance uuid=$uuid : push related readonly: " . _fs_path($node);
} else {
2019-05-22 15:33:48 +02:00
TRACE "related_nodes: d=$distance uuid=$uuid : " . ($opts{readonly} ? "" : "push ") . "related not readonly: " . _fs_path($node);
2019-05-02 18:41:15 +02:00
}
} else {
TRACE "related_nodes: d=$distance uuid=$uuid : related missing: <deleted>";
}
if($children && ($distance < $abort_distance)) {
TRACE "related_nodes: d=$distance uuid=$uuid : postpone " . scalar(@$children) . " children";
}
}
$node = shift @nn;
if(exists($node->{MARK_DISTANCE})) {
# marker reached, restore distance
$distance = $node->{MARK_DISTANCE};
2020-08-28 17:15:39 +02:00
TRACE "related_nodes: d=$distance uuid=$node->{MARK_UUID} : processing children" if($do_trace);
2019-05-02 18:41:15 +02:00
$node = shift @nn;
}
$uuid = $node->{uuid};
}
2019-05-22 15:33:48 +02:00
if($opts{omit_self}) {
2020-12-30 12:25:32 +01:00
my $snode_id = $snode->{id};
my @filtered = grep { $_->{id} != $snode_id } @related_nodes;
2020-08-28 17:15:39 +02:00
TRACE "related_nodes: found total=" . scalar(@filtered) . " related readonly subvolumes" if($do_trace);
2022-11-20 14:36:16 +01:00
return \@filtered;
2019-05-22 15:33:48 +02:00
}
2020-08-28 17:15:39 +02:00
TRACE "related_nodes: found total=" . scalar(@related_nodes) . " related readonly subvolumes (including self)" if($do_trace);
2022-11-20 14:36:16 +01:00
return \@related_nodes;
2018-02-15 17:42:41 +01:00
}
2019-04-09 22:09:12 +02:00
# returns parent, along with clone sources
2018-10-18 17:54:46 +02:00
sub get_best_parent($$$;@)
2018-02-15 17:42:41 +01:00
{
my $svol = shift // die;
2018-10-18 17:54:46 +02:00
my $snaproot = shift // die;
2018-02-15 17:42:41 +01:00
my $droot = shift || die;
2018-10-18 17:52:01 +02:00
my %opts = @_;
2019-04-09 22:09:12 +02:00
my $ret_clone_src = $opts{clone_src};
my $ret_target_parent_node = $opts{target_parent_node};
2019-04-09 22:15:18 +02:00
my $strict_related = $opts{strict_related};
2019-04-09 22:09:12 +02:00
2020-08-28 17:15:39 +02:00
TRACE "get_best_parent: resolving best common parent for subvolume: $svol->{PRINT} (droot=$droot->{PRINT})" if($do_trace);
2018-10-18 17:54:46 +02:00
# honor incremental_resolve option
my $source_incremental_resolve = config_key($svol, "incremental_resolve");
my $target_incremental_resolve = config_key($droot, "incremental_resolve");
my $resolve_sroot = ($source_incremental_resolve eq "mountpoint") ? $snaproot->{VINFO_MOUNTPOINT} : $snaproot;
my $resolve_droot = ($source_incremental_resolve eq "mountpoint") ? $droot->{VINFO_MOUNTPOINT} : $droot;
# NOTE: Using parents from different mount points does NOT work, see
# <https://github.com/kdave/btrfs-progs/issues/96>.
# btrfs-progs-4.20.2 fails if the parent subvolume is not on same
# mountpoint as the source subvolume:
# - btrfs send -p: "ERROR: not on mount point: /path/to/mountpoint"
# - btrfs receive: "ERROR: parent subvol is not reachable from inside the root subvol"
2021-07-24 11:19:04 +02:00
#
# Note that specifying clones from outside the mount point would work for btrfs send,
# but btrfs receive fails with same error as above (tested with v5.13).
2018-10-18 17:54:46 +02:00
my $source_fallback_all_mountpoints = ($source_incremental_resolve eq "_all_accessible");
my $target_fallback_all_mountpoints = ($target_incremental_resolve eq "_all_accessible");
2018-07-18 15:35:56 +02:00
2019-04-09 22:09:12 +02:00
my @inaccessible_nodes;
my %gbc_opts = ( push_inaccessible_nodes => \@inaccessible_nodes,
fallback_all_mountpoints => $target_fallback_all_mountpoints,
);
2018-10-18 17:52:01 +02:00
2019-05-02 18:41:15 +02:00
# resolve correlated subvolumes by parent_uuid relationship.
2021-08-19 16:20:16 +02:00
# no warnings on aborted search (due to deep relations).
2019-04-09 22:09:12 +02:00
my %c_rel_id; # map id to c_related
my @c_related; # candidates for parent (correlated + related), unsorted
2022-11-20 14:36:16 +01:00
foreach (@{_related_nodes($svol->{node}, readonly => 1, omit_self => 1, nowarn => 1)}) {
2018-10-18 17:54:46 +02:00
my $vinfo = vinfo_resolved($_, $resolve_sroot);
if((not $vinfo) && $source_fallback_all_mountpoints) { # related node is not under $resolve_sroot
$vinfo = vinfo_resolved_all_mountpoints($_, $svol);
}
if($vinfo) {
2019-05-19 19:01:50 +02:00
my $correlated = get_best_correlated($resolve_droot, $vinfo, %gbc_opts);
2019-04-09 22:09:12 +02:00
push @c_related, $correlated if($correlated);
$c_rel_id{$_->{id}} = $correlated;
2018-10-18 17:54:46 +02:00
} else {
DEBUG "Related subvolume is not accessible within $source_incremental_resolve \"$resolve_sroot->{PRINT}\": " . _fs_path($_);
}
2018-10-18 17:52:01 +02:00
}
2019-04-09 22:09:12 +02:00
# sort by cgen
my $cgen_ref = $svol->{node}{readonly} ? $svol->{node}{cgen} : $svol->{node}{gen};
2021-08-19 16:20:16 +02:00
my %c_map; # map correlated candidates to incremental_prefs strategy
# all_related: by parent_uuid relationship, ordered by cgen
$c_map{aro} = [ sort { ($cgen_ref - $a->[0]{node}{cgen}) <=> ($cgen_ref - $b->[0]{node}{cgen}) }
grep { $_->[0]{node}{cgen} <= $cgen_ref } @c_related ];
$c_map{arn} = [ sort { ($a->[0]{node}{cgen} - $cgen_ref) <=> ($b->[0]{node}{cgen} - $cgen_ref) }
grep { $_->[0]{node}{cgen} > $cgen_ref } @c_related ];
2018-10-18 17:52:01 +02:00
2020-12-30 12:25:32 +01:00
# NOTE: While _related_nodes() returns deep parent_uuid
2019-04-09 22:09:12 +02:00
# relations, there is always a chance that these relations get
# broken.
2018-10-18 17:54:46 +02:00
#
# Consider parent_uuid chain ($svol readonly)
# B->A, C->B, delete B: C has no relation to A.
# This is especially true for backups and archives (btrfs receive)
#
# For snapshots (here: S=$svol readwrite) the scenario is different:
# A->S, B->S, C->S, delete B: A still has a relation to C.
#
2019-04-09 22:09:12 +02:00
# resolve correlated subvolumes in same directory matching btrbk file name scheme
if(exists($svol->{node}{BTRBK_BASENAME})) {
2018-10-18 17:52:01 +02:00
my $snaproot_btrbk_direct_leaf = vinfo_subvol_list($snaproot, readonly => 1, btrbk_direct_leaf => $svol->{node}{BTRBK_BASENAME});
2019-04-09 22:09:12 +02:00
my @sbdl_older = sort { cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) }
grep { cmp_date($_->{node}{BTRBK_DATE}, $svol->{node}{BTRBK_DATE}) < 0 } @$snaproot_btrbk_direct_leaf;
my @sbdl_newer = sort { cmp_date($a->{node}{BTRBK_DATE}, $b->{node}{BTRBK_DATE}) }
grep { cmp_date($_->{node}{BTRBK_DATE}, $svol->{node}{BTRBK_DATE}) > 0 } @$snaproot_btrbk_direct_leaf;
2021-08-19 16:20:16 +02:00
# snapdir_all: btrbk_direct_leaf, ordered by btrbk timestamp
$c_map{sao} = [ map { $c_rel_id{$_->{node}{id}} // get_best_correlated($resolve_droot, $_, %gbc_opts) // () } @sbdl_older ];
$c_map{san} = [ map { $c_rel_id{$_->{node}{id}} // get_best_correlated($resolve_droot, $_, %gbc_opts) // () } @sbdl_newer ];
2019-04-09 22:09:12 +02:00
2021-08-19 16:20:16 +02:00
# snapdir_related: btrbk_direct_leaf with parent_uuid relationship, ordered by btrbk timestamp
$c_map{sro} = [ map { $c_rel_id{$_->{node}{id}} // () } @sbdl_older ];
$c_map{srn} = [ map { $c_rel_id{$_->{node}{id}} // () } @sbdl_newer ];
2019-04-09 22:09:12 +02:00
}
if(scalar @inaccessible_nodes) { # populated by get_best_correlated()
WARN "Best common parent for \"$svol->{PRINT}\" is not accessible within target $target_incremental_resolve \"$resolve_droot->{PRINT}\", ignoring: " . join(", ", map('"' . _fs_path($_) . '"',@inaccessible_nodes));
}
2021-08-19 16:20:16 +02:00
# resolve parent (and required clone sources) according to incremental_prefs
2021-08-18 17:34:39 +02:00
2021-08-19 16:20:16 +02:00
if($do_trace) {
TRACE "get_best_parent: related reference cgen=$svol->{node}{cgen}";
foreach my $search (@incremental_prefs_avail) {
TRACE map("get_best_parent: ${search}: $_->[0]{PRINT} (cgen=$_->[0]{node}{cgen}) $_->[1]{PRINT}", @{$c_map{$search}});
}
}
2021-08-18 17:34:39 +02:00
2021-08-19 16:20:16 +02:00
my @parent;
my @isk = map { $_ eq "defaults" ? @incremental_prefs_default : $_ } @{config_key($svol, "incremental_prefs")};
foreach(@isk) {
2021-08-28 12:55:34 +02:00
TRACE "processing incremental_prefs: $_";
2021-08-19 16:20:16 +02:00
my ($k, $n) = split /:/;
my $c_list = $c_map{$k} // next;
for(1 .. ($n // @$c_list)) {
my $cc = shift @$c_list // last;
next if(grep { $_->[0]{node}{id} == $cc->[0]{node}{id} } @parent);
DEBUG "Resolved " . (@parent ? "clone source" : "parent") . " (" .
2021-08-28 12:55:34 +02:00
"next closest " . ($k =~ /n/ ? "newer" : "older") .
2021-08-19 16:20:16 +02:00
" by " . ($k =~ /s/ ? "btrbk timestamp in snapdir" : "cgen") .
", " . ($k =~ /r/ ? "with" : "regardless of") . " parent_uuid relationship" .
"): $cc->[0]{PRINT}" if($loglevel >= 3);
push @parent, $cc;
}
}
2019-04-09 22:09:12 +02:00
# assemble results
unless(scalar @parent) {
2021-08-19 16:20:16 +02:00
DEBUG("No suitable common parents of \"$svol->{PRINT}\" found in src=\"$resolve_sroot->{PRINT}/\", target=\"$resolve_droot->{PRINT}/\"");
2018-10-18 17:54:46 +02:00
return undef;
}
2021-08-19 16:20:16 +02:00
if($strict_related && (!grep(exists($c_rel_id{$_->[0]{node}{id}}), @parent))) {
# no relations by parent_uuid found
2022-06-05 20:17:59 +02:00
WARN "No related common parent found (by parent_uuid relationship) for: $svol->{PRINT}",
"Hint: setting option \"incremental\" to \"yes\" (instead of \"strict\") will use parent: " . join(", ", map $_->[0]{PRINT}, @parent);
2019-04-09 22:15:18 +02:00
return undef;
}
2019-04-09 22:09:12 +02:00
my $ret_parent = shift @parent;
2021-08-19 18:01:53 +02:00
$$ret_clone_src = [ map $_->[0], @parent ] if($ret_clone_src);
2019-04-09 22:09:12 +02:00
$$ret_target_parent_node = $ret_parent->[1]{node} if($ret_target_parent_node);
return $ret_parent->[0];
2015-06-02 22:16:33 +02:00
}
2019-04-11 15:56:37 +02:00
sub get_latest_related_snapshot($$;$)
2015-06-02 22:16:33 +02:00
{
2016-03-15 11:21:59 +01:00
my $sroot = shift || die;
my $svol = shift // die;
2018-02-15 02:56:09 +01:00
my $btrbk_basename = shift;
2016-03-15 11:21:59 +01:00
my $latest = undef;
my $gen = -1;
2019-04-11 15:56:37 +02:00
foreach (get_related_snapshots($sroot, $svol, $btrbk_basename)) {
2016-03-15 11:21:59 +01:00
if($_->{node}{cgen} > $gen) {
$latest = $_;
$gen = $_->{node}{cgen};
}
}
if($latest) {
DEBUG "Latest snapshot child for \"$svol->{PRINT}#$svol->{node}{gen}\" is: $latest->{PRINT}#$latest->{node}{cgen}";
} else {
DEBUG "No latest snapshots found for: $svol->{PRINT}";
}
return $latest;
}
2015-06-02 22:16:33 +02:00
2016-08-29 14:43:29 +02:00
sub check_file($$;@)
2016-03-15 11:21:59 +01:00
{
my $file = shift // die;
my $accept = shift || die;
2016-08-29 14:43:29 +02:00
my %opts = @_;
my $sanitize = $opts{sanitize};
2018-02-13 17:21:44 +01:00
my $error_statement = $opts{error_statement}; # if not defined, no error messages are printed
2016-03-16 13:25:19 +01:00
2021-08-17 11:40:54 +02:00
if($accept->{absolute} && $accept->{relative}) {
# accepted, matches either absolute or relative
}
elsif($accept->{absolute}) {
unless($file =~ /^\//) {
ERROR "Only absolute files allowed $error_statement" if(defined($error_statement));
return undef;
2016-01-14 18:02:53 +01:00
}
2021-08-17 11:40:54 +02:00
}
elsif($accept->{relative}) {
if($file =~ /^\//) {
ERROR "Only relative files allowed $error_statement" if(defined($error_statement));
return undef;
2016-03-15 11:21:59 +01:00
}
2021-08-17 11:40:54 +02:00
}
elsif($accept->{name_only}) {
if($file =~ /\//) {
ERROR "Invalid file name ${error_statement}: $file" if(defined($error_statement));
return undef;
2016-03-15 11:21:59 +01:00
}
2015-06-02 22:16:33 +02:00
}
2021-08-17 11:40:54 +02:00
elsif(not $accept->{wildcards}) {
die("accept_type must contain either 'relative' or 'absolute'");
2015-06-02 22:16:33 +02:00
}
2021-08-17 11:40:54 +02:00
2022-11-19 19:31:57 +01:00
if($file =~ /\n/) {
ERROR "Unsupported newline in file ${error_statement}: " . ($file =~ s/\n/\\n/gr) if(defined($error_statement));
return undef;
}
2016-03-15 11:21:59 +01:00
if(($file =~ /^\.\.$/) || ($file =~ /^\.\.\//) || ($file =~ /\/\.\.\//) || ($file =~ /\/\.\.$/)) {
2018-02-13 17:21:44 +01:00
ERROR "Illegal directory traversal ${error_statement}: $file" if(defined($error_statement));
2016-03-15 11:21:59 +01:00
return undef;
}
2016-08-29 14:43:29 +02:00
if($sanitize) {
2021-08-15 18:24:41 +02:00
$file =~ s/^\s+//;
$file =~ s/\s+$//;
2021-07-24 14:33:32 +02:00
$file =~ s/\/(\.?\/)+/\//g; # sanitize "//", "/./" -> "/"
$file =~ s/\/\.$/\//; # sanitize trailing "/." -> "/"
2016-08-29 14:43:29 +02:00
$file =~ s/\/$// unless($file eq '/'); # remove trailing slash
}
2021-08-15 18:24:41 +02:00
elsif(($file =~ /^\s/) || ($file =~ /\s$/)) {
ERROR "Illegal leading/trailing whitespace ${error_statement}: \"$file\"" if(defined($error_statement));
return undef;
}
2022-11-20 10:35:34 +01:00
if($safe_commands && $file !~ /^$safe_file_match$/) {
ERROR "Invalid file name (restricted by \"safe_commands\" option) ${error_statement}: \"$file\"" if(defined($error_statement));
return undef;
}
2021-08-15 18:24:41 +02:00
2016-04-25 14:23:15 +02:00
return $file;
}
2018-02-13 17:21:44 +01:00
sub check_url($;@)
2016-04-25 14:23:15 +02:00
{
my $url = shift // die;
2018-02-13 17:21:44 +01:00
my %opts = @_;
2016-04-25 14:23:15 +02:00
my $url_prefix = "";
2016-04-25 16:07:40 +02:00
2018-07-09 16:13:48 +02:00
if($url =~ /^ssh:\/\//) {
2020-08-02 19:07:55 +02:00
if($url =~ s/^(ssh:\/\/($host_name_match|$ipv4_addr_match|\[$ipv6_addr_match\])(:[1-9][0-9]*)?)\//\//) {
2018-07-09 16:13:48 +02:00
$url_prefix = $1;
}
2016-04-25 16:07:40 +02:00
}
2020-08-02 19:07:55 +02:00
elsif($url =~ s/^($host_name_match|$ipv4_addr_match|\[$ipv6_addr_match\]):\//\//) {
# convert "my.host.com:/my/path", "[2001:db8::7]:/my/path" to ssh url
2016-04-25 16:07:40 +02:00
$url_prefix = "ssh://" . $1;
}
2020-08-02 19:07:55 +02:00
# if no url prefix match, treat it as file and let check_file() print errors
2016-04-25 16:07:40 +02:00
2019-04-17 15:56:35 +02:00
return ( $url_prefix, check_file($url, { absolute => 1, wildcards => $opts{accept_wildcards} }, sanitize => 1, %opts) );
2016-03-15 11:21:59 +01:00
}
2015-09-29 19:43:11 +02:00
2015-06-02 22:16:33 +02:00
2022-02-06 16:09:32 +01:00
sub config_key($$;$)
2014-12-14 19:23:02 +01:00
{
2016-03-15 11:21:59 +01:00
my $config = shift || die;
my $key = shift || die;
2022-02-06 16:09:32 +01:00
my $match = shift;
2016-03-15 11:21:59 +01:00
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
2015-04-21 14:53:31 +02:00
2022-02-06 16:09:32 +01:00
my $val;
2016-03-15 11:21:59 +01:00
if(exists($config_override{$key})) {
2020-08-28 17:15:39 +02:00
TRACE "config_key: OVERRIDE key=$key to value=" . ($config_override{$key} // "<undef>") if($do_trace);
2022-02-06 16:09:32 +01:00
$val = $config_override{$key};
2016-03-15 11:21:59 +01:00
}
2022-02-06 16:09:32 +01:00
else {
while(not exists($config->{$key})) {
# note: while all config keys exist in "meta" context (at least with default values),
# we also allow fake configs (CONTEXT="cmdline") which have no PARENT.
return undef unless($config->{PARENT});
$config = $config->{PARENT};
}
$val = $config->{$key};
}
return undef unless defined($val);
return $val unless defined($match);
2015-04-21 14:53:31 +02:00
2022-02-06 16:09:32 +01:00
if(ref($val) eq "ARRAY") {
return grep(/^$match$/, @$val) ? $match : undef;
} else {
return ($val eq $match) ? $match : undef;
2016-03-15 11:21:59 +01:00
}
}
2016-03-11 14:55:22 +01:00
2015-03-13 17:54:08 +01:00
2022-02-06 16:26:29 +01:00
sub config_key_lru($$;$) {
my $vinfo = shift || die;
my $key = shift || die;
my $match = shift;
my $retval;
if(defined($vinfo->{HOST})) {
$retval //= config_key($vinfo, $key . "_remote", $match);
} else {
$retval //= config_key($vinfo, $key . "_local_user", $match) if($>); # $EUID, $EFFECTIVE_USER_ID
$retval //= config_key($vinfo, $key . "_local", $match);
}
$retval //= config_key($vinfo, $key, $match);
return $retval;
}
2017-09-28 14:02:06 +02:00
sub config_preserve_hash($$;@)
2016-03-15 11:21:59 +01:00
{
my $config = shift || die;
my $prefix = shift || die;
2017-09-28 14:02:06 +02:00
my %opts = @_;
if($opts{wipe}) {
2018-01-05 19:28:10 +01:00
return { hod => 0, dow => 'sunday', min => 'latest', min_q => 'latest' };
2017-09-28 14:02:06 +02:00
}
2019-03-04 16:05:38 +01:00
my $preserve = config_key($config, $prefix . "_preserve") // {};
my %ret = ( %$preserve, # make a copy (don't pollute config)
hod => config_key($config, "preserve_hour_of_day"),
dow => config_key($config, "preserve_day_of_week")
);
2016-04-12 19:55:29 +02:00
my $preserve_min = config_key($config, $prefix . "_preserve_min");
if(defined($preserve_min)) {
2019-03-04 16:05:38 +01:00
$ret{min} = $preserve_min; # used for raw schedule output
2016-04-12 20:35:57 +02:00
if(($preserve_min eq 'all') || ($preserve_min eq 'latest')) {
2019-03-04 16:05:38 +01:00
$ret{min_q} = $preserve_min;
2016-04-12 11:47:28 +02:00
}
2016-04-12 19:55:29 +02:00
elsif($preserve_min =~ /^([0-9]+)([hdwmy])$/) {
2019-03-04 16:05:38 +01:00
$ret{min_n} = $1;
$ret{min_q} = $2;
2016-04-12 11:47:28 +02:00
}
else { die; }
}
2019-03-04 16:05:38 +01:00
return \%ret;
2016-03-15 11:21:59 +01:00
}
2015-03-13 17:54:08 +01:00
2016-03-15 11:21:59 +01:00
2016-05-11 20:15:46 +02:00
sub config_compress_hash($$)
{
my $config = shift || die;
my $config_key = shift || die;
my $compress_key = config_key($config, $config_key);
return undef unless($compress_key);
return {
key => $compress_key,
level => config_key($config, $config_key . "_level"),
2020-12-24 00:10:33 +01:00
long => config_key($config, $config_key . "_long"),
2016-05-11 20:15:46 +02:00
threads => config_key($config, $config_key . "_threads"),
2021-06-07 07:03:47 +02:00
adapt => config_key($config, $config_key . "_adapt"),
2016-05-11 20:15:46 +02:00
};
}
2019-07-29 21:59:03 +02:00
sub config_stream_hash($$)
{
my $source = shift || die;
my $target = shift || die;
return {
stream_compress => config_compress_hash($target, "stream_compress"),
# for remote source, limits read rate of ssh stream output after decompress
# for remote target, limits read rate of "btrfs send"
# for both local, limits read rate of "btrfs send"
# for raw targets, limits read rate of "btrfs send | xz" (raw_target_compress)
local_sink => {
stream_buffer => config_key($target, "stream_buffer"),
rate_limit => config_key($target, "rate_limit"),
show_progress => $show_progress,
},
# limits read rate of "btrfs send"
rsh_source => { # limit read rate after "btrfs send", before compression
stream_buffer => config_key($source, "stream_buffer_remote"),
rate_limit => config_key($source, "rate_limit_remote"),
#rate_limit_out => config_key($source, "rate_limit_remote"), # limit write rate
},
# limits read rate of ssh stream output
rsh_sink => {
stream_buffer => config_key($target, "stream_buffer_remote"),
rate_limit => config_key($target, "rate_limit_remote"),
#rate_limit_in => config_key($target, "rate_limit_remote"),
},
};
}
2017-03-18 15:06:48 +01:00
sub config_encrypt_hash($$)
{
my $config = shift || die;
my $config_key = shift || die;
my $encrypt_type = config_key($config, $config_key);
return undef unless($encrypt_type);
return {
type => $encrypt_type,
keyring => config_key($config, "gpg_keyring"),
recipient => config_key($config, "gpg_recipient"),
2017-06-16 17:04:18 +02:00
iv_size => config_key($config, "openssl_iv_size"),
ciphername => config_key($config, "openssl_ciphername"),
keyfile => config_key($config, "openssl_keyfile"),
2017-06-30 14:35:20 +02:00
kdf_keygen_each => (config_key($config, "kdf_keygen") eq "each"),
kdf_backend => config_key($config, "kdf_backend"),
kdf_keysize => config_key($config, "kdf_keysize"),
2017-03-18 15:06:48 +01:00
};
}
2016-03-15 11:21:59 +01:00
sub config_dump_keys($;@)
{
my $config = shift || die;
my %opts = @_;
my @ret;
my $maxlen = 0;
$config = $config->{CONFIG} if($config->{CONFIG}); # accept vinfo for $config
foreach my $key (sort keys %config_options)
2014-12-14 19:23:02 +01:00
{
2016-03-15 11:21:59 +01:00
my $val;
2022-07-27 18:36:20 +02:00
next if($config_options{$key}->{deprecated}{DEFAULT});
2022-05-29 12:23:40 +02:00
next unless($opts{all} || exists($config->{$key}) || exists($config_override{$key}));
next if($config_options{$key}{context} && !grep(/^$config->{CONTEXT}$/, @{$config_options{$key}{context}}));
$val = config_key($config, $key);
my @va = (ref($val) eq "ARRAY") ? ($config_options{$key}->{split} ? join(" ", @$val) : @$val) : $val;
foreach(@va) {
2018-02-13 19:26:54 +01:00
if(defined($_)) {
if($config_options{$key}->{accept_preserve_matrix}) {
2018-12-05 21:43:54 +01:00
$_ = format_preserve_matrix($_, format => "config");
2018-02-13 19:26:54 +01:00
}
2016-04-18 20:42:53 +02:00
}
2022-05-29 12:23:40 +02:00
$_ //= grep(/^no$/, @{$config_options{$key}{accept} // []}) ? "no" : "<unset>";
my $comment = $_ eq "<unset>" ? "# " : "";
2018-02-13 19:26:54 +01:00
my $len = length($key);
$maxlen = $len if($len > $maxlen);
2022-05-29 12:23:40 +02:00
push @ret, { comment => $comment, key => $key, val => $_, len => $len };
2016-03-15 11:21:59 +01:00
}
2015-06-17 12:42:29 +02:00
}
2022-05-29 12:23:40 +02:00
return map { ($opts{prefix} // "") . $_->{comment} . $_->{key} . (' ' x (1 + $maxlen - $_->{len})) . ' ' . $_->{val} } @ret;
2016-03-15 11:21:59 +01:00
}
2014-12-14 15:34:55 +01:00
2015-03-13 11:20:47 +01:00
2018-02-13 17:21:44 +01:00
sub append_config_option($$$$;@)
2016-03-15 11:21:59 +01:00
{
my $config = shift;
my $key = shift;
my $value = shift;
my $context = shift;
2018-02-13 17:21:44 +01:00
my %opts = @_;
my $error_statement = $opts{error_statement} // "";
2015-03-13 11:20:47 +01:00
2016-03-15 11:21:59 +01:00
my $opt = $config_options{$key};
# accept only keys listed in %config_options
unless($opt) {
2018-02-13 17:21:44 +01:00
ERROR "Unknown option \"$key\" $error_statement";
2016-03-15 11:21:59 +01:00
return undef;
}
2019-04-30 13:38:47 +02:00
if($opt->{context} && !grep(/^$context$/, @{$opt->{context}}) && ($context ne "OVERRIDE")) {
2020-08-28 18:48:52 +02:00
ERROR "Option \"$key\" is only allowed in " . join(" or ", @{$opt->{context}}) . " context $error_statement";
2016-03-15 11:21:59 +01:00
return undef;
}
2016-03-16 13:25:19 +01:00
if($opt->{deny_glob_context} && $config->{GLOB_CONTEXT}) {
2018-02-13 17:21:44 +01:00
ERROR "Option \"$key\" is not allowed on section with wildcards $error_statement";
2016-03-16 13:25:19 +01:00
return undef;
}
2022-05-28 17:56:00 +02:00
my $ovalue = $value;
2019-04-17 15:42:05 +02:00
if($value eq "") {
2022-05-28 17:56:00 +02:00
$value = "yes";
TRACE "option \"$key\" has no value, setting to \"yes\"" if($do_trace);
2019-04-17 15:42:05 +02:00
}
2022-05-28 17:56:00 +02:00
if($opt->{split}) {
2022-05-28 21:12:39 +02:00
$value = [ split($config_split_match, $value) ];
2016-03-15 11:21:59 +01:00
}
2022-05-28 17:56:00 +02:00
my $accepted;
if($opt->{accept}) {
$accepted = 1;
foreach my $val (ref($value) ? @$value : $value) {
$accepted = 0, last unless(grep { $val =~ /^$_$/ } @{$opt->{accept}});
TRACE "option \"$key=$val\" found in accept list" if($do_trace);
}
2016-03-15 11:21:59 +01:00
}
2022-05-28 17:56:00 +02:00
if(!$accepted && $opt->{accept_file}) {
2016-03-15 11:21:59 +01:00
# be very strict about file options, for security sake
2018-02-13 17:21:44 +01:00
$value = check_file($value, $opt->{accept_file}, sanitize => 1, error_statement => ($error_statement ? "for option \"$key\" $error_statement" : undef));
2016-04-25 14:23:15 +02:00
return undef unless(defined($value));
2015-03-13 11:20:47 +01:00
2020-08-28 17:15:39 +02:00
TRACE "option \"$key=$value\" is a valid file, accepted" if($do_trace);
2016-03-15 11:21:59 +01:00
$value = "no" if($value eq "."); # maps to undef later
2022-05-28 17:56:00 +02:00
$accepted = 1;
2015-04-21 14:53:31 +02:00
}
2022-05-28 17:56:00 +02:00
if(!$accepted && $opt->{accept_preserve_matrix}) {
2016-04-14 14:15:12 +02:00
my %preserve;
my $s = ' ' . $value;
while($s =~ s/\s+(\*|[0-9]+)([hdwmyHDWMY])//) {
my $n = $1;
my $q = lc($2); # qw( h d w m y )
$n = 'all' if($n eq '*');
if(exists($preserve{$q})) {
2018-02-13 17:21:44 +01:00
ERROR "Value \"$value\" failed input validation for option \"$key\": multiple definitions of '$q' $error_statement";
2016-04-14 14:15:12 +02:00
return undef;
}
$preserve{$q} = $n;
}
unless($s eq "") {
2018-02-13 17:21:44 +01:00
ERROR "Value \"$value\" failed input validation for option \"$key\" $error_statement";
2016-04-14 14:15:12 +02:00
return undef;
}
2020-08-28 17:15:39 +02:00
TRACE "adding preserve matrix $context context:" . Data::Dumper->new([\%preserve], [ $key ])->Indent(0)->Pad(' ')->Quotekeys(0)->Pair('=>')->Dump() if($do_trace && $do_dumper);
2016-04-14 14:15:12 +02:00
$config->{$key} = \%preserve;
return $config;
}
2022-05-28 17:56:00 +02:00
if(!$accepted) {
if($ovalue eq "") {
ERROR "Unsupported empty value for option \"$key\" $error_statement";
} else {
ERROR "Unsupported value \"$ovalue\" for option \"$key\" $error_statement";
}
2016-03-15 11:21:59 +01:00
return undef;
2016-03-14 16:39:13 +01:00
}
2016-05-03 14:34:04 +02:00
if($opt->{require_bin} && (not check_exe($opt->{require_bin}))) {
WARN "Found option \"$key\", but required executable \"$opt->{require_bin}\" does not exist on your system. Please install \"$opt->{require_bin}\".";
2018-02-13 17:21:44 +01:00
WARN "Ignoring option \"$key\" $error_statement";
2016-03-23 11:58:23 +01:00
$value = "no";
}
2016-03-15 11:21:59 +01:00
if($opt->{deprecated}) {
2021-08-19 17:13:49 +02:00
my $dh = $opt->{deprecated}{$value} // $opt->{deprecated}{DEFAULT} // {};
$dh = $opt->{deprecated}{MATCH} if($opt->{deprecated}{MATCH} && ($value =~ $opt->{deprecated}{MATCH}{regex}));
if($dh->{ABORT}) {
2022-07-27 18:20:33 +02:00
ERROR "Deprecated (incompatible) option \"$key\" found $error_statement, refusing to continue", $dh->{warn};
2016-04-13 17:13:03 +02:00
return undef;
}
2022-08-20 14:24:17 +02:00
my @wmsg;
push @wmsg, "Found deprecated option \"$key $value\" $error_statement", $dh->{warn} if($dh->{warn});
2022-07-27 18:20:33 +02:00
if(defined($dh->{replace_key})) {
$key = $dh->{replace_key};
$value = $dh->{replace_value};
push @wmsg, "Using \"$key $value\"";
}
2022-08-20 14:24:17 +02:00
WARN @wmsg if(@wmsg);
2021-08-19 17:13:49 +02:00
if($dh->{FAILSAFE_PRESERVE}) {
2016-04-14 13:01:28 +02:00
unless($config_override{FAILSAFE_PRESERVE}) { # warn only once
WARN "Entering failsafe mode:";
WARN " - preserving ALL snapshots for ALL subvolumes";
WARN " - ignoring ALL targets (skipping backup creation)";
2016-04-16 21:08:07 +02:00
WARN " - please read \"doc/upgrade_to_v0.23.0.md\"";
2016-04-14 13:01:28 +02:00
$config_override{FAILSAFE_PRESERVE} = "Failsafe mode active (deprecated configuration)";
}
$config_override{snapshot_preserve_min} = 'all';
return $config;
}
2016-03-11 14:55:22 +01:00
}
2016-03-15 11:21:59 +01:00
2018-02-13 19:26:54 +01:00
if($opt->{allow_multiple}) {
my $aref = $config->{$key} // [];
2022-05-28 17:56:00 +02:00
my @val = ref($value) ? @$value : $value;
push(@$aref, @val);
TRACE "pushing option \"$key=[" . join(",", @val) . "]\" to $aref=[" . join(',', @$aref) . "]" if($do_trace);
2018-02-13 19:26:54 +01:00
$value = $aref;
}
elsif(exists($config->{$key})) {
2018-02-26 15:59:20 +01:00
unless($opt->{c_default}) { # note: computed defaults are already present
WARN "Option \"$key\" redefined $error_statement";
}
2018-02-13 19:26:54 +01:00
}
2020-08-28 17:15:39 +02:00
TRACE "adding option \"$key=$value\" to $context context" if($do_trace);
2016-03-15 11:21:59 +01:00
$value = undef if($value eq "no"); # we don't want to check for "no" all the time
$config->{$key} = $value;
return $config;
2016-03-11 14:55:22 +01:00
}
2022-06-06 16:20:53 +02:00
sub parse_config_line($$$;@)
2014-12-14 19:23:02 +01:00
{
2022-06-06 16:20:53 +02:00
my ($cur, $key, $value, %opts) = @_;
my $root = $cur;
$root = $root->{PARENT} while($root->{CONTEXT} ne "global");
my $error_statement = $opts{error_statement} // "";
2016-03-09 19:52:45 +01:00
2016-03-15 11:21:59 +01:00
if($key eq "volume")
{
2022-06-06 18:00:51 +02:00
$value =~ s/^"(.*)"$/$1/;
$value =~ s/^'(.*)'$/$1/;
2016-03-15 11:21:59 +01:00
$cur = $root;
2020-08-28 17:15:39 +02:00
TRACE "config: context forced to: $cur->{CONTEXT}" if($do_trace);
2016-03-14 12:24:32 +01:00
2016-03-15 11:21:59 +01:00
# be very strict about file options, for security sake
2022-06-06 16:20:53 +02:00
my ($url_prefix, $path) = check_url($value, error_statement => "for option \"$key\" $error_statement");
2016-04-25 14:23:15 +02:00
return undef unless(defined($path));
2020-08-28 17:15:39 +02:00
TRACE "config: adding volume \"$url_prefix$path\" to global context" if($do_trace);
2020-08-28 18:48:52 +02:00
die unless($cur->{CONTEXT} eq "global");
2016-03-17 14:02:22 +01:00
my $volume = { CONTEXT => "volume",
PARENT => $cur,
SUBSECTION => [],
2016-04-25 14:23:15 +02:00
url => $url_prefix . $path,
2016-03-15 11:21:59 +01:00
};
push(@{$cur->{SUBSECTION}}, $volume);
$cur = $volume;
}
elsif($key eq "subvolume")
{
2022-06-06 18:00:51 +02:00
$value =~ s/^"(.*)"$/$1/;
$value =~ s/^'(.*)'$/$1/;
2016-03-15 11:21:59 +01:00
while($cur->{CONTEXT} ne "volume") {
2021-07-24 20:37:51 +02:00
if($cur->{CONTEXT} eq "global") {
TRACE "config: adding dummy volume context" if($do_trace);
my $volume = { CONTEXT => "volume",
PARENT => $cur,
SUBSECTION => [],
DUMMY => 1,
url => "/dev/null",
};
push(@{$cur->{SUBSECTION}}, $volume);
$cur = $volume;
last;
2016-03-15 11:21:59 +01:00
}
$cur = $cur->{PARENT} || die;
2020-08-28 17:15:39 +02:00
TRACE "config: context changed to: $cur->{CONTEXT}" if($do_trace);
2016-03-15 11:21:59 +01:00
}
# be very strict about file options, for security sake
2021-07-24 15:43:37 +02:00
my $url;
2021-07-24 20:37:51 +02:00
if(!$cur->{DUMMY} && (my $rel_path = check_file($value, { relative => 1, wildcards => 1 }, sanitize => 1))) {
2021-07-24 15:43:37 +02:00
$url = ($rel_path eq '.') ? $cur->{url} : $cur->{url} . '/' . $rel_path;
}
else {
2022-06-06 16:20:53 +02:00
my ($url_prefix, $path) = check_url($value, accept_wildcards => 1, error_statement => "for option \"$key\"" . ($cur->{DUMMY} ? " (if no \"volume\" section is declared)" : "") . " $error_statement");
2021-07-24 15:43:37 +02:00
return undef unless(defined($path));
$url = $url_prefix . $path;
}
2016-03-14 15:55:57 +01:00
2018-07-07 16:23:09 +02:00
# snapshot_name defaults to subvolume name (or volume name if subvolume=".")
2021-07-24 15:43:37 +02:00
my $default_snapshot_name = $url;
2018-07-07 16:23:09 +02:00
$default_snapshot_name =~ s/^.*\///;
$default_snapshot_name = 'ROOT' if($default_snapshot_name eq ""); # if volume="/"
2021-07-24 15:43:37 +02:00
TRACE "config: adding subvolume \"$url\" to volume context: $cur->{url}" if($do_trace);
2016-03-17 14:02:22 +01:00
my $subvolume = { CONTEXT => "subvolume",
PARENT => $cur,
# SUBSECTION => [], # handled by target propagation
2021-07-24 15:43:37 +02:00
url => $url,
2018-07-07 16:23:09 +02:00
snapshot_name => $default_snapshot_name, # computed default (c_default)
2016-03-15 11:21:59 +01:00
};
2016-03-16 13:25:19 +01:00
$subvolume->{GLOB_CONTEXT} = 1 if($value =~ /\*/);
2016-03-15 11:21:59 +01:00
push(@{$cur->{SUBSECTION}}, $subvolume);
$cur = $subvolume;
}
elsif($key eq "target")
{
if($cur->{CONTEXT} eq "target") {
$cur = $cur->{PARENT} || die;
2020-08-28 17:15:39 +02:00
TRACE "config: context changed to: $cur->{CONTEXT}" if($do_trace);
2016-03-14 15:55:57 +01:00
}
2022-06-06 18:00:51 +02:00
# As of btrbk-0.28.0, target_type is optional and defaults to "send-receive"
my $target_type = $config_target_types[0];
$target_type = lc($1) if($value =~ s/^([a-zA-Z_-]+)\s+//);
unless(grep(/^\Q$target_type\E$/, @config_target_types)) {
ERROR "Unknown target type \"$target_type\" $error_statement";
2016-03-15 11:21:59 +01:00
return undef;
2016-03-14 15:55:57 +01:00
}
2022-06-06 18:00:51 +02:00
$value =~ s/^"(.*)"$/$1/;
$value =~ s/^'(.*)'$/$1/;
my ($url_prefix, $path) = check_url($value, error_statement => "for option \"$key\" $error_statement");
return undef unless(defined($path));
TRACE "config: adding target \"$url_prefix$path\" (type=$target_type) to $cur->{CONTEXT} context" . ($cur->{url} ? ": $cur->{url}" : "") if($do_trace);
my $target = { CONTEXT => "target",
PARENT => $cur,
target_type => $target_type,
url => $url_prefix . $path,
};
# NOTE: target sections are propagated to the apropriate SUBSECTION in _config_propagate_target()
$cur->{TARGET} //= [];
push(@{$cur->{TARGET}}, $target);
$cur = $target;
2016-03-14 12:24:32 +01:00
}
2016-03-15 11:21:59 +01:00
else
{
2022-12-04 00:50:15 +01:00
$value =~ s/^"(.*)"$/$1/;
$value =~ s/^'(.*)'$/$1/;
2022-06-06 16:20:53 +02:00
return append_config_option($cur, $key, $value, $cur->{CONTEXT}, error_statement => $error_statement);
2016-03-10 05:26:43 +01:00
}
2015-03-13 11:44:04 +01:00
2016-03-15 11:21:59 +01:00
return $cur;
2016-03-14 16:39:13 +01:00
}
2016-03-17 14:02:22 +01:00
sub _config_propagate_target
{
my $cur = shift;
foreach my $subsection (@{$cur->{SUBSECTION}}) {
my @propagate_target;
foreach my $target (@{$cur->{TARGET}}) {
2020-08-28 17:15:39 +02:00
TRACE "propagating target \"$target->{url}\" from $cur->{CONTEXT} context to: $subsection->{CONTEXT} $subsection->{url}" if($do_trace);
2016-03-17 14:02:22 +01:00
die if($target->{SUBSECTION});
# don't propagate if a target of same target_type and url already exists in subsection
if($subsection->{TARGET} &&
grep({ ($_->{url} eq $target->{url}) && ($_->{target_type} eq $target->{target_type}) } @{$subsection->{TARGET}}))
{
DEBUG "Skip propagation of \"target $target->{target_type} $target->{url}\" from $cur->{CONTEXT} context to \"$subsection->{CONTEXT} $subsection->{url}\": same target already exists";
next;
}
2016-04-24 15:59:17 +02:00
my %copy = ( %$target, PARENT => $subsection );
2016-03-17 14:02:22 +01:00
push @propagate_target, \%copy;
}
$subsection->{TARGET} //= [];
unshift @{$subsection->{TARGET}}, @propagate_target; # maintain config order: propagated targets go in front of already defined targets
if($subsection->{CONTEXT} eq "subvolume") {
# finally create missing SUBSECTION in subvolume context
die if($subsection->{SUBSECTION});
$subsection->{SUBSECTION} = $subsection->{TARGET};
}
else {
# recurse into SUBSECTION
_config_propagate_target($subsection);
}
}
delete $cur->{TARGET};
return $cur;
}
2019-07-15 18:19:33 +02:00
sub _config_collect_values
{
my $config = shift;
my $key = shift;
my @values;
push(@values, @{$config->{$key}}) if(ref($config->{$key}) eq "ARRAY");
foreach (@{$config->{SUBSECTION}}) {
push(@values, _config_collect_values($_, $key));
}
return @values;
}
2016-03-15 16:54:54 +01:00
sub init_config(@)
{
2023-03-26 15:34:02 +02:00
my %defaults = ( CONTEXT => "meta", SRC_FILE => "DEFAULTS", @_ );
2016-03-15 16:54:54 +01:00
# set defaults
foreach (keys %config_options) {
next if $config_options{$_}->{deprecated}; # don't pollute hash with deprecated options
2018-02-13 18:40:35 +01:00
$defaults{$_} = $config_options{$_}->{default};
2016-03-15 16:54:54 +01:00
}
2020-08-28 18:48:52 +02:00
return { CONTEXT => "global", SUBSECTION => [], PARENT => \%defaults };
2016-03-15 16:54:54 +01:00
}
2019-08-05 14:31:48 +02:00
sub _config_file(@) {
2016-03-15 11:21:59 +01:00
my @config_files = @_;
2019-08-05 14:31:48 +02:00
foreach my $file (@config_files) {
2020-08-28 17:15:39 +02:00
TRACE "config: checking for file: $file" if($do_trace);
2019-08-05 14:31:48 +02:00
return $file if(-r "$file");
2016-03-14 16:39:13 +01:00
}
2019-08-05 14:31:48 +02:00
return undef;
}
sub parse_config($)
{
my $file = shift;
2019-07-28 18:54:34 +02:00
return undef unless($file);
2016-03-10 19:10:57 +01:00
2016-03-15 16:54:54 +01:00
my $root = init_config(SRC_FILE => $file);
2016-03-15 11:21:59 +01:00
my $cur = $root;
2016-03-10 19:10:57 +01:00
2020-08-28 17:15:39 +02:00
TRACE "config: open configuration file: $file" if($do_trace);
2016-03-15 11:21:59 +01:00
open(FILE, '<', $file) or die $!;
while (<FILE>) {
chomp;
2022-06-06 22:00:02 +02:00
s/((?:[^"'#]*(?:"[^"]*"|'[^']*'))*[^"'#]*)#.*/$1/; # remove comments
2016-03-15 11:21:59 +01:00
next if /^\s*$/; # ignore empty lines
2022-06-06 18:00:51 +02:00
s/^\s*//; # remove leading whitespace
s/\s*$//; # remove trailing whitespace
2020-08-28 17:15:39 +02:00
TRACE "config: parsing line $. with context=$cur->{CONTEXT}: \"$_\"" if($do_trace);
2022-06-06 18:00:51 +02:00
unless(/^([a-zA-Z_]+)(?:\s+(.*))?$/) {
2016-03-15 11:21:59 +01:00
ERROR "Parse error in \"$file\" line $.";
$root = undef;
last;
}
2022-06-06 18:00:51 +02:00
unless($cur = parse_config_line($cur, lc($1), $2 // "", error_statement => "in \"$file\" line $.")) {
$root = undef;
last;
}
TRACE "line processed: new context=$cur->{CONTEXT}" if($do_trace);
2016-03-14 16:39:13 +01:00
}
2016-03-15 11:21:59 +01:00
close FILE || ERROR "Failed to close configuration file: $!";
2016-03-17 14:02:22 +01:00
_config_propagate_target($root);
2016-03-15 11:21:59 +01:00
return $root;
2015-04-21 14:53:31 +02:00
}
2016-03-08 15:25:35 +01:00
# sets $target->{CONFIG}->{ABORTED} on failure
2016-03-07 21:45:12 +01:00
# sets $target->{SUBVOL_RECEIVED}
2016-03-07 20:47:24 +01:00
sub macro_send_receive(@)
2015-03-31 19:07:33 +02:00
{
my %info = @_;
2016-03-01 21:49:59 +01:00
my $source = $info{source} || die;
2015-04-16 12:00:04 +02:00
my $target = $info{target} || die;
my $parent = $info{parent};
2019-04-09 22:09:12 +02:00
my @clone_src = @{ $info{clone_src} // [] }; # copy array
2016-03-07 20:47:24 +01:00
my $config_target = $target->{CONFIG};
2015-06-02 22:16:33 +02:00
my $target_type = $config_target->{target_type} || die;
2015-04-19 11:36:40 +02:00
my $incremental = config_key($config_target, "incremental");
2015-03-31 19:07:33 +02:00
2015-05-15 16:06:36 +02:00
# check for existing target subvolume
2016-03-01 21:49:59 +01:00
if(my $err_vol = vinfo_subvol($target, $source->{NAME})) {
2019-07-15 18:19:33 +02:00
my $err_msg = "Please delete stray subvolume: \"btrfs subvolume delete $err_vol->{PRINT}\"";
2015-10-12 22:56:52 +02:00
ABORTED($config_target, "Target subvolume \"$err_vol->{PRINT}\" already exists");
2019-07-15 18:19:33 +02:00
FIX_MANUALLY($config_target, $err_msg);
2019-04-17 15:20:18 +02:00
ERROR ABORTED_TEXT($config_target) . ", aborting send/receive of: $source->{PRINT}";
2019-07-15 18:19:33 +02:00
ERROR $err_msg;
2015-05-15 16:06:36 +02:00
return undef;
}
2015-03-31 19:07:33 +02:00
if($incremental)
{
# create backup from latest common
2015-04-16 12:00:04 +02:00
if($parent) {
2016-04-19 19:36:58 +02:00
INFO "Creating incremental backup...";
2015-03-31 19:07:33 +02:00
}
elsif($incremental ne "strict") {
2020-08-02 14:06:17 +02:00
INFO "No common parent subvolume present, creating non-incremental backup...";
2015-03-31 19:07:33 +02:00
}
else {
2016-04-19 19:36:58 +02:00
WARN "Backup to $target->{PRINT} failed: no common parent subvolume found for \"$source->{PRINT}\", and option \"incremental\" is set to \"strict\"";
2015-10-12 22:56:52 +02:00
ABORTED($config_target, "No common parent subvolume found, and option \"incremental\" is set to \"strict\"");
2015-03-31 19:07:33 +02:00
return undef;
}
2021-08-19 18:01:53 +02:00
unless(config_key($target, "incremental_clones")) {
INFO "Ignoring " . scalar(@clone_src) . " clone sources (incremental_clones=no)" if(@clone_src);
@clone_src = ();
delete $info{clone_src};
}
2015-03-31 19:07:33 +02:00
}
else {
2020-08-02 14:06:17 +02:00
INFO "Creating non-incremental backup...";
2015-09-26 19:51:38 +02:00
$parent = undef;
2019-04-09 22:09:12 +02:00
@clone_src = ();
2015-03-31 19:07:33 +02:00
delete $info{parent};
2021-08-19 18:01:53 +02:00
delete $info{clone_src};
2015-03-31 19:07:33 +02:00
}
2015-06-02 22:16:33 +02:00
my $ret;
my $vol_received;
2017-06-16 17:43:17 +02:00
my $raw_info;
2015-06-02 22:16:33 +02:00
if($target_type eq "send-receive")
{
2019-04-09 22:09:12 +02:00
$ret = btrfs_send_receive($source, $target, $parent, \@clone_src, \$vol_received);
2015-10-12 22:56:52 +02:00
ABORTED($config_target, "Failed to send/receive subvolume") unless($ret);
2015-06-02 22:16:33 +02:00
}
elsif($target_type eq "raw")
{
unless($dryrun) {
2016-03-01 21:49:59 +01:00
# make sure we know the source uuid
2016-04-13 22:04:53 +02:00
if($source->{node}{uuid} =~ /^$fake_uuid_prefix/) {
2016-03-01 21:49:59 +01:00
DEBUG "Fetching uuid of new subvolume: $source->{PRINT}";
2016-03-14 15:55:57 +01:00
my $detail = btrfs_subvolume_show($source);
2019-08-16 01:22:49 +02:00
return undef unless($detail);
2015-06-02 22:16:33 +02:00
die unless($detail->{uuid});
2016-03-15 14:46:25 +01:00
$source->{node}{uuid} = $detail->{uuid};
2016-04-15 02:38:41 +02:00
$uuid_cache{$detail->{uuid}} = $source->{node};
2015-06-02 22:16:33 +02:00
}
2015-05-09 16:00:41 +02:00
}
2017-06-16 17:43:17 +02:00
$ret = btrfs_send_to_file($source, $target, $parent, \$vol_received, \$raw_info);
2015-10-12 22:56:52 +02:00
ABORTED($config_target, "Failed to send subvolume to raw file") unless($ret);
2015-06-02 22:16:33 +02:00
}
else
{
die "Illegal target type \"$target_type\"";
}
2016-04-12 17:50:12 +02:00
# inject fake vinfo
2016-08-19 16:33:30 +02:00
# NOTE: it's not possible to add (and compare) correct target $detail
# from btrfs_send_receive(), as source detail also has fake uuid.
2016-04-22 20:25:30 +02:00
if($ret) {
vinfo_inject_child($target, $vol_received, {
# NOTE: this is not necessarily the correct parent_uuid (on
# receive, btrfs-progs picks the uuid of the first (lowest id)
# matching possible parent), whereas the target_parent is the
2019-04-04 15:55:17 +02:00
# first from _correlated_nodes().
2016-04-22 20:25:30 +02:00
#
# NOTE: the parent_uuid of an injected receive target is not used
# anywhere in btrbk at the time of writing
2018-02-15 17:42:41 +01:00
parent_uuid => $parent ? $info{target_parent_node}->{uuid} : '-',
2016-04-22 20:25:30 +02:00
received_uuid => $source->{node}{received_uuid} eq '-' ? $source->{node}{uuid} : $source->{node}{received_uuid},
readonly => 1,
TARGET_TYPE => $target_type,
FORCE_PRESERVE => 'preserve forced: created just now',
2022-06-19 13:11:36 +02:00
INJECTED_BY => 'receive',
2017-06-16 17:43:17 +02:00
}, $raw_info);
2022-06-18 21:22:50 +02:00
$source->{SUBVOL_SENT}{$target->{URL}} = $vol_received;
2016-04-22 20:25:30 +02:00
}
2016-04-12 17:50:12 +02:00
2015-06-02 22:16:33 +02:00
# add info to $config->{SUBVOL_RECEIVED}
$info{received_type} = $target_type || die;
$info{received_subvolume} = $vol_received || die;
2016-03-07 21:45:12 +01:00
$target->{SUBVOL_RECEIVED} //= [];
push(@{$target->{SUBVOL_RECEIVED}}, \%info);
2015-06-02 22:16:33 +02:00
unless($ret) {
$info{ERROR} = 1;
2015-03-31 20:36:10 +02:00
return undef;
2015-03-31 19:07:33 +02:00
}
2015-06-02 22:16:33 +02:00
return 1;
2015-03-31 19:07:33 +02:00
}
2016-03-08 15:25:35 +01:00
# sets $result_vinfo->{CONFIG}->{ABORTED} on failure
# sets $result_vinfo->{SUBVOL_DELETED}
2021-07-21 19:38:25 +02:00
sub macro_delete($$$$;@)
2016-03-02 00:03:54 +01:00
{
my $root_subvol = shift || die;
2016-04-03 20:46:29 +02:00
my $subvol_basename = shift // die;
2016-03-08 15:25:35 +01:00
my $result_vinfo = shift || die;
2016-03-02 00:03:54 +01:00
my $schedule_options = shift || die;
2016-03-08 15:25:35 +01:00
my %delete_options = @_;
2016-03-02 00:03:54 +01:00
2022-11-19 15:01:59 +01:00
my $depends_fn = (($root_subvol->{CONFIG}{target_type} // "") eq "raw") && sub {
my $vol = shift->{value} // die;
my @ret;
if(my $related = _related_nodes($vol->{node}, omit_self => 1, fatal => 1)) {
foreach my $dep (@$related) {
TRACE "Checking parent of dependent raw target: $vol->{PRINT} <- $dep->{BTRBK_RAW}{NAME}" if($do_trace);
push @ret, grep($dep->{uuid} eq $_->{value}{node}{uuid}, @_);
}
return ("parent of preserved raw target", \@ret);
} else {
ABORTED($result_vinfo, "Failed to resolve related raw targets");
WARN "Skipping delete of \"$root_subvol->{PRINT}/$subvol_basename.*\": " . ABORTED_TEXT($result_vinfo);
return ("", []);
}
};
2016-03-02 00:03:54 +01:00
my @schedule;
2021-07-22 17:36:48 +02:00
foreach my $vol (@{vinfo_subvol_list($root_subvol, btrbk_direct_leaf => $subvol_basename)}) {
2022-06-05 23:20:52 +02:00
if(my $ff = vinfo_match(\@exclude_vf, $vol)) {
INFO "Skipping deletion of \"$vol->{PRINT}\": Match on exclude pattern \"$ff->{unparsed}\"";
$vol->{node}{FORCE_PRESERVE} ||= "preserve forced: Match on exclude pattern \"$ff->{unparsed}\"";
}
2016-03-02 00:03:54 +01:00
push(@schedule, { value => $vol,
2016-03-08 18:22:58 +01:00
# name => $vol->{PRINT}, # only for logging
2016-04-19 13:06:31 +02:00
btrbk_date => $vol->{node}{BTRBK_DATE},
2016-04-12 17:50:12 +02:00
preserve => $vol->{node}{FORCE_PRESERVE},
2016-03-02 00:03:54 +01:00
});
}
my (undef, $delete) = schedule(
2016-04-22 20:51:31 +02:00
%$schedule_options,
2016-03-08 18:22:58 +01:00
schedule => \@schedule,
2016-04-22 20:51:31 +02:00
preserve_date_in_future => 1,
2022-11-19 15:01:59 +01:00
depends => $depends_fn,
2016-03-02 00:03:54 +01:00
);
2022-11-19 15:01:59 +01:00
return undef if(IS_ABORTED($result_vinfo)); # if depends_fn fails
2017-09-27 20:23:08 +02:00
2022-07-27 20:35:58 +02:00
my @delete_success;
foreach my $vol (@$delete) {
2017-10-02 14:00:09 +02:00
# NOTE: we do not abort on qgroup destroy errors
2022-07-27 20:35:58 +02:00
btrfs_qgroup_destroy($vol, %{$delete_options{qgroup}}) if($delete_options{qgroup}->{destroy});
if(btrfs_subvolume_delete($vol, %delete_options)) {
push @delete_success, $vol;
}
2017-10-02 14:00:09 +02:00
}
2021-07-21 19:38:25 +02:00
INFO "Deleted " . scalar(@delete_success) . " subvolumes in: $root_subvol->{PRINT}/$subvol_basename.*";
2017-09-27 20:23:08 +02:00
$result_vinfo->{SUBVOL_DELETED} //= [];
push @{$result_vinfo->{SUBVOL_DELETED}}, @delete_success;
2022-07-27 20:35:58 +02:00
if(scalar(@delete_success) != scalar(@$delete)) {
2016-03-08 15:25:35 +01:00
ABORTED($result_vinfo, "Failed to delete subvolume");
2016-03-02 00:03:54 +01:00
return undef;
}
2022-07-27 20:35:58 +02:00
return 1;
2016-03-02 00:03:54 +01:00
}
2016-04-05 22:01:17 +02:00
sub cmp_date($$)
{
2016-04-20 22:45:11 +02:00
return (($_[0]->[0] <=> $_[1]->[0]) || # unix time
($_[0]->[1] <=> $_[1]->[1])); # NN
2016-04-05 22:01:17 +02:00
}
2015-04-02 15:53:53 +02:00
sub schedule(@)
2015-01-04 21:26:48 +01:00
{
my %args = @_;
2016-04-12 11:47:28 +02:00
my $schedule = $args{schedule} || die;
my $preserve = $args{preserve} || die;
2016-04-22 20:51:31 +02:00
my $preserve_date_in_future = $args{preserve_date_in_future};
2019-08-04 14:52:00 +02:00
my $preserve_threshold_date = $args{preserve_threshold_date};
2016-04-12 11:47:28 +02:00
my $results_list = $args{results};
my $result_hints = $args{result_hints} // {};
2016-04-14 15:39:50 +02:00
my $result_preserve_action_text = $args{result_preserve_action_text};
my $result_delete_action_text = $args{result_delete_action_text} // 'delete';
2022-11-19 13:45:19 +01:00
my $depends_fn = $args{depends};
2016-04-12 11:47:28 +02:00
my $preserve_day_of_week = $preserve->{dow} || die;
2018-01-05 19:28:10 +01:00
my $preserve_hour_of_day = $preserve->{hod} // die;
2016-04-12 19:55:29 +02:00
my $preserve_min_n = $preserve->{min_n};
my $preserve_min_q = $preserve->{min_q};
2016-04-12 11:47:28 +02:00
my $preserve_hourly = $preserve->{h};
my $preserve_daily = $preserve->{d};
my $preserve_weekly = $preserve->{w};
my $preserve_monthly = $preserve->{m};
my $preserve_yearly = $preserve->{y};
DEBUG "Schedule: " . format_preserve_matrix($preserve, format => "debug_text");
2015-01-13 12:38:01 +01:00
2016-04-20 22:45:11 +02:00
# 0 1 2 3 4 5 6 7 8
# sec, min, hour, mday, mon, year, wday, yday, isdst
2015-04-02 16:24:13 +02:00
# sort the schedule, ascending by date
2016-04-15 22:33:19 +02:00
# regular entries come in front of informative_only
my @sorted_schedule = sort { cmp_date($a->{btrbk_date}, $b->{btrbk_date} ) ||
(($a->{informative_only} ? ($b->{informative_only} ? 0 : 1) : ($b->{informative_only} ? -1 : 0)))
} @$schedule;
2015-04-02 16:24:13 +02:00
2016-04-21 13:27:54 +02:00
DEBUG "Scheduler reference time: " . timestamp(\@tm_now, 'debug-iso');
2015-01-25 18:05:52 +01:00
# first, do our calendar calculations
2018-04-05 00:17:12 +02:00
# - days start on $preserve_hour_of_day (or 00:00 if timestamp_format=short)
2016-04-20 22:45:11 +02:00
# - weeks start on $preserve_day_of_week
2018-04-05 16:42:26 +02:00
# - months start on first $preserve_day_of_week of month
# - years start on first $preserve_day_of_week of year
# NOTE: leap hours are NOT taken into account for $delta_hours
2016-04-21 13:27:54 +02:00
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
2016-04-20 22:45:11 +02:00
2015-04-02 16:24:13 +02:00
foreach my $href (@sorted_schedule)
2015-01-13 12:38:01 +01:00
{
2018-04-05 00:17:12 +02:00
my @tm = localtime($href->{btrbk_date}->[0]);
my $has_exact_time = $href->{btrbk_date}->[2];
my $delta_hours_from_hod = $tm[2] - ($has_exact_time ? $preserve_hour_of_day : 0);
2016-04-21 13:27:54 +02:00
my $delta_days_from_eow = $tm[6] - $day_of_week_map{$preserve_day_of_week};
2018-01-05 19:28:10 +01:00
if($delta_hours_from_hod < 0) {
$delta_hours_from_hod += 24;
$delta_days_from_eow -= 1;
}
if($delta_days_from_eow < 0) {
$delta_days_from_eow += 7;
}
2018-04-05 16:42:26 +02:00
my $month_corr = $tm[4]; # [0..11]
my $year_corr = $tm[5];
if($tm[3] <= $delta_days_from_eow) {
# our month/year start on first $preserve_day_of_week, corrected value
$month_corr -= 1;
if($month_corr < 0) {
$month_corr = 11;
$year_corr -= 1;
}
}
2016-04-20 22:45:11 +02:00
2016-04-21 13:27:54 +02:00
# check timegm: ignores leap hours
my $delta_hours = int(($now_h - timegm_nocheck( 0, 0, $tm[2], $tm[3], $tm[4], $tm[5] ) ) / (60 * 60));
2018-01-05 19:28:10 +01:00
my $delta_days = int(($delta_hours + $delta_hours_from_hod) / 24); # days from beginning of day
2016-04-20 22:45:11 +02:00
my $delta_weeks = int(($delta_days + $delta_days_from_eow) / 7); # weeks from beginning of week
2018-04-05 16:42:26 +02:00
my $delta_years = ($tm_now[5] - $year_corr);
my $delta_months = $delta_years * 12 + ($tm_now[4] - $month_corr);
2016-04-20 22:45:11 +02:00
$href->{delta_hours} = $delta_hours;
$href->{delta_days} = $delta_days;
$href->{delta_weeks} = $delta_weeks;
$href->{delta_months} = $delta_months;
$href->{delta_years} = $delta_years;
2018-01-14 21:47:25 +01:00
# these are only needed for text output (format_preserve_delta)
2018-04-05 16:42:26 +02:00
$href->{year} = $year_corr + 1900;
$href->{month} = $month_corr + 1;
2018-01-14 21:47:25 +01:00
$href->{delta_hours_from_hod} = $delta_hours_from_hod;
$href->{delta_days_from_eow} = $delta_days_from_eow;
2018-04-05 00:17:12 +02:00
$href->{real_hod} = $preserve_hour_of_day if($has_exact_time);
2016-04-22 20:51:31 +02:00
2018-01-14 21:47:25 +01:00
if($preserve_date_in_future && ($delta_hours < 0)) {
$href->{preserve} = "preserve forced: " . -($delta_hours) . " hours in the future";
2016-04-22 20:51:31 +02:00
}
2015-01-04 21:26:48 +01:00
}
2016-04-11 19:54:56 +02:00
my %first_in_delta_hours;
my %first_in_delta_days;
2015-01-25 18:05:52 +01:00
my %first_in_delta_weeks;
2016-04-11 19:54:56 +02:00
my %first_weekly_in_delta_months;
my %first_monthly_in_delta_years;
2016-04-12 11:47:28 +02:00
# filter "preserve all within N days/weeks/..."
2015-04-02 16:24:13 +02:00
foreach my $href (@sorted_schedule) {
2016-04-12 20:35:57 +02:00
if($preserve_min_q) {
if($preserve_min_q eq 'all') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: all";
2016-04-12 19:55:29 +02:00
} elsif($preserve_min_q eq 'h') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: $href->{delta_hours} hours ago" if($href->{delta_hours} <= $preserve_min_n);
2016-04-12 19:55:29 +02:00
} elsif($preserve_min_q eq 'd') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: $href->{delta_days} days ago" if($href->{delta_days} <= $preserve_min_n);
2016-04-12 19:55:29 +02:00
} elsif($preserve_min_q eq 'w') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: $href->{delta_weeks} weeks ago" if($href->{delta_weeks} <= $preserve_min_n);
2016-04-12 19:55:29 +02:00
} elsif($preserve_min_q eq 'm') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: $href->{delta_months} months ago" if($href->{delta_months} <= $preserve_min_n);
2016-04-12 19:55:29 +02:00
} elsif($preserve_min_q eq 'y') {
2016-04-12 21:06:46 +02:00
$href->{preserve} = "preserve min: $href->{delta_years} years ago" if($href->{delta_years} <= $preserve_min_n);
2016-04-12 11:47:28 +02:00
}
}
2016-04-11 19:54:56 +02:00
$first_in_delta_hours{$href->{delta_hours}} //= $href;
2016-04-12 11:47:28 +02:00
}
2016-04-12 20:35:57 +02:00
if($preserve_min_q && ($preserve_min_q eq 'latest') && (scalar @sorted_schedule)) {
my $href = $sorted_schedule[-1];
2016-04-12 21:06:46 +02:00
$href->{preserve} = 'preserve min: latest';
2016-04-12 20:35:57 +02:00
}
2016-04-12 11:47:28 +02:00
# filter hourly, daily, weekly, monthly, yearly
2016-04-11 19:54:56 +02:00
foreach (sort {$b <=> $a} keys %first_in_delta_hours) {
my $href = $first_in_delta_hours{$_} || die;
2016-04-12 21:06:46 +02:00
if($preserve_hourly && (($preserve_hourly eq 'all') || ($href->{delta_hours} <= $preserve_hourly))) {
$href->{preserve} = "preserve hourly: first of hour, $href->{delta_hours} hours ago";
2016-04-12 11:47:28 +02:00
}
2016-04-11 19:54:56 +02:00
$first_in_delta_days{$href->{delta_days}} //= $href;
2016-04-12 11:47:28 +02:00
}
2016-04-11 19:54:56 +02:00
foreach (sort {$b <=> $a} keys %first_in_delta_days) {
my $href = $first_in_delta_days{$_} || die;
2016-04-12 11:47:28 +02:00
if($preserve_daily && (($preserve_daily eq 'all') || ($href->{delta_days} <= $preserve_daily))) {
2018-04-05 00:17:12 +02:00
$href->{preserve} = "preserve daily: first of day" . ($href->{real_hod} ? sprintf(" (starting at %02u:00)", $href->{real_hod}) : "") . ", $href->{delta_days} days ago"
. (defined($href->{real_hod}) ? ($href->{delta_hours_from_hod} ? ", $href->{delta_hours_from_hod}h after " : ", at ") . sprintf("%02u:00", $href->{real_hod}) : "");
2015-01-25 18:05:52 +01:00
}
$first_in_delta_weeks{$href->{delta_weeks}} //= $href;
}
2015-12-17 19:00:45 +01:00
foreach (sort {$b <=> $a} keys %first_in_delta_weeks) {
2015-01-20 16:53:35 +01:00
my $href = $first_in_delta_weeks{$_} || die;
2016-04-12 11:47:28 +02:00
if($preserve_weekly && (($preserve_weekly eq 'all') || ($href->{delta_weeks} <= $preserve_weekly))) {
2018-04-05 00:17:12 +02:00
$href->{preserve} = "preserve weekly: $href->{delta_weeks} weeks ago," . _format_preserve_delta($href, $preserve_day_of_week);
2015-01-04 21:26:48 +01:00
}
2016-04-11 19:54:56 +02:00
$first_weekly_in_delta_months{$href->{delta_months}} //= $href;
2015-01-04 21:26:48 +01:00
}
2016-04-11 19:54:56 +02:00
foreach (sort {$b <=> $a} keys %first_weekly_in_delta_months) {
my $href = $first_weekly_in_delta_months{$_} || die;
2016-04-12 11:47:28 +02:00
if($preserve_monthly && (($preserve_monthly eq 'all') || ($href->{delta_months} <= $preserve_monthly))) {
2018-04-05 00:17:12 +02:00
$href->{preserve} = "preserve monthly: first weekly of month $href->{year}-" . sprintf("%02u", $href->{month}) . " ($href->{delta_months} months ago," . _format_preserve_delta($href, $preserve_day_of_week) . ")";
2015-01-13 12:38:01 +01:00
}
2016-04-11 19:54:56 +02:00
$first_monthly_in_delta_years{$href->{delta_years}} //= $href;
2016-02-29 18:00:55 +01:00
}
2016-04-11 19:54:56 +02:00
foreach (sort {$b <=> $a} keys %first_monthly_in_delta_years) {
my $href = $first_monthly_in_delta_years{$_} || die;
2016-04-12 11:47:28 +02:00
if($preserve_yearly && (($preserve_yearly eq 'all') || ($href->{delta_years} <= $preserve_yearly))) {
2018-04-05 00:17:12 +02:00
$href->{preserve} = "preserve yearly: first weekly of year $href->{year} ($href->{delta_years} years ago," . _format_preserve_delta($href, $preserve_day_of_week) . ")";
2016-02-29 18:00:55 +01:00
}
2015-01-04 21:26:48 +01:00
}
2022-11-19 13:45:19 +01:00
if($depends_fn) {
# for all preserved, check depends against all non-preserved
foreach my $href (grep $_->{preserve}, @sorted_schedule) {
my ($dtxt, $deps) = $depends_fn->($href, grep(!$_->{preserve}, @sorted_schedule));
foreach my $dep (@$deps) {
DEBUG "Preserving dependent: $dep->{value}{PRINT} <- $href->{value}{PRINT}";
$dep->{preserve} = "preserve forced: $dtxt";
}
}
}
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;
2016-04-12 11:47:28 +02:00
my %result_base = ( %$preserve,
scheme => format_preserve_matrix($preserve),
2015-10-12 20:46:05 +02:00
%$result_hints,
);
2016-03-08 18:22:58 +01:00
my $count_defined = 0;
2015-04-02 16:24:13 +02:00
foreach my $href (@sorted_schedule)
2015-01-13 12:38:01 +01:00
{
2019-08-04 14:52:00 +02:00
my $result_reason_text = $href->{preserve};
my $result_action_text;
unless($href->{informative_only}) {
if($href->{preserve}) {
if($preserve_threshold_date && (cmp_date($href->{btrbk_date}, $preserve_threshold_date) <= 0)) {
# older than threshold, do not add to preserve list
2019-08-07 21:30:59 +02:00
$result_reason_text = "$result_reason_text, ignored (archive_exclude_older) older than existing archive";
2019-08-04 14:52:00 +02:00
}
else {
push(@preserve, $href->{value});
$result_action_text = $result_preserve_action_text;
}
}
else {
push(@delete, $href->{value});
$result_action_text = $result_delete_action_text;
}
$count_defined++;
2015-01-13 12:38:01 +01:00
}
2019-08-04 14:52:00 +02:00
2020-08-28 17:15:39 +02:00
TRACE join(" ", "schedule: $href->{value}{PRINT}", ($href->{informative_only} ? "(informative_only)" : uc($result_action_text || "-")), ($result_reason_text // "-")) if($do_trace && $href->{value} && $href->{value}{PRINT});
2019-08-04 14:52:00 +02:00
push @$results_list, { %result_base,
action => $result_action_text,
reason => $result_reason_text,
value => $href->{value},
} if($results_list);
2015-01-13 12:38:01 +01:00
}
2016-03-08 18:22:58 +01:00
DEBUG "Preserving " . @preserve . "/" . $count_defined . " items";
2015-04-02 15:53:53 +02:00
return (\@preserve, \@delete);
2015-01-04 21:26:48 +01:00
}
2018-01-14 21:47:25 +01:00
sub _format_preserve_delta($$$)
{
my $href = shift;
my $preserve_day_of_week = shift;
my $s = "";
$s .= " $href->{delta_days_from_eow}d" if($href->{delta_days_from_eow});
$s .= " $href->{delta_hours_from_hod}h" if($href->{delta_hours_from_hod});
2018-04-05 00:17:12 +02:00
return ($s ? "$s after " : " at ") . $preserve_day_of_week . (defined($href->{real_hod}) ? sprintf(" %02u:00", $href->{real_hod}) : "");
2018-01-14 21:47:25 +01:00
}
2016-04-12 11:47:28 +02:00
sub format_preserve_matrix($@)
2015-10-11 19:01:59 +02:00
{
2016-04-12 11:47:28 +02:00
my $preserve = shift || die;
my %opts = @_;
my $format = $opts{format} // "short";
2016-03-08 18:22:58 +01:00
if($format eq "debug_text") {
2016-04-12 11:47:28 +02:00
my @out;
my %trans = ( h => 'hours', d => 'days', w => 'weeks', m => 'months', y => 'years' );
2016-04-12 20:35:57 +02:00
if($preserve->{min_q} && ($preserve->{min_q} eq 'all')) {
2016-04-12 21:06:46 +02:00
push @out, "all forever";
2016-04-12 11:47:28 +02:00
}
else {
2016-04-12 21:06:46 +02:00
push @out, "latest" if($preserve->{min_q} && ($preserve->{min_q} eq 'latest'));
push @out, "all within $preserve->{min_n} $trans{$preserve->{min_q}}" if($preserve->{min_n} && $preserve->{min_q});
2018-01-05 19:28:10 +01:00
push @out, "first of day (starting at " . sprintf("%02u:00", $preserve->{hod}) . ") for $preserve->{d} days" if($preserve->{d});
2016-04-12 11:47:28 +02:00
unless($preserve->{d} && ($preserve->{d} eq 'all')) {
push @out, "first daily in week (starting on $preserve->{dow}) for $preserve->{w} weeks" if($preserve->{w});
unless($preserve->{w} && ($preserve->{w} eq 'all')) {
2016-04-11 19:54:56 +02:00
push @out, "first weekly of month for $preserve->{m} months" if($preserve->{m});
2016-04-12 11:47:28 +02:00
unless($preserve->{m} && ($preserve->{m} eq 'all')) {
2016-04-11 19:54:56 +02:00
push @out, "first weekly of year for $preserve->{y} years" if($preserve->{y});
2016-04-12 11:47:28 +02:00
}
2016-03-08 18:22:58 +01:00
}
}
}
2016-04-12 21:06:46 +02:00
return 'preserving ' . join('; ', @out);
2016-03-08 18:22:58 +01:00
}
2016-04-12 11:47:28 +02:00
my $s = "";
2016-04-12 20:35:57 +02:00
if($preserve->{min_q} && ($preserve->{min_q} eq 'all')) {
2016-04-12 11:47:28 +02:00
$s = '*d+';
2015-10-11 19:01:59 +02:00
}
2016-04-12 11:47:28 +02:00
else {
2016-04-12 21:06:46 +02:00
# $s .= '.+' if($preserve->{min_q} && ($preserve->{min_q} eq 'latest'));
2016-04-12 19:55:29 +02:00
$s .= $preserve->{min_n} . $preserve->{min_q} . '+' if($preserve->{min_n} && $preserve->{min_q});
2016-04-12 11:47:28 +02:00
foreach (qw(h d w m y)) {
my $val = $preserve->{$_} // 0;
next unless($val);
$val = '*' if($val eq 'all');
$s .= ($s ? ' ' : '') . $val . $_;
}
2018-12-05 21:43:54 +01:00
if(($format ne "config") && ($preserve->{d} || $preserve->{w} || $preserve->{m} || $preserve->{y})) {
2018-01-05 19:28:10 +01:00
$s .= " ($preserve->{dow}, " . sprintf("%02u:00", $preserve->{hod}) . ")";
}
2016-04-12 11:47:28 +02:00
}
return $s;
2015-10-11 19:01:59 +02:00
}
2016-04-21 13:27:54 +02:00
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 .= '+'; }
2016-04-23 14:37:24 +02:00
my $h = int($offset / (60 * 60));
die if($h > 24); # sanity check, something went really wrong
$ts .= sprintf('%02u%02u', $h, int($offset / 60) % 60);
2016-04-21 13:27:54 +02:00
}
return $ts;
return undef;
}
2015-09-20 14:25:20 +02:00
sub print_header(@)
{
2015-05-26 20:05:40 +02:00
my %args = @_;
my $config = $args{config};
print "--------------------------------------------------------------------------------\n";
2016-04-15 01:22:19 +02:00
print "$args{title} ($VERSION_INFO)\n\n";
2015-05-26 20:05:40 +02:00
if($args{time}) {
print " Date: " . localtime($args{time}) . "\n";
}
if($config) {
2018-02-13 18:40:35 +01:00
print " Config: " . config_key($config, "SRC_FILE") . "\n";
2015-10-10 15:13:32 +02:00
}
if($dryrun) {
print " Dryrun: YES\n";
}
if($config && $config->{CMDLINE_FILTER_LIST}) {
2019-04-17 15:56:35 +02:00
my @list = @{$config->{CMDLINE_FILTER_LIST}};
2015-10-10 15:13:32 +02:00
print " Filter: ";
2019-04-17 15:56:35 +02:00
print join("\n ", @list);
2015-10-10 15:13:32 +02:00
print "\n";
2015-05-26 20:05:40 +02:00
}
if($args{info}) {
print "\n" . join("\n", grep(defined, @{$args{info}})) . "\n";
}
2017-08-21 13:23:20 +02:00
if($args{options} && (scalar @{$args{options}})) {
print "\nOptions:\n ";
2022-06-07 00:31:58 +02:00
print join("\n ", grep(defined, @{$args{options}}));
2017-08-21 13:23:20 +02:00
print "\n";
}
2015-05-26 20:05:40 +02:00
if($args{legend}) {
print "\nLegend:\n ";
2022-06-07 00:31:58 +02:00
print join("\n ", grep(defined, @{$args{legend}}));
2015-05-26 20:05:40 +02:00
print "\n";
}
print "--------------------------------------------------------------------------------\n";
2020-12-21 00:20:02 +01:00
print "\n" if($args{paragraph});
2015-05-26 20:05:40 +02:00
}
2019-07-15 18:19:33 +02:00
sub print_footer($$)
{
my $config = shift;
my $exit_status = shift;
if($exit_status) {
print "\nNOTE: Some errors occurred, which may result in missing backups!\n";
print "Please check warning and error messages above.\n";
my @fix_manually_text = _config_collect_values($config, "FIX_MANUALLY");
if(scalar(@fix_manually_text)) {
my @unique = do { my %seen; grep { !$seen{$_}++ } @fix_manually_text };
print join("\n", @unique) . "\n";
}
}
if($dryrun) {
print "\nNOTE: Dryrun was active, none of the operations above were actually executed!\n";
}
}
2016-01-15 02:06:03 +01:00
sub print_table($;$)
{
my $data = shift;
my $spacing = shift // " ";
my $maxlen = 0;
foreach (@$data) {
$maxlen = length($_->[0]) if($maxlen < length($_->[0]));
}
foreach (@$data) {
print $_->[0] . ((' ' x ($maxlen - length($_->[0]))) . $spacing) . $_->[1] . "\n";
}
}
2015-10-11 19:01:59 +02:00
sub print_formatted(@)
2015-10-11 01:44:13 +02:00
{
2015-10-13 01:10:06 +02:00
my $format_key = shift || die;
my $data = shift || die;
my $default_format = "table";
2015-10-11 19:01:59 +02:00
my %args = @_;
2015-10-12 20:46:05 +02:00
my $title = $args{title};
2019-08-07 20:32:01 +02:00
my $table_format = ref($format_key) ? $format_key : $table_formats{$format_key};
2015-10-13 01:10:06 +02:00
my $format = $args{output_format} || $output_format || $default_format;
2019-08-06 17:28:05 +02:00
my $pretty = $args{pretty} // $output_pretty;
2021-03-28 17:02:15 +02:00
my $no_header = $args{no_header};
2015-10-20 20:16:34 +02:00
my $fh = $args{outfile} // *STDOUT;
2015-10-19 22:10:08 +02:00
my $table_spacing = 2;
2021-02-14 15:30:15 +01:00
my $empty_cell_char = $args{empty_cell_char} // "-";
2015-10-11 01:44:13 +02:00
2021-03-28 17:02:15 +02:00
my @keys;
my %ralign;
my %hide_column;
2020-12-20 20:12:20 +01:00
if($format =~ s/^col:\s*(h:)?\s*//) {
2021-03-28 17:02:15 +02:00
$no_header = 1 if($1);
2020-12-20 20:12:20 +01:00
foreach (split(/\s*,\s*/, $format)) {
2021-03-28 17:02:15 +02:00
$ralign{$_} = 1 if s/:R(ALIGN)?$//i;
push @keys, lc($_);
2020-12-20 20:12:20 +01:00
}
}
2021-03-28 17:02:15 +02:00
else {
unless(exists($table_format->{$format})) {
WARN "Unsupported output format \"$format\", defaulting to \"$default_format\" format.";
$format = $default_format;
}
@keys = @{$table_format->{$format}};
%ralign = %{$table_format->{RALIGN} // {}};
2019-04-01 03:24:00 +02:00
}
2021-03-28 17:02:15 +02:00
# strips leading "-" from @keys
%hide_column = map { $_ => 1 } grep { s/^-// } @keys;
2015-10-11 15:38:43 +02:00
2021-04-16 15:29:23 +02:00
if($format eq "single_column")
{
# single-column: newline separated values, no headers
my $key = $keys[0];
foreach (grep defined, map $_->{$key}, @$data) {
print $fh $_ . "\n" if($_ ne "");
}
}
elsif($format eq "raw")
2015-10-11 19:01:59 +02:00
{
# output: key0="value0" key1="value1" ...
foreach my $row (@$data) {
2015-10-20 20:16:34 +02:00
print $fh "format=\"$format_key\" ";
2021-08-16 17:50:42 +02:00
print $fh join(' ', map { "$_=" . quoteshell(($row->{$_} // "")) } @keys) . "\n";
2015-10-11 19:01:59 +02:00
}
}
2016-04-25 19:40:11 +02:00
elsif(($format eq "tlog") || ($format eq "syslog"))
2015-10-13 18:24:30 +02:00
{
# output: value0 value1, ...
2021-03-28 17:02:15 +02:00
unless($no_header) {
2019-08-02 22:56:36 +02:00
print $fh join(' ', map uc($_), @keys) . "\n"; # unaligned upper case headings
2015-10-20 20:16:34 +02:00
}
foreach my $row (@$data) {
2019-04-01 03:24:00 +02:00
my $line = join(' ', map { ((defined($row->{$_}) && ($_ eq "message")) ? '# ' : '') . ($row->{$_} // "-") } @keys);
2016-04-25 19:40:11 +02:00
if($format eq "syslog") { # dirty hack, ignore outfile on syslog format
syslog($line);
} else {
print $fh ($line . "\n");
}
2015-10-13 18:24:30 +02:00
}
}
2015-10-11 19:01:59 +02:00
else
2015-10-11 01:44:13 +02:00
{
2021-08-15 16:10:54 +02:00
# Text::CharWidth does it correctly with wide chars (e.g. asian) taking up two columns
my $termwidth = eval_quiet { require Text::CharWidth; } ? \&Text::CharWidth::mbswidth :
eval_quiet { require Encode; } ? sub { length(Encode::decode_utf8(shift)) } :
sub { length(shift) };
2015-10-11 15:38:43 +02:00
# sanitize and calculate maxlen for each column
2021-03-28 17:02:15 +02:00
my %maxlen = map { $_ => $no_header ? 0 : length($_) } @keys;
my @formatted_data;
2015-10-11 01:44:13 +02:00
foreach my $row (@$data) {
2021-03-28 17:02:15 +02:00
my %formatted_row;
2019-04-01 03:24:00 +02:00
foreach my $key (@keys) {
2015-10-11 15:38:43 +02:00
my $val = $row->{$key};
2021-03-28 17:02:15 +02:00
$val = join(',', @$val) if(ref $val eq "ARRAY");
$hide_column{$key} = 0 if(defined($val));
2021-02-14 15:30:15 +01:00
$val = $empty_cell_char if(!defined($val) || ($val eq ""));
2021-03-28 17:02:15 +02:00
$formatted_row{$key} = $val;
2021-08-15 16:10:54 +02:00
my $vl = $termwidth->($val);
$maxlen{$key} = $vl if($maxlen{$key} < $vl);
2015-10-11 01:44:13 +02:00
}
2021-03-28 17:02:15 +02:00
push @formatted_data, \%formatted_row;
2015-10-11 01:44:13 +02:00
}
2021-03-28 17:02:15 +02:00
my @visible_keys = grep !$hide_column{$_}, @keys;
2015-10-11 01:44:13 +02:00
2018-10-31 14:06:14 +01:00
# print title
if($title) {
print $fh "$title\n";
2019-08-02 22:56:36 +02:00
print $fh '-' x length($title) . "\n"; # separator line
2018-10-31 14:06:14 +01:00
}
2015-10-11 15:38:43 +02:00
# print keys (headings)
2021-03-28 17:02:15 +02:00
unless($no_header) {
2016-04-15 21:42:38 +02:00
my $fill = 0;
2021-03-28 17:02:15 +02:00
foreach (@visible_keys) {
2015-10-20 20:16:34 +02:00
print $fh ' ' x $fill;
2016-04-15 21:42:38 +02:00
$fill = $maxlen{$_} - length($_);
2019-08-02 22:56:36 +02:00
if($pretty) {
# use aligned lower case headings (with separator line below)
2021-03-28 17:02:15 +02:00
if($ralign{$_}) {
2019-08-02 22:56:36 +02:00
print $fh ' ' x $fill;
$fill = 0;
}
print $fh $_;
} else {
print $fh uc($_); # default unaligned upper case headings
2016-04-15 21:42:38 +02:00
}
$fill += $table_spacing;
2015-10-19 22:10:08 +02:00
}
2016-04-15 21:42:38 +02:00
print $fh "\n";
2019-04-01 03:24:00 +02:00
$fill = 0;
2019-08-02 22:56:36 +02:00
if($pretty) { # separator line after header
2021-03-28 17:02:15 +02:00
foreach (@visible_keys) {
2019-08-02 22:56:36 +02:00
print $fh ' ' x $fill;
print $fh '-' x $maxlen{$_};
$fill = $table_spacing;
}
print $fh "\n";
# alternative (all above in one line ;)
#print $fh join(' ' x $table_spacing, map { '-' x ($maxlen{$_}) } @keys) . "\n";
2019-04-01 03:24:00 +02:00
}
2015-10-13 01:39:58 +02:00
}
2015-10-11 01:44:13 +02:00
# print values
2021-03-28 17:02:15 +02:00
foreach my $row (@formatted_data) {
2015-10-19 22:10:08 +02:00
my $fill = 0;
2021-03-28 17:02:15 +02:00
foreach (@visible_keys) {
2015-10-11 15:38:43 +02:00
my $val = $row->{$_};
2015-10-20 20:16:34 +02:00
print $fh ' ' x $fill;
2021-08-15 16:10:54 +02:00
$fill = $maxlen{$_} - $termwidth->($val);
2021-03-28 17:02:15 +02:00
if($ralign{$_}) {
2015-10-20 20:16:34 +02:00
print $fh ' ' x $fill;
2015-10-19 22:10:08 +02:00
$fill = 0;
}
2015-10-20 20:16:34 +02:00
print $fh $val;
2015-10-19 22:10:08 +02:00
$fill += $table_spacing;
2015-10-11 01:44:13 +02:00
}
2015-10-20 20:16:34 +02:00
print $fh "\n";
2015-10-11 01:44:13 +02:00
}
2018-10-31 14:33:07 +01:00
# print additional newline for paragraphs
if($args{paragraph}) {
print $fh "\n";
}
2015-10-11 01:44:13 +02:00
}
}
2020-10-30 18:35:52 +01:00
sub print_size($)
{
my $size = shift;
if($output_format && ($output_format eq "raw")) {
return $size;
}
return "-" if($size == 0);
my ($unit, $mul);
if(@output_unit) {
($unit, $mul) = @output_unit;
}
else {
($unit, $mul) = ("KiB", 1024);
($unit, $mul) = ("MiB", $mul * 1024) if($size > $mul * 1024);
($unit, $mul) = ("GiB", $mul * 1024) if($size > $mul * 1024);
($unit, $mul) = ("TiB", $mul * 1024) if($size > $mul * 1024);
}
return $size if($mul == 1);
return sprintf('%.2f', ($size / $mul)) . " $unit";
}
2016-03-15 11:21:59 +01:00
sub _origin_tree
2015-10-14 16:51:39 +02:00
{
2016-03-15 11:21:59 +01:00
my $prefix = shift;
2016-04-15 22:00:10 +02:00
my $node = shift // die;
2016-03-15 11:21:59 +01:00
my $lines = shift;
2016-04-15 22:00:10 +02:00
my $nodelist = shift;
my $depth = shift // 0;
my $seen = shift // [];
my $norecurse = shift;
my $uuid = $node->{uuid} || die;
# cache a bit, this might be large
2019-07-15 18:36:06 +02:00
# note: root subvolumes dont have REL_PATH
$nodelist //= [ (sort { ($a->{REL_PATH} // "") cmp ($b->{REL_PATH} // "") } values %uuid_cache) ];
2015-10-14 16:51:39 +02:00
2016-04-15 22:00:10 +02:00
my $prefix_spaces = ' ' x (($depth * 4) - ($prefix ? 4 : 0));
2020-12-12 20:11:00 +01:00
push(@$lines, { tree => "${prefix_spaces}${prefix}" . _fs_path($node),
2016-04-15 22:00:10 +02:00
uuid => $node->{uuid},
parent_uuid => $node->{parent_uuid},
received_uuid => $node->{received_uuid},
});
# handle deep recursion
return 0 if(grep /^$uuid$/, @$seen);
2015-10-14 16:51:39 +02:00
2016-03-15 11:21:59 +01:00
if($node->{parent_uuid} ne '-') {
2016-04-15 22:00:10 +02:00
my $parent_node = $uuid_cache{$node->{parent_uuid}};
if($parent_node) {
if($norecurse) {
push(@$lines,{ tree => "${prefix_spaces} ^-- ...",
uuid => $parent_node->{uuid},
parent_uuid => $parent_node->{parent_uuid},
received_uuid => $parent_node->{received_uuid},
recursion => 'stop_recursion',
});
return 0;
}
if($parent_node->{readonly}) {
_origin_tree("^-- ", $parent_node, $lines, $nodelist, $depth + 1, undef, 1); # end recursion
}
else {
_origin_tree("^-- ", $parent_node, $lines, $nodelist, $depth + 1);
}
}
else {
2019-07-15 18:36:12 +02:00
push(@$lines,{ tree => "${prefix_spaces} ^-- <unknown uuid=$node->{parent_uuid}>" });
2016-04-15 22:00:10 +02:00
}
2016-03-07 19:20:15 +01:00
}
2016-04-15 22:00:10 +02:00
return 0 if($norecurse);
push(@$seen, $uuid);
if($node->{received_uuid} ne '-') {
my $received_uuid = $node->{received_uuid};
my @receive_parents; # there should be only one!
my @receive_twins;
foreach (@$nodelist) {
next if($_->{uuid} eq $uuid);
if($received_uuid eq $_->{uuid} && $_->{readonly}) {
_origin_tree("", $_, \@receive_parents, $nodelist, $depth, $seen);
}
elsif(($_->{received_uuid} ne '-') && ($received_uuid eq $_->{received_uuid}) && $_->{readonly}) {
_origin_tree("", $_, \@receive_twins, $nodelist, $depth, $seen, 1); # end recursion
}
}
push @$lines, @receive_twins;
push @$lines, @receive_parents;
}
return 0;
2016-03-15 11:21:59 +01:00
}
2016-03-07 19:20:15 +01:00
2016-03-15 11:21:59 +01:00
sub exit_status
{
my $config = shift;
foreach my $subsection (@{$config->{SUBSECTION}}) {
2019-04-17 15:20:18 +02:00
return 10 if(IS_ABORTED($subsection, "abort_"));
2019-07-15 18:19:33 +02:00
return 10 if(defined($subsection->{FIX_MANUALLY})); # treated as errors
2016-03-15 11:21:59 +01:00
return 10 if(exit_status($subsection));
}
return 0;
2016-03-07 17:35:17 +01:00
}
2014-12-11 18:03:10 +01:00
MAIN:
{
2017-09-25 16:05:42 +02:00
# NOTE: Since v0.26.0, btrbk does not enable taint mode (perl -T) by
# default, and does not hardcode $PATH anymore.
#
# btrbk still does all taint checks, and can be run in taint mode.
# In order to enable taint mode, run `perl -T btrbk`.
#
# see: perlrun(1), perlsec(1)
#
my $taint_mode_enabled = eval '${^TAINT}';
if($taint_mode_enabled) {
# we are running in tainted mode (perl -T), sanitize %ENV
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
# in taint mode, perl needs an untainted $PATH.
$ENV{PATH} = '/sbin:/bin:/usr/sbin:/usr/bin';
}
2015-05-18 21:18:57 +02:00
2015-08-15 17:51:00 +02:00
Getopt::Long::Configure qw(gnu_getopt);
2015-01-17 14:55:46 +01:00
my $start_time = time;
2016-04-21 13:27:54 +02:00
@tm_now = localtime($start_time);
2014-12-11 18:03:10 +01:00
2018-02-13 19:26:54 +01:00
my @config_override_cmdline;
2019-04-17 16:10:15 +02:00
my @exclude_cmdline;
2019-08-04 22:48:50 +02:00
my ($config_cmdline, $lockfile_cmdline, $print_schedule,
$preserve_snapshots, $preserve_backups, $wipe_snapshots, $skip_snapshots, $skip_backups,
2022-06-19 14:08:25 +02:00
$raw_cmdline, $extents_related,
2019-08-04 22:48:50 +02:00
);
2020-02-29 17:25:35 +01:00
# Calling btrbk via "lsbtr" symlink acts as an alias for "btrbk ls",
# while also changing the semantics of the command line options.
$program_name = $0;
$program_name =~ s/^.*\///; # remove path
2021-04-16 15:34:47 +02:00
my @getopt_options = (
2020-02-29 17:25:35 +01:00
# common options
2022-03-25 15:20:20 +01:00
'help|h' => sub { HELP_MESSAGE; exit 0; },
'version' => sub { VERSION_MESSAGE; exit 0; },
2020-02-29 17:25:35 +01:00
'quiet|q' => \$quiet,
'verbose|v' => sub { $loglevel = ($loglevel =~ /^[0-9]+$/) ? $loglevel+1 : 2; },
'loglevel|l=s' => \$loglevel,
'format=s' => \$output_format,
2021-04-16 15:35:48 +02:00
'single-column|1' => sub { $output_format = "single_column" },
2020-02-29 17:25:35 +01:00
'pretty' => \$output_pretty,
2017-08-21 13:23:20 +02:00
'config|c=s' => \$config_cmdline,
2020-02-29 17:25:35 +01:00
'override=s' => \@config_override_cmdline, # e.g. --override=incremental=no
'lockfile=s' => \$lockfile_cmdline,
);
2021-04-16 15:34:47 +02:00
push @getopt_options, ($program_name eq "lsbtr") ? (
2020-02-29 17:25:35 +01:00
# "lsbtr" options
'long|l' => sub { $output_format = "table" },
'uuid|u' => sub { $output_format = "long" },
'raw' => sub { $output_format = "raw" },
) : (
# "btrbk" options
2017-08-21 13:23:20 +02:00
'dry-run|n' => \$dryrun,
2019-04-17 16:10:15 +02:00
'exclude=s' => \@exclude_cmdline,
2018-05-10 18:58:17 +02:00
'preserve|p' => sub { $preserve_snapshots = "preserve", $preserve_backups = "preserve" },
'preserve-snapshots' => sub { $preserve_snapshots = "preserve-snapshots" },
'preserve-backups' => sub { $preserve_backups = "preserve-backups" },
2017-09-28 14:02:06 +02:00
'wipe' => \$wipe_snapshots,
2017-08-21 13:23:20 +02:00
'progress' => \$show_progress,
2020-12-12 17:44:57 +01:00
'related' => \$extents_related,
2017-08-21 13:23:20 +02:00
'table|t' => sub { $output_format = "table" },
2019-08-02 22:39:35 +02:00
'long|L' => sub { $output_format = "long" },
2018-10-15 16:25:07 +02:00
'print-schedule|S' => \$print_schedule,
2022-06-19 14:08:25 +02:00
'raw' => \$raw_cmdline,
2020-10-30 18:35:52 +01:00
'bytes' => sub { @output_unit = ("", 1 ) },
'kbytes' => sub { @output_unit = ("KiB", 1024 ) },
'mbytes' => sub { @output_unit = ("MiB", 1024 * 1024 ) },
'gbytes' => sub { @output_unit = ("GiB", 1024 * 1024 * 1024 ) },
'tbytes' => sub { @output_unit = ("TiB", 1024 * 1024 * 1024 * 1024 ) },
2020-02-29 17:25:35 +01:00
);
2021-04-16 15:34:47 +02:00
unless(GetOptions(@getopt_options)) {
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2015-09-30 14:00:39 +02:00
exit 2;
2015-01-10 16:02:35 +01:00
}
2020-02-29 17:25:35 +01:00
if($program_name eq "lsbtr") {
unshift @ARGV, './' unless(@ARGV); # default to current path
unshift @ARGV, "ls"; # implicit "btrbk ls"
}
2014-12-13 15:15:58 +01:00
my $command = shift @ARGV;
2015-08-15 17:51:00 +02:00
unless($command) {
2022-03-25 15:20:20 +01:00
HELP_MESSAGE;
2015-09-30 14:00:39 +02:00
exit 2;
2015-08-15 17:51:00 +02:00
}
2014-12-12 12:32:04 +01:00
# assign command line options
2016-04-18 16:40:49 +02:00
@config_src = ( $config_cmdline ) if($config_cmdline);
2020-08-21 16:14:17 +02:00
$loglevel = { error => 0, warn => 1, warning => 1, info => 2, debug => 3, trace => 4 }->{$loglevel} // $loglevel;
unless($loglevel =~ /^[0-9]+$/) {
ERROR "Unknown loglevel: $loglevel";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2020-08-21 16:14:17 +02:00
exit 2;
}
2020-08-28 17:15:39 +02:00
$do_trace = 1 if($loglevel >= 4);
require_data_dumper() if($do_trace || ($VERSION =~ /-dev$/));
2014-12-11 18:03:10 +01:00
2014-12-12 12:32:04 +01:00
# check command line options
2019-07-28 15:04:23 +02:00
if($show_progress && (not check_exe('mbuffer'))) {
WARN 'Found option "--progress", but required executable "mbuffer" does not exist on your system. Please install "mbuffer".';
2015-08-15 18:23:48 +02:00
$show_progress = 0;
}
2020-12-12 17:44:57 +01:00
my ($action_run, $action_usage, $action_resolve, $action_diff, $action_extents, $action_origin, $action_config_print, $action_list, $action_clean, $action_archive, $action_ls);
2015-09-02 11:04:22 +02:00
my @filter_args;
2019-04-17 15:11:49 +02:00
my @subvol_args;
2015-10-22 17:45:27 +02:00
my $args_expected_min = 0;
my $args_expected_max = 9999;
2019-07-28 18:54:34 +02:00
my $fallback_default_config;
2020-05-23 18:00:31 +02:00
my $subvol_args_allow_relative;
2019-05-23 15:36:34 +02:00
my $subvol_args_init;
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-09-02 11:04:22 +02:00
@filter_args = @ARGV;
2014-12-13 15:15:58 +01:00
}
2017-08-21 13:23:20 +02:00
elsif($command eq "snapshot") {
$action_run = 1;
2018-05-10 18:58:17 +02:00
$skip_backups = "snapshot";
$preserve_backups = "snapshot";
2017-08-21 13:23:20 +02:00
@filter_args = @ARGV;
}
elsif($command eq "resume") {
$action_run = 1;
2018-05-10 18:58:17 +02:00
$skip_snapshots = "resume";
2017-08-21 13:23:20 +02:00
@filter_args = @ARGV;
}
2017-09-28 13:18:40 +02:00
elsif($command eq "prune") {
$action_run = 1;
2018-05-10 18:58:17 +02:00
$skip_snapshots = "prune";
$skip_backups = "prune";
2017-09-28 13:18:40 +02:00
@filter_args = @ARGV;
}
2016-01-14 15:52:33 +01:00
elsif ($command eq "clean") {
$action_clean = 1;
@filter_args = @ARGV;
}
2016-04-16 01:09:17 +02:00
elsif ($command eq "archive") {
$action_archive = 1;
2019-08-05 14:31:48 +02:00
$fallback_default_config = 1;
2016-04-07 15:33:32 +02:00
$args_expected_min = $args_expected_max = 2;
2021-07-25 13:33:08 +02:00
$subvol_args_allow_relative = 1;
2019-04-17 15:11:49 +02:00
@subvol_args = @ARGV;
2016-04-07 15:33:32 +02:00
}
2015-10-19 22:10:08 +02:00
elsif ($command eq "usage") {
$action_usage = 1;
2015-09-02 11:04:22 +02:00
@filter_args = @ARGV;
2015-01-20 19:18:38 +01:00
}
2019-07-28 18:54:37 +02:00
elsif ($command eq "ls") {
$action_ls = 1;
$fallback_default_config = 1;
2019-12-23 13:44:44 +01:00
$args_expected_min = 1;
2020-05-23 18:00:31 +02:00
$subvol_args_allow_relative = 1;
2021-07-25 13:33:08 +02:00
@subvol_args = @ARGV;
2019-07-28 18:54:37 +02:00
}
2015-01-04 19:30:41 +01:00
elsif ($command eq "diff") {
2015-01-03 21:25:46 +01:00
$action_diff = 1;
2019-07-28 18:54:34 +02:00
$fallback_default_config = 1;
2015-03-01 14:28:26 +01:00
$args_expected_min = $args_expected_max = 2;
2019-05-23 15:36:34 +02:00
$subvol_args_init = "restrict_same_fs deny_root_subvol";
2021-07-25 13:33:08 +02:00
$subvol_args_allow_relative = 1;
2019-04-17 15:11:49 +02:00
@subvol_args = @ARGV;
2014-12-14 21:29:22 +01:00
}
2020-12-12 17:44:57 +01:00
elsif ($command eq "extents") {
my $subcommand = shift @ARGV // "";
if(($subcommand eq "list") ||
($subcommand eq "diff")) {
$action_extents = $subcommand;
}
else { # defaults to "list"
unshift @ARGV, $subcommand;
$action_extents = "list";
}
2019-08-08 11:57:35 +02:00
$fallback_default_config = 1;
$args_expected_min = 1;
2019-08-07 14:43:08 +02:00
$subvol_args_init = "restrict_same_fs";
2021-07-25 13:33:08 +02:00
$subvol_args_allow_relative = 1;
2019-08-07 14:43:08 +02:00
my $excl;
foreach(@ARGV) {
# subvol_arg... "exclusive" filter_arg...
if($_ eq "exclusive") {
$excl = 1;
} else {
push @subvol_args, $_;
push @filter_args, $_ if($excl);
}
}
2019-08-08 11:57:35 +02: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;
2020-12-12 20:11:00 +01:00
$subvol_args_init = "deny_root_subvol";
2021-07-25 13:33:08 +02:00
$subvol_args_allow_relative = 1;
2019-04-17 15:11:49 +02:00
@subvol_args = @ARGV;
2015-01-26 17:31:18 +01:00
}
2015-10-11 02:02:45 +02:00
elsif($command eq "list") {
2015-10-22 17:45:27 +02:00
my $subcommand = shift @ARGV // "";
if(($subcommand eq "config") ||
($subcommand eq "volume") ||
($subcommand eq "source") ||
($subcommand eq "target"))
{
$action_list = $subcommand;
}
2020-12-13 23:54:25 +01:00
elsif(($subcommand eq "all") ||
($subcommand eq "snapshots") ||
2015-10-22 17:45:27 +02:00
($subcommand eq "backups") ||
($subcommand eq "latest"))
{
$action_resolve = $subcommand;
}
else {
2020-12-13 23:54:25 +01:00
$action_resolve = "all";
2015-10-22 17:45:27 +02:00
unshift @ARGV, $subcommand if($subcommand ne "");
2015-10-12 14:59:02 +02:00
}
2015-10-11 02:02:45 +02:00
@filter_args = @ARGV;
}
2016-01-15 02:06:03 +01:00
elsif($command eq "stats") {
$action_resolve = "stats";
@filter_args = @ARGV;
}
2015-09-24 13:51:15 +02:00
elsif ($command eq "config") {
2015-10-10 21:26:59 +02:00
my $subcommand = shift @ARGV // "";
2015-10-22 17:45:27 +02:00
@filter_args = @ARGV;
2015-10-10 21:26:59 +02:00
if(($subcommand eq "print") || ($subcommand eq "print-all")) {
$action_config_print = $subcommand;
2015-10-22 17:45:27 +02:00
}
elsif($subcommand eq "list") {
$action_list = "config";
2015-10-10 21:26:59 +02:00
}
else {
ERROR "Unknown subcommand for \"config\" command: $subcommand";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2015-09-30 14:00:39 +02:00
exit 2;
2015-09-24 13:51:15 +02:00
}
}
2014-12-13 15:15:58 +01:00
else {
ERROR "Unrecognized command: $command";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2015-09-30 14:00:39 +02:00
exit 2;
2014-12-13 13:52:43 +01:00
}
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";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2015-09-30 14:00:39 +02:00
exit 2;
2015-02-28 13:49:36 +01:00
}
# input validation
2019-04-17 15:11:49 +02:00
foreach (@subvol_args) {
my ($url_prefix, $path) = check_url($_);
2021-08-14 18:11:45 +02:00
if(!defined($path) && $subvol_args_allow_relative && ($url_prefix eq "") && (-d $_)) {
2022-02-26 18:09:42 +01:00
$path = check_file(abs_path($_), { absolute => 1, sanitize => 1 });
2020-05-23 18:00:31 +02:00
}
2019-04-17 15:56:35 +02:00
unless(defined($path)) {
ERROR "Bad argument: not a subvolume declaration: $_";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2019-04-17 15:56:35 +02:00
exit 2;
2019-04-17 15:11:49 +02:00
}
2019-04-17 15:56:35 +02:00
$_ = $url_prefix . $path;
}
my @filter_vf;
foreach (@filter_args) {
my $vf = vinfo_filter_statement($_);
unless($vf) {
ERROR "Bad argument: invalid filter statement: $_";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2019-04-17 15:56:35 +02:00
exit 2;
}
push @filter_vf, $vf;
2015-02-28 13:49:36 +01:00
}
2019-04-17 16:10:15 +02:00
foreach (@exclude_cmdline) {
my $vf = vinfo_filter_statement($_);
unless($vf) {
ERROR "Bad argument: invalid filter statement: --exclude='$_'";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2019-04-17 16:10:15 +02:00
exit 2;
}
push @exclude_vf, $vf;
}
2018-02-13 19:26:54 +01:00
foreach(@config_override_cmdline) {
if(/(.*?)=(.*)/) {
my $key = $1;
my $value = $2;
2019-04-30 13:38:47 +02:00
unless(append_config_option(\%config_override, $key, $value, "OVERRIDE", error_statement => "in option \"--override\"")) {
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2018-02-13 19:26:54 +01:00
exit 2;
}
}
else {
ERROR "Option \"override\" requires \"<config_option>=<value>\" format";
2022-03-25 15:20:14 +01:00
ERROR_HELP_MESSAGE;
2015-10-23 14:43:36 +02:00
exit 2;
}
}
2016-06-07 16:17:02 +02:00
if(defined($lockfile_cmdline)) {
2021-08-17 11:40:54 +02:00
unless($lockfile = check_file($lockfile_cmdline, { absolute => 1, relative => 1 },
error_statement => 'for option --lockfile')) {
2016-06-07 16:17:02 +02:00
exit 2;
}
}
2015-02-28 13:49:36 +01:00
2014-12-13 13:52:43 +01:00
2016-04-15 01:22:19 +02:00
INFO "$VERSION_INFO (" . localtime($start_time) . ")";
2019-04-18 16:28:53 +02:00
action("startup", status => "v$VERSION", message => $VERSION_INFO, time => $start_time);
2014-12-14 21:29:22 +01:00
2019-07-28 18:54:34 +02:00
#
# parse config file
#
2019-08-05 14:31:48 +02:00
my $config;
if(my $config_file = _config_file(@config_src)) {
INFO "Using configuration: $config_file";
$config = parse_config($config_file);
exit 2 unless($config);
}
2023-03-26 15:34:02 +02:00
elsif($fallback_default_config && !$config_cmdline) {
INFO "Configuration file not found: " . join(', ', @config_src);
INFO "Using default configuration";
2019-08-05 14:31:48 +02:00
$config = init_config();
2019-07-28 18:54:34 +02:00
}
2019-08-05 14:31:48 +02:00
else {
ERROR "Configuration file not found: " . join(', ', @config_src);
exit 2;
}
2021-11-06 16:10:26 +01:00
$safe_commands = config_key($config, 'safe_commands');
2019-08-05 14:31:48 +02:00
2019-07-28 18:54:34 +02:00
unless(ref($config->{SUBSECTION}) eq "ARRAY") {
ERROR "No volumes defined in configuration file";
exit 2;
}
2019-05-23 15:36:34 +02:00
# input validation (part 2, after config is initialized)
@subvol_args = map { vinfo($_, $config) } @subvol_args;
if($subvol_args_init) {
foreach(@subvol_args) {
unless(vinfo_init_root($_)) {
ERROR "Failed to fetch subvolume detail for '$_->{PRINT}'" , @stderr;
exit 1;
}
if(defined($_->{NODE_SUBDIR})) {
ERROR "Argument is not a subvolume: $_->{PATH}";
exit 1;
}
if(($subvol_args_init =~ /deny_root_subvol/) && $_->{node}{is_root}) {
ERROR "Subvolume is btrfs root: $_->{PATH}";
exit 1;
}
if(($subvol_args_init =~ /restrict_same_fs/) && (not _is_same_fs_tree($subvol_args[0]->{node}, $_->{node}))) {
ERROR "Subvolumes are not on the same btrfs filesystem!";
exit 1;
}
}
}
2019-07-28 18:54:34 +02:00
2014-12-14 21:29:22 +01:00
if($action_diff)
{
2015-01-04 19:30:41 +01:00
#
2019-05-23 15:36:34 +02:00
# print snapshot diff (btrfs find-new)
2015-01-04 19:30:41 +01:00
#
2019-05-23 15:36:34 +02:00
my $src_vol = $subvol_args[0];
my $target_vol = $subvol_args[1];
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)
2016-05-03 13:19:42 +02:00
my $lastgen = $src_vol->{node}{gen} + 1;
2015-01-03 21:25:46 +01:00
2014-12-14 22:03:31 +01:00
# dump files, sorted and unique
2015-04-23 16:19:34 +02:00
my $ret = btrfs_subvolume_find_new($target_vol, $lastgen);
2015-01-03 21:25:46 +01:00
exit 1 unless(ref($ret));
2020-12-21 00:30:06 +01:00
INFO "Listing changed files for subvolume: $target_vol->{PRINT} (gen=$target_vol->{node}{gen})";
INFO "Starting at generation after subvolume: $src_vol->{PRINT} (gen=$src_vol->{node}{gen})";
INFO "Listing files modified within generation range: [$lastgen..$target_vol->{node}{gen}]";
DEBUG "Newest file generation (transid marker) was: $ret->{transid_marker}";
2020-10-30 18:38:24 +01:00
my $files = $ret->{files};
my $total_len = 0;
my @data;
foreach my $name (sort keys %$files) {
my $finfo = $files->{$name};
$total_len += $finfo->{len};
push @data, {
flags => ($finfo->{new} ? '+' : '.') .
2020-12-21 00:20:02 +01:00
($finfo->{flags}->{COMPRESS} ? 'c' : '.') .
($finfo->{flags}->{INLINE} ? 'i' : '.'),
2020-10-30 18:38:24 +01:00
count => scalar(keys(%{$finfo->{gen}})),
size => print_size($finfo->{len}),
file => $name,
};
}
my $raw = ($output_format && $output_format eq "raw");
2020-12-21 00:20:02 +01:00
print_formatted("diff", \@data, paragraph => 1);
print "Total size: " . print_size($total_len) . "\n" unless($raw);
2014-12-14 21:29:22 +01:00
exit 0;
}
2020-12-12 17:44:57 +01:00
if($action_extents)
2019-08-08 11:57:35 +02:00
{
#
# print extents diff (filefrag)
#
# check system requirements
2019-05-28 20:45:50 +02:00
my $extentmap_fn;
if($dryrun) {
$extentmap_fn = sub {
INFO("Fetching extent information (dryrun) for: $_[0]->{PRINT}");
return undef;
};
}
elsif(eval_quiet { require IO::AIO; }) {
2020-11-08 22:59:30 +01:00
# this is slightly faster (multithreaded) than filefrag
2019-05-28 20:45:50 +02:00
$extentmap_fn=\&aio_extentmap;
}
elsif(check_exe("filefrag")) {
2020-11-08 22:59:30 +01:00
INFO "IO::AIO module not present, falling back to 'filefrag' (slower)";
2019-05-28 20:45:50 +02:00
$extentmap_fn=\&filefrag_extentmap;
}
else {
2020-11-08 22:59:30 +01:00
ERROR 'Please install either "IO::AIO" perl module or "filefrag" (from e2fsprogs package)';
2019-08-08 11:57:35 +02:00
exit 1;
}
2020-11-08 22:59:30 +01:00
INFO "Extent map caching disabled (consider setting \"cache_dir\" configuration option)" unless(config_key($config, 'cache_dir'));
2019-08-08 11:57:35 +02:00
# resolve related subvolumes
my @resolved_vol;
2020-12-12 17:44:57 +01:00
if($extents_related) {
2019-08-08 11:57:35 +02:00
# add all related subvolumes
foreach my $svol (@subvol_args) {
my $svol_gen = $svol->{node}{readonly} ? $svol->{node}{cgen} : $svol->{node}{gen};
my @related = map({ vinfo_resolved_all_mountpoints($_, $svol->{VINFO_MOUNTPOINT}) // () }
2022-11-20 14:36:16 +01:00
@{_related_nodes($svol->{node})}); # includes $svol
2019-08-08 11:57:35 +02:00
push @resolved_vol, @related;
}
}
else {
@resolved_vol = @subvol_args;
}
my @data;
# print results on ctrl-c
$SIG{INT} = sub {
2020-11-07 14:36:36 +01:00
print STDERR "\nERROR: Caught SIGINT, dumping incomplete list:\n";
2019-08-08 11:57:35 +02:00
print_formatted("extent_diff", \@data);
exit 1;
};
2020-12-12 17:44:57 +01:00
my $do_diff = ($action_extents eq "diff");
2019-08-07 14:43:08 +02:00
my $prev_data;
2020-11-08 21:02:07 +01:00
# sort by gen for r/w subvolumes, cgen on readonly subvolumes, as
# "gen" is increased on readonly subvolume when snapshotted.
# crawl descending, but display ascending (unshift):
2019-08-07 14:43:08 +02:00
foreach my $vol (sort { ($b->{node}{readonly} ? $b->{node}{cgen} : $b->{node}{gen}) <=>
($a->{node}{readonly} ? $a->{node}{cgen} : $a->{node}{gen}) }
2020-11-08 21:02:07 +01:00
@resolved_vol) {
2020-12-12 17:05:14 +01:00
if($prev_data && ($prev_data->{_vinfo}{node}{id} == $vol->{node}{id})) {
INFO "Skipping duplicate of \"$prev_data->{_vinfo}{PRINT}\": $vol->{PRINT}";
next;
}
2019-08-07 00:26:12 +02:00
2020-11-08 22:59:30 +01:00
# read extent map
2019-08-07 14:43:08 +02:00
if($vol->{EXTENTMAP} = read_extentmap_cache($vol)) {
2020-11-08 22:59:30 +01:00
INFO "Using cached extent map: $vol->{PRINT}";
2019-08-07 00:26:12 +02:00
} else {
2019-05-28 20:45:50 +02:00
$vol->{EXTENTMAP} = $extentmap_fn->($vol);
2019-08-07 14:43:08 +02:00
write_extentmap_cache($vol);
2019-08-07 00:26:12 +02:00
}
2019-08-07 14:43:08 +02:00
next unless($vol->{EXTENTMAP});
2019-08-07 00:26:12 +02:00
2020-12-12 17:44:57 +01:00
if($do_diff && $prev_data) {
2019-08-07 14:43:08 +02:00
my $diff_map = extentmap_diff($prev_data->{_vinfo}{EXTENTMAP}, $vol->{EXTENTMAP});
$prev_data->{diff} = print_size(extentmap_size($diff_map));
}
$prev_data = {
2020-12-12 16:30:40 +01:00
%{$vol->{node}}, # copy node
2019-08-07 14:43:08 +02:00
total => print_size(extentmap_size($vol->{EXTENTMAP})),
subvol => $vol->{PRINT},
_vinfo => $vol,
};
unshift @data, $prev_data;
}
2019-08-08 11:57:35 +02:00
2019-08-07 14:43:08 +02:00
my @universe_set = map $_->{_vinfo}{EXTENTMAP}, @data;
unless(scalar(@universe_set)) {
ERROR "No extent map data, exiting";
exit -1;
}
2019-08-08 11:57:35 +02:00
2019-08-07 14:43:08 +02:00
my @summary;
INFO "Calculating union of " . scalar(@data) . " subvolumes";
push @summary, {
a => "Union (" . scalar(@data) . " subvolumes):",
b => print_size(extentmap_size(extentmap_merge(@universe_set)))
};
2020-11-08 23:22:01 +01:00
INFO "Calculating set-exclusive size for " . scalar(@data) . " subvolumes";
foreach my $d (@data) {
my $vol = $d->{_vinfo};
DEBUG "Calculating exclusive for: $vol->{PRINT}";
my @others = grep { $_ != $vol->{EXTENTMAP} } @universe_set;
$d->{exclusive} = print_size(extentmap_size(extentmap_diff($vol->{EXTENTMAP}, extentmap_merge(@others)))),
2019-08-08 11:57:35 +02:00
}
2019-08-07 14:43:08 +02:00
if(scalar(@filter_vf)) {
2020-11-08 22:59:30 +01:00
INFO "Calculating set difference (X \\ A)";
2019-08-07 14:43:08 +02:00
my @excl;
my @others;
foreach(@data) {
if(vinfo_match(\@filter_vf, $_->{_vinfo})) {
$_->{set} = "X";
push @excl, $_->{_vinfo}{EXTENTMAP};
} else {
$_->{set} = "A";
push @others, $_->{_vinfo}{EXTENTMAP};
}
}
push @summary, {
a => "Exclusive data ( X \\ A ):",
b => print_size(extentmap_size(extentmap_diff(extentmap_merge(@excl), extentmap_merge(@others)))),
2020-12-12 17:44:57 +01:00
};
}
unless($do_diff) {
@data = sort { $a->{subvol} cmp $b->{subvol} } @data;
2019-08-07 14:43:08 +02:00
}
2022-05-27 15:03:01 +02:00
INFO "Printing extents map set difference: (extents \\ extents-on-prev-line)" if $do_diff;
2019-08-07 14:43:08 +02:00
print_formatted("extent_diff", \@data, paragraph => 1);
print_formatted({ table => [ qw( a b ) ], RALIGN => { b=>1 } },
\@summary, output_format => "table", no_header => 1);
2019-08-08 11:57:35 +02:00
exit 0;
}
2019-07-28 18:54:37 +02:00
if($action_ls)
{
#
# print accessible subvolumes for local path
#
2019-12-23 13:44:44 +01:00
my $exit_status = 0;
2020-05-23 18:00:31 +02:00
my %data_uniq;
2020-12-12 20:11:00 +01:00
foreach my $root_vol (@subvol_args) {
2021-08-17 16:50:22 +02:00
my ($root_path, $mountpoint) = vinfo_mountpoint($root_vol);
unless($mountpoint) {
2022-07-28 13:44:42 +02:00
ERROR "Failed to read filesystem details for: $root_vol->{PRINT}", @stderr;
2019-12-23 13:44:44 +01:00
$exit_status = 1;
next;
}
2020-05-23 18:00:31 +02:00
$root_vol = vinfo($root_vol->{URL_PREFIX} . $root_path, $config);
INFO "Listing subvolumes for directory: $root_vol->{PRINT}";
2019-12-23 13:44:44 +01:00
2021-08-17 16:50:22 +02:00
my @search = ( $mountpoint );
while(my $mnt = shift @search) {
unshift @search, @{$mnt->{SUBTREE}} if($mnt->{SUBTREE});
next if($mnt->{fs_type} ne "btrfs");
my $vol = vinfo($root_vol->{URL_PREFIX} . $mnt->{mount_point}, $config);
unless(vinfo_init_root($vol)) {
ERROR "Failed to fetch subvolume detail for: $vol->{PRINT}", @stderr;
2019-12-23 13:44:44 +01:00
$exit_status = 1;
next;
2019-07-28 18:54:37 +02:00
}
2020-05-23 18:00:31 +02:00
2021-08-17 16:50:22 +02:00
my $subvol_list = vinfo_subvol_list($vol);
my $count_added = 0;
foreach my $svol ($vol, @$subvol_list) {
my $svol_path = $svol->{PATH};
$svol_path =~ s/^\/\//\//; # sanitize "//" (see vinfo_child)
2020-05-23 18:00:31 +02:00
2022-02-26 18:05:40 +01:00
next unless($root_path eq "/" || $svol_path =~ /^\Q$root_path\E(\/|\z)/);
2021-08-17 16:50:22 +02:00
if(_find_mountpoint($mnt, $svol_path) ne $mnt) {
DEBUG "Subvolume is hidden by another mount point: $svol->{PRINT}";
2019-07-28 18:54:37 +02:00
next;
}
2021-08-17 16:50:22 +02:00
$data_uniq{$svol->{PRINT}} = {
%{$svol->{node}}, # copy node
top => $svol->{node}{top_level}, # alias (narrow column)
mount_point => $svol->{VINFO_MOUNTPOINT}{PATH},
mount_source => $svol->{node}{TREE_ROOT}{mount_source},
mount_subvolid => $mnt->{MNTOPS}{subvolid},
mount_subvol => $mnt->{MNTOPS}{subvol},
subvolume_path => $svol->{node}{path},
subvolume_rel_path => $svol->{node}{REL_PATH},
url => $svol->{URL},
host => $svol->{HOST},
path => $svol_path,
flags => ($svol->{node}{readonly} ? "readonly" : undef),
};
$count_added++;
2020-05-23 18:00:31 +02:00
}
2021-08-17 16:50:22 +02:00
DEBUG "Listing $count_added/" . (scalar(@$subvol_list) + 1) . " subvolumes for btrfs mount: $vol->{PRINT}";
2019-07-28 18:54:37 +02:00
}
}
2020-05-23 18:00:31 +02:00
my @sorted = sort { (($a->{host} // "") cmp ($b->{host} // "")) ||
($a->{mount_point} cmp $b->{mount_point}) ||
($a->{path} cmp $b->{path})
} values %data_uniq;
2019-12-23 14:28:35 +01:00
$output_format ||= "short";
2020-05-23 18:00:31 +02:00
print_formatted("fs_list", \@sorted, no_header => !scalar(@sorted));
2019-12-23 13:44:44 +01:00
exit $exit_status;
2019-07-28 18:54:37 +02:00
}
2015-01-20 19:18:38 +01:00
2016-06-07 16:17:02 +02:00
#
# try exclusive lock if set in config or command-line option
#
$lockfile //= config_key($config, "lockfile");
if(defined($lockfile) && (not $dryrun)) {
2021-08-16 16:58:51 +02:00
unless(open(LOCKFILE, '>>', $lockfile)) {
2016-06-07 16:17:02 +02:00
# NOTE: the lockfile is never deleted by design
ERROR "Failed to open lock file '$lockfile': $!";
exit 3;
}
unless(flock(LOCKFILE, 6)) { # exclusive, non-blocking (LOCK_EX | LOCK_NB)
ERROR "Failed to take lock (another btrbk instance is running): $lockfile";
exit 3;
}
}
2015-01-20 19:18:38 +01:00
2016-04-16 01:09:17 +02:00
if($action_archive)
2016-04-07 15:33:32 +02:00
{
#
2016-04-16 01:09:17 +02:00
# archive (clone) tree
2016-04-07 15:33:32 +02:00
#
# NOTE: This is intended to work without a config file! The only
# thing used from the configuration is the SSH and transaction log
# stuff.
#
2022-06-06 15:04:42 +02:00
# FIXME: add command line options for preserve logic
2016-04-07 15:33:32 +02:00
2022-06-19 14:08:25 +02:00
my $sroot = $subvol_args[0] || die;
my $droot = $subvol_args[1] || die;
2016-04-07 15:33:32 +02:00
2022-06-19 14:08:25 +02:00
unless(vinfo_init_root($sroot)) {
ERROR "Failed to fetch subvolume detail for '$sroot->{PRINT}'", @stderr;
2016-04-07 15:33:32 +02:00
exit 1;
}
2022-06-19 14:08:25 +02:00
unless($raw_cmdline ? vinfo_init_raw_root($droot) : vinfo_init_root($droot)) {
ERROR "Failed to fetch " . ($raw_cmdline ? "raw target metadata" : "subvolume detail") . " for '$droot->{PRINT}'", @stderr;
2016-04-07 15:33:32 +02:00
exit 1;
}
2022-06-06 18:22:19 +02:00
$config->{SUBSECTION} = []; # clear configured subsections, we build them dynamically
2022-06-06 15:04:42 +02:00
2022-06-06 18:22:19 +02:00
my $cur = $config;
2016-04-07 15:33:32 +02:00
my %name_uniq;
2022-06-06 18:22:19 +02:00
foreach my $vol (sort { ($a->{subtree_depth} <=> $b->{subtree_depth}) ||
($a->{SUBVOL_DIR} cmp $b->{SUBVOL_DIR})
2022-06-19 14:08:25 +02:00
} @{vinfo_subvol_list($sroot)})
2022-06-06 18:22:19 +02:00
{
2016-04-07 15:33:32 +02:00
next unless($vol->{node}{readonly});
2016-04-19 13:06:31 +02:00
my $snapshot_name = $vol->{node}{BTRBK_BASENAME};
2016-04-07 15:33:32 +02:00
unless(defined($snapshot_name)) {
WARN "Skipping subvolume (not a btrbk subvolume): $vol->{PRINT}";
next;
}
2022-06-06 18:22:19 +02:00
my $subdir = $vol->{SUBVOL_DIR} ? "/" . $vol->{SUBVOL_DIR} : "";
next if($name_uniq{"$subdir/$snapshot_name"});
$name_uniq{"$subdir/$snapshot_name"} = 1;
$cur = parse_config_line($cur, $_->[0], $_->[1]) // die for(
2022-06-07 00:34:14 +02:00
[ subvolume => $sroot->{URL_PREFIX} . "/dev/null" ],
2022-06-19 14:08:25 +02:00
[ snapshot_dir => $sroot->{PATH} . $subdir ],
2022-06-06 18:22:19 +02:00
[ snapshot_name => $snapshot_name ],
2022-06-19 14:08:25 +02:00
[ target => ($raw_cmdline ? "raw" : "send-receive") . " '" . $droot->{URL} . $subdir . "'" ],
2023-04-11 00:48:55 +02:00
# [ target_create_dir => "yes" ], # not user-settable yet, see below
2022-06-06 18:22:19 +02:00
);
2023-04-11 00:48:55 +02:00
$cur->{target_create_dir} = "yes";
if($dryrun && !vinfo_realpath(my $dr = vinfo_child($droot, $subdir))) {
# vinfo_mkdir below does not know about $droot, and thus cannot fake realpath_cache correctly.
# hackily set cache here for now, while keeping target_create_dir disabled for the user.
# TODO: implement pinned target root subvolume, and enable target_create_dir as regular option.
system_mkdir($dr); # required for correct transaction log
$realpath_cache{$droot->{URL} . $subdir} = $droot->{PATH} . $subdir;
}
2016-04-07 15:33:32 +02:00
}
2022-06-06 18:22:19 +02:00
_config_propagate_target($config);
2016-04-07 15:33:32 +02:00
2019-04-17 16:10:15 +02:00
# translate archive_exclude globs, add to exclude args
my $archive_exclude = config_key($config, 'archive_exclude') // [];
push @exclude_vf, map(vinfo_filter_statement($_), (@$archive_exclude));
2016-04-07 15:33:32 +02:00
}
2016-03-16 13:25:19 +01:00
#
# expand subvolume globs (wildcards)
#
2021-07-24 20:11:32 +02:00
foreach my $config_vol (config_subsection($config, "volume")) {
2016-03-16 13:25:19 +01:00
# read-in subvolume list (and expand globs) only if needed
next unless(grep defined($_->{GLOB_CONTEXT}), @{$config_vol->{SUBSECTION}});
my @vol_subsection_expanded;
2021-07-24 20:11:32 +02:00
foreach my $config_subvol (config_subsection($config_vol, "subvolume")) {
2016-03-16 13:25:19 +01:00
if($config_subvol->{GLOB_CONTEXT}) {
2021-07-24 16:47:26 +02:00
my ($url_prefix, $globs) = check_url($config_subvol->{url}, accept_wildcards => 1);
$globs =~ s/([^\*]*)\///;
my $sroot_glob = vinfo($url_prefix . $1, $config_subvol);
2019-12-14 17:06:21 +01:00
INFO "Expanding wildcards: $sroot_glob->{PRINT}/$globs";
2021-07-24 16:47:26 +02:00
unless(vinfo_init_root($sroot_glob)) {
WARN "Failed to fetch subvolume detail for: $sroot_glob->{PRINT}", @stderr;
WARN "No subvolumes found matching: $sroot_glob->{PRINT}/$globs";
next;
}
2016-03-16 13:25:19 +01:00
# support "*some*file*", "*/*"
my $match = join('[^\/]*', map(quotemeta($_), split(/\*+/, $globs, -1)));
2020-08-28 17:15:39 +02:00
TRACE "translated globs \"$globs\" to regex \"$match\"" if($do_trace);
2016-03-16 13:25:19 +01:00
my $expand_count = 0;
2019-12-14 17:06:21 +01:00
foreach my $vol (@{vinfo_subvol_list($sroot_glob, sort => 'path')})
2016-03-16 13:25:19 +01:00
{
if($vol->{node}{readonly}) {
2020-08-28 17:15:39 +02:00
TRACE "skipping readonly subvolume: $vol->{PRINT}" if($do_trace);
2016-03-16 13:25:19 +01:00
next;
}
unless($vol->{SUBVOL_PATH} =~ /^$match$/) {
2020-08-28 17:15:39 +02:00
TRACE "skipping non-matching subvolume: $vol->{PRINT}" if($do_trace);
2016-03-16 13:25:19 +01:00
next;
}
2017-09-11 18:49:14 +02:00
unless(defined(check_file($vol->{SUBVOL_PATH}, { relative => 1 }))) {
WARN "Ambiguous subvolume path \"$vol->{SUBVOL_PATH}\" while expanding \"$globs\", ignoring";
next;
}
2016-03-16 13:25:19 +01:00
INFO "Found source subvolume: $vol->{PRINT}";
my %conf = ( %$config_subvol,
2021-07-24 16:47:26 +02:00
url_glob => $config_subvol->{url},
2016-03-16 13:25:19 +01:00
url => $vol->{URL},
snapshot_name => $vol->{NAME}, # snapshot_name defaults to subvolume name
);
# deep copy of target subsection
my @subsection_copy = map { { %$_, PARENT => \%conf }; } @{$config_subvol->{SUBSECTION}};
$conf{SUBSECTION} = \@subsection_copy;
push @vol_subsection_expanded, \%conf;
$expand_count += 1;
}
unless($expand_count) {
2019-12-14 17:06:21 +01:00
WARN "No subvolumes found matching: $sroot_glob->{PRINT}/$globs";
2016-03-16 13:25:19 +01:00
}
}
else {
push @vol_subsection_expanded, $config_subvol;
}
}
$config_vol->{SUBSECTION} = \@vol_subsection_expanded;
}
2020-08-28 17:15:39 +02:00
TRACE(Data::Dumper->Dump([$config], ["config"])) if($do_trace && $do_dumper);
2016-03-16 13:25:19 +01:00
2016-03-07 17:35:17 +01:00
#
# create vinfo nodes (no readin yet)
#
2021-07-24 20:11:32 +02:00
foreach my $config_vol (config_subsection($config, "volume")) {
2021-07-24 20:37:51 +02:00
my $sroot = $config_vol->{DUMMY} ? { CONFIG => $config_vol, PRINT => "*default*" } : vinfo($config_vol->{url}, $config_vol);
2016-05-10 15:51:44 +02:00
vinfo_assign_config($sroot);
2021-07-24 20:11:32 +02:00
foreach my $config_subvol (config_subsection($config_vol, "subvolume")) {
2021-07-24 15:43:37 +02:00
my $svol = vinfo($config_subvol->{url}, $config_subvol);
2021-07-24 12:48:11 +02:00
my $snapshot_dir = config_key($svol, "snapshot_dir");
my $url;
if(!defined($snapshot_dir)) {
2021-07-24 20:37:51 +02:00
if($config_vol->{DUMMY}) {
ABORTED($svol, "No snapshot_dir defined for subvolume");
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
} else {
$url = $sroot->{URL};
}
2021-07-24 12:48:11 +02:00
} elsif($snapshot_dir =~ /^\//) {
2021-07-24 20:37:51 +02:00
$url = $svol->{URL_PREFIX} . $snapshot_dir;
2021-07-24 12:48:11 +02:00
} else {
2021-07-24 20:37:51 +02:00
if($config_vol->{DUMMY}) {
ABORTED($svol, "Relative snapshot_dir path defined, but no volume context present");
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
} else {
$url = $sroot->{URL} . '/' . $snapshot_dir;
}
2021-07-24 12:48:11 +02:00
}
2021-07-24 20:37:51 +02:00
$url //= "/dev/null"; # snaproot cannot be undef, even if ABORTED
2021-07-24 12:48:11 +02:00
my $snaproot = vinfo($url, $config_subvol);
2018-02-15 00:17:01 +01:00
vinfo_assign_config($svol, $snaproot);
2016-03-07 17:46:53 +01:00
foreach my $config_target (@{$config_subvol->{SUBSECTION}}) {
die unless($config_target->{CONTEXT} eq "target");
2016-03-07 17:35:17 +01:00
my $droot = vinfo($config_target->{url}, $config_target);
2016-05-10 15:51:44 +02:00
vinfo_assign_config($droot);
2016-03-07 17:35:17 +01:00
}
}
}
2015-05-27 15:00:25 +02:00
#
2019-04-17 15:43:08 +02:00
# filter subvolumes matching command line arguments, handle noauto option
2015-05-27 15:00:25 +02:00
#
2019-04-17 15:56:35 +02:00
if(scalar @filter_vf)
2015-05-27 15:00:25 +02:00
{
2016-03-07 21:54:51 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume', 1)) {
2015-09-20 14:25:20 +02:00
my $found_vol = 0;
2019-04-17 15:56:35 +02:00
if(vinfo_match(\@filter_vf, $sroot, flag_matched => '_matched')) {
next;
2015-05-27 15:00:25 +02:00
}
2016-03-07 21:54:51 +01:00
foreach my $svol (vinfo_subsection($sroot, 'subvolume', 1)) {
2015-09-20 14:25:20 +02:00
my $found_subvol = 0;
2019-04-17 15:56:35 +02:00
my $snaproot = vinfo_snapshot_root($svol);
2016-03-07 21:54:51 +01:00
my $snapshot_name = config_key($svol, "snapshot_name") // die;
2019-04-17 15:56:35 +02:00
if(vinfo_match(\@filter_vf, $svol, flag_matched => '_matched') ||
vinfo_match(\@filter_vf, vinfo_child($snaproot, $snapshot_name), flag_matched => '_matched'))
{
$found_vol = 1;
next;
}
2016-03-07 21:54:51 +01:00
foreach my $droot (vinfo_subsection($svol, 'target', 1)) {
2019-04-17 15:56:35 +02:00
if(vinfo_match(\@filter_vf, $droot, flag_matched => '_matched') ||
vinfo_match(\@filter_vf, vinfo_child($droot, $snapshot_name), flag_matched => '_matched'))
{
$found_subvol = 1;
$found_vol = 1;
2015-09-20 14:25:20 +02:00
}
2019-04-17 15:56:35 +02:00
else {
2019-04-17 15:20:18 +02:00
ABORTED($droot, "skip_cmdline_filter", "No match on filter command line argument");
DEBUG "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot);
2015-09-20 14:25:20 +02:00
}
}
unless($found_subvol) {
2019-04-17 15:20:18 +02:00
ABORTED($svol, "skip_cmdline_filter", "No match on filter command line argument");
DEBUG "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
2015-05-27 15:00:25 +02:00
}
}
2015-09-20 14:25:20 +02:00
unless($found_vol) {
2019-04-17 15:20:18 +02:00
ABORTED($sroot, "skip_cmdline_filter", "No match on filter command line argument");
DEBUG "Skipping volume \"$sroot->{PRINT}\": " . ABORTED_TEXT($sroot);
2015-05-27 15:00:25 +02:00
}
}
# make sure all args have a match
2019-04-17 15:56:35 +02:00
my @nomatch = map { $_->{_matched} ? () : $_->{unparsed} } @filter_vf;
2015-05-27 15:00:25 +02:00
if(@nomatch) {
foreach(@nomatch) {
2019-04-17 15:56:35 +02:00
ERROR "Filter argument \"$_\" does not match any volume, subvolume, target or group declaration";
2015-05-27 15:00:25 +02:00
}
2015-09-30 14:00:39 +02:00
exit 2;
2015-05-27 15:00:25 +02:00
}
2019-04-17 15:56:35 +02:00
$config->{CMDLINE_FILTER_LIST} = [ map { $_->{unparsed} } @filter_vf ];
2015-05-27 15:00:25 +02:00
}
2019-04-17 15:43:08 +02:00
elsif(not $action_config_print)
{
# no filter_args present, abort "noauto" contexts
if(config_key($config, "noauto")) {
2020-08-28 18:48:52 +02:00
WARN "Option \"noauto\" is set in global context, and no filter argument present, exiting";
2019-04-17 15:43:08 +02:00
exit 0;
}
foreach my $sroot (vinfo_subsection($config, 'volume')) {
if(config_key($sroot, "noauto")) {
ABORTED($sroot, "skip_noauto", 'option "noauto" is set');
DEBUG "Skipping volume \"$sroot->{PRINT}\": " . ABORTED_TEXT($sroot);
next;
}
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
if(config_key($svol, "noauto")) {
ABORTED($svol, "skip_noauto", 'option "noauto" is set');
DEBUG "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
next;
}
foreach my $droot (vinfo_subsection($svol, 'target')) {
if(config_key($droot, "noauto")) {
ABORTED($droot, "skip_noauto", 'option "noauto" is set');
DEBUG "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot);
}
}
}
}
}
2015-05-27 15:00:25 +02:00
2019-04-17 16:10:15 +02:00
if(scalar @exclude_vf)
{
# handle --exclude command line option
foreach my $sroot (vinfo_subsection($config, 'volume')) {
if(my $ff = vinfo_match(\@exclude_vf, $sroot)) {
ABORTED($sroot, "skip_cmdline_exclude", "command line argument \"--exclude=$ff->{unparsed}\"");
DEBUG "Skipping volume \"$sroot->{PRINT}\": " . ABORTED_TEXT($sroot);
next;
}
my $all_svol_aborted = 1;
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
my $snaproot = vinfo_snapshot_root($svol);
my $snapshot_name = config_key($svol, "snapshot_name") // die;
if(my $ff = (vinfo_match(\@exclude_vf, $svol) ||
vinfo_match(\@exclude_vf, vinfo_child($snaproot, $snapshot_name))))
{
ABORTED($svol, "skip_cmdline_exclude", "command line argument \"--exclude=$ff->{unparsed}\"");
DEBUG "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
next;
}
$all_svol_aborted = 0;
foreach my $droot (vinfo_subsection($svol, 'target')) {
if(my $ff = (vinfo_match(\@exclude_vf, $droot) ||
vinfo_match(\@exclude_vf, vinfo_child($droot, $snapshot_name))))
{
ABORTED($droot, "skip_cmdline_exclude", "command line argument \"--exclude=$ff->{unparsed}\"");
DEBUG "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot);
next;
}
}
}
if($all_svol_aborted) {
ABORTED($sroot, "skip_cmdline_exclude", "All subvolumes excluded");
DEBUG "Skipping volume \"$sroot->{PRINT}\": " . ABORTED_TEXT($sroot);
}
}
}
2015-01-20 19:18:38 +01:00
2015-10-19 22:10:08 +02:00
if($action_usage)
2015-01-20 19:18:38 +01:00
{
#
# print filesystem information
#
2015-10-19 22:10:08 +02:00
my @data;
2021-07-25 16:36:18 +02:00
my %usage_cache;
2015-01-20 19:18:38 +01:00
my %processed;
2021-07-25 16:36:18 +02:00
my $push_data = sub {
my ($vol, $type) = @_;
return if $processed{$vol->{URL}};
2021-08-17 16:50:22 +02:00
my $mountpoint = vinfo_mountpoint($vol, fs_type => 'btrfs');
return unless($mountpoint);
my $mount_source = $mountpoint->{mount_source};
2021-07-25 16:36:18 +02:00
my $mid = $vol->{MACHINE_ID} . $mount_source;
2022-02-22 20:28:23 +01:00
$usage_cache{$mid} //= btrfs_filesystem_usage(vinfo($vol->{URL_PREFIX} . $mountpoint->{mount_point}, $vol->{CONFIG})) // {};
2021-07-25 16:36:18 +02:00
push @data, { %{$usage_cache{$mid}},
type => $type,
mount_source => $mount_source,
vinfo_prefixed_keys("", $vol),
};
$processed{$vol->{URL}} = 1;
};
2016-03-07 21:54:51 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2022-02-22 20:28:17 +01:00
$push_data->($svol, "source");
2016-03-07 21:54:51 +01:00
foreach my $droot (vinfo_subsection($svol, 'target')) {
2021-08-18 00:57:56 +02:00
$push_data->($droot, "target");
2015-01-20 19:18:38 +01:00
}
}
}
2022-02-22 20:28:17 +01:00
@data = sort { $a->{url} cmp $b->{url} } @data;
2015-10-19 22:10:08 +02:00
print_formatted("usage", \@data);
2015-10-14 16:51:39 +02:00
exit exit_status($config);
2015-01-20 19:18:38 +01:00
}
2015-04-14 13:52:16 +02:00
2015-10-10 21:26:59 +02:00
if($action_config_print)
{
#
# print configuration lines, machine readable
#
2022-05-29 12:23:40 +02:00
my %opts = (all => ($action_config_print eq "print-all"));
2015-10-10 21:26:59 +02:00
my @out;
2022-05-29 12:23:40 +02:00
push @out, config_dump_keys($config, %opts);
2021-07-25 17:32:06 +02:00
my $indent = "";
foreach my $sroot (vinfo_subsection($config, 'volume', 1)) {
unless($sroot->{CONFIG}{DUMMY}) {
push @out, "";
push @out, "volume $sroot->{URL}";
$indent .= "\t";
2022-05-29 12:23:40 +02:00
push @out, config_dump_keys($sroot, prefix => $indent, %opts);
2021-07-25 17:32:06 +02:00
}
foreach my $svol (vinfo_subsection($sroot, 'subvolume', 1)) {
push @out, "";
push @out, "${indent}# subvolume $svol->{CONFIG}->{url_glob}" if(defined($svol->{CONFIG}->{url_glob}));
push @out, "${indent}subvolume $svol->{URL}";
$indent .= "\t";
2022-05-29 12:23:40 +02:00
push @out, config_dump_keys($svol, prefix => $indent, %opts);
2021-07-25 17:32:06 +02:00
foreach my $droot (vinfo_subsection($svol, 'target', 1)) {
push @out, "";
push @out, "${indent}target $droot->{CONFIG}->{target_type} $droot->{URL}";
2022-05-29 12:23:40 +02:00
push @out, config_dump_keys($droot, prefix => "\t$indent", %opts);
2015-10-10 21:26:59 +02:00
}
2021-07-25 17:32:06 +02:00
$indent =~ s/\t//;
2015-10-10 21:26:59 +02:00
}
2021-07-25 17:32:06 +02:00
$indent = "";
2015-10-10 21:26:59 +02:00
}
print_header(title => "Configuration Dump",
config => $config,
time => $start_time,
);
print join("\n", @out) . "\n";
2015-10-14 16:51:39 +02:00
exit exit_status($config);
2015-10-10 21:26:59 +02:00
}
2015-10-11 01:44:13 +02:00
2015-10-11 02:02:45 +02:00
if($action_list)
2015-09-24 13:51:15 +02:00
{
2015-10-11 19:01:59 +02:00
my @vol_data;
my @subvol_data;
my @target_data;
my @mixed_data;
2015-10-11 01:44:13 +02:00
my %target_uniq;
2015-09-24 13:51:15 +02:00
#
# print configuration lines, machine readable
#
2016-03-07 21:54:51 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
2015-10-12 23:58:38 +02:00
my $volh = { vinfo_prefixed_keys("volume", $sroot) };
2015-10-11 19:01:59 +02:00
push @vol_data, $volh;
2015-09-24 13:51:15 +02:00
2016-03-07 21:54:51 +01:00
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2018-02-15 00:17:01 +01:00
my $snaproot = vinfo_snapshot_root($svol);
2015-10-11 01:44:13 +02:00
my $subvolh = { %$volh,
2015-10-12 23:58:38 +02:00
vinfo_prefixed_keys("source", $svol),
2018-02-15 00:17:01 +01:00
snapshot_path => $snaproot->{PATH},
2016-03-07 21:54:51 +01:00
snapshot_name => config_key($svol, "snapshot_name"),
2016-03-08 18:22:58 +01:00
snapshot_preserve => format_preserve_matrix(config_preserve_hash($svol, "snapshot")),
2015-10-11 01:44:13 +02:00
};
2015-10-11 19:01:59 +02:00
push @subvol_data, $subvolh;
2015-09-24 13:51:15 +02:00
2015-10-11 19:01:59 +02:00
my $found = 0;
2016-03-07 21:54:51 +01:00
foreach my $droot (vinfo_subsection($svol, 'target')) {
2015-10-11 01:44:13 +02:00
my $targeth = { %$subvolh,
2015-10-12 23:58:38 +02:00
vinfo_prefixed_keys("target", $droot),
2016-03-08 18:22:58 +01:00
target_preserve => format_preserve_matrix(config_preserve_hash($droot, "target")),
2020-12-14 03:57:15 +01:00
target_type => $droot->{CONFIG}{target_type}, # "send-receive" or "raw"
2015-10-11 01:44:13 +02:00
};
2015-10-13 01:10:06 +02:00
if($action_list eq "target") {
2015-10-11 01:44:13 +02:00
next if($target_uniq{$droot->{URL}});
$target_uniq{$droot->{URL}} = 1;
2015-09-24 13:51:15 +02:00
}
2015-10-11 19:01:59 +02:00
push @target_data, $targeth;
push @mixed_data, $targeth;
$found = 1;
2015-09-24 13:51:15 +02:00
}
2015-10-11 19:01:59 +02:00
# make sure the subvol is always printed (even if no targets around)
push @mixed_data, $subvolh unless($found);
2015-09-24 13:51:15 +02:00
}
}
2015-10-12 14:59:02 +02:00
if($action_list eq "volume") {
2021-04-16 21:31:17 +02:00
print_formatted("config_volume", \@vol_data);
2015-10-12 14:59:02 +02:00
}
elsif($action_list eq "source") {
2021-04-16 21:31:17 +02:00
print_formatted("config_source", \@subvol_data);
2015-10-12 14:59:02 +02:00
}
elsif($action_list eq "target") {
2021-04-16 21:31:17 +02:00
print_formatted("config_target", \@target_data);
}
elsif($action_list eq "config") {
print_formatted("config", \@mixed_data);
2015-10-12 14:59:02 +02:00
}
else {
2021-04-16 21:31:17 +02:00
die "unknown action_list=$action_list";
2015-10-12 14:59:02 +02:00
}
2015-10-14 16:51:39 +02:00
exit exit_status($config);
2015-09-24 13:51:15 +02:00
}
2015-04-14 13:52:16 +02:00
#
2015-04-19 11:36:40 +02:00
# fill vinfo hash, basic checks on configuration
2015-04-14 13:52:16 +02:00
#
2016-03-09 19:52:45 +01:00
# read volume btrfs tree, and make sure subvolume exist
2016-03-07 20:47:24 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
2016-03-08 16:41:02 +01:00
DEBUG "Initializing volume section: $sroot->{PRINT}";
2021-07-24 20:45:18 +02:00
unless(scalar(vinfo_subsection($sroot, 'subvolume', 1))) {
2021-03-18 19:54:46 +01:00
WARN "No subvolume configured for \"volume $sroot->{URL}\"";
}
2016-03-07 20:47:24 +01:00
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2022-06-07 00:32:44 +02:00
my $snaproot = vinfo_snapshot_root($svol);
2023-04-14 16:16:12 +02:00
DEBUG "Initializing snapshot root: $snaproot->{PRINT}";
2022-06-07 00:32:44 +02:00
unless(vinfo_init_root($snaproot)) {
ABORTED($svol, "Failed to fetch subvolume detail for snapshot_dir");
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol), @stderr;
next;
}
next if($skip_snapshots || $action_archive);
2023-04-14 16:16:12 +02:00
DEBUG "Initializing subvolume section: $svol->{PRINT}";
2016-03-14 16:39:13 +01:00
unless(vinfo_init_root($svol)) {
2019-08-19 13:04:44 +02:00
ABORTED($svol, "Failed to fetch subvolume detail");
2019-12-15 18:01:16 +01:00
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol), @stderr;
2017-07-29 19:03:23 +02:00
next;
}
2018-06-29 12:49:04 +02:00
if((not $svol->{node}{uuid}) || ($svol->{node}{uuid} eq '-')) {
ABORTED($svol, "subvolume has no UUID");
2019-04-17 15:20:18 +02:00
ERROR "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
2018-06-29 12:49:04 +02:00
next;
}
2017-07-29 19:03:23 +02:00
if($svol->{node}{readonly}) {
ABORTED($svol, "subvolume is readonly");
2019-04-17 15:20:18 +02:00
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
2017-07-29 19:03:23 +02:00
next;
}
if($svol->{node}{received_uuid} ne '-') {
ABORTED($svol, "\"Received UUID\" is set");
2019-04-17 15:20:18 +02:00
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
2016-03-14 16:39:13 +01:00
next;
2016-03-07 19:20:15 +01:00
}
2018-02-15 00:17:01 +01:00
unless(_is_same_fs_tree($snaproot->{node}, $svol->{node})) {
ABORTED($svol, "Snapshot path is not on same filesystem");
2019-04-17 15:20:18 +02:00
WARN "Skipping subvolume \"$svol->{PRINT}\": " . ABORTED_TEXT($svol);
2018-02-15 00:17:01 +01:00
next;
}
2016-03-09 19:52:45 +01:00
}
}
2015-04-19 11:36:40 +02:00
2022-06-06 14:50:22 +02:00
# read target btrfs tree, create target directories
2018-05-09 12:33:10 +02:00
if($action_run && $skip_backups && $preserve_snapshots && $preserve_backups) {
# if running "btrbk snapshot --preserve", there is no need to
# initialize targets, and we don't want to fail on missing targets.
DEBUG "Skipping target tree readin (preserving all snapshots and backups)";
}
else {
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
foreach my $droot (vinfo_subsection($svol, 'target')) {
DEBUG "Initializing target section: $droot->{PRINT}";
my $target_type = $droot->{CONFIG}->{target_type} || die;
2022-06-06 14:50:22 +02:00
if($config_override{FAILSAFE_PRESERVE}) {
ABORTED($droot, $config_override{FAILSAFE_PRESERVE});
WARN "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot);
next;
}
if(config_key($droot, "target_create_dir")) {
unless(my $ret = vinfo_mkdir($droot)) {
ABORTED($droot, "Failed to create directory: $droot->{PRINT}/");
WARN "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot), @stderr;
next;
}
}
2018-05-09 12:33:10 +02:00
if($target_type eq "send-receive")
{
2019-04-24 23:46:44 +02:00
unless(vinfo_init_root($droot)) {
2019-08-19 13:04:44 +02:00
ABORTED($droot, "Failed to fetch subvolume detail");
2019-12-15 18:01:16 +01:00
WARN "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot), @stderr;
2018-05-09 12:33:10 +02:00
next;
}
2015-06-02 22:16:33 +02:00
}
2018-05-09 12:33:10 +02:00
elsif($target_type eq "raw")
{
unless(vinfo_init_raw_root($droot)) {
2019-08-19 13:04:44 +02:00
ABORTED($droot, "Failed to fetch raw target metadata");
2019-12-15 18:01:16 +01:00
WARN "Skipping target \"$droot->{PRINT}\": " . ABORTED_TEXT($droot), @stderr;
2018-05-09 12:33:10 +02:00
next;
}
2015-06-02 22:16:33 +02:00
}
2016-04-14 13:01:28 +02:00
}
2016-03-06 17:46:46 +01:00
}
}
}
# check for duplicate snapshot locations
2016-03-07 19:20:15 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2016-03-07 17:36:02 +01:00
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
2021-01-16 20:42:45 +01:00
# check for duplicate snapshot locations
if(config_key($svol, "snapshot_create")) {
my $snaproot = vinfo_snapshot_root($svol);
my $snaproot_subdir_path = (defined($snaproot->{NODE_SUBDIR}) ? $snaproot->{NODE_SUBDIR} . '/' : "") . $snapshot_basename;
if(my $prev = $snaproot->{node}->{_SNAPSHOT_CHECK}->{$snaproot_subdir_path}) {
ERROR "Subvolume \"$prev\" and \"$svol->{PRINT}\" will create same snapshot: $snaproot->{PRINT}/${snapshot_basename}.*";
ERROR "Please fix \"snapshot_name\" configuration options!";
exit 1;
}
$snaproot->{node}->{_SNAPSHOT_CHECK}->{$snaproot_subdir_path} = $svol->{PRINT};
2016-03-06 17:46:46 +01:00
}
2021-01-16 20:42:45 +01:00
# check for duplicate target locations
2016-03-07 19:20:15 +01:00
foreach my $droot (vinfo_subsection($svol, 'target')) {
2019-04-01 00:33:02 +02:00
my $droot_subdir_path = (defined($droot->{NODE_SUBDIR}) ? $droot->{NODE_SUBDIR} . '/' : "") . $snapshot_basename;
if(my $prev = $droot->{node}->{_BACKUP_CHECK}->{$droot_subdir_path}) {
ERROR "Subvolume \"$prev\" and \"$svol->{PRINT}\" will create same backup target: $droot->{PRINT}/${snapshot_basename}.*";
2015-04-20 18:19:55 +02:00
ERROR "Please fix \"snapshot_name\" or \"target\" configuration options!";
2015-04-18 20:18:11 +02:00
exit 1;
}
2019-04-01 00:33:02 +02:00
$droot->{node}->{_BACKUP_CHECK}->{$droot_subdir_path} = $svol->{PRINT};
2015-01-10 16:02:35 +01:00
}
2014-12-13 20:33:31 +01:00
}
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
#
2020-12-12 20:11:00 +01:00
my $vol = $subvol_args[0] || die;
2015-01-26 17:31:18 +01:00
my $lines = [];
2016-04-15 22:00:10 +02:00
_origin_tree("", $vol->{node}, $lines);
$output_format ||= "custom";
if($output_format eq "custom") {
print_header(title => "Origin Tree",
config => $config,
time => $start_time,
legend => [
"^-- : parent subvolume",
"newline : received-from relationship with subvolume (identical content)",
]
);
print join("\n", map { $_->{tree} } @$lines) . "\n";
2015-01-26 17:31:18 +01:00
}
2016-04-15 22:00:10 +02:00
else {
print_formatted('origin_tree', $lines );
2015-01-26 17:31:18 +01:00
}
2015-03-13 17:54:08 +01:00
exit 0;
2015-01-26 17:31:18 +01:00
}
2015-10-14 17:02:25 +02:00
if($action_resolve)
2014-12-12 12:32:04 +01:00
{
2015-10-20 16:33:23 +02:00
my @data;
2020-12-13 23:54:25 +01:00
my %stats = ( snapshots => 0, backups => 0, correlated => 0, incomplete => 0, orphaned => 0 );
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
my $snaproot = vinfo_snapshot_root($svol);
my $snapshot_name = config_key($svol, "snapshot_name") // die;
2022-06-05 19:58:50 +02:00
my @snapshots = @{vinfo_subvol_list($snaproot, readonly => 1, btrbk_direct_leaf => $snapshot_name, sort => 'path')};
2020-12-13 23:54:25 +01:00
my %svol_data = (
vinfo_prefixed_keys("source", $svol),
snapshot_name => $snapshot_name,
);
my @sdata = map +{
%svol_data,
type => "snapshot",
status => ($_->{node}{cgen} == $svol->{node}{gen}) ? "up-to-date" : "",
vinfo_prefixed_keys("snapshot", $_),
_vinfo => $_,
_btrbk_date => $_->{node}{BTRBK_DATE},
2022-06-05 19:58:50 +02:00
}, @snapshots;
2020-12-13 23:54:25 +01:00
my %svol_stats_data = (
%svol_data,
snapshot_subvolume => "$snaproot->{PATH}/$snapshot_name.*",
snapshot_status => (grep { $_->{status} eq "up-to-date" } @sdata) ? "up-to-date" : "",
snapshots => scalar(@sdata),
);
$stats{snapshots} += scalar(@sdata);
my (@bdata, @ldata, @stdata);
foreach my $droot (vinfo_subsection($svol, 'target')) {
2021-02-14 14:23:17 +01:00
my %dstats = ( backups => 0, correlated => 0, orphaned => 0, incomplete => 0, uptodate => 0 );
2020-12-13 23:54:25 +01:00
my $latest_backup;
foreach my $target_vol (@{vinfo_subvol_list($droot, btrbk_direct_leaf => $snapshot_name, sort => 'path')}) {
my $target_data = {
%svol_data,
type => "backup",
target_type => $target_vol->{CONFIG}{target_type}, # "send-receive" or "raw"
vinfo_prefixed_keys("target", $target_vol),
_btrbk_date => $target_vol->{node}{BTRBK_DATE},
};
# incomplete received (garbled) subvolumes have no received_uuid (as of btrfs-progs v4.3.1).
# a subvolume in droot matching our naming is considered incomplete if received_uuid is not set!
if($target_vol->{node}{received_uuid} eq '-') {
$dstats{incomplete}++;
$target_data->{status} = "incomplete";
push @bdata, $target_data;
next;
2014-12-13 16:51:30 +01:00
}
2020-12-13 23:54:25 +01:00
foreach (@sdata) {
if(_is_correlated($_->{_vinfo}{node}, $target_vol->{node})) {
$target_data = {
%$_,
%$target_data,
type => "snapshot,backup",
_correlated => 1,
};
$_->{_correlated} = 1;
2018-02-15 17:42:41 +01:00
last;
}
2015-10-14 17:02:25 +02:00
}
2020-12-13 23:54:25 +01:00
push @bdata, $target_data;
$latest_backup = $target_data if(!defined($latest_backup) || (cmp_date($latest_backup->{_btrbk_date}, $target_data->{_btrbk_date}) < 0));
$dstats{uptodate} ||= ($target_data->{status} // "") eq "up-to-date";
$dstats{backups}++;
if($target_data->{_correlated}) { $dstats{correlated}++; } else { $dstats{orphaned}++; }
2015-10-14 17:02:25 +02:00
}
2020-12-13 23:54:25 +01:00
push @ldata, $latest_backup;
push @stdata, {
%svol_stats_data,
%dstats,
vinfo_prefixed_keys("target", $droot),
target_subvolume => "$droot->{PATH}/$snapshot_name.*",
backup_status => $dstats{uptodate} ? "up-to-date" : "",
};
$stats{$_} += $dstats{$_} foreach(qw(backups correlated incomplete orphaned));
}
if($action_resolve eq "snapshots") {
push @data, @sdata;
} elsif($action_resolve eq "backups") {
push @data, @bdata;
} elsif($action_resolve eq "all") {
push @data, sort { cmp_date($a->{_btrbk_date}, $b->{_btrbk_date}) } (@bdata, grep { !$_->{_correlated} } @sdata);
} elsif($action_resolve eq "latest") {
my $latest_snapshot = (sort { cmp_date($b->{_btrbk_date}, $a->{_btrbk_date}) } (@sdata, @bdata))[0];
push @data, @ldata;
2020-12-20 18:20:43 +01:00
push @data, $latest_snapshot if($latest_snapshot && !$latest_snapshot->{_correlated});
2020-12-13 23:54:25 +01:00
} elsif($action_resolve eq "stats") {
@stdata = ( \%svol_stats_data ) unless(@stdata);
push @data, @stdata;
2015-10-14 17:02:25 +02:00
}
2015-03-24 13:13:00 +01:00
}
2015-10-11 15:38:43 +02:00
}
2015-10-20 16:33:23 +02:00
2016-01-15 02:06:03 +01:00
if($action_resolve eq "stats") {
2020-12-13 23:54:25 +01:00
my $filter = $config->{CMDLINE_FILTER_LIST} ? " (" . join(", ", @{$config->{CMDLINE_FILTER_LIST}}) . ")" : "";
my @backup_total = map { $stats{$_} ? "$stats{$_} $_" : () } qw( correlated incomplete );
2021-02-14 15:30:46 +01:00
my $bflags = @backup_total ? "(" . join(', ', @backup_total) . ")" : undef;
2020-12-13 23:54:25 +01:00
print_formatted("stats", \@data, paragraph => 1);
print "Total${filter}:\n";
2021-02-14 15:30:46 +01:00
print_formatted({ table => [ qw( a b -c ) ], RALIGN => { a=>1 } },
[ { a => $stats{snapshots}, b => "snapshots" },
2020-12-13 23:54:25 +01:00
{ a => $stats{backups}, b => "backups", c => $bflags } ],
2021-02-14 15:30:46 +01:00
output_format => "table", no_header => 1, empty_cell_char => "");
2020-12-20 18:32:41 +01:00
}
elsif($action_resolve eq "snapshots") {
print_formatted("snapshots", \@data);
}
2021-04-16 22:10:50 +02:00
elsif($action_resolve eq "backups") {
print_formatted("backups", \@data);
}
2021-04-16 22:08:39 +02:00
elsif($action_resolve eq "latest") {
print_formatted("latest", \@data);
}
2016-01-15 02:06:03 +01:00
else {
print_formatted("resolved", \@data);
}
2015-10-20 16:33:23 +02:00
2015-10-14 16:51:39 +02:00
exit exit_status($config);
2014-12-13 13:52:43 +01:00
}
2015-01-03 21:25:46 +01:00
2016-01-14 15:52:33 +01:00
if($action_clean)
{
#
# identify and delete incomplete backups
#
btrbk: add transaction logging to syslog
Add configuration option transaction_syslog, which can be set to a short
name of a syslog facility, like user or local5. Most of the ones besides
localX do not really make sense, but whatever, let the user decide.
The only logging that is relevant for logging to syslog is the logging
generated inside sub action, so it's easy to hijack all messages in
there and also send them to syslog if needed.
All output is done via print_formatted, which expects a file handle.
So, abuse a file handle to a string to be able to change as less code as
needed for this feature.
Since syslog already adds the timestamps for us, I added a syslog
formatting pattern, which is very similar to tlog, omitting the
timestap.
2016-04-22 23:11:00 +02:00
init_transaction_log(config_key($config, "transaction_log"),
config_key($config, "transaction_syslog"));
2016-04-06 20:19:12 +02:00
2016-01-14 15:52:33 +01:00
my @out;
2016-03-07 21:54:51 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
my $snapshot_name = config_key($svol, "snapshot_name") // die;
foreach my $droot (vinfo_subsection($svol, 'target')) {
2016-01-14 15:52:33 +01:00
INFO "Cleaning incomplete backups in: $droot->{PRINT}/$snapshot_name.*";
push @out, "$droot->{PRINT}/$snapshot_name.*";
2022-07-27 20:35:58 +02:00
# incomplete received (garbled) subvolumes are not readonly and have no received_uuid (as of btrfs-progs v4.3.1).
# a subvolume in droot matching our naming is considered incomplete if received_uuid is not set!
my @delete = grep $_->{node}{received_uuid} eq '-', @{vinfo_subvol_list($droot, btrbk_direct_leaf => $snapshot_name, sort => 'path')};
my @delete_success;
foreach my $target_vol (@delete) {
DEBUG "Found incomplete target subvolume: $target_vol->{PRINT}";
if(btrfs_subvolume_delete($target_vol, commit => config_key($droot, "btrfs_commit_delete"), type => "delete_garbled")) {
push(@delete_success, $target_vol);
2016-01-14 15:52:33 +01:00
}
}
2017-09-27 20:23:08 +02:00
INFO "Deleted " . scalar(@delete_success) . " incomplete backups in: $droot->{PRINT}/$snapshot_name.*";
$droot->{SUBVOL_DELETED} //= [];
push @{$droot->{SUBVOL_DELETED}}, @delete_success;
push @out, map("--- $_->{PRINT}", @delete_success);
if(scalar(@delete_success) != scalar(@delete)) {
2016-03-07 21:54:51 +01:00
ABORTED($droot, "Failed to delete incomplete target subvolume");
2019-04-17 15:20:18 +02:00
push @out, "!!! Target \"$droot->{PRINT}\" aborted: " . ABORTED_TEXT($droot);
2016-01-14 15:52:33 +01:00
}
push(@out, "<no_action>") unless(scalar(@delete));
push(@out, "");
}
}
}
my $exit_status = exit_status($config);
my $time_elapsed = time - $start_time;
INFO "Completed within: ${time_elapsed}s (" . localtime(time) . ")";
action("finished",
status => $exit_status ? "partial" : "success",
duration => $time_elapsed,
message => $exit_status ? "At least one delete operation failed" : undef,
);
close_transaction_log();
#
# print summary
#
unless($quiet)
{
$output_format ||= "custom";
if($output_format eq "custom")
{
2016-03-07 21:54:51 +01:00
print_header(title => "Cleanup Summary",
2016-01-14 15:52:33 +01:00
config => $config,
2016-03-07 21:54:51 +01:00
time => $start_time,
2016-01-14 15:52:33 +01:00
legend => [
"--- deleted subvolume (incomplete backup)",
],
);
print join("\n", @out);
2019-07-15 18:19:33 +02:00
print_footer($config, $exit_status);
2016-01-14 15:52:33 +01:00
}
else
{
# print action log (without transaction start messages)
2017-09-27 19:35:43 +02:00
my @data = grep { $_->{status} !~ /starting$/ } @transaction_log;
2016-01-14 15:52:33 +01:00
print_formatted("transaction", \@data, title => "TRANSACTION LOG");
}
}
exit $exit_status;
}
2022-06-06 15:04:42 +02:00
if($action_run || $action_archive)
2014-12-13 13:52:43 +01:00
{
btrbk: add transaction logging to syslog
Add configuration option transaction_syslog, which can be set to a short
name of a syslog facility, like user or local5. Most of the ones besides
localX do not really make sense, but whatever, let the user decide.
The only logging that is relevant for logging to syslog is the logging
generated inside sub action, so it's easy to hijack all messages in
there and also send them to syslog if needed.
All output is done via print_formatted, which expects a file handle.
So, abuse a file handle to a string to be able to change as less code as
needed for this feature.
Since syslog already adds the timestamps for us, I added a syslog
formatting pattern, which is very similar to tlog, omitting the
timestap.
2016-04-22 23:11:00 +02:00
init_transaction_log(config_key($config, "transaction_log"),
config_key($config, "transaction_syslog"));
2016-04-06 20:19:12 +02:00
2022-06-06 15:04:42 +02:00
if($skip_snapshots || $action_archive) {
INFO "Skipping snapshot creation (btrbk resume)" unless($action_archive);
2015-05-15 20:24:14 +02:00
}
else
2014-12-13 13:52:43 +01:00
{
2015-05-15 20:24:14 +02:00
#
# create snapshots
#
2016-03-07 20:47:24 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2018-02-15 00:17:01 +01:00
my $snaproot = vinfo_snapshot_root($svol);
2016-03-07 20:47:24 +01:00
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
2019-04-12 20:52:46 +02:00
DEBUG "Evaluating snapshot creation for: $svol->{PRINT}";
2014-12-14 20:35:15 +01:00
2015-05-15 20:24:14 +02:00
# check if we need to create a snapshot
2016-03-07 20:47:24 +01:00
my $snapshot_create = config_key($svol, "snapshot_create");
2015-05-25 14:38:32 +02:00
if(not $snapshot_create) {
DEBUG "Snapshot creation disabled (snapshot_create=no)";
2015-05-15 13:36:18 +02:00
next;
}
2015-05-25 14:38:32 +02:00
elsif($snapshot_create eq "always") {
DEBUG "Snapshot creation enabled (snapshot_create=always)";
}
elsif($snapshot_create eq "onchange") {
2018-02-15 00:17:01 +01:00
# check if latest (btrbk only!) snapshot is up-to-date with source subvolume (by generation)
2019-04-11 15:56:37 +02:00
my $latest = get_latest_related_snapshot($snaproot, $svol, $snapshot_basename);
2015-05-26 18:09:36 +02:00
if($latest) {
2016-03-14 16:39:13 +01:00
if($latest->{node}{cgen} == $svol->{node}{gen}) {
2015-05-26 18:09:36 +02:00
INFO "Snapshot creation skipped: snapshot_create=onchange, snapshot is up-to-date: $latest->{PRINT}";
2016-03-07 21:45:12 +01:00
$svol->{SNAPSHOT_UP_TO_DATE} = $latest;
2015-05-26 18:09:36 +02:00
next;
}
2016-03-14 16:39:13 +01:00
DEBUG "Snapshot creation enabled: snapshot_create=onchange, gen=$svol->{node}{gen} > snapshot_cgen=$latest->{node}{cgen}";
2015-05-26 18:09:36 +02:00
}
else {
DEBUG "Snapshot creation enabled: snapshot_create=onchange, no snapshots found";
2015-05-25 14:38:32 +02:00
}
2015-05-20 20:20:14 +02:00
}
elsif($snapshot_create eq "ondemand") {
2015-05-25 14:45:56 +02:00
# check if at least one target is present
2016-03-07 20:47:24 +01:00
if(scalar vinfo_subsection($svol, 'target')) {
2015-06-02 22:16:33 +02:00
DEBUG "Snapshot creation enabled (snapshot_create=ondemand): at least one target is present";
2015-05-20 20:20:14 +02:00
}
else {
2015-06-02 22:16:33 +02:00
INFO "Snapshot creation skipped: snapshot_create=ondemand, and no target is present for: $svol->{PRINT}";
2015-05-20 20:20:14 +02:00
next;
}
}
else {
die "illegal value for snapshot_create configuration option: $snapshot_create";
}
2015-05-15 13:36:18 +02:00
2015-05-15 20:24:14 +02:00
# find unique snapshot name
2016-04-21 13:27:54 +02:00
my $timestamp = timestamp(\@tm_now, config_key($svol, "timestamp_format"));
2015-05-15 20:24:14 +02:00
my @unconfirmed_target_name;
2018-02-15 00:17:01 +01:00
my @lookup = map { $_->{SUBVOL_PATH} } @{vinfo_subvol_list($snaproot)};
2016-03-07 20:47:24 +01:00
foreach my $droot (vinfo_subsection($svol, 'target', 1)) {
2019-04-17 15:20:18 +02:00
if(IS_ABORTED($droot)) {
2016-03-07 20:47:24 +01:00
push(@unconfirmed_target_name, $droot);
2015-05-15 20:24:14 +02:00
next;
}
2016-03-10 05:26:43 +01:00
push(@lookup, map { $_->{SUBVOL_PATH} } @{vinfo_subvol_list($droot)});
2015-05-15 20:24:14 +02:00
}
@lookup = grep /^\Q$snapshot_basename.$timestamp\E(_[0-9]+)?$/ ,@lookup;
2020-08-28 17:15:39 +02:00
TRACE "Present snapshot names for \"$svol->{PRINT}\": " . join(', ', @lookup) if($do_trace);
2015-05-15 20:24:14 +02:00
@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" : "");
if(@unconfirmed_target_name) {
2015-09-20 14:25:20 +02:00
INFO "Assuming non-present subvolume \"$snapshot_name\" in skipped targets: " . join(", ", map { "\"$_->{PRINT}\"" } @unconfirmed_target_name);
2015-05-15 20:24:14 +02:00
}
# finally create the snapshot
INFO "Creating subvolume snapshot for: $svol->{PRINT}";
2018-02-15 00:17:01 +01:00
my $snapshot = vinfo_child($snaproot, "$snapshot_name");
2016-04-12 17:50:12 +02:00
if(btrfs_subvolume_snapshot($svol, $snapshot))
{
2018-02-15 00:17:01 +01:00
vinfo_inject_child($snaproot, $snapshot, {
2016-04-12 17:50:12 +02:00
parent_uuid => $svol->{node}{uuid},
received_uuid => '-',
readonly => 1,
FORCE_PRESERVE => 'preserve forced: created just now',
2022-06-19 13:11:36 +02:00
INJECTED_BY => 'snapshot',
2016-04-12 17:50:12 +02:00
});
2016-03-07 23:53:47 +01:00
$svol->{SNAPSHOT_CREATED} = $snapshot;
2015-05-15 20:24:14 +02:00
}
else {
2018-02-15 00:17:01 +01:00
ABORTED($svol, "Failed to create snapshot: $svol->{PRINT} -> $snapshot->{PRINT}");
2019-04-17 15:20:18 +02:00
WARN "Skipping subvolume section: " . ABORTED_TEXT($svol);
2015-05-15 20:24:14 +02:00
}
2014-12-19 13:31:31 +01:00
}
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
#
2022-06-18 21:22:50 +02:00
my $schedule_results;
2017-08-21 13:23:20 +02:00
if($skip_backups) {
INFO "Skipping backup creation (btrbk snapshot)";
}
else {
2022-06-18 21:22:50 +02:00
$schedule_results = [];
2017-08-21 13:23:20 +02:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2018-02-15 00:17:01 +01:00
my $snaproot = vinfo_snapshot_root($svol);
2017-08-21 13:23:20 +02:00
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
2022-06-05 19:58:50 +02:00
my @snapshots = sort({ cmp_date($a->{node}{BTRBK_DATE}, $b->{node}{BTRBK_DATE}) }
@{vinfo_subvol_list($snaproot, readonly => 1, btrbk_direct_leaf => $snapshot_basename)});
2017-08-21 13:23:20 +02:00
foreach my $droot (vinfo_subsection($svol, 'target')) {
2023-04-14 16:16:12 +02:00
INFO "Checking for missing backups of \"$snaproot->{PRINT}/${snapshot_basename}.*\" in: $droot->{PRINT}/";
2017-08-21 13:23:20 +02:00
my @schedule;
my $resume_total = 0;
my $resume_success = 0;
2015-05-15 16:06:36 +02:00
2022-06-05 17:36:01 +02:00
my $unexpected_only = [];
2022-06-05 19:58:50 +02:00
foreach my $snapshot (@snapshots)
2017-08-21 13:23:20 +02:00
{
2022-06-05 17:36:01 +02:00
if(get_receive_targets($droot, $snapshot, exact => 1, warn => 1, ret_unexpected_only => $unexpected_only)) {
2019-04-11 15:56:37 +02:00
DEBUG "Found correlated target of: $snapshot->{PRINT}";
2016-04-12 19:43:12 +02:00
next;
}
2022-06-05 20:51:32 +02:00
if(my $ff = vinfo_match(\@exclude_vf, $snapshot)) {
INFO "Skipping backup candidate \"$snapshot->{PRINT}\": Match on exclude pattern \"$ff->{unparsed}\"";
next;
}
2017-08-21 13:23:20 +02:00
2019-04-11 15:56:37 +02:00
DEBUG "Adding backup candidate: $snapshot->{PRINT}";
push(@schedule, { value => $snapshot,
btrbk_date => $snapshot->{node}{BTRBK_DATE},
2017-08-21 13:23:20 +02:00
# not enforcing resuming of latest snapshot anymore (since v0.23.0)
2019-04-11 15:56:37 +02:00
# preserve => $snapshot->{node}{FORCE_PRESERVE},
2016-04-07 14:34:51 +02:00
});
2015-06-02 22:16:33 +02:00
}
2017-08-21 13:23:20 +02:00
2022-06-05 17:36:01 +02:00
if(scalar @$unexpected_only && ((config_key($droot, "incremental") // "") eq "strict")) {
# If target exists at unexpected location ONLY, we can't send/receive it.
ABORTED($droot, "Receive targets of backup candidates exist at unexpected location only");
WARN "Skipping backup of \"$sroot->{PRINT}/${snapshot_basename}.*\": " . ABORTED_TEXT($droot),
"Please check your target configuration, or fix manually by running" . ($droot->{URL_PREFIX} ? " (on $droot->{URL_PREFIX}):" : ":"),
"`btrfs subvolume snapshot -r <found> <target>`",
map { "target: $droot->{PATH}/$_->{src_vol}{NAME}, found: " . _fs_path($_->{target_node}) } @$unexpected_only;
next;
}
2017-08-21 13:23:20 +02:00
if(scalar @schedule)
2015-06-02 22:16:33 +02:00
{
2017-08-21 13:23:20 +02:00
DEBUG "Checking schedule for backup candidates";
2022-06-18 17:16:33 +02:00
my $last_dvol_date; # oldest present archive (by btrbk_date)
2022-06-06 11:49:54 +02:00
# Add all present backups as informative_only: these are needed for correct results of schedule().
# Note that we don't filter readonly here, in order to also get garbled targets.
2022-06-18 17:16:33 +02:00
foreach my $dvol (@{vinfo_subvol_list($droot, btrbk_direct_leaf => $snapshot_basename)}) {
my $btrbk_date = $dvol->{node}{BTRBK_DATE};
2017-08-21 13:23:20 +02:00
push(@schedule, { informative_only => 1,
2022-06-18 17:16:33 +02:00
value => $dvol,
btrbk_date => $dvol->{node}{BTRBK_DATE},
2017-08-21 13:23:20 +02:00
});
2022-06-18 17:16:33 +02:00
$last_dvol_date = $btrbk_date if(!defined($last_dvol_date) || cmp_date($btrbk_date, $last_dvol_date) > 0);
2016-04-16 19:25:46 +02:00
}
2022-06-18 21:22:50 +02:00
my $schedule_results_mixed = [];
2017-08-21 13:23:20 +02:00
my ($preserve, undef) = schedule(
schedule => \@schedule,
2022-06-18 17:35:03 +02:00
preserve => config_preserve_hash($droot, $action_archive ? "archive" : "target"),
2022-06-18 17:16:33 +02:00
preserve_threshold_date => ($action_archive && config_key($droot, "archive_exclude_older") ? $last_dvol_date : undef),
2022-06-18 21:22:50 +02:00
results => $schedule_results_mixed,
result_hints => { topic => "backup", root_path => $snaproot->{PATH} },
2022-06-19 13:11:36 +02:00
result_delete_action_text => 'REMOVE_FROM_OUTPUT',
# not required, we patch this in late, before displaying
#result_preserve_action_text => 'create',
2017-08-21 13:23:20 +02:00
);
my @resume = grep defined, @$preserve; # remove entries with no value from list (target subvolumes)
$resume_total = scalar @resume;
2019-04-11 15:56:37 +02:00
foreach my $snapshot (sort { $a->{node}{cgen} <=> $b->{node}{cgen} } @resume)
2015-04-02 15:53:53 +02:00
{
2017-08-21 13:23:20 +02:00
# Continue gracefully (skip instead of abort) on existing (possibly garbled) target
2019-07-15 18:19:33 +02:00
if(my $err_vol = vinfo_subvol($droot, $snapshot->{NAME})) {
my $err_msg = "Please delete stray subvolumes: \"btrbk clean $droot->{PRINT}\"";
FIX_MANUALLY($droot, $err_msg);
2019-04-11 15:56:37 +02:00
WARN "Target subvolume \"$err_vol->{PRINT}\" exists, but is not a receive target of \"$snapshot->{PRINT}\"";
2019-07-15 18:19:33 +02:00
WARN $err_msg;
2019-04-11 15:56:37 +02:00
WARN "Skipping backup of: $snapshot->{PRINT}";
2017-08-21 13:23:20 +02:00
$droot->{SUBVOL_RECEIVED} //= [];
2019-07-15 18:19:33 +02:00
push(@{$droot->{SUBVOL_RECEIVED}}, { ERROR => 1, received_subvolume => $err_vol });
2017-08-21 13:23:20 +02:00
next;
}
2022-06-19 00:42:30 +02:00
# Note: strict_related does not make much sense on archive:
# on targets, parent_uuid chain is broken after first prune.
2021-08-19 18:01:53 +02:00
my ($clone_src, $target_parent_node);
2019-04-11 15:56:37 +02:00
my $parent = get_best_parent($snapshot, $snaproot, $droot,
2022-06-19 00:42:30 +02:00
strict_related => ((config_key($droot, "incremental") // "") eq "strict") && !$action_archive,
clone_src => \$clone_src,
target_parent_node => \$target_parent_node,
);
2021-08-19 18:01:53 +02:00
if(macro_send_receive(source => $snapshot,
target => $droot,
parent => $parent, # this is <undef> if no suitable parent found
clone_src => $clone_src,
2018-02-15 17:42:41 +01:00
target_parent_node => $target_parent_node,
2017-08-21 13:23:20 +02:00
))
{
$resume_success++;
}
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
}
2022-06-18 21:22:50 +02:00
# replace results with target value
foreach (@$schedule_results_mixed) {
my $replace = $_->{value}{SUBVOL_SENT}{$droot->{URL}} // next;
$_->{value} = $replace;
}
push @$schedule_results, @$schedule_results_mixed;
2015-06-02 22:16:33 +02:00
}
2015-04-02 16:24:13 +02:00
2017-08-21 13:23:20 +02:00
if($resume_total) {
INFO "Created $resume_success/$resume_total missing backups";
} else {
INFO "No missing backups found";
}
2016-04-12 19:43:12 +02: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
#
2017-08-21 13:23:20 +02:00
if($preserve_snapshots && $preserve_backups) {
INFO "Preserving all snapshots and backups";
2015-02-28 12:02:28 +01:00
}
else
2015-01-04 19:30:41 +01:00
{
2017-08-21 13:23:20 +02:00
$schedule_results = [];
2016-03-07 20:47:24 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
2018-02-15 00:17:01 +01:00
my $snaproot = vinfo_snapshot_root($svol);
2016-03-07 20:47:24 +01:00
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
2015-02-28 12:02:28 +01:00
my $target_aborted = 0;
2022-06-05 19:58:50 +02:00
my @snapshots = sort({ cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } # sort descending
@{vinfo_subvol_list($snaproot, readonly => 1, btrbk_direct_leaf => $snapshot_basename)});
2015-05-20 20:20:14 +02:00
2016-03-07 20:47:24 +01:00
foreach my $droot (vinfo_subsection($svol, 'target', 1)) {
2019-04-17 15:20:18 +02:00
if(IS_ABORTED($droot)) {
if(IS_ABORTED($droot, "skip_cmdline_")) {
2015-09-20 14:25:20 +02:00
$target_aborted ||= -1;
} else {
$target_aborted = 1;
}
2015-02-28 12:02:28 +01:00
next;
}
2016-04-13 14:47:38 +02:00
2018-10-02 17:22:57 +02:00
# preserve latest common snapshot/backup (for incremental targets)
if(config_key($droot, "incremental")) {
2022-06-05 19:58:50 +02:00
foreach my $snapshot (@snapshots) {
2019-04-11 15:56:37 +02:00
my @receive_targets = get_receive_targets($droot, $snapshot, exact => 1);
2018-10-02 17:22:57 +02:00
if(scalar(@receive_targets)) {
2019-04-11 15:56:37 +02:00
DEBUG "Force preserve for latest common snapshot: $snapshot->{PRINT}";
$snapshot->{node}{FORCE_PRESERVE} = 'preserve forced: latest common snapshot';
2018-10-02 17:22:57 +02:00
foreach(@receive_targets) {
DEBUG "Force preserve for latest common target: $_->{PRINT}";
$_->{node}{FORCE_PRESERVE} = 'preserve forced: latest common target';
}
last;
2016-04-13 14:47:38 +02:00
}
2015-09-29 14:07:58 +02:00
}
2015-06-02 22:16:33 +02:00
}
2015-02-28 12:02:28 +01:00
2017-08-21 13:23:20 +02:00
if($preserve_backups) {
INFO "Preserving all backups";
}
else {
#
# delete backups
#
2023-04-14 16:16:12 +02:00
INFO "Cleaning backups of \"$snaproot->{PRINT}/${snapshot_basename}.*\" in: $droot->{PRINT}/";
2021-07-21 19:38:25 +02:00
unless(macro_delete($droot, $snapshot_basename, $droot,
2022-06-18 17:35:03 +02:00
{ preserve => config_preserve_hash($droot, $action_archive ? "archive" : "target"),
2017-08-21 13:23:20 +02:00
results => $schedule_results,
result_hints => { topic => "backup", root_path => $droot->{PATH} },
},
commit => config_key($droot, "btrfs_commit_delete"),
type => "delete_target",
2017-10-02 14:00:09 +02:00
qgroup => { destroy => config_key($droot, "target_qgroup_destroy"),
type => "qgroup_destroy_target" },
2017-08-21 13:23:20 +02:00
))
{
$target_aborted = 1;
}
2015-02-28 12:02:28 +01:00
}
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
#
2022-06-06 15:04:42 +02:00
if($preserve_snapshots || $action_archive) {
INFO "Preserving all snapshots" unless($action_archive);
2017-08-21 13:23:20 +02:00
}
elsif($target_aborted) {
2015-09-20 14:25:20 +02:00
if($target_aborted == -1) {
INFO "Skipping cleanup of snapshots for subvolume \"$svol->{PRINT}\", as at least one target is skipped by command line argument";
} else {
WARN "Skipping cleanup of snapshots for subvolume \"$svol->{PRINT}\", as at least one target aborted earlier";
}
2015-02-28 12:02:28 +01:00
}
2017-08-21 13:23:20 +02:00
else {
2023-04-14 16:16:12 +02:00
INFO "Cleaning snapshots" . ($wipe_snapshots ? " (wipe)" : "") . ": $snaproot->{PRINT}/${snapshot_basename}.*";
2021-07-21 19:38:25 +02:00
macro_delete($snaproot, $snapshot_basename, $svol,
2017-09-28 14:02:06 +02:00
{ preserve => config_preserve_hash($svol, "snapshot", wipe => $wipe_snapshots),
2017-08-21 13:23:20 +02:00
results => $schedule_results,
2018-02-15 00:17:01 +01:00
result_hints => { topic => "snapshot", root_path => $snaproot->{PATH} },
2017-08-21 13:23:20 +02:00
},
commit => config_key($svol, "btrfs_commit_delete"),
type => "delete_snapshot",
2017-10-02 14:00:09 +02:00
qgroup => { destroy => config_key($svol, "snapshot_qgroup_destroy"),
type => "qgroup_destroy_snapshot" },
2017-08-21 13:23:20 +02:00
);
}
2015-01-04 19:30:41 +01:00
}
}
}
2015-01-13 14:38:44 +01:00
2015-10-14 16:51:39 +02:00
my $exit_status = exit_status($config);
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-10-13 18:24:30 +02:00
action("finished",
2015-10-14 16:51:39 +02:00
status => $exit_status ? "partial" : "success",
2015-10-13 18:24:30 +02:00
duration => $time_elapsed,
2015-10-14 16:51:39 +02:00
message => $exit_status ? "At least one backup task aborted" : undef,
2015-10-13 18:24:30 +02:00
);
close_transaction_log();
2015-01-13 17:51:24 +01:00
2015-10-12 20:46:05 +02:00
2015-01-13 17:51:24 +01:00
unless($quiet)
{
2015-10-12 20:46:05 +02:00
#
# print scheduling results
#
2017-08-21 13:23:20 +02:00
if($print_schedule && $schedule_results) {
2022-06-18 21:22:50 +02:00
foreach my $topic (qw(snapshot backup)) {
my @data = map +{ %$_, vinfo_prefixed_keys("", $_->{value}) },
2022-06-19 13:11:36 +02:00
grep { $_->{action} = "create" if($_->{value}{node}{INJECTED_BY});
!($_->{action} && $_->{action} eq "REMOVE_FROM_OUTPUT") }
2022-06-18 21:22:50 +02:00
grep { $_->{topic} eq $topic } @$schedule_results;
next unless(@data);
print_formatted("schedule", \@data, title => (uc($topic) . " SCHEDULE"), paragraph => 1);
2015-10-21 21:58:30 +02:00
}
2015-10-12 20:46:05 +02:00
}
#
# print summary
#
2015-10-12 22:26:36 +02:00
$output_format ||= "custom";
if($output_format eq "custom")
2015-01-13 17:51:24 +01:00
{
2015-10-12 22:26:36 +02:00
my @out;
2016-03-07 20:47:24 +01:00
foreach my $sroot (vinfo_subsection($config, 'volume', 1)) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume', 1)) {
2015-10-12 22:26:36 +02:00
my @subvol_out;
2016-03-07 21:45:12 +01:00
if($svol->{SNAPSHOT_UP_TO_DATE}) {
push @subvol_out, "=== $svol->{SNAPSHOT_UP_TO_DATE}->{PRINT}";
2015-10-11 15:38:43 +02:00
}
2016-03-07 21:45:12 +01:00
if($svol->{SNAPSHOT_CREATED}) {
push @subvol_out, "+++ $svol->{SNAPSHOT_CREATED}->{PRINT}";
2015-03-31 19:07:33 +02:00
}
2017-09-27 20:41:51 +02:00
foreach(@{$svol->{SUBVOL_DELETED} // []}) {
2016-03-07 21:45:12 +01:00
push @subvol_out, "--- $_->{PRINT}";
2015-01-13 17:51:24 +01:00
}
2016-03-07 20:47:24 +01:00
foreach my $droot (vinfo_subsection($svol, 'target', 1)) {
2022-06-06 14:50:22 +02:00
if($droot->{SUBDIR_CREATED}) {
push @subvol_out, "++. $droot->{PRINT}/";
}
2016-03-07 21:45:12 +01:00
foreach(@{$droot->{SUBVOL_RECEIVED} // []}) {
2015-10-12 22:26:36 +02:00
my $create_mode = "***";
$create_mode = ">>>" if($_->{parent});
# substr($create_mode, 0, 1, '%') if($_->{resume});
$create_mode = "!!!" if($_->{ERROR});
push @subvol_out, "$create_mode $_->{received_subvolume}->{PRINT}";
}
2015-03-31 19:07:33 +02:00
2017-09-27 20:41:51 +02:00
foreach(@{$droot->{SUBVOL_DELETED} // []}) {
2016-03-07 21:45:12 +01:00
push @subvol_out, "--- $_->{PRINT}";
2015-10-12 22:26:36 +02:00
}
2019-04-17 15:20:18 +02:00
if(IS_ABORTED($droot, "abort_")) {
push @subvol_out, "!!! Target \"$droot->{PRINT}\" aborted: " . ABORTED_TEXT($droot);
2015-10-12 22:26:36 +02:00
}
}
2019-04-17 15:20:18 +02:00
2019-09-07 13:44:25 +02:00
if(IS_ABORTED($sroot, "abort_")) {
# repeat volume errors in subvolume context
push @subvol_out, "!!! Volume \"$sroot->{PRINT}\" aborted: " . ABORTED_TEXT($sroot);
}
if(IS_ABORTED($svol, "abort_")) {
# don't print "<no_action>" on skip_cmdline or skip_noauto
push @subvol_out, "!!! Aborted: " . ABORTED_TEXT($svol);
}
2019-04-17 15:20:18 +02:00
2019-09-07 13:44:25 +02:00
# print "<no_action>" for subvolume, unless aborted by "skip_"
unless(scalar(@subvol_out) || IS_ABORTED($sroot, "skip_") || IS_ABORTED($svol, "skip_")) {
@subvol_out = "<no_action>";
2015-03-31 19:07:33 +02:00
}
2015-05-15 16:06:36 +02:00
2015-10-12 22:26:36 +02:00
if(@subvol_out) {
2022-06-07 00:31:58 +02:00
if($action_archive) {
my $snaproot = vinfo_snapshot_root($svol);
my $snapshot_basename = config_key($svol, "snapshot_name");
push @out, "$snaproot->{PRINT}/${snapshot_basename}.*";
} else {
push @out, "$svol->{PRINT}";
}
push @out, @subvol_out, "";
2015-10-12 22:26:36 +02:00
}
2015-01-13 17:51:24 +01:00
}
}
2015-04-20 18:53:44 +02:00
2019-04-17 16:10:15 +02:00
my @cmdline_options = map { "exclude: $_" } @exclude_cmdline;
2018-05-10 18:58:17 +02:00
push @cmdline_options, "$skip_snapshots: No snapshots created" if($skip_snapshots);
push @cmdline_options, "$skip_backups: No backups created" if($skip_backups);
push @cmdline_options, "$preserve_snapshots: Preserved all snapshots" if($preserve_snapshots);
push @cmdline_options, "$preserve_backups: Preserved all backups" if($preserve_backups);
2017-08-21 13:23:20 +02:00
2022-06-07 00:31:58 +02:00
print_header(title => $action_archive ? "Archive Summary" : "Backup Summary",
2015-09-23 11:27:36 +02:00
config => $config,
time => $start_time,
2017-08-21 13:23:20 +02:00
options => \@cmdline_options,
2015-09-23 11:27:36 +02:00
legend => [
2022-06-07 00:31:58 +02:00
$action_archive && "++. created directory",
$action_run && "=== up-to-date subvolume (source snapshot)",
$action_run && "+++ created subvolume (source snapshot)",
2015-09-23 11:27:36 +02:00
"--- deleted subvolume",
"*** received subvolume (non-incremental)",
">>> received subvolume (incremental)",
],
);
print join("\n", @out);
2019-07-15 18:19:33 +02:00
print_footer($config, $exit_status);
2015-01-16 17:41:57 +01:00
}
2015-10-11 15:38:43 +02:00
else
{
2015-10-20 18:23:54 +02:00
# print action log (without transaction start messages)
2017-09-27 19:35:43 +02:00
my @data = grep { $_->{status} !~ /starting$/ } @transaction_log;
2015-10-20 18:23:54 +02:00
print_formatted("transaction", \@data, title => "TRANSACTION LOG");
2015-10-11 15:38:43 +02:00
}
2015-01-13 17:51:24 +01:00
}
2015-09-30 14:00:39 +02:00
2015-10-14 16:51:39 +02:00
exit $exit_status if($exit_status);
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;