root/zetaback

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

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