root/zetaback.in

Revision 7a45c4d93a9a6ac0a6ff8932f5ea1ca388fa8a6e, 38.3 kB (checked in by Mark Harrison <mark@omniti.com>, 5 years ago)

Expunge and '--files' support for datasets

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