| 1 |
#!/usr/bin/perl |
|---|
| 2 |
|
|---|
| 3 |
# Copyright (c) 2007 OmniTI Computer Consulting, Inc. All rights reserved. |
|---|
| 4 |
# For information on licensing see: |
|---|
| 5 |
# https://labs.omniti.com/zetaback/trunk/LICENSE |
|---|
| 6 |
|
|---|
| 7 |
use strict; |
|---|
| 8 |
use Getopt::Long; |
|---|
| 9 |
use MIME::Base64; |
|---|
| 10 |
use POSIX qw/strftime/; |
|---|
| 11 |
use Fcntl qw/:flock/; |
|---|
| 12 |
use File::Copy; |
|---|
| 13 |
use IO::File; |
|---|
| 14 |
use Pod::Usage; |
|---|
| 15 |
|
|---|
| 16 |
use vars qw/%conf %locks $version_string |
|---|
| 17 |
$PREFIX $CONF $BLOCKSIZE $DEBUG $HOST $BACKUP |
|---|
| 18 |
$RESTORE $RESTORE_HOST $RESTORE_ZFS $TIMESTAMP |
|---|
| 19 |
$LIST $SUMMARY $SUMMARY_EXT $SUMMARY_VIOLATORS |
|---|
| 20 |
$FORCE_FULL $FORCE_INC $EXPUNGE $NEUTERED $ZFS |
|---|
| 21 |
$SHOW_FILENAMES $ARCHIVE $VERSION $HELP/; |
|---|
| 22 |
$version_string = '0.1'; |
|---|
| 23 |
$PREFIX = q^__PREFIX__^; |
|---|
| 24 |
$CONF = qq^$PREFIX/etc/zetaback.conf^; |
|---|
| 25 |
$BLOCKSIZE = 1024*64; |
|---|
| 26 |
|
|---|
| 27 |
$conf{'default'}->{'time_format'} = "%Y-%m-%d %H:%M:%S"; |
|---|
| 28 |
$conf{'default'}->{'retention'} = 14 * 86400; |
|---|
| 29 |
$conf{'default'}->{'compressionlevel'} = 1; |
|---|
| 30 |
|
|---|
| 31 |
=pod |
|---|
| 32 |
|
|---|
| 33 |
=head1 NAME |
|---|
| 34 |
|
|---|
| 35 |
zetaback - perform backup, restore and retention policies for ZFS backups. |
|---|
| 36 |
|
|---|
| 37 |
=head1 SYNOPSIS |
|---|
| 38 |
|
|---|
| 39 |
zetaback -v |
|---|
| 40 |
|
|---|
| 41 |
zetaback [-l | -s | -sx | -sv] [--files] [-c conf] [-d] [-h host] [-z zfs] |
|---|
| 42 |
|
|---|
| 43 |
zetaback -a [-c conf] [-d] [-h host] [-z zfs] |
|---|
| 44 |
|
|---|
| 45 |
zetaback -b [-ff] [-fi] [-x] [-c conf] [-d] [-n] [-h host] [-z zfs] |
|---|
| 46 |
|
|---|
| 47 |
zetaback -x [-b] [-c conf] [-d] [-n] [-h host] [-z zfs] |
|---|
| 48 |
|
|---|
| 49 |
zetaback -r [-c conf] [-d] [-n] [-h host] [-z zfs] [-t timestamp] |
|---|
| 50 |
[-rhost host] [-rzfs fs] |
|---|
| 51 |
|
|---|
| 52 |
=cut |
|---|
| 53 |
|
|---|
| 54 |
GetOptions( |
|---|
| 55 |
"h=s" => \$HOST, |
|---|
| 56 |
"z=s" => \$ZFS, |
|---|
| 57 |
"c=s" => \$CONF, |
|---|
| 58 |
"a" => \$ARCHIVE, |
|---|
| 59 |
"b" => \$BACKUP, |
|---|
| 60 |
"l" => \$LIST, |
|---|
| 61 |
"s" => \$SUMMARY, |
|---|
| 62 |
"sx" => \$SUMMARY_EXT, |
|---|
| 63 |
"sv" => \$SUMMARY_VIOLATORS, |
|---|
| 64 |
"r" => \$RESTORE, |
|---|
| 65 |
"t=i" => \$TIMESTAMP, |
|---|
| 66 |
"rhost=s" => \$RESTORE_HOST, |
|---|
| 67 |
"rzfs=s" => \$RESTORE_ZFS, |
|---|
| 68 |
"d" => \$DEBUG, |
|---|
| 69 |
"n" => \$NEUTERED, |
|---|
| 70 |
"x" => \$EXPUNGE, |
|---|
| 71 |
"v" => \$VERSION, |
|---|
| 72 |
"ff" => \$FORCE_FULL, |
|---|
| 73 |
"fi" => \$FORCE_INC, |
|---|
| 74 |
"files" => \$SHOW_FILENAMES, |
|---|
| 75 |
); |
|---|
| 76 |
|
|---|
| 77 |
# actions allowed together 'x' and 'b' all others are exclusive: |
|---|
| 78 |
my $actions = 0; |
|---|
| 79 |
$actions++ if($ARCHIVE); |
|---|
| 80 |
$actions++ if($BACKUP || $EXPUNGE); |
|---|
| 81 |
$actions++ if($RESTORE); |
|---|
| 82 |
$actions++ if($LIST); |
|---|
| 83 |
$actions++ if($SUMMARY); |
|---|
| 84 |
$actions++ if($SUMMARY_EXT); |
|---|
| 85 |
$actions++ if($SUMMARY_VIOLATORS); |
|---|
| 86 |
$actions++ if($VERSION); |
|---|
| 87 |
$actions++ if($BACKUP && $FORCE_FULL && $FORCE_INC); |
|---|
| 88 |
if($actions != 1) { |
|---|
| 89 |
pod2usage({ -verbose => 0 }); |
|---|
| 90 |
exit -1; |
|---|
| 91 |
} |
|---|
| 92 |
|
|---|
| 93 |
=pod |
|---|
| 94 |
|
|---|
| 95 |
=head1 DESCRIPTION |
|---|
| 96 |
|
|---|
| 97 |
The B<zetaback> program orchestrates the backup (either full or |
|---|
| 98 |
incremental) of remote ZFS filesystems to a local store. It handles |
|---|
| 99 |
frequency requirements for both full and incemental backups as well |
|---|
| 100 |
as retention policies. In addition to backups, the B<zetaback> tool |
|---|
| 101 |
allows for the restore of any backup to a specified host and zfs |
|---|
| 102 |
filesystem. |
|---|
| 103 |
|
|---|
| 104 |
=head1 OPTIONS |
|---|
| 105 |
|
|---|
| 106 |
The non-optional action command line arguments define the invocation purpose |
|---|
| 107 |
of B<zetaback>. All other arguments are optional and refine the target |
|---|
| 108 |
of the action specified. |
|---|
| 109 |
|
|---|
| 110 |
=head2 Generic Options |
|---|
| 111 |
|
|---|
| 112 |
The following arguments have the same meaning over several actions: |
|---|
| 113 |
|
|---|
| 114 |
=over |
|---|
| 115 |
|
|---|
| 116 |
=item -c <conf> |
|---|
| 117 |
|
|---|
| 118 |
Use the specified file as the configuration file. The default file, if |
|---|
| 119 |
none is specified is /usr/local/etc/zetaback.conf. The prefix of this |
|---|
| 120 |
file may also be specified as an argument to the configure script. |
|---|
| 121 |
|
|---|
| 122 |
=item -d |
|---|
| 123 |
|
|---|
| 124 |
Enable debugging output. |
|---|
| 125 |
|
|---|
| 126 |
=item -n |
|---|
| 127 |
|
|---|
| 128 |
Don't actually perform any remote commands or expunging. This is useful with |
|---|
| 129 |
the -d argument to ascertain what would be done if the command was actually |
|---|
| 130 |
executed. |
|---|
| 131 |
|
|---|
| 132 |
=item -t <timestamp> |
|---|
| 133 |
|
|---|
| 134 |
Used during the restore process to specify a backup image from the desired |
|---|
| 135 |
point in time. If omitted, the command becomes interactive. This timestamp |
|---|
| 136 |
is a UNIX timestamp and is shown in the output of the -s and -sx actions. |
|---|
| 137 |
|
|---|
| 138 |
=item -rhost <host> |
|---|
| 139 |
|
|---|
| 140 |
Specify the remote host that is the target for a restore operation. If |
|---|
| 141 |
omitted the command becomes interactive. |
|---|
| 142 |
|
|---|
| 143 |
=item -rzfs <zfs> |
|---|
| 144 |
|
|---|
| 145 |
Specify the remote ZFS filesystem that is the target for a restore |
|---|
| 146 |
operation. If omitted the command becomes interactive. |
|---|
| 147 |
|
|---|
| 148 |
=item -h <host> |
|---|
| 149 |
|
|---|
| 150 |
Filters the operation to the host specified. If <host> is of the form |
|---|
| 151 |
/pattern/, it matches 'pattern' as a perl regular expression against available |
|---|
| 152 |
hosts. If omitted, no limit is enforced and all hosts are used for the action. |
|---|
| 153 |
|
|---|
| 154 |
=item -z <zfs> |
|---|
| 155 |
|
|---|
| 156 |
Filters the operation to the zfs filesystem specified. If <zfs> is of the |
|---|
| 157 |
form /pattern/, it matches 'pattern' as a perl regular expression against |
|---|
| 158 |
available zfs filesystems. If omitted, no filter is enforced and all zfs |
|---|
| 159 |
filesystems are used for the action. |
|---|
| 160 |
|
|---|
| 161 |
=back |
|---|
| 162 |
|
|---|
| 163 |
=head2 Actions |
|---|
| 164 |
|
|---|
| 165 |
=over |
|---|
| 166 |
|
|---|
| 167 |
=item -v |
|---|
| 168 |
|
|---|
| 169 |
Show the version. |
|---|
| 170 |
|
|---|
| 171 |
=item -l |
|---|
| 172 |
|
|---|
| 173 |
Show a brief listing of available backups. |
|---|
| 174 |
|
|---|
| 175 |
=item -s |
|---|
| 176 |
|
|---|
| 177 |
Like -l, -s will show a list of backups but provides additional information |
|---|
| 178 |
about the backups including timestamp, type (full or incremental) and the |
|---|
| 179 |
size on disk. |
|---|
| 180 |
|
|---|
| 181 |
=item -sx |
|---|
| 182 |
|
|---|
| 183 |
Shows an extended summary. In addition to the output provided by the -s |
|---|
| 184 |
action, the -sx action will show detail for each availble backup. For |
|---|
| 185 |
full backups, the detail will include any more recent full backups, if |
|---|
| 186 |
they exist. For incremental backups, the detail will include any |
|---|
| 187 |
incremental backups that are more recent than the last full backup. |
|---|
| 188 |
|
|---|
| 189 |
=item -sv |
|---|
| 190 |
|
|---|
| 191 |
Display all backups in the current store that violate the configured |
|---|
| 192 |
retention policy. |
|---|
| 193 |
|
|---|
| 194 |
=item --files |
|---|
| 195 |
|
|---|
| 196 |
Display the on-disk file corresponding to each backup named in the output. |
|---|
| 197 |
This is useful with the -sv flag to name violating files. Often times, |
|---|
| 198 |
violators are filesystems that have been removed on the host machines and |
|---|
| 199 |
zetaback can no longer back them up. Be very careful if you choose to |
|---|
| 200 |
automate the removal of such backups as filesystems that would be backed up |
|---|
| 201 |
by the next regular zetaback run will often show up as violators. |
|---|
| 202 |
|
|---|
| 203 |
=item -a |
|---|
| 204 |
|
|---|
| 205 |
Performs an archive. This option will look at all eligible backup points |
|---|
| 206 |
(as restricted by -z and -h) and move those to the configured archive |
|---|
| 207 |
directory. The recommended use is to first issue -sx --files then |
|---|
| 208 |
carefully review available backup points and prune those that are |
|---|
| 209 |
unneeded. Then invoke with -a to move only the remaining "desired" |
|---|
| 210 |
backup points into the archives. Archived backups do not appear in any |
|---|
| 211 |
listings or in the list of policy violators generated by the -sv option. |
|---|
| 212 |
In effect, they are no longer "visible" to zetaback. |
|---|
| 213 |
|
|---|
| 214 |
=item -b |
|---|
| 215 |
|
|---|
| 216 |
Performs a backup. This option will investigate all eligible hosts, query |
|---|
| 217 |
the available filesystems from the remote agent and determine if any such |
|---|
| 218 |
filesystems require a new full or incremental backup to be taken. This |
|---|
| 219 |
option may be combined with the -x option (to clean up afterwards.) |
|---|
| 220 |
|
|---|
| 221 |
=item -ff |
|---|
| 222 |
|
|---|
| 223 |
Forces a full backup to be taken on each filesystem encountered. This is |
|---|
| 224 |
used in combination with -b. It is recommended to use this option only when |
|---|
| 225 |
targeting specific filesystems (via the -h and -z options.) Forcing a full |
|---|
| 226 |
backup across all machines will cause staggered backups to coalesce and |
|---|
| 227 |
could cause performance issues. |
|---|
| 228 |
|
|---|
| 229 |
=item -fi |
|---|
| 230 |
|
|---|
| 231 |
Forces an incremental backup to be taken on each filesystem encountered. |
|---|
| 232 |
This is used in combination with -b. It is recommended to use this option |
|---|
| 233 |
only when targeting specific filesystems (via the -h and -z options.) Forcing |
|---|
| 234 |
an incremental backup across all machines will cause staggered backups |
|---|
| 235 |
to coalesce and could cause performance issues. |
|---|
| 236 |
|
|---|
| 237 |
=item -x |
|---|
| 238 |
|
|---|
| 239 |
Perform an expunge. This option will determine which, if any, of the local |
|---|
| 240 |
backups may be deleted given the retention policy specified in the |
|---|
| 241 |
configuration. |
|---|
| 242 |
|
|---|
| 243 |
=item -r |
|---|
| 244 |
|
|---|
| 245 |
Perform a restore. This option will operate on the specified backup and |
|---|
| 246 |
restore it to the ZFS filesystem specified with -rzfs on the host specified |
|---|
| 247 |
with the -rhost option. The -h, -z and -t options may be used to filter |
|---|
| 248 |
the source backup list. If the filtered list contains more than one |
|---|
| 249 |
source backup image, the command will act interactively. If the -rhost |
|---|
| 250 |
and -rzfs command are not specified, the command will act interactively. |
|---|
| 251 |
|
|---|
| 252 |
=back |
|---|
| 253 |
|
|---|
| 254 |
=cut |
|---|
| 255 |
|
|---|
| 256 |
if($VERSION) { |
|---|
| 257 |
print "zetaback: $version_string\n"; |
|---|
| 258 |
exit 0; |
|---|
| 259 |
} |
|---|
| 260 |
|
|---|
| 261 |
=pod |
|---|
| 262 |
|
|---|
| 263 |
=head1 CONFIGURATION |
|---|
| 264 |
|
|---|
| 265 |
The zetaback configuration file consists of a default stanza, containing |
|---|
| 266 |
settings that can be overridden on a per-host basis. A stanza begins |
|---|
| 267 |
either with the string 'default', or a fully-qualified hostname, with |
|---|
| 268 |
settings enclosed in braces ({}). Single-line comments begin with a hash |
|---|
| 269 |
('#'), and whitespace is ignored, so feel free to indent for better |
|---|
| 270 |
readability. Every host to be backed up must have a host stanza in the |
|---|
| 271 |
configuration file. |
|---|
| 272 |
|
|---|
| 273 |
=head2 Settings |
|---|
| 274 |
|
|---|
| 275 |
The following settings are valid in both the default and host scopes: |
|---|
| 276 |
|
|---|
| 277 |
=over |
|---|
| 278 |
|
|---|
| 279 |
=item store |
|---|
| 280 |
|
|---|
| 281 |
The base directory under which to keep backups. An interpolated variable |
|---|
| 282 |
'%h' can be used, which expands to the hostname. There is no default for |
|---|
| 283 |
this setting. |
|---|
| 284 |
|
|---|
| 285 |
=item archive |
|---|
| 286 |
|
|---|
| 287 |
The base directory under which archives are stored. The format is the same |
|---|
| 288 |
as the store setting. This is the destination to which files are relocated |
|---|
| 289 |
when issuing an archive action (-a). |
|---|
| 290 |
|
|---|
| 291 |
=item agent |
|---|
| 292 |
|
|---|
| 293 |
The location of the zetaback_agent binary on the host. There is no default |
|---|
| 294 |
for this setting. |
|---|
| 295 |
|
|---|
| 296 |
=item time_format |
|---|
| 297 |
|
|---|
| 298 |
All timestamps within zetaback are in UNIX timestamp format. This setting |
|---|
| 299 |
provides a string for formatting all timestamps on output. The sequences |
|---|
| 300 |
available are identical to those in strftime(3). If not specified, the |
|---|
| 301 |
default is '%Y-%m-%d %H:%M:%S'. |
|---|
| 302 |
|
|---|
| 303 |
=item backup_interval |
|---|
| 304 |
|
|---|
| 305 |
The frequency (in seconds) at which to perform incremental backups. An |
|---|
| 306 |
incremental backup will be performed if the current time is more than |
|---|
| 307 |
backup_interval since the last incremental backup. If there is no full backup |
|---|
| 308 |
for a particular filesystem, then a full backup is performed. There is no |
|---|
| 309 |
default for this setting. |
|---|
| 310 |
|
|---|
| 311 |
=item full_interval |
|---|
| 312 |
|
|---|
| 313 |
The frequency (in seconds) at which to perform full backups. A full backup will |
|---|
| 314 |
be performed if the current time is more than full_interval since the last full |
|---|
| 315 |
backup. |
|---|
| 316 |
|
|---|
| 317 |
=item retention |
|---|
| 318 |
|
|---|
| 319 |
The retention time (in seconds) for backups. Defaults to (14 * 86400), or two |
|---|
| 320 |
weeks. |
|---|
| 321 |
|
|---|
| 322 |
=item compressionlevel |
|---|
| 323 |
|
|---|
| 324 |
Compress files using gzip at the specified compression level. 0 means no |
|---|
| 325 |
compression. Accepted values are 1-9. Defaults to 1 (fastest/minimal |
|---|
| 326 |
compression.) |
|---|
| 327 |
|
|---|
| 328 |
=item ssh_config |
|---|
| 329 |
|
|---|
| 330 |
Full path to an alternate ssh client config. This is useful for specifying a |
|---|
| 331 |
less secure but faster cipher for some hosts, or using a different private |
|---|
| 332 |
key. There is no default for this setting. |
|---|
| 333 |
|
|---|
| 334 |
=back |
|---|
| 335 |
|
|---|
| 336 |
=head1 CONFIGURATION EXAMPLES |
|---|
| 337 |
|
|---|
| 338 |
=head2 Uniform hosts |
|---|
| 339 |
|
|---|
| 340 |
This config results in backups stored in /var/spool/zfs_backups, with a |
|---|
| 341 |
subdirectory for each host. Incremental backups will be performed |
|---|
| 342 |
approximately once per day, assuming zetaback is run hourly. Full backups |
|---|
| 343 |
will be done once per week. Time format and retention are default. |
|---|
| 344 |
|
|---|
| 345 |
default { |
|---|
| 346 |
store = /var/spool/zfs_backups/%h |
|---|
| 347 |
agent = /usr/local/bin/zetaback_agent |
|---|
| 348 |
backup_interval = 83000 |
|---|
| 349 |
full_interval = 604800 |
|---|
| 350 |
} |
|---|
| 351 |
|
|---|
| 352 |
host1 {} |
|---|
| 353 |
|
|---|
| 354 |
host2 {} |
|---|
| 355 |
|
|---|
| 356 |
=head2 Non-uniform hosts |
|---|
| 357 |
|
|---|
| 358 |
Here, host1's and host2's agents are found in different places, and host2's |
|---|
| 359 |
backups should be stored in a different path. |
|---|
| 360 |
|
|---|
| 361 |
default { |
|---|
| 362 |
store = /var/spool/zfs_backups/%h |
|---|
| 363 |
agent = /usr/local/bin/zetaback_agent |
|---|
| 364 |
backup_interval = 83000 |
|---|
| 365 |
full_interval = 604800 |
|---|
| 366 |
} |
|---|
| 367 |
|
|---|
| 368 |
host1 { |
|---|
| 369 |
agent = /opt/local/bin/zetaback_agent |
|---|
| 370 |
} |
|---|
| 371 |
|
|---|
| 372 |
host2 { |
|---|
| 373 |
store = /var/spool/alt_backups/%h |
|---|
| 374 |
agent = /www/bin/zetaback_agent |
|---|
| 375 |
} |
|---|
| 376 |
|
|---|
| 377 |
=cut |
|---|
| 378 |
|
|---|
| 379 |
# Make the parser more formal: |
|---|
| 380 |
# config => stanza* |
|---|
| 381 |
# stanza => string { kvp* } |
|---|
| 382 |
# kvp => string = string |
|---|
| 383 |
my $str_re = qr/(?:"(?:\\\\|\\"|[^"])*"|\S+)/; |
|---|
| 384 |
my $kvp_re = qr/($str_re)\s*=\s*($str_re)/; |
|---|
| 385 |
my $stanza_re = qr/($str_re)\s*\{((?:\s*$kvp_re)*)\s*\}/; |
|---|
| 386 |
|
|---|
| 387 |
sub parse_config() { |
|---|
| 388 |
local($/); |
|---|
| 389 |
$/ = undef; |
|---|
| 390 |
open(CONF, "<$CONF") || die "Unable to open config file: $CONF"; |
|---|
| 391 |
my $file = <CONF>; |
|---|
| 392 |
# Rip comments |
|---|
| 393 |
$file =~ s/^\s*#.*$//mg; |
|---|
| 394 |
while($file =~ m/$stanza_re/gm) { |
|---|
| 395 |
my $scope = $1; |
|---|
| 396 |
my $filepart = $2; |
|---|
| 397 |
$scope =~ s/^"(.*)"$/$1/; |
|---|
| 398 |
$conf{$scope} ||= {}; |
|---|
| 399 |
while($filepart =~ m/$kvp_re/gm) { |
|---|
| 400 |
my $key = $1; |
|---|
| 401 |
my $value = $2; |
|---|
| 402 |
$key =~ s/^"(.*)"$/$1/; |
|---|
| 403 |
$value =~ s/^"(.*)"$/$1/; |
|---|
| 404 |
$conf{$scope}->{lc($key)} = $value; |
|---|
| 405 |
} |
|---|
| 406 |
} |
|---|
| 407 |
close(CONF); |
|---|
| 408 |
} |
|---|
| 409 |
sub config_get($$) { |
|---|
| 410 |
return $conf{$_[0]}->{$_[1]} || $conf{'default'}->{$_[1]}; |
|---|
| 411 |
} |
|---|
| 412 |
|
|---|
| 413 |
sub dir_encode($) { |
|---|
| 414 |
my $d = shift; |
|---|
| 415 |
my $e = encode_base64($d, ''); |
|---|
| 416 |
$e =~ s/\//_/; |
|---|
| 417 |
return $e; |
|---|
| 418 |
} |
|---|
| 419 |
sub dir_decode($) { |
|---|
| 420 |
my $e = shift; |
|---|
| 421 |
$e =~ s/_/\//; |
|---|
| 422 |
return decode_base64($e); |
|---|
| 423 |
} |
|---|
| 424 |
sub pretty_size($) { |
|---|
| 425 |
my $bytes = shift; |
|---|
| 426 |
if($bytes > 1024*1024*1024) { |
|---|
| 427 |
return sprintf("%0.2f Gb", $bytes / (1024*1024*1024)); |
|---|
| 428 |
} |
|---|
| 429 |
if($bytes > 1024*1024) { |
|---|
| 430 |
return sprintf("%0.2f Mb", $bytes / (1024*1024)); |
|---|
| 431 |
} |
|---|
| 432 |
if($bytes > 1024) { |
|---|
| 433 |
return sprintf("%0.2f Kb", $bytes / (1024)); |
|---|
| 434 |
} |
|---|
| 435 |
return "$bytes b"; |
|---|
| 436 |
} |
|---|
| 437 |
sub lock($;$$) { |
|---|
| 438 |
my ($host, $file, $nowait) = @_; |
|---|
| 439 |
print "Acquiring lock for $host:$file\n" if($DEBUG); |
|---|
| 440 |
$file ||= 'master.lock'; |
|---|
| 441 |
my $store = config_get($host, 'store'); |
|---|
| 442 |
$store =~ s/%h/$host/g; |
|---|
| 443 |
return 1 if(exists($locks{"$host:$file"})); |
|---|
| 444 |
open(LOCK, "+>>$store/$file") || return 0; |
|---|
| 445 |
unless(flock(LOCK, LOCK_EX | ($nowait ? LOCK_NB : 0))) { |
|---|
| 446 |
close(LOCK); |
|---|
| 447 |
return 0; |
|---|
| 448 |
} |
|---|
| 449 |
$locks{"$host:$file"} = \*LOCK; |
|---|
| 450 |
return 1; |
|---|
| 451 |
} |
|---|
| 452 |
sub unlock($;$$) { |
|---|
| 453 |
my ($host, $file, $remove) = @_; |
|---|
| 454 |
print "Releasing lock for $host:$file\n" if($DEBUG); |
|---|
| 455 |
$file ||= 'master.lock'; |
|---|
| 456 |
my $store = config_get($host, 'store'); |
|---|
| 457 |
$store =~ s/%h/$host/g; |
|---|
| 458 |
return 0 unless(exists($locks{"$host:$file"})); |
|---|
| 459 |
*UNLOCK = $locks{$file}; |
|---|
| 460 |
unlink("$store/$file") if($remove); |
|---|
| 461 |
flock(UNLOCK, LOCK_UN); |
|---|
| 462 |
close(UNLOCK); |
|---|
| 463 |
return 1; |
|---|
| 464 |
} |
|---|
| 465 |
sub scan_for_backups($) { |
|---|
| 466 |
my %info = (); |
|---|
| 467 |
my $dir = shift; |
|---|
| 468 |
$info{last_full} = $info{last_incremental} = $info{last_backup} = 0; |
|---|
| 469 |
opendir(D, $dir) || return \%info; |
|---|
| 470 |
foreach my $file (readdir(D)) { |
|---|
| 471 |
if($file =~ /^(\d+)\.([^\.]+)\.full$/) { |
|---|
| 472 |
my $whence = $1; |
|---|
| 473 |
my $fs = dir_decode($2); |
|---|
| 474 |
$info{$fs}->{full}->{$whence}->{'file'} = "$dir/$file"; |
|---|
| 475 |
$info{$fs}->{last_full} = $whence if($whence > $info{$fs}->{last_full}); |
|---|
| 476 |
$info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ? |
|---|
| 477 |
$info{$fs}->{last_incremental} : $info{$fs}->{last_full}; |
|---|
| 478 |
} |
|---|
| 479 |
elsif($file =~ /^(\d+).([^\.]+)\.incremental.(\d+)$/) { |
|---|
| 480 |
my $whence = $1; |
|---|
| 481 |
my $fs = dir_decode($2); |
|---|
| 482 |
$info{$fs}->{incremental}->{$whence}->{'depends'} = $3; |
|---|
| 483 |
$info{$fs}->{incremental}->{$whence}->{'file'} = "$dir/$file"; |
|---|
| 484 |
$info{$fs}->{last_incremental} = $whence if($whence > $info{$fs}->{last_incremental}); |
|---|
| 485 |
$info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ? |
|---|
| 486 |
$info{$fs}->{last_incremental} : $info{$fs}->{last_full}; |
|---|
| 487 |
} |
|---|
| 488 |
} |
|---|
| 489 |
closedir(D); |
|---|
| 490 |
return \%info; |
|---|
| 491 |
} |
|---|
| 492 |
|
|---|
| 493 |
parse_config(); |
|---|
| 494 |
|
|---|
| 495 |
sub zetaback_log($$;@) { |
|---|
| 496 |
my ($host, $mess, @args) = @_; |
|---|
| 497 |
my $tf = config_get($host, 'time_format'); |
|---|
| 498 |
my $file = config_get($host, 'logfile'); |
|---|
| 499 |
my $fileh; |
|---|
| 500 |
if(defined($file)) { |
|---|
| 501 |
$fileh = IO::File->new(">>$file"); |
|---|
| 502 |
} |
|---|
| 503 |
$fileh ||= IO::File->new(">&STDERR"); |
|---|
| 504 |
printf $fileh "%s: $mess", strftime($tf, localtime(time)), @args; |
|---|
| 505 |
$fileh->close(); |
|---|
| 506 |
} |
|---|
| 507 |
|
|---|
| 508 |
sub zfs_remove_snap($$$) { |
|---|
| 509 |
my ($host, $fs, $snap) = @_; |
|---|
| 510 |
my $agent = config_get($host, 'agent'); |
|---|
| 511 |
my $ssh_config = config_get($host, 'ssh_config'); |
|---|
| 512 |
$ssh_config = "-F $ssh_config" if($ssh_config); |
|---|
| 513 |
print "Using custom ssh config file: $ssh_config\n" if($DEBUG); |
|---|
| 514 |
return unless($snap); |
|---|
| 515 |
print "Dropping $snap on $fs\n" if($DEBUG); |
|---|
| 516 |
`ssh $ssh_config $host $agent -z $fs -d $snap`; |
|---|
| 517 |
} |
|---|
| 518 |
|
|---|
| 519 |
# Lots of args.. internally called. |
|---|
| 520 |
sub zfs_do_backup($$$$$$) { |
|---|
| 521 |
my ($host, $fs, $type, $point, $store, $dumpfile) = @_; |
|---|
| 522 |
my $agent = config_get($host, 'agent'); |
|---|
| 523 |
my $ssh_config = config_get($host, 'ssh_config'); |
|---|
| 524 |
$ssh_config = "-F $ssh_config" if($ssh_config); |
|---|
| 525 |
print "Using custom ssh config file: $ssh_config\n" if($DEBUG); |
|---|
| 526 |
|
|---|
| 527 |
# Do it. yeah. |
|---|
| 528 |
my $cl = config_get($host, 'compressionlevel'); |
|---|
| 529 |
if ($cl >= 1 && $cl <= 9) { |
|---|
| 530 |
open(LBACKUP, "|gzip -$cl >$store/.$dumpfile") || |
|---|
| 531 |
die "zfs_full_backup: cannot create dump\n"; |
|---|
| 532 |
} else { |
|---|
| 533 |
open(LBACKUP, ">$store/.$dumpfile") || |
|---|
| 534 |
die "zfs_full_backup: cannot create dump\n"; |
|---|
| 535 |
} |
|---|
| 536 |
eval { |
|---|
| 537 |
if(my $pid = fork()) { |
|---|
| 538 |
close(LBACKUP); |
|---|
| 539 |
waitpid($pid, 0); |
|---|
| 540 |
die "error: $?" if($?); |
|---|
| 541 |
} |
|---|
| 542 |
else { |
|---|
| 543 |
my @cmd = ('ssh', split(/ /, $ssh_config), $host, $agent, '-z', $fs, "-$type", $point); |
|---|
| 544 |
open STDIN, "/dev/null" || exit(-1); |
|---|
| 545 |
open STDOUT, ">&LBACKUP" || exit(-1); |
|---|
| 546 |
exec { $cmd[0] } @cmd; |
|---|
| 547 |
print STDERR "$cmd[0] failed: $?\n"; |
|---|
| 548 |
exit($?); |
|---|
| 549 |
} |
|---|
| 550 |
die "dump failed (zero bytes)\n" if(-z "$store/.$dumpfile"); |
|---|
| 551 |
rename("$store/.$dumpfile", "$store/$dumpfile") || die "cannot rename dump\n"; |
|---|
| 552 |
}; |
|---|
| 553 |
if($@) { |
|---|
| 554 |
unlink("$store/.$dumpfile"); |
|---|
| 555 |
chomp(my $error = $@); |
|---|
| 556 |
$error =~ s/[\r\n]+/ /gsm; |
|---|
| 557 |
zetaback_log($host, "FAILED[$error] $host:$fs $type\n"); |
|---|
| 558 |
die "zfs_full_backup: failed $@"; |
|---|
| 559 |
} |
|---|
| 560 |
my @st = stat("$store/$dumpfile"); |
|---|
| 561 |
my $size = pretty_size($st[7]); |
|---|
| 562 |
zetaback_log($host, "SUCCESS[$size] $host:$fs $type\n"); |
|---|
| 563 |
} |
|---|
| 564 |
|
|---|
| 565 |
sub zfs_full_backup($$$) { |
|---|
| 566 |
my ($host, $fs, $store) = @_; |
|---|
| 567 |
|
|---|
| 568 |
# Translate into a proper dumpfile nameA |
|---|
| 569 |
my $point = time(); |
|---|
| 570 |
my $efs = dir_encode($fs); |
|---|
| 571 |
my $dumpfile = "$point.$efs.full"; |
|---|
| 572 |
|
|---|
| 573 |
zfs_do_backup($host, $fs, 'f', $point, $store, $dumpfile); |
|---|
| 574 |
} |
|---|
| 575 |
|
|---|
| 576 |
sub zfs_incremental_backup($$$$) { |
|---|
| 577 |
my ($host, $fs, $base, $store) = @_; |
|---|
| 578 |
my $agent = config_get($host, 'agent'); |
|---|
| 579 |
|
|---|
| 580 |
# Translate into a proper dumpfile nameA |
|---|
| 581 |
my $point = time(); |
|---|
| 582 |
my $efs = dir_encode($fs); |
|---|
| 583 |
my $dumpfile = "$point.$efs.incremental.$base"; |
|---|
| 584 |
|
|---|
| 585 |
zfs_do_backup($host, $fs, 'i', $base, $store, $dumpfile); |
|---|
| 586 |
} |
|---|
| 587 |
|
|---|
| 588 |
sub perform_retention($$) { |
|---|
| 589 |
my ($host, $store) = @_; |
|---|
| 590 |
my $cutoff = time() - config_get($host, 'retention'); |
|---|
| 591 |
my $backup_info = scan_for_backups($store); |
|---|
| 592 |
|
|---|
| 593 |
foreach my $disk (sort keys %{$backup_info}) { |
|---|
| 594 |
my $info = $backup_info->{$disk}; |
|---|
| 595 |
next unless(ref($info) eq 'HASH'); |
|---|
| 596 |
my %must_save; |
|---|
| 597 |
|
|---|
| 598 |
# Get a list of all the full and incrementals, sorts newest to oldest |
|---|
| 599 |
my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}}); |
|---|
| 600 |
@backup_points = sort { $b <=> $a } @backup_points; |
|---|
| 601 |
|
|---|
| 602 |
# We _cannot_ throw away _all_ our backups, |
|---|
| 603 |
# so save the most recent incremental and full no matter what |
|---|
| 604 |
$must_save{$backup_points[0]} = 1; |
|---|
| 605 |
my @fulls = grep { exists($info->{full}->{$_}) } @backup_points; |
|---|
| 606 |
$must_save{$fulls[0]} = 1; |
|---|
| 607 |
|
|---|
| 608 |
# Walk the list for backups within our retention period. |
|---|
| 609 |
foreach (@backup_points) { |
|---|
| 610 |
if($_ >= $cutoff) { |
|---|
| 611 |
$must_save{$_} = 1; |
|---|
| 612 |
} |
|---|
| 613 |
else { |
|---|
| 614 |
# they are in decending order, once we miss, all will miss |
|---|
| 615 |
last; |
|---|
| 616 |
} |
|---|
| 617 |
} |
|---|
| 618 |
|
|---|
| 619 |
# Look for dependencies |
|---|
| 620 |
foreach (@backup_points) { |
|---|
| 621 |
if(exists($info->{incremental}->{$_})) { |
|---|
| 622 |
print " => $_ depends on $info->{incremental}->{$_}->{depends}\n" if($DEBUG); |
|---|
| 623 |
$must_save{$info->{incremental}->{$_}->{depends}} = 1 |
|---|
| 624 |
} |
|---|
| 625 |
} |
|---|
| 626 |
my @removals = grep { !exists($must_save{$_}) } @backup_points; |
|---|
| 627 |
if($DEBUG) { |
|---|
| 628 |
my $tf = config_get($host, 'time_format'); |
|---|
| 629 |
print " => I can remove:\n"; |
|---|
| 630 |
foreach (@backup_points) { |
|---|
| 631 |
print " => ". strftime($tf, localtime($_)); |
|---|
| 632 |
print " [". (exists($info->{full}->{$_}) ? "full":"incremental") ."]"; |
|---|
| 633 |
print " XXX" if(!exists($must_save{$_})); |
|---|
| 634 |
print "\n"; |
|---|
| 635 |
} |
|---|
| 636 |
} |
|---|
| 637 |
foreach (@removals) { |
|---|
| 638 |
my $efs = dir_encode($disk); |
|---|
| 639 |
my $filename; |
|---|
| 640 |
if(exists($info->{full}->{$_})) { |
|---|
| 641 |
$filename = "$store/$_.$efs.full"; |
|---|
| 642 |
} |
|---|
| 643 |
elsif(exists($info->{incremental}->{$_})) { |
|---|
| 644 |
$filename = "$store/$_.$efs.incremental.$info->{incremental}->{$_}->{depends}"; |
|---|
| 645 |
} |
|---|
| 646 |
else { |
|---|
| 647 |
print "ERROR: We tried to expunge $host $disk [$_], but couldn't find it.\n"; |
|---|
| 648 |
} |
|---|
| 649 |
print " => expunging $filename\n" if($DEBUG); |
|---|
| 650 |
unless($NEUTERED) { |
|---|
| 651 |
unlink($filename) || print "ERROR: unlink $filename: $?\n"; |
|---|
| 652 |
} |
|---|
| 653 |
} |
|---|
| 654 |
} |
|---|
| 655 |
} |
|---|
| 656 |
|
|---|
| 657 |
sub __default_sort($$) { return $_[0] cmp $_[1]; } |
|---|
| 658 |
|
|---|
| 659 |
sub choose($$;$) { |
|---|
| 660 |
my($name, $obj, $sort) = @_; |
|---|
| 661 |
$sort ||= \&__default_sort;; |
|---|
| 662 |
my @list; |
|---|
| 663 |
my $hash; |
|---|
| 664 |
if(ref $obj eq 'ARRAY') { |
|---|
| 665 |
@list = sort { $sort->($a,$b); } (@$obj); |
|---|
| 666 |
map { $hash->{$_} = $_; } @list; |
|---|
| 667 |
} |
|---|
| 668 |
elsif(ref $obj eq 'HASH') { |
|---|
| 669 |
@list = sort { $sort->($a,$b); } (keys %$obj); |
|---|
| 670 |
$hash = $obj; |
|---|
| 671 |
} |
|---|
| 672 |
else { |
|---|
| 673 |
die "choose passed bad object: " . ref($obj) . "\n"; |
|---|
| 674 |
} |
|---|
| 675 |
return $list[0] if(scalar(@list) == 1); |
|---|
| 676 |
print "\n"; |
|---|
| 677 |
my $i = 1; |
|---|
| 678 |
for (@list) { |
|---|
| 679 |
printf " %3d) $hash->{$_}\n", $i++; |
|---|
| 680 |
} |
|---|
| 681 |
my $selection = 0; |
|---|
| 682 |
while($selection !~ /^\d+$/ or |
|---|
| 683 |
$selection < 1 or |
|---|
| 684 |
$selection >= $i) { |
|---|
| 685 |
print "$name: "; |
|---|
| 686 |
chomp($selection = <>); |
|---|
| 687 |
} |
|---|
| 688 |
return $list[$selection - 1]; |
|---|
| 689 |
} |
|---|
| 690 |
|
|---|
| 691 |
sub backup_chain($$) { |
|---|
| 692 |
my ($info, $ts) = @_; |
|---|
| 693 |
my @list; |
|---|
| 694 |
push @list, $info->{full}->{$ts} if(exists($info->{full}->{$ts})); |
|---|
| 695 |
if(exists($info->{incremental}->{$ts})) { |
|---|
| 696 |
push @list, $info->{incremental}->{$ts}; |
|---|
| 697 |
push @list, backup_chain($info, $info->{incremental}->{$ts}->{depends}); |
|---|
| 698 |
} |
|---|
| 699 |
return @list; |
|---|
| 700 |
} |
|---|
| 701 |
|
|---|
| 702 |
sub perform_restore() { |
|---|
| 703 |
my %source; |
|---|
| 704 |
|
|---|
| 705 |
foreach my $host (grep { $_ ne "default" } keys %conf) { |
|---|
| 706 |
# If -h was specific, we will skip this host if the arg isn't |
|---|
| 707 |
# an exact match or a pattern match |
|---|
| 708 |
if($HOST && |
|---|
| 709 |
!(($HOST eq $host) || |
|---|
| 710 |
($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) { |
|---|
| 711 |
next; |
|---|
| 712 |
} |
|---|
| 713 |
|
|---|
| 714 |
my $store = config_get($host, 'store'); |
|---|
| 715 |
$store =~ s/%h/$host/g;; |
|---|
| 716 |
mkdir $store if(! -d $store); |
|---|
| 717 |
|
|---|
| 718 |
my $backup_info = scan_for_backups($store); |
|---|
| 719 |
foreach my $disk (sort keys %{$backup_info}) { |
|---|
| 720 |
my $info = $backup_info->{$disk}; |
|---|
| 721 |
next unless(ref($info) eq 'HASH'); |
|---|
| 722 |
next |
|---|
| 723 |
if($ZFS && # if the pattern was specified it could |
|---|
| 724 |
!($disk eq $ZFS || # be a specific match or a |
|---|
| 725 |
($ZFS =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex |
|---|
| 726 |
# We want to see this one |
|---|
| 727 |
my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}}); |
|---|
| 728 |
my @source_points; |
|---|
| 729 |
foreach (@backup_points) { |
|---|
| 730 |
push @source_points, $_ if(!$TIMESTAMP || $TIMESTAMP == $_) |
|---|
| 731 |
} |
|---|
| 732 |
if(@source_points) { |
|---|
| 733 |
$source{$host}->{$disk} = \@source_points; |
|---|
| 734 |
} |
|---|
| 735 |
} |
|---|
| 736 |
} |
|---|
| 737 |
|
|---|
| 738 |
if(! keys %source) { |
|---|
| 739 |
print "No matching backups found\n"; |
|---|
| 740 |
return; |
|---|
| 741 |
} |
|---|
| 742 |
|
|---|
| 743 |
# Here goes the possibly interactive dialog |
|---|
| 744 |
my $host = choose("Restore from host", [keys %source]); |
|---|
| 745 |
my $disk = choose("Restore from ZFS", [keys %{$source{$host}}]); |
|---|
| 746 |
|
|---|
| 747 |
# Times are special. We build a human readable form and use a numerical |
|---|
| 748 |
# sort function instead of the default lexical one. |
|---|
| 749 |
my %times; |
|---|
| 750 |
my $tf = config_get($host, 'time_format'); |
|---|
| 751 |
map { $times{$_} = strftime($tf, localtime($_)); } @{$source{$host}->{$disk}}; |
|---|
| 752 |
my $timestamp = choose("Restore as of timestamp", \%times, |
|---|
| 753 |
sub { $_[0] <=> $_[1]; }); |
|---|
| 754 |
|
|---|
| 755 |
my $store = config_get($host, 'store'); |
|---|
| 756 |
$store =~ s/%h/$host/g;; |
|---|
| 757 |
mkdir $store if(! -d $store); |
|---|
| 758 |
my $backup_info = scan_for_backups($store); |
|---|
| 759 |
my @backup_list = reverse backup_chain($backup_info->{$disk}, $timestamp); |
|---|
| 760 |
|
|---|
| 761 |
if(!$RESTORE_HOST) { |
|---|
| 762 |
print "Restore to host [$host]:"; |
|---|
| 763 |
chomp(my $input = <>); |
|---|
| 764 |
$RESTORE_HOST = length($input) ? $input : $host; |
|---|
| 765 |
} |
|---|
| 766 |
if(!$RESTORE_ZFS) { |
|---|
| 767 |
print "Restore to zfs [$disk]:"; |
|---|
| 768 |
chomp(my $input = <>); |
|---|
| 769 |
$RESTORE_ZFS = length($input) ? $input : $disk; |
|---|
| 770 |
} |
|---|
| 771 |
|
|---|
| 772 |
# show intentions |
|---|
| 773 |
print "Going to restore:\n"; |
|---|
| 774 |
print "\tfrom: $host\n"; |
|---|
| 775 |
print "\tfrom: $disk\n"; |
|---|
| 776 |
print "\t at: $timestamp [" . strftime($tf, localtime($timestamp)) . "]\n"; |
|---|
| 777 |
print "\t to: $RESTORE_HOST\n"; |
|---|
| 778 |
print "\t to: $RESTORE_ZFS\n"; |
|---|
| 779 |
print "\n"; |
|---|
| 780 |
|
|---|
| 781 |
foreach(@backup_list) { |
|---|
| 782 |
$_->{success} = zfs_restore_part($RESTORE_HOST, $RESTORE_ZFS, $_->{file}, $_->{depends}); |
|---|
| 783 |
} |
|---|
| 784 |
} |
|---|
| 785 |
|
|---|
| 786 |
sub zfs_restore_part($$$;$) { |
|---|
| 787 |
my ($host, $fs, $file, $dep) = @_; |
|---|
| 788 |
my $ssh_config = config_get($host, 'ssh_config'); |
|---|
| 789 |
$ssh_config = "-F $ssh_config" if($ssh_config); |
|---|
| 790 |
print "Using custom ssh config file: $ssh_config\n" if($DEBUG); |
|---|
| 791 |
my $command; |
|---|
| 792 |
if(exists($conf{$host})) { |
|---|
| 793 |
my $agent = config_get($host, 'agent'); |
|---|
| 794 |
$command = "$agent -r -z $fs"; |
|---|
| 795 |
$command .= " -b $dep" if($dep); |
|---|
| 796 |
} |
|---|
| 797 |
else { |
|---|
| 798 |
$command = "__ZFS__ recv $fs"; |
|---|
| 799 |
} |
|---|
| 800 |
print " => piping $file to $command\n" if($DEBUG); |
|---|
| 801 |
if($NEUTERED) { |
|---|
| 802 |
print "gzip -dfc $file | ssh $ssh_config $host $command\n" if ($DEBUG); |
|---|
| 803 |
} |
|---|
| 804 |
else { |
|---|
| 805 |
open(DUMP, "gzip -dfc $file |"); |
|---|
| 806 |
eval { |
|---|
| 807 |
open(RECEIVER, "| ssh $ssh_config $host $command"); |
|---|
| 808 |
my $buffer; |
|---|
| 809 |
while(my $len = sysread(DUMP, $buffer, $BLOCKSIZE)) { |
|---|
| 810 |
if(syswrite(RECEIVER, $buffer, $len) != $len) { |
|---|
| 811 |
die "$!"; |
|---|
| 812 |
} |
|---|
| 813 |
} |
|---|
| 814 |
}; |
|---|
| 815 |
close(DUMP); |
|---|
| 816 |
close(RECEIVER); |
|---|
| 817 |
} |
|---|
| 818 |
return $?; |
|---|
| 819 |
} |
|---|
| 820 |
|
|---|
| 821 |
sub pretty_print_backup($$$) { |
|---|
| 822 |
my ($info, $host, $point) = @_; |
|---|
| 823 |
my $tf = config_get($host, 'time_format'); |
|---|
| 824 |
print "\t" . strftime($tf, localtime($point)) . " [$point] "; |
|---|
| 825 |
if(exists($info->{full}->{$point})) { |
|---|
| 826 |
my @st = stat($info->{full}->{$point}->{file}); |
|---|
| 827 |
print "FULL " . pretty_size($st[7]); |
|---|
| 828 |
print "\n\tfile: $info->{full}->{$point}->{file}" if($SHOW_FILENAMES); |
|---|
| 829 |
} else { |
|---|
| 830 |
my @st = stat($info->{incremental}->{$point}->{file}); |
|---|
| 831 |
print "INCR from [$info->{incremental}->{$point}->{depends}] " . pretty_size($st[7]); |
|---|
| 832 |
print "\n\tfile: $info->{incremental}->{$point}->{file}" if($SHOW_FILENAMES); |
|---|
| 833 |
} |
|---|
| 834 |
print "\n"; |
|---|
| 835 |
} |
|---|
| 836 |
|
|---|
| 837 |
sub show_backups($$$) { |
|---|
| 838 |
my ($host, $store, $diskpat) = @_; |
|---|
| 839 |
my $backup_info = scan_for_backups($store); |
|---|
| 840 |
my $tf = config_get($host, 'time_format'); |
|---|
| 841 |
my @files; |
|---|
| 842 |
foreach my $disk (sort keys %{$backup_info}) { |
|---|
| 843 |
my $info = $backup_info->{$disk}; |
|---|
| 844 |
next unless(ref($info) eq 'HASH'); |
|---|
| 845 |
next |
|---|
| 846 |
if($diskpat && # if the pattern was specified it could |
|---|
| 847 |
!($disk eq $diskpat || # be a specific match or a |
|---|
| 848 |
($diskpat =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex |
|---|
| 849 |
|
|---|
| 850 |
my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}}); |
|---|
| 851 |
@backup_points = sort { $a <=> $b } @backup_points; |
|---|
| 852 |
@backup_points = (pop @backup_points) unless ($ARCHIVE || $SUMMARY_EXT || $SUMMARY_VIOLATORS); |
|---|
| 853 |
|
|---|
| 854 |
# Quick short-circuit in the case of retention violation checks |
|---|
| 855 |
if($SUMMARY_VIOLATORS) { |
|---|
| 856 |
if(time() > $info->{last_full} + config_get($host, 'full_interval') || |
|---|
| 857 |
time() > $info->{last_backup} + config_get($host, 'backup_interval')) { |
|---|
| 858 |
print "$host:$disk\n"; |
|---|
| 859 |
pretty_print_backup($info, $host, $backup_points[0]); |
|---|
| 860 |
} |
|---|
| 861 |
next; |
|---|
| 862 |
} |
|---|
| 863 |
|
|---|
| 864 |
# We want to see this one |
|---|
| 865 |
print "$host:$disk\n"; |
|---|
| 866 |
next unless($SUMMARY || $SUMMARY_EXT || $ARCHIVE); |
|---|
| 867 |
if($SUMMARY_EXT) { |
|---|
| 868 |
print "\tLast Full: ". ($info->{last_full} ? strftime($tf, localtime($info->{last_full})) : "Never") . "\n"; |
|---|
| 869 |
if($info->{last_full} < $info->{last_incremental}) { |
|---|
| 870 |
print "\tLast Incr: ". strftime($tf, localtime($info->{last_incremental})). "\n"; |
|---|
| 871 |
} |
|---|
| 872 |
} |
|---|
| 873 |
foreach (@backup_points) { |
|---|
| 874 |
pretty_print_backup($info, $host, $_); |
|---|
| 875 |
push @files, exists($info->{full}->{$_}) ? $info->{full}->{$_}->{file} : $info->{incremental}->{$_}->{file}; |
|---|
| 876 |
} |
|---|
| 877 |
print "\n"; |
|---|
| 878 |
} |
|---|
| 879 |
if($ARCHIVE && scalar(@files)) { |
|---|
| 880 |
my $archive = config_get($host, 'archive'); |
|---|
| 881 |
$archive =~ s/%h/$host/g; |
|---|
| 882 |
if(! -d $archive) { |
|---|
| 883 |
mkdir $archive || die "Cannot mkdir($archive)\n"; |
|---|
| 884 |
} |
|---|
| 885 |
print "\nAre you sure you would like to archive ".scalar(@files)." file(s)? "; |
|---|
| 886 |
while(($_ = <>) !~ /(?:y|n|yes|no)$/i) { |
|---|
| 887 |
print "Are you sure you would like to archive ".scalar(@files)." file(s)? "; |
|---|
| 888 |
} |
|---|
| 889 |
if(/^y/i) { |
|---|
| 890 |
foreach my $file (@files) { |
|---|
| 891 |
(my $afile = $file) =~ s/^$store/$archive/; |
|---|
| 892 |
move($file, $afile) || print "Error archiving $file: $!\n"; |
|---|
| 893 |
} |
|---|
| 894 |
} |
|---|
| 895 |
} |
|---|
| 896 |
} |
|---|
| 897 |
|
|---|
| 898 |
sub plan_and_run($$$) { |
|---|
| 899 |
my ($host, $store, $diskpat) = @_; |
|---|
| 900 |
my $ssh_config = config_get($host, 'ssh_config'); |
|---|
| 901 |
$ssh_config = "-F $ssh_config" if($ssh_config); |
|---|
| 902 |
my %suppress; |
|---|
| 903 |
print "Planning '$host'\n" if($DEBUG); |
|---|
| 904 |
my $agent = config_get($host, 'agent'); |
|---|
| 905 |
my $took_action = 1; |
|---|
| 906 |
while($took_action) { |
|---|
| 907 |
$took_action = 0; |
|---|
| 908 |
my @disklist; |
|---|
| 909 |
|
|---|
| 910 |
# We need a lock for the listing. |
|---|
| 911 |
return unless(lock($host, ".list")); |
|---|
| 912 |
open(SILENT, ">&", \*STDERR); |
|---|
| 913 |
close(STDERR); |
|---|
| 914 |
my $rv = open(ZFSLIST, "ssh $ssh_config $host $agent -l |"); |
|---|
| 915 |
open(STDERR, ">&", \*SILENT); |
|---|
| 916 |
close(SILENT); |
|---|
| 917 |
next unless $rv; |
|---|
| 918 |
@disklist = grep { chomp } (<ZFSLIST>); |
|---|
| 919 |
close(ZFSLIST); |
|---|
| 920 |
|
|---|
| 921 |
foreach my $diskline (@disklist) { |
|---|
| 922 |
chomp($diskline); |
|---|
| 923 |
next unless($diskline =~ /^(\S+) \[([^\]]*)\]/); |
|---|
| 924 |
my $diskname = $1; |
|---|
| 925 |
my %snaps; |
|---|
| 926 |
map { $snaps{$_} = 1 } (split(/,/, $2)); |
|---|
| 927 |
|
|---|
| 928 |
# We've just done this. |
|---|
| 929 |
next if($suppress{"$host:$diskname"}); |
|---|
| 930 |
# If we are being selective (via -z) now is the time. |
|---|
| 931 |
next |
|---|
| 932 |
if($diskpat && # if the pattern was specified it could |
|---|
| 933 |
!($diskname eq $diskpat || # be a specific match or a |
|---|
| 934 |
($diskpat =~ /^\/(.+)\/$/ && $diskname =~ /$1/))); # regex |
|---|
| 935 |
|
|---|
| 936 |
print " => Scanning '$store' for old backups of '$diskname'.\n" if($DEBUG); |
|---|
| 937 |
|
|---|
| 938 |
# Make directory on demand |
|---|
| 939 |
my $backup_info = scan_for_backups($store); |
|---|
| 940 |
# That gave us info on all backups, we just want this disk |
|---|
| 941 |
$backup_info = $backup_info->{$diskname} || {}; |
|---|
| 942 |
|
|---|
| 943 |
# Should we do a backup? |
|---|
| 944 |
my $backup_type = 'no'; |
|---|
| 945 |
if(time() > $backup_info->{last_backup} + config_get($host, 'backup_interval')) { |
|---|
| 946 |
$backup_type = 'incremental'; |
|---|
| 947 |
} |
|---|
| 948 |
if(time() > $backup_info->{last_full} + config_get($host, 'full_interval')) { |
|---|
| 949 |
$backup_type = 'full'; |
|---|
| 950 |
} |
|---|
| 951 |
|
|---|
| 952 |
# If we want an incremental, but have no full, then we need to upgrade to full |
|---|
| 953 |
if($backup_type eq 'incremental') { |
|---|
| 954 |
my $have_full_locally = 0; |
|---|
| 955 |
# For each local full backup, see if the full backup still exists on the other end. |
|---|
| 956 |
foreach (keys %{$backup_info->{'full'}}) { |
|---|
| 957 |
$have_full_locally = 1 if(exists($snaps{'__zb_full_' . $_})); |
|---|
| 958 |
} |
|---|
| 959 |
$backup_type = 'full' unless($have_full_locally); |
|---|
| 960 |
} |
|---|
| 961 |
$backup_type = 'full' if($FORCE_FULL); |
|---|
| 962 |
$backup_type = 'incremental' if($FORCE_INC); |
|---|
| 963 |
|
|---|
| 964 |
print " => doing $backup_type backup\n" if($DEBUG); |
|---|
| 965 |
# We need to drop a __zb_base snap or a __zb_incr snap before we proceed |
|---|
| 966 |
unless($NEUTERED || $backup_type eq 'no') { |
|---|
| 967 |
# attempt to lock this action, if it fails, skip -- someone else is working it. |
|---|
| 968 |
next unless(lock($host, dir_encode($diskname), 1)); |
|---|
| 969 |
unlock($host, '.list'); |
|---|
| 970 |
|
|---|
| 971 |
if($backup_type eq 'full') { |
|---|
| 972 |
eval { zfs_full_backup($host, $diskname, $store); }; |
|---|
| 973 |
if ($@) { |
|---|
| 974 |
chomp(my $err = $@); |
|---|
| 975 |
print " => failure $err\n"; |
|---|
| 976 |
} |
|---|
| 977 |
else { |
|---|
| 978 |
# Unless there was an error backing up, remove all the other full snaps |
|---|
| 979 |
foreach (keys %snaps) { |
|---|
| 980 |
zfs_remove_snap($host, $diskname, $_) if(/^__zb_full_(\d+)/) |
|---|
| 981 |
} |
|---|
| 982 |
} |
|---|
| 983 |
$took_action = 1; |
|---|
| 984 |
} |
|---|
| 985 |
if($backup_type eq 'incremental') { |
|---|
| 986 |
eval { |
|---|
| 987 |
zfs_remove_snap($host, $diskname, '__zb_incr') if($snaps{'__zb_incr'}); |
|---|
| 988 |
# Find the newest full from which to do an incremental (NOTE: reverse numeric sort) |
|---|
| 989 |
my @fulls = sort { $b <=> $a } (keys %{$backup_info->{'full'}}); |
|---|
| 990 |
zfs_incremental_backup($host, $diskname, $fulls[0], $store); |
|---|
| 991 |
}; |
|---|
| 992 |
if ($@) { |
|---|
| 993 |
chomp(my $err = $@); |
|---|
| 994 |
print " => failure $err\n"; |
|---|
| 995 |
} |
|---|
| 996 |
else { |
|---|
| 997 |
$took_action = 1; |
|---|
| 998 |
} |
|---|
| 999 |
} |
|---|
| 1000 |
unlock($host, dir_encode($diskname), 1); |
|---|
| 1001 |
} |
|---|
| 1002 |
$suppress{"$host:$diskname"} = 1; |
|---|
| 1003 |
last if($took_action); |
|---|
| 1004 |
} |
|---|
| 1005 |
unlock($host, '.list'); |
|---|
| 1006 |
} |
|---|
| 1007 |
} |
|---|
| 1008 |
|
|---|
| 1009 |
if($RESTORE) { |
|---|
| 1010 |
perform_restore(); |
|---|
| 1011 |
} |
|---|
| 1012 |
else { |
|---|
| 1013 |
foreach my $host (grep { $_ ne "default" } keys %conf) { |
|---|
| 1014 |
# If -h was specific, we will skip this host if the arg isn't |
|---|
| 1015 |
# an exact match or a pattern match |
|---|
| 1016 |
if($HOST && |
|---|
| 1017 |
!(($HOST eq $host) || |
|---|
| 1018 |
($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) { |
|---|
| 1019 |
next; |
|---|
| 1020 |
} |
|---|
| 1021 |
|
|---|
| 1022 |
my $store = config_get($host, 'store'); |
|---|
| 1023 |
$store =~ s/%h/$host/g;; |
|---|
| 1024 |
mkdir $store if(! -d $store); |
|---|
| 1025 |
|
|---|
| 1026 |
if($LIST || $SUMMARY || $SUMMARY_EXT || $SUMMARY_VIOLATORS || $ARCHIVE) { |
|---|
| 1027 |
show_backups($host, $store, $ZFS); |
|---|
| 1028 |
} |
|---|
| 1029 |
if($BACKUP) { |
|---|
| 1030 |
plan_and_run($host, $store, $ZFS); |
|---|
| 1031 |
} |
|---|
| 1032 |
if($EXPUNGE) { |
|---|
| 1033 |
perform_retention($host, $store); |
|---|
| 1034 |
} |
|---|
| 1035 |
} |
|---|
| 1036 |
} |
|---|
| 1037 |
|
|---|
| 1038 |
exit 0; |
|---|
| 1039 |
|
|---|
| 1040 |
=pod |
|---|
| 1041 |
|
|---|
| 1042 |
=head1 FILES |
|---|
| 1043 |
|
|---|
| 1044 |
=over |
|---|
| 1045 |
|
|---|
| 1046 |
=item zetaback.conf |
|---|
| 1047 |
|
|---|
| 1048 |
The main zetaback configuration file. The location of the file can be |
|---|
| 1049 |
specified on the command line with the -c flag. The prefix of this |
|---|
| 1050 |
file may also be specified as an argument to the configure script. |
|---|
| 1051 |
|
|---|
| 1052 |
=back |
|---|
| 1053 |
|
|---|
| 1054 |
=head1 SEE ALSO |
|---|
| 1055 |
|
|---|
| 1056 |
zetaback_agent(1) |
|---|
| 1057 |
|
|---|
| 1058 |
=cut |
|---|