root/zetaback

Revision 0ca27bd34eb64aac08058f459315eeaa5ec227b7, 23.1 kB (checked in by Eric Sproul <esproul@omniti.com>, 7 years ago)

Config file documentation. Refs #2

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