root/zetaback.in

Revision 3a5618e3083b95ce7bb8dc12bdd2ab67b5538762, 32.0 kB (checked in by Eric Sproul <esproul@omniti.com>, 5 years ago)

Full path not appropriate here

  • Property mode set to 100755
Line 
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
Note: See TracBrowser for help on using the browser.