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