root/zetaback.in

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

closes #16

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