line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# Author: Nicholas Hubbard |
2
|
|
|
|
|
|
|
# WWW: https://github.com/NicholasBHubbard/yabsm |
3
|
|
|
|
|
|
|
# License: MIT |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
# Functions needed for both SSH and local backups. |
6
|
|
|
|
|
|
|
|
7
|
4
|
|
|
4
|
|
612
|
use strict; |
|
4
|
|
|
|
|
13
|
|
|
4
|
|
|
|
|
102
|
|
8
|
4
|
|
|
4
|
|
18
|
use warnings; |
|
4
|
|
|
|
|
7
|
|
|
4
|
|
|
|
|
86
|
|
9
|
4
|
|
|
4
|
|
34
|
use v5.16.3; |
|
4
|
|
|
|
|
11
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
package App::Yabsm::Backup::Generic; |
12
|
|
|
|
|
|
|
|
13
|
4
|
|
|
4
|
|
745
|
use App::Yabsm::Tools qw( :ALL ); |
|
4
|
|
|
|
|
9
|
|
|
4
|
|
|
|
|
667
|
|
14
|
4
|
|
|
4
|
|
1120
|
use App::Yabsm::Config::Query qw( :ALL ); |
|
4
|
|
|
|
|
22
|
|
|
4
|
|
|
|
|
1589
|
|
15
|
|
|
|
|
|
|
|
16
|
4
|
|
|
|
|
253
|
use App::Yabsm::Snapshot qw(take_snapshot |
17
|
|
|
|
|
|
|
delete_snapshot |
18
|
|
|
|
|
|
|
current_time_snapshot_name |
19
|
|
|
|
|
|
|
is_snapshot_name |
20
|
4
|
|
|
4
|
|
986
|
); |
|
4
|
|
|
|
|
10
|
|
21
|
|
|
|
|
|
|
|
22
|
4
|
|
|
4
|
|
24
|
use Carp q(confess); |
|
4
|
|
|
|
|
6
|
|
|
4
|
|
|
|
|
161
|
|
23
|
4
|
|
|
4
|
|
2639
|
use File::Temp; |
|
4
|
|
|
|
|
66463
|
|
|
4
|
|
|
|
|
314
|
|
24
|
4
|
|
|
4
|
|
28
|
use File::Basename qw(basename); |
|
4
|
|
|
|
|
8
|
|
|
4
|
|
|
|
|
147
|
|
25
|
4
|
|
|
4
|
|
22
|
use Feature::Compat::Try; |
|
4
|
|
|
|
|
7
|
|
|
4
|
|
|
|
|
34
|
|
26
|
|
|
|
|
|
|
|
27
|
4
|
|
|
4
|
|
374
|
use Exporter 'import'; |
|
4
|
|
|
|
|
9
|
|
|
4
|
|
|
|
|
5767
|
|
28
|
|
|
|
|
|
|
our @EXPORT_OK = qw(take_tmp_snapshot |
29
|
|
|
|
|
|
|
tmp_snapshot_dir |
30
|
|
|
|
|
|
|
take_bootstrap_snapshot |
31
|
|
|
|
|
|
|
maybe_take_bootstrap_snapshot |
32
|
|
|
|
|
|
|
bootstrap_snapshot_dir |
33
|
|
|
|
|
|
|
the_local_bootstrap_snapshot |
34
|
|
|
|
|
|
|
bootstrap_lock_file |
35
|
|
|
|
|
|
|
create_bootstrap_lock_file |
36
|
|
|
|
|
|
|
is_backup_type_or_die |
37
|
|
|
|
|
|
|
); |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
#################################### |
40
|
|
|
|
|
|
|
# SUBROUTINES # |
41
|
|
|
|
|
|
|
#################################### |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
sub take_tmp_snapshot { |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
# Take a tmp snapshot for $backup. The tmp snapshot is the snapshot that is |
46
|
|
|
|
|
|
|
# actually replicated in an incremental backup with 'btrfs send -p'. |
47
|
|
|
|
|
|
|
|
48
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(4, 4, @_); |
49
|
|
|
|
|
|
|
|
50
|
0
|
|
|
|
|
|
my $backup = shift; |
51
|
0
|
|
|
|
|
|
my $backup_type = shift; |
52
|
0
|
|
|
|
|
|
my $tframe = shift; |
53
|
0
|
|
|
|
|
|
my $config_ref = shift; |
54
|
|
|
|
|
|
|
|
55
|
0
|
|
|
|
|
|
my $tmp_snapshot_dir = tmp_snapshot_dir( |
56
|
|
|
|
|
|
|
$backup, |
57
|
|
|
|
|
|
|
$backup_type, |
58
|
|
|
|
|
|
|
$tframe, |
59
|
|
|
|
|
|
|
$config_ref, |
60
|
|
|
|
|
|
|
DIE_UNLESS_EXISTS => 1 |
61
|
|
|
|
|
|
|
); |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
# Remove any old tmp snapshots that were never deleted because of a failed |
64
|
|
|
|
|
|
|
# incremental backup attempt. |
65
|
0
|
0
|
|
|
|
|
opendir my $dh, $tmp_snapshot_dir or confess("yabsm: internal error: cannot opendir '$tmp_snapshot_dir'"); |
66
|
0
|
|
|
|
|
|
my @tmp_snapshots = grep { is_snapshot_name($_, ALLOW_BOOTSTRAP => 0) } readdir($dh); |
|
0
|
|
|
|
|
|
|
67
|
0
|
|
|
|
|
|
closedir $dh; |
68
|
0
|
|
|
|
|
|
map { $_ = "$tmp_snapshot_dir/$_" } @tmp_snapshots; |
|
0
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# The old tmp snapshot may be in the process of being sent which will cause |
71
|
|
|
|
|
|
|
# the deletion to fail. In this case we can just ignore the failure. |
72
|
0
|
|
|
|
|
|
for (@tmp_snapshots) { |
73
|
|
|
|
|
|
|
try { |
74
|
|
|
|
|
|
|
delete_snapshot($_); |
75
|
|
|
|
|
|
|
} |
76
|
0
|
|
|
|
|
|
catch ($e) { |
77
|
|
|
|
|
|
|
; # do nothing |
78
|
|
|
|
|
|
|
} |
79
|
|
|
|
|
|
|
} |
80
|
|
|
|
|
|
|
|
81
|
0
|
|
|
|
|
|
my $mountpoint; |
82
|
|
|
|
|
|
|
|
83
|
0
|
0
|
|
|
|
|
if ($backup_type eq 'ssh') { |
|
|
0
|
|
|
|
|
|
84
|
0
|
|
|
|
|
|
$mountpoint = ssh_backup_mountpoint($backup, $config_ref); |
85
|
|
|
|
|
|
|
} |
86
|
|
|
|
|
|
|
elsif ($backup_type eq 'local') { |
87
|
0
|
|
|
|
|
|
$mountpoint = local_backup_mountpoint($backup, $config_ref); |
88
|
|
|
|
|
|
|
} |
89
|
0
|
|
|
|
|
|
else { is_backup_type_or_die($backup_type) } |
90
|
|
|
|
|
|
|
|
91
|
0
|
|
|
|
|
|
return take_snapshot($mountpoint, $tmp_snapshot_dir); |
92
|
|
|
|
|
|
|
} |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
sub tmp_snapshot_dir { |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
# Return path to $backup's tmp snapshot directory. If passed |
97
|
|
|
|
|
|
|
# 'DIE_UNLESS_EXISTS => 1' # then die unless the directory exists and is |
98
|
|
|
|
|
|
|
# readable+writable for the current user. |
99
|
|
|
|
|
|
|
|
100
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(4, 6, @_); |
101
|
|
|
|
|
|
|
|
102
|
0
|
|
|
|
|
|
my $backup = shift; |
103
|
0
|
|
|
|
|
|
my $backup_type = shift; |
104
|
0
|
|
|
|
|
|
my $tframe = shift; |
105
|
0
|
|
|
|
|
|
my $config_ref = shift; |
106
|
0
|
|
|
|
|
|
my %die_unless_exists = (DIE_UNLESS_EXISTS => 0, @_); |
107
|
|
|
|
|
|
|
|
108
|
0
|
|
|
|
|
|
is_timeframe_or_die($tframe); |
109
|
|
|
|
|
|
|
|
110
|
0
|
0
|
|
|
|
|
if ($backup_type eq 'ssh') { |
|
|
0
|
|
|
|
|
|
111
|
0
|
|
|
|
|
|
ssh_backup_exists_or_die($backup, $config_ref); |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
elsif ($backup_type eq 'ssh') { |
114
|
0
|
|
|
|
|
|
local_backup_exists_or_die($backup, $config_ref); |
115
|
|
|
|
|
|
|
} |
116
|
0
|
|
|
|
|
|
else { is_backup_type_or_die($backup_type) } |
117
|
|
|
|
|
|
|
|
118
|
0
|
|
|
|
|
|
my $tmp_snapshot_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/tmp-snapshot/$tframe"; |
119
|
|
|
|
|
|
|
|
120
|
0
|
0
|
|
|
|
|
if ($die_unless_exists{DIE_UNLESS_EXISTS}) { |
121
|
0
|
0
|
0
|
|
|
|
unless (-d $tmp_snapshot_dir && -r $tmp_snapshot_dir) { |
122
|
0
|
|
|
|
|
|
my $username = getpwuid $<; |
123
|
0
|
|
|
|
|
|
die "yabsm: error: no directory '$tmp_snapshot_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n"; |
124
|
|
|
|
|
|
|
} |
125
|
|
|
|
|
|
|
} |
126
|
|
|
|
|
|
|
|
127
|
0
|
|
|
|
|
|
return $tmp_snapshot_dir; |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
sub take_bootstrap_snapshot { |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
# Take a btrfs bootstrap snapshot of $backup and return its path. |
133
|
|
|
|
|
|
|
# If there is already a bootstrap snapshot for $backup then delete |
134
|
|
|
|
|
|
|
# it and take a new one. |
135
|
|
|
|
|
|
|
|
136
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 3, @_); |
137
|
|
|
|
|
|
|
|
138
|
0
|
|
|
|
|
|
my $backup = shift; |
139
|
0
|
|
|
|
|
|
my $backup_type = shift; |
140
|
0
|
|
|
|
|
|
my $config_ref = shift; |
141
|
|
|
|
|
|
|
|
142
|
0
|
|
|
|
|
|
my $mountpoint; |
143
|
|
|
|
|
|
|
|
144
|
0
|
0
|
|
|
|
|
if ($backup_type eq 'ssh') { |
|
|
0
|
|
|
|
|
|
145
|
0
|
|
|
|
|
|
$mountpoint = ssh_backup_mountpoint($backup, $config_ref); |
146
|
|
|
|
|
|
|
} |
147
|
|
|
|
|
|
|
elsif ($backup_type eq 'local') { |
148
|
0
|
|
|
|
|
|
$mountpoint = local_backup_mountpoint($backup, $config_ref); |
149
|
|
|
|
|
|
|
} |
150
|
0
|
|
|
|
|
|
else { is_backup_type_or_die($backup_type) } |
151
|
|
|
|
|
|
|
|
152
|
0
|
0
|
|
|
|
|
if (my $bootstrap_snapshot = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) { |
153
|
0
|
|
|
|
|
|
delete_snapshot($bootstrap_snapshot); |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
|
156
|
0
|
|
|
|
|
|
my $bootstrap_dir = bootstrap_snapshot_dir($backup, $backup_type, $config_ref, DIE_UNLESS_EXISTS => 1); |
157
|
0
|
|
|
|
|
|
my $snapshot_name = '.BOOTSTRAP-' . current_time_snapshot_name(); |
158
|
|
|
|
|
|
|
|
159
|
0
|
|
|
|
|
|
return take_snapshot($mountpoint, $bootstrap_dir, $snapshot_name); |
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
sub maybe_take_bootstrap_snapshot { |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
# If $backup does not already have a bootstrap snapshot then take |
165
|
|
|
|
|
|
|
# a bootstrap snapshot and return its path. Otherwise return the |
166
|
|
|
|
|
|
|
# path of the existing bootstrap snapshot. |
167
|
|
|
|
|
|
|
|
168
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 3, @_); |
169
|
|
|
|
|
|
|
|
170
|
0
|
|
|
|
|
|
my $backup = shift; |
171
|
0
|
|
|
|
|
|
my $backup_type = shift; |
172
|
0
|
|
|
|
|
|
my $config_ref = shift; |
173
|
|
|
|
|
|
|
|
174
|
0
|
0
|
|
|
|
|
if (my $boot_snap = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) { |
175
|
0
|
|
|
|
|
|
return $boot_snap; |
176
|
|
|
|
|
|
|
} |
177
|
|
|
|
|
|
|
|
178
|
0
|
|
|
|
|
|
return take_bootstrap_snapshot($backup, $backup_type, $config_ref); |
179
|
|
|
|
|
|
|
} |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
sub bootstrap_snapshot_dir { |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# Return the path to $ssh_backup's bootstrap snapshot directory. |
184
|
|
|
|
|
|
|
# Logdie if the bootstrap snapshot directory does not exist. |
185
|
|
|
|
|
|
|
|
186
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 5, @_); |
187
|
|
|
|
|
|
|
|
188
|
0
|
|
|
|
|
|
my $backup = shift; |
189
|
0
|
|
|
|
|
|
my $backup_type = shift; |
190
|
0
|
|
|
|
|
|
my $config_ref = shift; |
191
|
0
|
|
|
|
|
|
my %or_die = (DIE_UNLESS_EXISTS => 0, @_); |
192
|
|
|
|
|
|
|
|
193
|
0
|
|
|
|
|
|
is_backup_type_or_die($backup_type); |
194
|
|
|
|
|
|
|
|
195
|
0
|
0
|
|
|
|
|
if ($backup_type eq 'ssh') { |
196
|
0
|
|
|
|
|
|
ssh_backup_exists_or_die($backup, $config_ref); |
197
|
|
|
|
|
|
|
} |
198
|
0
|
0
|
|
|
|
|
if ($backup_type eq 'local') { |
199
|
0
|
|
|
|
|
|
local_backup_exists_or_die($backup, $config_ref); |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
|
202
|
0
|
|
|
|
|
|
my $bootstrap_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/bootstrap-snapshot"; |
203
|
|
|
|
|
|
|
|
204
|
0
|
0
|
|
|
|
|
if ($or_die{DIE_UNLESS_EXISTS}) { |
205
|
0
|
0
|
0
|
|
|
|
unless (-d $bootstrap_dir && -r $bootstrap_dir) { |
206
|
0
|
|
|
|
|
|
my $username = getpwuid $<; |
207
|
0
|
|
|
|
|
|
die "yabsm: error: no directory '$bootstrap_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n"; |
208
|
|
|
|
|
|
|
} |
209
|
|
|
|
|
|
|
} |
210
|
|
|
|
|
|
|
|
211
|
0
|
|
|
|
|
|
return $bootstrap_dir; |
212
|
|
|
|
|
|
|
} |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
sub the_local_bootstrap_snapshot { |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
# Return the local bootstrap snapshot for $backup if it exists and return |
217
|
|
|
|
|
|
|
# undef otherwise. Die if there are multiple bootstrap snapshots. |
218
|
|
|
|
|
|
|
|
219
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 3, @_); |
220
|
|
|
|
|
|
|
|
221
|
0
|
|
|
|
|
|
my $backup = shift; |
222
|
0
|
|
|
|
|
|
my $backup_type = shift; |
223
|
0
|
|
|
|
|
|
my $config_ref = shift; |
224
|
|
|
|
|
|
|
|
225
|
0
|
|
|
|
|
|
my $bootstrap_dir = bootstrap_snapshot_dir( |
226
|
|
|
|
|
|
|
$backup, |
227
|
|
|
|
|
|
|
$backup_type, |
228
|
|
|
|
|
|
|
$config_ref, |
229
|
|
|
|
|
|
|
DIE_UNLESS_EXISTS => 1 |
230
|
|
|
|
|
|
|
); |
231
|
|
|
|
|
|
|
|
232
|
0
|
0
|
|
|
|
|
opendir my $dh, $bootstrap_dir or confess "yabsm: internal error: cannot opendir '$bootstrap_dir'"; |
233
|
0
|
|
|
|
|
|
my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh); |
|
0
|
|
|
|
|
|
|
234
|
0
|
|
|
|
|
|
map { $_ = "$bootstrap_dir/$_" } @boot_snaps; |
|
0
|
|
|
|
|
|
|
235
|
0
|
|
|
|
|
|
close $dh; |
236
|
|
|
|
|
|
|
|
237
|
0
|
0
|
|
|
|
|
if (0 == @boot_snaps) { |
|
|
0
|
|
|
|
|
|
238
|
0
|
|
|
|
|
|
return undef; |
239
|
|
|
|
|
|
|
} |
240
|
|
|
|
|
|
|
elsif (1 == @boot_snaps) { |
241
|
0
|
|
|
|
|
|
return $boot_snaps[0]; |
242
|
|
|
|
|
|
|
} |
243
|
|
|
|
|
|
|
else { |
244
|
0
|
|
|
|
|
|
die "yabsm: error: found multiple local bootstrap snapshots for ${backup_type}_backup '$backup' in '$bootstrap_dir'\n"; |
245
|
|
|
|
|
|
|
} |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
sub bootstrap_lock_file { |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
# Return the path to the BOOTSTRAP-LOCK for $backup if it exists and return |
251
|
|
|
|
|
|
|
# undef otherwise. |
252
|
|
|
|
|
|
|
|
253
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 3, @_); |
254
|
|
|
|
|
|
|
|
255
|
0
|
|
|
|
|
|
my $backup = shift; |
256
|
0
|
|
|
|
|
|
my $backup_type = shift; |
257
|
0
|
|
|
|
|
|
my $config_ref = shift; |
258
|
|
|
|
|
|
|
|
259
|
0
|
|
|
|
|
|
my $rx = qr/yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCK/; |
260
|
|
|
|
|
|
|
|
261
|
0
|
|
|
|
|
|
my $lock_file = [ grep /$rx/, glob('/tmp/*') ]->[0]; |
262
|
|
|
|
|
|
|
|
263
|
0
|
|
|
|
|
|
return $lock_file; |
264
|
|
|
|
|
|
|
} |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
sub create_bootstrap_lock_file { |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
# Create the bootstrap lock file for $backup. This function should be called |
269
|
|
|
|
|
|
|
# when performing the bootstrap phase of an incremental backup after checking |
270
|
|
|
|
|
|
|
# to make sure a lock file doesn't already exist. If a lock file already |
271
|
|
|
|
|
|
|
# exists we die, so check beforehand! |
272
|
|
|
|
|
|
|
|
273
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(3, 3, @_); |
274
|
|
|
|
|
|
|
|
275
|
0
|
|
|
|
|
|
my $backup = shift; |
276
|
0
|
|
|
|
|
|
my $backup_type = shift; |
277
|
0
|
|
|
|
|
|
my $config_ref = shift; |
278
|
|
|
|
|
|
|
|
279
|
0
|
|
|
|
|
|
backup_exists_or_die($backup, $config_ref); |
280
|
0
|
|
|
|
|
|
is_backup_type_or_die($backup_type); |
281
|
|
|
|
|
|
|
|
282
|
0
|
0
|
|
|
|
|
if (my $existing_lock_file = bootstrap_lock_file($backup, $backup_type, $config_ref)) { |
283
|
0
|
|
|
|
|
|
die "yabsm: error: ${backup_type}_backup '$backup' is already locked out of performing a bootstrap. This was determined by the existence of '$existing_lock_file'\n"; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
# The file will be deleted when $tmp_fh is destroyed. |
287
|
0
|
|
|
|
|
|
my $tmp_fh = File::Temp->new( |
288
|
|
|
|
|
|
|
TEMPLATE => "yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCKXXXX", |
289
|
|
|
|
|
|
|
DIR => '/tmp', |
290
|
|
|
|
|
|
|
UNLINK => 1 |
291
|
|
|
|
|
|
|
); |
292
|
|
|
|
|
|
|
|
293
|
0
|
|
|
|
|
|
return $tmp_fh; |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
sub is_backup_type_or_die { |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
# Logdie unless $backup_type equals 'ssh' or 'local'. |
299
|
|
|
|
|
|
|
|
300
|
0
|
|
|
0
|
0
|
|
arg_count_or_die(1, 1, @_); |
301
|
|
|
|
|
|
|
|
302
|
0
|
|
|
|
|
|
my $backup_type = shift; |
303
|
|
|
|
|
|
|
|
304
|
0
|
0
|
|
|
|
|
unless ( $backup_type =~ /^(ssh|local)$/ ) { |
305
|
0
|
|
|
|
|
|
confess("yabsm: internal error: '$backup_type' is not 'ssh' or 'local'"); |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
|
308
|
0
|
|
|
|
|
|
return 1; |
309
|
|
|
|
|
|
|
} |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
1; |