root/zetaback.in

Revision ba545b02dd3de977f1a4125386b55b74cc869924, 26.0 kB (checked in by Eric Sproul <esproul@omniti.com>, 8 years ago)

Use .in files as the source, keeping things safe and making cleaning simpler.

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