root/zetaback.in

Revision 66246e8c0e2e7c145eae1e7dd674a17b4297abad, 32.0 kB (checked in by Eric Sproul <esproul@omniti.com>, 6 years ago)

Maintenance-free version string. Refs #31

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