root/zetaback.in

Revision 73753d8151fa641351c9c813595feeb75aaf3520, 29.7 kB (checked in by Theo Schlossnagle <jesus@omniti.com>, 7 years ago)

always explicitly retain the last full, closes #14

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