root/zetaback.in

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

closes #12

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