root/zetaback.in

Revision 53f08ad11972b3da0c57137e8af5c5b35caa41fe, 28.1 kB (checked in by Theo Schlossnagle <jesus@omniti.com>, 10 years ago)

closes #15

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