root/zetaback

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

Add a -b option to the agent to assist working around this bug.
In the master process, if the application has a dependency, invoke the
agent with the -b option to tell it to attempt to work around the issue.

fixes #9

  • 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 # Make the parser more formal:
302 # config => stanza*
303 # stanza => string { kvp* }
304 # kvp    => string = string
305 my $str_re = qr/(?:"(?:\\\\|\\"|[^"])*"|\S+)/;
306 my $kvp_re = qr/($str_re)\s*=\s*($str_re)/;
307 my $stanza_re = qr/($str_re)\s*\{((?:\s*$kvp_re)*)\s*\}/;
308
309 sub parse_config() {
310   local($/);
311   $/ = undef;
312   open(CONF, "<$CONF");
313   my $file = <CONF>;
314   # Rip comments
315   $file =~ s/^\s*#.*$//mg;
316   while($file =~ m/$stanza_re/gm) {
317     my $scope = $1;
318     my $filepart = $2;
319     $scope =~ s/^"(.*)"$/$1/;
320     $conf{$scope} ||= {};
321     while($filepart =~ m/$kvp_re/gm) {
322       my $key = $1;
323       my $value = $2;
324       $key =~ s/^"(.*)"$/$1/;
325       $value =~ s/^"(.*)"$/$1/;
326       $conf{$scope}->{lc($key)} = $value;
327     }
328   }
329   close(CONF);
330 }
331 sub config_get($$) {
332   return $conf{$_[0]}->{$_[1]} || $conf{'default'}->{$_[1]};
333 }
334
335 sub dir_encode($) {
336   my $d = shift;
337   my $e = encode_base64($d, '');
338   $e =~ s/\//_/;
339   return $e;
340 }
341 sub dir_decode($) {
342   my $e = shift;
343   $e =~ s/_/\//;
344   return decode_base64($e);
345 }
346 sub pretty_size($) {
347   my $bytes = shift;
348   if($bytes > 1024*1024*1024) {
349     return sprintf("%0.2f Gb", $bytes / (1024*1024*1024));
350   }
351   if($bytes > 1024*1024) {
352     return sprintf("%0.2f Mb", $bytes / (1024*1024));
353   }
354   if($bytes > 1024) {
355     return sprintf("%0.2f Kb", $bytes / (1024));
356   }
357   return "$bytes b";
358 }
359 sub scan_for_backups($) {
360   my %info = ();
361   my $dir = shift;
362   $info{last_full} = $info{last_incremental} = $info{last_backup} = 0;
363   opendir(D, $dir) || return \%info;
364   foreach my $file (readdir(D)) {
365     if($file =~ /^(\d+)\.([^\.]+)\.full$/) {
366       my $whence = $1;
367       my $fs = dir_decode($2);
368       $info{$fs}->{full}->{$whence}->{'file'} = "$dir/$file";
369       $info{$fs}->{last_full} = $whence if($whence > $info{$fs}->{last_full});
370       $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
371                                      $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
372     }
373     elsif($file =~ /^(\d+).([^\.]+)\.incremental.(\d+)$/) {
374       my $whence = $1;
375       my $fs = dir_decode($2);
376       $info{$fs}->{incremental}->{$whence}->{'depends'} = $3;
377       $info{$fs}->{incremental}->{$whence}->{'file'} = "$dir/$file";
378       $info{$fs}->{last_incremental} = $whence if($whence > $info{$fs}->{last_incremental});
379       $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
380                                      $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
381     }
382   }
383   closedir(D);
384   return \%info;
385 }
386
387 parse_config();
388
389 sub zfs_remove_snap($$$) {
390   my ($host, $fs, $snap) = @_;
391   my $agent = config_get($host, 'agent');
392   return unless($snap);
393   print "Dropping $snap on $fs\n" if($DEBUG);
394   `ssh $host $agent -z $fs -d $snap`;
395 }
396
397 # Lots of args.. internally called.
398 sub zfs_do_backup($$$$$$) {
399   my ($host, $fs, $type, $point, $store, $dumpfile) = @_;
400   my $agent = config_get($host, 'agent');
401
402   # Do it. yeah.
403   open(LBACKUP, ">$store/.$dumpfile") || die "zfs_full_backup: cannot create dump\n";
404   eval {
405     if(my $pid = fork()) {
406       close(LBACKUP);
407       waitpid($pid, 0);
408       die "error: $?" if($?);
409     }
410     else {
411       my @cmd = ('ssh', $host, $agent, '-z', $fs, "-$type", $point);
412       open STDIN, "/dev/null" || exit(-1);
413       open STDOUT, ">&LBACKUP" || exit(-1);
414       exec { $cmd[0] } @cmd;
415       print STDERR "$cmd[0] failed: $?\n";
416       exit($?);
417     }
418     die "dump failed (zero bytes)\n" if(-z "$store/.$dumpfile");
419     rename("$store/.$dumpfile", "$store/$dumpfile") || die "cannot rename dump\n";
420   };
421   if($@) {
422     unlink("$store/.$dumpfile");
423     die "zfs_full_backup: failed $@";
424   }
425 }
426
427 sub zfs_full_backup($$$) {
428   my ($host, $fs, $store) = @_;
429
430   # Translate into a proper dumpfile nameA
431   my $point = time();
432   my $efs = dir_encode($fs);
433   my $dumpfile = "$point.$efs.full";
434
435   zfs_do_backup($host, $fs, 'f', $point, $store, $dumpfile);
436 }
437
438 sub zfs_incremental_backup($$$$) {
439   my ($host, $fs, $base, $store) = @_;
440   my $agent = config_get($host, 'agent');
441
442   # Translate into a proper dumpfile nameA
443   my $point = time();
444   my $efs = dir_encode($fs);
445   my $dumpfile = "$point.$efs.incremental.$base";
446
447   zfs_do_backup($host, $fs, 'i', $base, $store, $dumpfile);
448 }
449
450 sub perform_retention($$) {
451   my ($host, $store) = @_;
452   my $cutoff = time() - config_get($host, 'retention');
453   my $backup_info = scan_for_backups($store);
454  
455   foreach my $disk (sort keys %{$backup_info}) {
456     my $info = $backup_info->{$disk};
457     next unless(ref($info) eq 'HASH');
458     my %must_save;
459
460     # Get a list of all the full and incrementals, sorts newest to oldest
461     my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
462     @backup_points = sort { $b <=> $a } @backup_points;
463
464     # We _cannot_ throw away _all_ out backups, so save the most recent no matter what
465     $must_save{$backup_points[0]} = 1;
466
467     # Walk the list for backups within our retention period.
468     foreach (@backup_points) {
469       if($_ >= $cutoff) {
470         $must_save{$_} = 1;
471       }
472       else {
473         # they are in decending order, once we miss, all will miss
474         last;
475       }
476     }
477
478     # Look for dependencies
479     foreach (@backup_points) {
480       if(exists($info->{incremental}->{$_})) {
481         print "   => $_ depends on $info->{incremental}->{$_}->{depends}\n" if($DEBUG);
482         $must_save{$info->{incremental}->{$_}} = 1
483       }
484     }
485     my @removals = grep { !exists($must_save{$_}) } @backup_points;
486     if($DEBUG) {
487       my $tf = config_get($host, 'time_format');
488       print "    => I can remove:\n";
489       foreach (@backup_points) {
490         print "      => ". strftime($tf, localtime($_));
491         print " [". (exists($info->{full}->{$_}) ? "full":"incremental") ."]";
492         print " XXX" if(!exists($must_save{$_}));
493         print "\n";
494       }
495     }
496     foreach (@removals) {
497       my $efs = dir_encode($disk);
498       my $filename;
499       if(exists($info->{full}->{$_})) {
500         $filename = "$store/$_.$efs.full";
501       }
502       elsif(exists($info->{incremental}->{$_})) {
503         $filename = "$store/$_.$efs.incremental.$info->{incremental}->{$_}->{depends}";
504       }
505       else {
506         print "ERROR: We tried to expunge $host $disk [$_], but couldn't find it.\n";
507       }
508       print "    => expunging $filename\n" if($DEBUG);
509       unless($NUETERED) {
510         unlink($filename) || print "ERROR: unlink $filename: $?\n";
511       }
512     }
513   }
514 }
515
516 sub __default_sort($$) { return $_[0] cmp $_[1]; }
517    
518 sub choose($$;$) {
519   my($name, $obj, $sort) = @_;
520   $sort ||= \&__default_sort;;
521   my @list;
522   my $hash;
523   if(ref $obj eq 'ARRAY') {
524     @list = sort { $sort->($a,$b); } (@$obj);
525     map { $hash->{$_} = $_; } @list;
526   }
527   elsif(ref $obj eq 'HASH') {
528     @list = sort { $sort->($a,$b); } (keys %$obj);
529     $hash = $obj;
530   }
531   else {
532     die "choose passed bad object: " . ref($obj) . "\n";
533   }
534   return $list[0] if(scalar(@list) == 1);
535   print "\n";
536   my $i = 1;
537   for (@list) {
538     printf " %3d) $hash->{$_}\n", $i++;
539   }
540   my $selection = 0;
541   while($selection !~ /^\d+$/ or
542         $selection < 1 or
543         $selection >= $i) {
544     print "$name: ";
545     chomp($selection = <>);
546   }
547   return $list[$selection - 1];
548 }
549
550 sub backup_chain($$) {
551   my ($info, $ts) = @_;
552   my @list;
553   push @list, $info->{full}->{$ts} if(exists($info->{full}->{$ts}));
554   if(exists($info->{incremental}->{$ts})) {
555     push @list, $info->{incremental}->{$ts};
556     push @list, backup_chain($info, $info->{incremental}->{$ts}->{depends});
557   }
558   return @list;
559 }
560
561 sub perform_restore() {
562   my %source;
563
564   foreach my $host (grep { $_ ne "default" } keys %conf) {
565     # If -h was specific, we will skip this host if the arg isn't
566     # an exact match or a pattern match
567     if($HOST &&
568        !(($HOST eq $host) ||
569          ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
570       next;
571     }
572
573     my $store = config_get($host, 'store');
574     $store =~ s/%h/$host/g;;
575     mkdir $store if(! -d $store);
576
577     my $backup_info = scan_for_backups($store);
578     foreach my $disk (sort keys %{$backup_info}) {
579       my $info = $backup_info->{$disk};
580       next unless(ref($info) eq 'HASH');
581       next
582         if($ZFS &&      # if the pattern was specified it could
583            !($disk eq $ZFS ||        # be a specific match or a
584              ($ZFS =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
585       # We want to see this one
586       my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
587       my @source_points;
588       foreach (@backup_points) {
589         push @source_points, $_ if(!$TIMESTAMP || $TIMESTAMP == $_)
590       }
591       if(@source_points) {
592         $source{$host}->{$disk} = \@source_points;
593       }
594     }
595   }
596
597   if(! keys %source) {
598     print "No matching backups found\n";
599     return;
600   }
601
602   # Here goes the possibly interactive dialog
603   my $host = choose("Restore from host",  [keys %source]);
604   my $disk = choose("Restore from ZFS", [keys %{$source{$host}}]);
605  
606   # Times are special.  We build a human readable form and use a numerical
607   # sort function instead of the default lexical one.
608   my %times;
609   my $tf = config_get($host, 'time_format');
610   map { $times{$_} = strftime($tf, localtime($_)); } @{$source{$host}->{$disk}};
611   my $timestamp = choose("Restore as of timestamp", \%times,
612                          sub { $_[0] <=> $_[1]; });
613
614   my $store = config_get($host, 'store');
615   $store =~ s/%h/$host/g;;
616   mkdir $store if(! -d $store);
617   my $backup_info = scan_for_backups($store);
618   my @backup_list = reverse backup_chain($backup_info->{$disk}, $timestamp);
619
620   if(!$RESTORE_HOST) {
621     print "Restore to host [$host]:";
622     chomp(my $input = <>);
623     $RESTORE_HOST = length($input) ? $input : $host;
624   }
625   if(!$RESTORE_ZFS) {
626     print "Restore to zfs [$disk]:";
627     chomp(my $input = <>);
628     $RESTORE_ZFS = length($input) ? $input : $disk;
629   }
630
631   # show intentions
632   print "Going to restore:\n";
633   print "\tfrom: $host\n";
634   print "\tfrom: $disk\n";
635   print "\t  at: $timestamp [" . strftime($tf, localtime($timestamp)) . "]\n";
636   print "\t  to: $RESTORE_HOST\n";
637   print "\t  to: $RESTORE_ZFS\n";
638   print "\n";
639
640   foreach(@backup_list) {
641     $_->{success} = zfs_restore_part($RESTORE_HOST, $RESTORE_ZFS, $_->{file}, $_->{depends});
642   }
643 }
644
645 sub zfs_restore_part($$$;$) {
646   my ($host, $fs, $file, $dep) = @_;
647   my $command;
648   if(exists($conf{$host})) {
649     my $agent = config_get($host, 'agent');
650     $command = "$agent -r -z $fs";
651     $command .= " -b $dep" if($dep);
652   }
653   else {
654     $command = "/usr/sbin/zfs recv $fs";
655   }
656   print " => piping $file to $command\n" if($DEBUG);
657   if($NUETERED) {
658     print "gzip -dfc $file | ssh $host $command\n" if ($DEBUG);
659   }
660   else {
661     open(DUMP, "gzip -dfc $file |");
662     eval {
663       open(RECEIVER, "| ssh $host $command");
664       my $buffer;
665       while(my $len = sysread(DUMP, $buffer, $BLOCKSIZE)) {
666         if(syswrite(RECEIVER, $buffer, $len) != $len) {
667           die "$!";
668         }
669       }
670     };
671     close(DUMP);
672     close(RECEIVER);
673   }
674   return $?;
675 }
676
677 sub show_backups($$$) {
678   my ($host, $store, $diskpat) = @_;
679   my $backup_info = scan_for_backups($store);
680   my $tf = config_get($host, 'time_format');
681   foreach my $disk (sort keys %{$backup_info}) {
682     my $info = $backup_info->{$disk};
683     next unless(ref($info) eq 'HASH');
684     next
685       if($diskpat &&      # if the pattern was specified it could
686          !($disk eq $diskpat ||        # be a specific match or a
687            ($diskpat =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
688     # We want to see this one
689     print "$host:$disk\n";
690     next unless($SUMMARY || $SUMMARY_EXT);
691     if($SUMMARY_EXT) {
692       print "\tLast Full: ". ($info->{last_full} ? strftime($tf, localtime($info->{last_full})) : "Never") . "\n";
693       if($info->{last_full} < $info->{last_incremental}) {
694         print "\tLast Incr: ". strftime($tf, localtime($info->{last_incremental})). "\n";
695       }
696     }
697     my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
698     @backup_points = sort { $a <=> $b } @backup_points;
699     unless ($SUMMARY_EXT) {
700       @backup_points = (pop @backup_points);
701     }
702     foreach (@backup_points) {
703       print "\t" . strftime($tf, localtime($_)) . " [$_] ";
704       if(exists($info->{full}->{$_})) {
705         my @st = stat($info->{full}->{$_}->{file});
706         print "FULL " . pretty_size($st[7]);
707       } else {
708         my @st = stat($info->{incremental}->{$_}->{file});
709         print "INCR from [$info->{incremental}->{$_}->{depends}] " . pretty_size($st[7]);
710       }
711       print "\n";
712     }
713     print "\n";
714   }
715 }
716
717 sub plan_and_run($$$) {
718   my ($host, $store, $diskpat) = @_;
719   print "Planning '$host'\n" if($DEBUG);
720   my $agent = config_get($host, 'agent');
721   open(ZFSLIST, "ssh $host $agent -l |") || next;
722   foreach my $diskline (<ZFSLIST>) {
723     chomp($diskline);
724     next unless($diskline =~ /^(\S+) \[([^\]]*)\]/);
725     my $diskname = $1;
726     my %snaps;
727     map { $snaps{$_} = 1 } (split(/,/, $2));
728
729     # If we are being selective (via -z) now is the time.
730     next
731       if($diskpat &&          # if the pattern was specified it could
732          !($diskname eq $diskpat ||        # be a specific match or a
733            ($diskpat =~ /^\/(.+)\/$/ && $diskname =~ /$1/))); # regex
734
735     print " => Scanning '$store' for old backups of '$diskname'.\n" if($DEBUG);
736     # Make directory on demand
737     my $backup_info = scan_for_backups($store);
738     # That gave us info on all backups, we just want this disk
739     $backup_info = $backup_info->{$diskname} || {};
740
741     # Should we do a backup?
742     my $backup_type = 'no';
743     if(time() > $backup_info->{last_backup} + config_get($host, 'backup_interval')) {
744       $backup_type = 'incremental';
745     }
746     if(time() > $backup_info->{last_full} + config_get($host, 'full_interval')) {
747       $backup_type = 'full';
748     }
749
750     # If we want an incremental, but have no full, then we need to upgrade to full
751     if($backup_type eq 'incremental') {
752       my $have_full_locally = 0;
753       # For each local full backup, see if the full backup still exists on the other end.
754       foreach (keys %{$backup_info->{'full'}}) {
755         $have_full_locally = 1 if(exists($snaps{'__zb_full_' . $_}));
756       }
757       $backup_type = 'full' unless($have_full_locally);
758     }
759
760     print " => doing $backup_type backup\n" if($DEBUG);
761     # We need to drop a __zb_base snap or a __zb_incr snap before we proceed
762     unless($NUETERED) {
763       if($backup_type eq 'full') {
764         eval { zfs_full_backup($host, $diskname, $store); };
765         if ($@) {
766           chomp(my $err = $@);
767           print " => failure $err\n";
768         }
769         else {
770           # Unless there was an error backing up, remove all the other full snaps
771           foreach (keys %snaps) {
772             zfs_remove_snap($host, $diskname, $_) if(/^__zb_full_(\d+)/)
773           }
774         }
775       }
776       if($backup_type eq 'incremental') {
777         zfs_remove_snap($host, $diskname, '__zb_incr') if($snaps{'__zb_incr'});
778         # Find the newest full from which to do an incremental (NOTE: reverse numeric sort)
779         my @fulls = sort { $b <=> $a } (keys %{$backup_info->{'full'}});
780         zfs_incremental_backup($host, $diskname, $fulls[0], $store);
781       }
782     }
783   }
784   close(ZFSLIST);
785 }
786
787 if($RESTORE) {
788   perform_restore();
789 }
790 else {
791   foreach my $host (grep { $_ ne "default" } keys %conf) {
792     # If -h was specific, we will skip this host if the arg isn't
793     # an exact match or a pattern match
794     if($HOST &&
795        !(($HOST eq $host) ||
796          ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
797       next;
798     }
799  
800     my $store = config_get($host, 'store');
801     $store =~ s/%h/$host/g;;
802     mkdir $store if(! -d $store);
803  
804     if($LIST || $SUMMARY || $SUMMARY_EXT) {
805       show_backups($host, $store, $ZFS);
806     }
807     if($BACKUP) {
808       plan_and_run($host, $store, $ZFS);
809     }
810     if($EXPUNGE) {
811       perform_retention($host, $store);
812     }
813   }
814 }
815
816 exit 0;
817
818 =pod
819
820 =head1 FILES
821
822 =over
823
824 =item /etc/zetaback.conf
825
826 The main zetaback configuration file.  The location of the file can be
827 specified on the command line with the -c flag.
828
829 =back
830
831 =head1 SEE ALSO
832
833 zetaback_agent(1)
834
835 =cut
Note: See TracBrowser for help on using the browser.