root/zetaback

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

closes #5

  • 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 sub parse_config() {
201   local($/);
202   $/ = undef;
203   open(CONF, "<$CONF");
204   my $file = <CONF>;
205   while($file =~ m/^\s*(\S+)\s\s*{(.*?)}/gms) {
206     my $scope = $1;
207     my $filepart = $2;
208     $conf{$scope} ||= {};
209     foreach my $line (split /\n/, $filepart) {
210       if($line =~ /^\s*([^#]\S*)\s*=\s*(\S+)/) {
211         $conf{$scope}->{lc($1)} = $2;
212       }
213     }
214   }
215   close(CONF);
216 }
217 sub config_get($$) {
218   return $conf{$_[0]}->{$_[1]} || $conf{'default'}->{$_[1]};
219 }
220
221 sub dir_encode($) {
222   my $d = shift;
223   my $e = encode_base64($d, '');
224   $e =~ s/\//_/;
225   return $e;
226 }
227 sub dir_decode($) {
228   my $e = shift;
229   $e =~ s/_/\//;
230   return decode_base64($e);
231 }
232 sub pretty_size($) {
233   my $bytes = shift;
234   if($bytes > 1024*1024*1024) {
235     return sprintf("%0.2f Gb", $bytes / (1024*1024*1024));
236   }
237   if($bytes > 1024*1024) {
238     return sprintf("%0.2f Mb", $bytes / (1024*1024));
239   }
240   if($bytes > 1024) {
241     return sprintf("%0.2f Kb", $bytes / (1024));
242   }
243   return "$bytes b";
244 }
245 sub scan_for_backups($) {
246   my %info = ();
247   my $dir = shift;
248   $info{last_full} = $info{last_incremental} = $info{last_backup} = 0;
249   opendir(D, $dir) || return \%info;
250   foreach my $file (readdir(D)) {
251     if($file =~ /^(\d+)\.([^\.]+)\.full$/) {
252       my $whence = $1;
253       my $fs = dir_decode($2);
254       $info{$fs}->{full}->{$whence}->{'file'} = "$dir/$file";
255       $info{$fs}->{last_full} = $whence if($whence > $info{$fs}->{last_full});
256       $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
257                                      $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
258     }
259     elsif($file =~ /^(\d+).([^\.]+)\.incremental.(\d+)$/) {
260       my $whence = $1;
261       my $fs = dir_decode($2);
262       $info{$fs}->{incremental}->{$whence}->{'depends'} = $3;
263       $info{$fs}->{incremental}->{$whence}->{'file'} = "$dir/$file";
264       $info{$fs}->{last_incremental} = $whence if($whence > $info{$fs}->{last_incremental});
265       $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
266                                      $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
267     }
268   }
269   closedir(D);
270   return \%info;
271 }
272
273 parse_config();
274
275 sub zfs_remove_snap($$$) {
276   my ($host, $fs, $snap) = @_;
277   my $agent = config_get($host, 'agent');
278   return unless($snap);
279   print "Dropping $snap on $fs\n" if($DEBUG);
280   `ssh $host $agent -z $fs -d $snap`;
281 }
282
283 # Lots of args.. internally called.
284 sub zfs_do_backup($$$$$$) {
285   my ($host, $fs, $type, $point, $store, $dumpfile) = @_;
286   my $agent = config_get($host, 'agent');
287
288   # Do it. yeah.
289   open(LBACKUP, ">$store/.$dumpfile") || die "zfs_full_backup: cannot create dump\n";
290   eval {
291     open(RBACKUP, "ssh $host $agent -z $fs -$type $point |") || die "zfs_full_backup: cannot perform send\n";
292     my $buffer;
293     while(my $len = sysread(RBACKUP, $buffer, $BLOCKSIZE)) {
294       if(syswrite(LBACKUP, $buffer, $len) != $len) {
295         die "$!";
296       }
297     }
298     close(LBACKUP);
299     close(RBACKUP);
300     die "dump failed (zero bytes)\n" if(-z "$store/.$dumpfile");
301     rename("$store/.$dumpfile", "$store/$dumpfile") || die "cannot rename dump\n";
302   };
303   if($@) {
304     unlink("$store/.$dumpfile");
305     die "zfs_full_backup: failed $@";
306   }
307 }
308
309 sub zfs_full_backup($$$) {
310   my ($host, $fs, $store) = @_;
311
312   # Translate into a proper dumpfile nameA
313   my $point = time();
314   my $efs = dir_encode($fs);
315   my $dumpfile = "$point.$efs.full";
316
317   zfs_do_backup($host, $fs, 'f', $point, $store, $dumpfile);
318 }
319
320 sub zfs_incremental_backup($$$$) {
321   my ($host, $fs, $base, $store) = @_;
322   my $agent = config_get($host, 'agent');
323
324   # Translate into a proper dumpfile nameA
325   my $point = time();
326   my $efs = dir_encode($fs);
327   my $dumpfile = "$point.$efs.incremental.$base";
328
329   zfs_do_backup($host, $fs, 'i', $base, $store, $dumpfile);
330 }
331
332 sub perform_retention($$) {
333   my ($host, $store) = @_;
334   my $cutoff = time() - config_get($host, 'retention');
335   my $backup_info = scan_for_backups($store);
336  
337   foreach my $disk (sort keys %{$backup_info}) {
338     my $info = $backup_info->{$disk};
339     next unless(ref($info) eq 'HASH');
340     my %must_save;
341
342     # Get a list of all the full and incrementals, sorts newest to oldest
343     my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
344     @backup_points = sort { $b <=> $a } @backup_points;
345
346     # We _cannot_ throw away _all_ out backups, so save the most recent no matter what
347     $must_save{$backup_points[0]} = 1;
348
349     # Walk the list for backups within our retention period.
350     foreach (@backup_points) {
351       if($_ >= $cutoff) {
352         $must_save{$_} = 1;
353       }
354       else {
355         # they are in decending order, once we miss, all will miss
356         last;
357       }
358     }
359
360     # Look for dependencies
361     foreach (@backup_points) {
362       if(exists($info->{incremental}->{$_})) {
363         print "   => $_ depends on $info->{incremental}->{$_}->{depends}\n" if($DEBUG);
364         $must_save{$info->{incremental}->{$_}} = 1
365       }
366     }
367     my @removals = grep { !exists($must_save{$_}) } @backup_points;
368     if($DEBUG) {
369       my $tf = config_get($host, 'time_format');
370       print "    => I can remove:\n";
371       foreach (@backup_points) {
372         print "      => ". strftime($tf, localtime($_));
373         print " [". (exists($info->{full}->{$_}) ? "full":"incremental") ."]";
374         print " XXX" if(!exists($must_save{$_}));
375         print "\n";
376       }
377     }
378     foreach (@removals) {
379       my $efs = dir_encode($disk);
380       my $filename;
381       if(exists($info->{full}->{$_})) {
382         $filename = "$store/$_.$efs.full";
383       }
384       elsif(exists($info->{incremental}->{$_})) {
385         $filename = "$store/$_.$efs.incremental.$info->{incremental}->{$_}->{depends}";
386       }
387       else {
388         print "ERROR: We tried to expunge $host $disk [$_], but couldn't find it.\n";
389       }
390       print "    => expunging $filename\n" if($DEBUG);
391       unless($NUETERED) {
392         unlink($filename) || print "ERROR: unlink $filename: $?\n";
393       }
394     }
395   }
396 }
397
398 sub __default_sort($$) { return $_[0] cmp $_[1]; }
399    
400 sub choose($$;$) {
401   my($name, $obj, $sort) = @_;
402   $sort ||= \&__default_sort;;
403   my @list;
404   my $hash;
405   if(ref $obj eq 'ARRAY') {
406     @list = sort { $sort->($a,$b); } (@$obj);
407     map { $hash->{$_} = $_; } @list;
408   }
409   elsif(ref $obj eq 'HASH') {
410     @list = sort { $sort->($a,$b); } (keys %$obj);
411     $hash = $obj;
412   }
413   else {
414     die "choose passed bad object: " . ref($obj) . "\n";
415   }
416   return $list[0] if(scalar(@list) == 1);
417   print "\n";
418   my $i = 1;
419   for (@list) {
420     printf " %3d) $hash->{$_}\n", $i++;
421   }
422   my $selection = 0;
423   while($selection !~ /^\d+$/ or
424         $selection < 1 or
425         $selection >= $i) {
426     print "$name: ";
427     chomp($selection = <>);
428   }
429   return $hash->{$list[$selection - 1]};
430 }
431
432 sub backup_chain($$) {
433   my ($info, $ts) = @_;
434   my @list;
435   push @list, $info->{full}->{$ts} if(exists($info->{full}->{$ts}));
436   if(exists($info->{incremental}->{$ts})) {
437     push @list, $info->{incremental}->{$ts};
438     push @list, backup_chain($info, $info->{incremental}->{$ts}->{depends});
439   }
440   return @list;
441 }
442
443 sub perform_restore() {
444   my %source;
445
446   foreach my $host (grep { $_ ne "default" } keys %conf) {
447     # If -h was specific, we will skip this host if the arg isn't
448     # an exact match or a pattern match
449     if($HOST &&
450        !(($HOST eq $host) ||
451          ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
452       next;
453     }
454
455     my $store = config_get($host, 'store');
456     $store =~ s/%h/$host/g;;
457     mkdir $store if(! -d $store);
458
459     my $backup_info = scan_for_backups($store);
460     foreach my $disk (sort keys %{$backup_info}) {
461       my $info = $backup_info->{$disk};
462       next unless(ref($info) eq 'HASH');
463       next
464         if($ZFS &&      # if the pattern was specified it could
465            !($disk eq $ZFS ||        # be a specific match or a
466              ($ZFS =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
467       # We want to see this one
468       my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
469       my @source_points;
470       foreach (@backup_points) {
471         push @source_points, $_ if(!$TIMESTAMP || $TIMESTAMP == $_)
472       }
473       if(@source_points) {
474         $source{$host}->{$disk} = \@source_points;
475       }
476     }
477   }
478
479   if(! keys %source) {
480     print "No matching backups found\n";
481     return;
482   }
483
484   # Here goes the possibly interactive dialog
485   my $host = choose("Restore from host",  [keys %source]);
486   my $disk = choose("Restore from ZFS", [keys %{$source{$host}}]);
487  
488   # Times are special.  We build a human readable form and use a numerical
489   # sort function instead of the default lexical one.
490   my %times;
491   my $tf = config_get($host, 'time_format');
492   map { $times{$_} = strftime($tf, localtime($_)); } @{$source{$host}->{$disk}};
493   my $timestamp = choose("Restore as of timestamp", \%times,
494                          sub { $_[0] <=> $_[1]; });
495
496   my $store = config_get($host, 'store');
497   $store =~ s/%h/$host/g;;
498   mkdir $store if(! -d $store);
499   my $backup_info = scan_for_backups($store);
500   my @backup_list = reverse backup_chain($backup_info->{$disk}, $timestamp);
501
502   if(!$RESTORE_HOST) {
503     print "Restore to host [$host]:";
504     chomp(my $input = <>);
505     $RESTORE_HOST = length($input) ? $input : $host;
506   }
507   if(!$RESTORE_ZFS) {
508     print "Restore to zfs [$disk]:";
509     chomp(my $input = <>);
510     $RESTORE_ZFS = length($input) ? $input : $disk;
511   }
512
513   # show intentions
514   print "Going to restore:\n";
515   print "\tfrom: $host\n";
516   print "\tfrom: $disk\n";
517   print "\t  at: $timestamp [" . strftime($tf, localtime($timestamp)) . "]\n";
518   print "\t  to: $RESTORE_HOST\n";
519   print "\t  to: $RESTORE_ZFS\n";
520   print "\n";
521
522   foreach(@backup_list) {
523     $_->{success} = zfs_restore_part($RESTORE_HOST, $RESTORE_ZFS, $_->{file});
524   }
525 }
526
527 sub zfs_restore_part($$$) {
528   my ($host, $fs, $file) = @_;
529   my $command;
530   if(exists($conf{$host})) {
531     my $agent = config_get($host, 'agent');
532     $command = "$agent -r -z $fs";
533   }
534   else {
535     $command = "/usr/sbin/zfs recv $fs";
536   }
537   print " => piping $file to $command\n" if($DEBUG);
538   unless($NUETERED) {
539     open(DUMP, "gzip -dfc $file |");
540     eval {
541       open(RECEIVER, "| ssh $host $command");
542       my $buffer;
543       while(my $len = sysread(DUMP, $buffer, $BLOCKSIZE)) {
544         if(syswrite(RECEIVER, $buffer, $len) != $len) {
545           die "$!";
546         }
547       }
548     };
549     close(DUMP);
550     close(RECEIVER);
551   }
552   return $?;
553 }
554
555 sub show_backups($$$) {
556   my ($host, $store, $diskpat) = @_;
557   my $backup_info = scan_for_backups($store);
558   my $tf = config_get($host, 'time_format');
559   foreach my $disk (sort keys %{$backup_info}) {
560     my $info = $backup_info->{$disk};
561     next unless(ref($info) eq 'HASH');
562     next
563       if($diskpat &&      # if the pattern was specified it could
564          !($disk eq $diskpat ||        # be a specific match or a
565            ($diskpat =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
566     # We want to see this one
567     print "$host:$disk\n";
568     next unless($SUMMARY || $SUMMARY_EXT);
569     if($SUMMARY_EXT) {
570       print "\tLast Full: ". ($info->{last_full} ? strftime($tf, localtime($info->{last_full})) : "Never") . "\n";
571       if($info->{last_full} < $info->{last_incremental}) {
572         print "\tLast Incr: ". strftime($tf, localtime($info->{last_incremental})). "\n";
573       }
574     }
575     my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
576     @backup_points = sort { $a <=> $b } @backup_points;
577     unless ($SUMMARY_EXT) {
578       @backup_points = (pop @backup_points);
579     }
580     foreach (@backup_points) {
581       print "\t" . strftime($tf, localtime($_)) . " [$_] ";
582       if(exists($info->{full}->{$_})) {
583         my @st = stat($info->{full}->{$_}->{file});
584         print "FULL " . pretty_size($st[7]);
585       } else {
586         my @st = stat($info->{incremental}->{$_}->{file});
587         print "INCR from [$info->{incremental}->{$_}->{depends}] " . pretty_size($st[7]);
588       }
589       print "\n";
590     }
591     print "\n";
592   }
593 }
594
595 sub plan_and_run($$$) {
596   my ($host, $store, $diskpat) = @_;
597   print "Planning '$host'\n" if($DEBUG);
598   my $agent = config_get($host, 'agent');
599   open(ZFSLIST, "ssh $host $agent -l |") || next;
600   foreach my $diskline (<ZFSLIST>) {
601     chomp($diskline);
602     next unless($diskline =~ /^(\S+) \[([^\]]*)\]/);
603     my $diskname = $1;
604     my %snaps;
605     map { $snaps{$_} = 1 } (split(/,/, $2));
606
607     # If we are being selective (via -z) now is the time.
608     next
609       if($diskpat &&          # if the pattern was specified it could
610          !($diskname eq $diskpat ||        # be a specific match or a
611            ($diskpat =~ /^\/(.+)\/$/ && $diskname =~ /$1/))); # regex
612
613     print " => Scanning '$store' for old backups of '$diskname'.\n" if($DEBUG);
614     # Make directory on demand
615     my $backup_info = scan_for_backups($store);
616     # That gave us info on all backups, we just want this disk
617     $backup_info = $backup_info->{$diskname} || {};
618
619     # Should we do a backup?
620     my $backup_type = 'no';
621     if(time() > $backup_info->{last_backup} + config_get($host, 'backup_interval')) {
622       $backup_type = 'incremental';
623     }
624     if(time() > $backup_info->{last_full} + config_get($host, 'full_interval')) {
625       $backup_type = 'full';
626     }
627
628     # If we want an incremental, but have no full, then we need to upgrade to full
629     if($backup_type eq 'incremental') {
630       my $have_full_locally = 0;
631       # For each local full backup, see if the full backup still exists on the other end.
632       foreach (keys %{$backup_info->{'full'}}) {
633         $have_full_locally = 1 if(exists($snaps{'__zb_full_' . $_}));
634       }
635       $backup_type = 'full' unless($have_full_locally);
636     }
637
638     print " => doing $backup_type backup\n" if($DEBUG);
639     # We need to drop a __zb_base snap or a __zb_incr snap before we proceed
640     unless($NUETERED) {
641       if($backup_type eq 'full') {
642         eval { zfs_full_backup($host, $diskname, $store); };
643         if ($@) {
644           chomp(my $err = $@);
645           print " => failure $err\n";
646         }
647         else {
648           # Unless there was an error backing up, remove all the other full snaps
649           foreach (keys %snaps) {
650             zfs_remove_snap($host, $diskname, $_) if(/^__zb_full_(\d+)/)
651           }
652         }
653       }
654       if($backup_type eq 'incremental') {
655         zfs_remove_snap($host, $diskname, '__zb_incr') if($snaps{'__zb_incr'});
656         # Find the newest full from which to do an incremental (NOTE: reverse numeric sort)
657         my @fulls = sort { $b <=> $a } (keys %{$backup_info->{'full'}});
658         zfs_incremental_backup($host, $diskname, $fulls[0], $store);
659       }
660     }
661   }
662   close(ZFSLIST);
663 }
664
665 if($RESTORE) {
666   perform_restore();
667 }
668 else {
669   foreach my $host (grep { $_ ne "default" } keys %conf) {
670     # If -h was specific, we will skip this host if the arg isn't
671     # an exact match or a pattern match
672     if($HOST &&
673        !(($HOST eq $host) ||
674          ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
675       next;
676     }
677  
678     my $store = config_get($host, 'store');
679     $store =~ s/%h/$host/g;;
680     mkdir $store if(! -d $store);
681  
682     if($LIST || $SUMMARY || $SUMMARY_EXT) {
683       show_backups($host, $store, $ZFS);
684     }
685     if($BACKUP) {
686       plan_and_run($host, $store, $ZFS);
687     }
688     if($EXPUNGE) {
689       perform_retention($host, $store);
690     }
691   }
692 }
693
694 exit 0;
695
696 =pod
697
698 =head1 FILES
699
700 =over
701
702 =item /etc/zetaback.conf
703
704 The main zetaback configuration file.  The location of the file can be
705 specified on the command line with the -c flag.
706
707 =back
708
709 =head1 SEE ALSO
710
711 zetaback.conf(5), zetaback_agent(1), zetback_agent.conf(5)
712
713 =cut
Note: See TracBrowser for help on using the browser.