root/zetaback

Revision 328ad1c4651498e9673afe731179373986393c5a, 25.8 kB (checked in by Theo Schlossnagle <jesus@omniti.com>, 8 years ago)

Add logging, closes #11

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