root/zetaback

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

first pass at lock handling, refs #6

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