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
8use strict;
9use Getopt::Long;
10use MIME::Base64;
11use POSIX qw/strftime/;
12use Fcntl qw/:flock/;
13use File::Path qw/mkpath/;
14use File::Copy;
15use IO::File;
16use Pod::Usage;
17
18use vars qw/%conf %locks $version_string $process_lock
19            $PREFIX $CONF $BLOCKSIZE $DEBUG $HOST $BACKUP
20            $RESTORE $RESTORE_HOST $RESTORE_ZFS $TIMESTAMP
21            $LIST $SUMMARY $SUMMARY_EXT $SUMMARY_VIOLATORS
22            $SUMMARY_VIOLATORS_VERBOSE $FORCE_FULL $FORCE_INC
23            $EXPUNGE $NEUTERED $ZFS $SHOW_FILENAMES $ARCHIVE
24            $VERSION $HELP/;
25$version_string = '1.0.6';
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$conf{'default'}->{'violator_grace_period'} = 21600;
35
36=pod
37
38=head1 NAME
39
40zetaback - perform backup, restore and retention policies for ZFS backups.
41
42=head1 SYNOPSIS
43
44  zetaback -v
45
46  zetaback [-l|-s|-sx|-sv|-svv] [--files] [-c conf] [-d] [-h host] [-z zfs]
47
48  zetaback -a [-c conf] [-d] [-h host] [-z zfs]
49
50  zetaback -b [-ff] [-fi] [-x] [-c conf] [-d] [-n] [-h host] [-z zfs]
51
52  zetaback -x [-b] [-c conf] [-d] [-n] [-h host] [-z zfs]
53
54  zetaback -r [-c conf] [-d] [-n] [-h host] [-z zfs] [-t timestamp]
55              [-rhost host] [-rzfs fs]
56
57=cut
58
59GetOptions(
60  "h=s"     => \$HOST,
61  "z=s"     => \$ZFS,
62  "c=s"     => \$CONF,
63  "a"       => \$ARCHIVE,
64  "b"       => \$BACKUP,
65  "l"       => \$LIST,
66  "s"       => \$SUMMARY,
67  "sx"      => \$SUMMARY_EXT,
68  "sv"      => \$SUMMARY_VIOLATORS,
69  "svv"     => \$SUMMARY_VIOLATORS_VERBOSE,
70  "r"       => \$RESTORE,
71  "t=i"     => \$TIMESTAMP,
72  "rhost=s" => \$RESTORE_HOST,
73  "rzfs=s"  => \$RESTORE_ZFS,
74  "d"       => \$DEBUG,
75  "n"       => \$NEUTERED,
76  "x"       => \$EXPUNGE,
77  "v"       => \$VERSION,
78  "ff"      => \$FORCE_FULL,
79  "fi"      => \$FORCE_INC,
80  "files"   => \$SHOW_FILENAMES,
81);
82
83# actions allowed together 'x' and 'b' all others are exclusive:
84my $actions = 0;
85$actions++ if($ARCHIVE);
86$actions++ if($BACKUP || $EXPUNGE);
87$actions++ if($RESTORE);
88$actions++ if($LIST);
89$actions++ if($SUMMARY);
90$actions++ if($SUMMARY_EXT);
91$actions++ if($SUMMARY_VIOLATORS);
92$actions++ if($SUMMARY_VIOLATORS_VERBOSE);
93$actions++ if($VERSION);
94$actions++ if($BACKUP && $FORCE_FULL && $FORCE_INC);
95if($actions != 1) {
96  pod2usage({ -verbose => 0 });
97  exit -1;
98}
99
100=pod
101
102=head1 DESCRIPTION
103
104The B<zetaback> program orchestrates the backup (either full or
105incremental) of remote ZFS filesystems to a local store.  It handles
106frequency requirements for both full and incemental backups as well
107as retention policies.  In addition to backups, the B<zetaback> tool
108allows for the restore of any backup to a specified host and zfs
109filesystem.
110
111=head1 OPTIONS
112
113The non-optional action command line arguments define the invocation purpose
114of B<zetaback>.  All other arguments are optional and refine the target
115of the action specified.
116
117=head2 Generic Options
118
119The following arguments have the same meaning over several actions:
120
121=over
122
123=item -c <conf>
124
125Use the specified file as the configuration file.  The default file, if
126none is specified is /usr/local/etc/zetaback.conf.  The prefix of this
127file may also be specified as an argument to the configure script.
128
129=item -d
130
131Enable debugging output.
132
133=item -n
134
135Don't actually perform any remote commands or expunging.  This is useful with
136the -d argument to ascertain what would be done if the command was actually
137executed.
138
139=item -t <timestamp>
140
141Used during the restore process to specify a backup image from the desired
142point in time.  If omitted, the command becomes interactive.  This timestamp
143is a UNIX timestamp and is shown in the output of the -s and -sx actions.
144
145=item -rhost <host>
146
147Specify the remote host that is the target for a restore operation.  If
148omitted the command becomes interactive.
149
150=item -rzfs <zfs>
151
152Specify the remote ZFS filesystem that is the target for a restore
153operation.  If omitted the command becomes interactive.
154
155=item -h <host>
156
157Filters the operation to the host specified.  If <host> is of the form
158/pattern/, it matches 'pattern' as a perl regular expression against available
159hosts.  If omitted, no limit is enforced and all hosts are used for the action.
160
161=item -z <zfs>
162
163Filters the operation to the zfs filesystem specified.  If <zfs> is of the
164form /pattern/, it matches 'pattern' as a perl regular expression against
165available zfs filesystems.  If omitted, no filter is enforced and all zfs
166filesystems are used for the action.
167
168=back
169
170=head2 Actions
171
172=over
173
174=item -v
175
176Show the version.
177
178=item -l
179
180Show a brief listing of available backups.
181
182=item -s
183
184Like -l, -s will show a list of backups but provides additional information
185about the backups including timestamp, type (full or incremental) and the
186size on disk.
187
188=item -sx
189
190Shows an extended summary.  In addition to the output provided by the -s
191action, the -sx action will show detail for each availble backup.  For
192full backups, the detail will include any more recent full backups, if
193they exist.  For incremental backups, the detail will include any
194incremental backups that are more recent than the last full backup.
195
196=item -sv
197
198Display all backups in the current store that violate the configured
199backup policy. This is where the most recent full backup is older than
200full_interval seconds ago, or the most recent incremental backup is older
201than backup_interval seconds ago.
202
203If, at the time of the most recent backup, a filesystem no longer exists on
204the server (because it was deleted), then backups of this filesystem are not
205included in the list of violators. To include these filesystems, use the -svv
206option instead.
207
208=item -svv
209
210The violators summary will exclude backups of filesystems that are no longer
211on the server in the list of violators. Use this option to include those
212filesystems.
213
214=item --files
215
216Display the on-disk file corresponding to each backup named in the output.
217This is useful with the -sv flag to name violating files.  Often times,
218violators are filesystems that have been removed on the host machines and
219zetaback can no longer back them up.  Be very careful if you choose to
220automate the removal of such backups as filesystems that would be backed up
221by the next regular zetaback run will often show up as violators.
222
223=item -a
224
225Performs an archive.  This option will look at all eligible backup points
226(as restricted by -z and -h) and move those to the configured archive
227directory.  The recommended use is to first issue -sx --files then
228carefully review available backup points and prune those that are
229unneeded.  Then invoke with -a to move only the remaining "desired"
230backup points into the archives.  Archived backups do not appear in any
231listings or in the list of policy violators generated by the -sv option.
232In effect, they are no longer "visible" to zetaback.
233
234=item -b
235
236Performs a backup.  This option will investigate all eligible hosts, query
237the available filesystems from the remote agent and determine if any such
238filesystems require a new full or incremental backup to be taken.  This
239option may be combined with the -x option (to clean up afterwards.)
240
241=item -ff
242
243Forces a full backup to be taken on each filesystem encountered.  This is
244used in combination with -b.  It is recommended to use this option only when
245targeting specific filesystems (via the -h and -z options.)  Forcing a full
246backup across all machines will cause staggered backups to coalesce and
247could cause performance issues.
248
249=item -fi
250
251Forces an incremental backup to be taken on each filesystem encountered.
252This is used in combination with -b.  It is recommended to use this option
253only when targeting specific filesystems (via the -h and -z options.)  Forcing
254an incremental backup across all machines will cause staggered backups
255to coalesce and could cause performance issues.
256
257=item -x
258
259Perform an expunge.  This option will determine which, if any, of the local
260backups may be deleted given the retention policy specified in the
261configuration.
262
263=item -r
264
265Perform a restore.  This option will operate on the specified backup and
266restore it to the ZFS filesystem specified with -rzfs on the host specified
267with the -rhost option.  The -h, -z and -t options may be used to filter
268the source backup list.  If the filtered list contains more than one
269source backup image, the command will act interactively.  If the -rhost
270and -rzfs command are not specified, the command will act interactively.
271
272When running interactively, you can choose multiple filesystems from the list
273using ranges. For example 1-4,5,10-11. If you do this, zetaback will enter
274multi-restore mode. In this mode it will automatically select the most recent
275backup, and restore filessytems in bulk.
276
277In multi-restore mode, you have the option to specify a base filesystem to
278restore to. This filesystem will be added as a prefix to the original
279filesystem name, so if you picked a prefix of data/restore, and one of the
280filesystems you are restoring is called data/set/myfilesystem, then the
281filesystem will be restored to data/restore/data/set/myfilesystem.
282
283Note that, just like in regular restore mode, zetaback won't create
284intermediate filesystems for you when restoring, and these should either exist
285beforehand, or you should make sure you pick a set of filesystems that will
286restore the entire tree for you, for example, you should restore data as well
287as data/set before restoring data/set/foo.
288
289=back
290
291=cut
292
293if($VERSION) {
294  print "zetaback: $version_string\n";
295  exit 0;
296}
297
298=pod
299
300=head1 CONFIGURATION
301
302The zetaback configuration file consists of a default stanza, containing
303settings that can be overridden on a per-host basis.  A stanza begins
304either with the string 'default', or a fully-qualified hostname, with
305settings enclosed in braces ({}).  Single-line comments begin with a hash
306('#'), and whitespace is ignored, so feel free to indent for better
307readability.  Every host to be backed up must have a host stanza in the
308configuration file.
309
310=head2 Storage Classes
311
312In addition to the default and host stanzas, the configuration file can also
313contain 'class' stanzas. Classes allow you to override settings on a
314per-filesystem basis rather than a per-host basis. A class stanza begins with
315the name of the class, and has a setting 'type = class'. For example:
316
317  myclass {
318    type = class
319    store = /path/to/alternate/store
320  }
321
322To add a filesystem to a class, set a zfs user property on the relevant
323filesystem. This must be done on the server that runs the zetaback agent, and
324not the zetaback server itself.
325
326  zfs set com.omniti.labs.zetaback:class=myclass pool/fs
327
328Note that user properties (and therefore classes) are are only available on
329Solaris 10 8/07 and newer, and on Solaris Express build 48 and newer. Only the
330server running the agent needs to have user property support, not the zetaback
331server itself.
332
333The following settings can be included in a class stanza. All other settings
334will be ignored, and their default (or per host) settings used instead:
335
336=over
337
338=item *
339
340store
341
342=item *
343
344full_interval
345
346=item *
347
348backup_interval
349
350=item *
351
352retention
353
354=item *
355
356dataset_backup
357
358=item *
359
360violator_grace_period
361
362=back
363
364=head2 Settings
365
366The following settings are valid in both the default and host scopes:
367
368=over
369
370=item store
371
372The base directory under which to keep backups.  An interpolated variable
373'%h' can be used, which expands to the hostname.  There is no default for
374this setting.
375
376=item archive
377
378The base directory under which archives are stored.  The format is the same
379as the store setting.  This is the destination to which files are relocated
380when issuing an archive action (-a).
381
382=item agent
383
384The location of the zetaback_agent binary on the host.  There is no default
385for this setting.
386
387=item time_format
388
389All timestamps within zetaback are in UNIX timestamp format.  This setting
390provides a string for formatting all timestamps on output.  The sequences
391available are identical to those in strftime(3).  If not specified, the
392default is '%Y-%m-%d %H:%M:%S'.
393
394=item backup_interval
395
396The frequency (in seconds) at which to perform incremental backups.  An
397incremental backup will be performed if the current time is more than
398backup_interval since the last incremental backup.  If there is no full backup
399for a particular filesystem, then a full backup is performed.  There is no
400default for this setting.
401
402=item full_interval
403
404The frequency (in seconds) at which to perform full backups.  A full backup will
405be performed if the current time is more than full_interval since the last full
406backup.
407
408=item retention
409
410The retention time (in seconds) for backups.  This can be a simple number, in
411which case all backups older than this will be expunged.
412
413The retention specification can also be more complex, and consist of pairs of
414values separated by a comma. The first value is a time period in seconds, and
415the second value is how many backups should be retained within that period.
416For example:
417
418retention = 3600,4;86400,11
419
420This will keep up to 4 backups for the first hour, and an additional 11
421backups over 24 hours. The times do not stack. In other words, the 11 backups
422would be kept during the period from 1 hour old to 24 hours old, or one every
4232 hours.
424
425Any backups older than the largest time given are deleted. In the above
426example, all backups older than 24 hours are deleted.
427
428If a second number is not specified, then all backups are kept within that
429period.
430
431Note: Full backups are never deleted if they are depended upon by an
432incremental. In addition, the most recent backup is never deleted, regardless
433of how old it is.
434
435This value defaults to (14 * 86400), or two weeks.
436
437=item compressionlevel
438
439Compress files using gzip at the specified compression level. 0 means no
440compression. Accepted values are 1-9. Defaults to 1 (fastest/minimal
441compression.)
442
443=item ssh_config
444
445Full path to an alternate ssh client config.  This is useful for specifying a
446less secure but faster cipher for some hosts, or using a different private
447key.  There is no default for this setting.
448
449=item dataset_backup
450
451By default zetaback backs zfs filesystems up to files. This option lets you
452specify that the backup go be stored as a zfs dataset on the backup host.
453
454=item offline
455
456Setting this option to 1 for a host will mark it as being 'offline'. Hosts
457that are marked offline will not be backed up, will not have any old backups
458expunged and will not be included in the list of policy violators. However,
459the host will still be shown when listing backups and archiving.
460
461=item violator_grace_period
462
463This setting controls the grace period used when deciding if a backup has
464violated its backup window. It is used to prevent false positives in the case
465where a filesystem is still being backed up. For example, if it is 25 hours
466since the last daily backup, but the daily backup is in progress, the grace
467period will mean that it is not shown in the violators list.
468
469Like all intervals, this period is in seconds. The default is 21600 seconds (6
470hours).
471
472=back
473
474=head2 Global Settings
475
476The following settings are only valid in the default scope:
477
478=over
479
480=item process_limit
481
482This setting limits the number of concurrent zetaback processes that can run
483at one time. Zetaback already has locks on hosts and datasets to prevent
484conflicting backups, and this allows you to have multiple zetaback instances
485running in the event a backup takes some time to complete, while still keeping
486a limit on the resources used. If this configuration entry is missing, then no
487limiting will occur.
488
489=back
490
491=head1 CONFIGURATION EXAMPLES
492
493=head2 Uniform hosts
494
495This config results in backups stored in /var/spool/zfs_backups, with a
496subdirectory for each host.  Incremental backups will be performed
497approximately once per day, assuming zetaback is run hourly.  Full backups
498will be done once per week.  Time format and retention are default.
499
500  default {
501    store = /var/spool/zfs_backups/%h
502    agent = /usr/local/bin/zetaback_agent
503    backup_interval = 83000
504    full_interval = 604800
505  }
506
507  host1 {}
508
509  host2 {}
510
511=head2 Non-uniform hosts
512
513Here, host1's and host2's agents are found in different places, and host2's
514backups should be stored in a different path.
515
516  default {
517    store = /var/spool/zfs_backups/%h
518    agent = /usr/local/bin/zetaback_agent
519    backup_interval = 83000
520    full_interval = 604800
521  }
522
523  host1 {
524    agent = /opt/local/bin/zetaback_agent
525  }
526
527  host2 {
528    store = /var/spool/alt_backups/%h
529    agent = /www/bin/zetaback_agent
530  }
531
532=cut
533
534# Make the parser more formal:
535# config => stanza*
536# stanza => string { kvp* }
537# kvp    => string = string
538my $str_re = qr/(?:"(?:\\\\|\\"|[^"])*"|\S+)/;
539my $kvp_re = qr/($str_re)\s*=\s*($str_re)/;
540my $stanza_re = qr/($str_re)\s*\{((?:\s*$kvp_re)*)\s*\}/;
541
542sub parse_config() {
543  local($/);
544  $/ = undef;
545  open(CONF, "<$CONF") || die "Unable to open config file: $CONF";
546  my $file = <CONF>;
547  # Rip comments
548  $file =~ s/^\s*#.*$//mg;
549  while($file =~ m/$stanza_re/gm) {
550    my $scope = $1;
551    my $filepart = $2;
552    $scope =~ s/^"(.*)"$/$1/;
553    $conf{$scope} ||= {};
554    while($filepart =~ m/$kvp_re/gm) {
555      my $key = $1;
556      my $value = $2;
557      $key =~ s/^"(.*)"$/$1/;
558      $value =~ s/^"(.*)"$/$1/;
559      $conf{$scope}->{lc($key)} = $value;
560    }
561  }
562  close(CONF);
563}
564sub config_get($$;$) {
565  # Params: host, key, class
566  # Order of precedence: class, host, default
567  if ($_[2]) {
568    return $conf{$_[2]}->{$_[1]} || $conf{$_[0]}->{$_[1]} ||
569        $conf{'default'}->{$_[1]};
570  } else {
571    return $conf{$_[0]}->{$_[1]} || $conf{'default'}->{$_[1]};
572  }
573}
574
575sub get_store($;$) {
576  my ($host, $class) = @_;
577  my $store = config_get($host, 'store', $class);
578  $store =~ s/%h/$host/g;;
579  return $store;
580}
581
582sub get_classes() {
583  my @classes = (""); # The default/blank class is always present
584  foreach my $key (keys %conf) {
585    if ($conf{$key}->{'type'} eq 'class') {
586      push @classes, $key;
587    }
588  }
589  return @classes;
590}
591
592sub fs_encode($) {
593  my $d = shift;
594  my @parts = split('@', $d);
595  my $e = encode_base64($parts[0], '');
596  $e =~ s/\//_/g;
597  $e =~ s/=/-/g;
598  $e =~ s/\+/\./g;
599  if (exists $parts[1]) {
600    $e .= "\@$parts[1]";
601  }
602  return $e;
603}
604sub fs_decode($) {
605  my $e = shift;
606  $e =~ s/_/\//g;
607  $e =~ s/-/=/g;
608  $e =~ s/\./\+/g;
609  return decode_base64($e);
610}
611sub dir_encode($) {
612  my $d = shift;
613  my $e = encode_base64($d, '');
614  $e =~ s/\//_/;
615  return $e;
616}
617sub dir_decode($) {
618  my $e = shift;
619  $e =~ s/_/\//;
620  return decode_base64($e);
621}
622sub pretty_size($) {
623  my $bytes = shift;
624  if($bytes > 1024*1024*1024) {
625    return sprintf("%0.2f Gb", $bytes / (1024*1024*1024));
626  }
627  if($bytes > 1024*1024) {
628    return sprintf("%0.2f Mb", $bytes / (1024*1024));
629  }
630  if($bytes > 1024) {
631    return sprintf("%0.2f Kb", $bytes / (1024));
632  }
633  return "$bytes b";
634}
635sub lock($;$$) {
636  my ($host, $file, $nowait) = @_;
637  print "Acquiring lock for $host:$file\n" if($DEBUG);
638  $file ||= 'master.lock';
639  my $store = get_store($host); # Don't take classes into account - not needed
640  mkpath($store) if(! -d $store);
641  return 1 if(exists($locks{"$host:$file"}));
642  open(LOCK, "+>>$store/$file") || return 0;
643  unless(flock(LOCK, LOCK_EX | ($nowait ? LOCK_NB : 0))) {
644    close(LOCK);
645    return 0;
646  }
647  $locks{"$host:$file"} = \*LOCK;
648  return 1;
649}
650sub unlock($;$$) {
651  my ($host, $file, $remove) = @_;
652  print "Releasing lock for $host:$file\n" if($DEBUG);
653  $file ||= 'master.lock';
654  my $store = get_store($host); # Don't take classes into account - not needed
655  mkpath($store) if(! -d $store);
656  return 0 unless(exists($locks{"$host:$file"}));
657  *UNLOCK = $locks{$file};
658  unlink("$store/$file") if($remove);
659  flock(UNLOCK, LOCK_UN);
660  close(UNLOCK);
661  return 1;
662}
663sub limit_running_processes() {
664    my $max = $conf{'default'}->{'process_limit'};
665    return unless defined($max);
666    print "Aquiring process lock\n" if $DEBUG;
667    for (my $i=0; $i < $max; $i++) {
668        my $file = "/tmp/.zetaback_$i.lock";
669        print "$file\n" if $DEBUG;
670        open ($process_lock, "+>>$file") || next;
671        if (flock($process_lock, LOCK_EX | LOCK_NB)) {
672            print "Process lock succeeded: $file\n" if $DEBUG;
673            return 1;
674        } else {
675            close($process_lock);
676        }
677    }
678    print "Too many zetaback processes running. Exiting...\n" if $DEBUG;
679    exit 0;
680}
681sub scan_for_backups($) {
682  my %info = ();
683  my $dir = shift;
684  $info{last_full} = $info{last_incremental} = $info{last_backup} = 0;
685  # Look for standard file based backups first
686  opendir(D, $dir) || return \%info;
687  foreach my $file (readdir(D)) {
688    if($file =~ /^(\d+)\.([^\.]+)\.full$/) {
689      my $whence = $1;
690      my $fs = dir_decode($2);
691      $info{$fs}->{full}->{$whence}->{'file'} = "$dir/$file";
692      $info{$fs}->{last_full} = $whence if($whence > $info{$fs}->{last_full});
693      $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
694                                     $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
695    }
696    elsif($file =~ /^(\d+).([^\.]+)\.incremental.(\d+)$/) {
697      my $whence = $1;
698      my $fs = dir_decode($2);
699      $info{$fs}->{incremental}->{$whence}->{'depends'} = $3;
700      $info{$fs}->{incremental}->{$whence}->{'file'} = "$dir/$file";
701      $info{$fs}->{last_incremental} = $whence if($whence > $info{$fs}->{last_incremental});
702      $info{$fs}->{last_backup} = $info{$fs}->{last_incremental} > $info{$fs}->{last_full} ?
703                                     $info{$fs}->{last_incremental} : $info{$fs}->{last_full};
704    }
705  }
706  closedir(D);
707  # Now look for zfs based backups
708  my $storefs;
709  eval {
710    $storefs = get_fs_from_mountpoint($dir);
711  };
712  return \%info if ($@);
713  my $rv = open(ZFSLIST, "__ZFS__ list -H -r -t snapshot $storefs |");
714  return \%info unless $rv;
715  while (<ZFSLIST>) {
716      my @F = split(' ');
717      my ($rawfs, $snap) = split('@', $F[0]);
718      my ($whence) = ($snap =~ /(\d+)/);
719      next unless $whence;
720      my @fsparts = split('/', $rawfs);
721      my $fs = fs_decode($fsparts[-1]);
722      # Treat a dataset backup as a full backup from the point of view of the
723      # backup lists
724      $info{$fs}->{full}->{$whence}->{'snapshot'} = $snap;
725      $info{$fs}->{full}->{$whence}->{'dataset'} = "$rawfs\@$snap";
726      # Note - this field isn't set for file backups - we probably should do
727      # this
728      $info{$fs}->{full}->{$whence}->{'pretty_size'} = "$F[1]";
729      $info{$fs}->{last_full} = $whence if ($whence >
730          $info{$fs}->{last_full});
731      $info{$fs}->{last_backup} = $whence if ($whence >
732          $info{$fs}->{last_backup});
733  }
734  close(ZFSLIST);
735
736  return \%info;
737}
738
739parse_config();
740
741sub zetaback_log($$;@) {
742  my ($host, $mess, @args) = @_;
743  my $tf = config_get($host, 'time_format');
744  my $file = config_get($host, 'logfile');
745  my $fileh;
746  if(defined($file)) {
747    $fileh = IO::File->new(">>$file");
748  }
749  $fileh ||= IO::File->new(">&STDERR");
750  printf $fileh "%s: $mess", strftime($tf, localtime(time)), @args;
751  $fileh->close();
752}
753
754sub zfs_remove_snap($$$) {
755  my ($host, $fs, $snap) = @_;
756  my $agent = config_get($host, 'agent');
757  my $ssh_config = config_get($host, 'ssh_config');
758  $ssh_config = "-F $ssh_config" if($ssh_config);
759  print "Using custom ssh config file: $ssh_config\n" if($DEBUG);
760  return unless($snap);
761  print "Dropping $snap on $fs\n" if($DEBUG);
762  `ssh $ssh_config $host $agent -z $fs -d $snap`;
763}
764
765# Lots of args.. internally called.
766sub zfs_do_backup($$$$$$;$) {
767  my ($host, $fs, $type, $point, $store, $dumpname, $base) = @_;
768  my ($storefs, $encodedname);
769  my $agent = config_get($host, 'agent');
770  my $ssh_config = config_get($host, 'ssh_config');
771  $ssh_config = "-F $ssh_config" if($ssh_config);
772  print "Using custom ssh config file: $ssh_config\n" if($DEBUG);
773
774  # compression is meaningless for dataset backups
775  if ($type ne "s") {
776    my $cl = config_get($host, 'compressionlevel');
777    if ($cl >= 1 && $cl <= 9) {
778        open(LBACKUP, "|gzip -$cl >$store/.$dumpname") ||
779        die "zfs_do_backup $host:$fs $type: cannot create dump\n";
780    } else {
781        open(LBACKUP, ">$store/.$dumpname") ||
782        die "zfs_do_backup $host:$fs $type: cannot create dump\n";
783    }
784  } else {
785    # Dataset backup - pipe received filesystem to zfs recv
786    eval {
787      $storefs = get_fs_from_mountpoint($store);
788    };
789    if ($@) {
790      # The zfs filesystem doesn't exist, so we have to work out what it
791      # would be
792      my $basestore = $store;
793      $basestore =~ s/\/?%h//g;
794      $storefs = get_fs_from_mountpoint($basestore);
795      $storefs="$storefs/$host";
796    }
797    $encodedname = fs_encode($dumpname);
798    print STDERR "Receiving to zfs filesystem $storefs/$encodedname\n"
799      if($DEBUG);
800    zfs_create_intermediate_filesystems("$storefs/$encodedname");
801    open(LBACKUP, "|__ZFS__ recv $storefs/$encodedname");
802  }
803  # Do it. yeah.
804  eval {
805    if(my $pid = fork()) {
806      close(LBACKUP);
807      waitpid($pid, 0);
808      die "error: $?" if($?);
809    }
810    else {
811      my @cmd = ('ssh', split(/ /, $ssh_config), $host, $agent, '-z', $fs);
812      if ($type eq "i" || ($type eq "s" && $base)) {
813        push @cmd, ("-i", $base);
814      }
815      if ($type eq "f" || $type eq "s") {
816        push @cmd, ("-$type", $point);
817      }
818      open STDIN, "/dev/null" || exit(-1);
819      open STDOUT, ">&LBACKUP" || exit(-1);
820      print STDERR "   => @cmd\n" if($DEBUG);
821      unless (exec { $cmd[0] } @cmd) {
822        print STDERR "$cmd[0] failed: $!\n";
823        exit(1);
824      }
825    }
826    if ($type ne "s") {
827      die "dump failed (zero bytes)\n" if(-z "$store/.$dumpname");
828      rename("$store/.$dumpname", "$store/$dumpname") || die "cannot rename dump\n";
829    } else {
830      # Check everything is ok
831      `__ZFS__ list $storefs/$encodedname`;
832      die "dump failed (received snapshot $storefs/$encodedname does not exist)\n"
833        if $?;
834    }
835  };
836  if($@) {
837    if ($type ne "s") {
838        unlink("$store/.$dumpname");
839    }
840    chomp(my $error = $@);
841    $error =~ s/[\r\n]+/ /gsm;
842    zetaback_log($host, "FAILED[$error] $host:$fs $type\n");
843    die "zfs_do_backup $host:$fs $type: $error";
844  }
845  my $size;
846  if ($type ne "s") {
847    my @st = stat("$store/$dumpname");
848    $size = pretty_size($st[7]);
849  } else {
850    $size = `__ZFS__ get -Ho value used $storefs/$encodedname`;
851    chomp $size;
852  }
853  zetaback_log($host, "SUCCESS[$size] $host:$fs $type\n");
854}
855
856sub zfs_create_intermediate_filesystems($) {
857  my ($fs) = @_;
858  my $idx=0;
859  while (($idx = index($fs, '/', $idx+1)) != -1) {
860      my $fspart = substr($fs, 0, $idx);
861      `__ZFS__ list $fspart 2>&1`;
862      if ($?) {
863        print STDERR "Creating intermediate zfs filesystem: $fspart\n"
864          if $DEBUG;
865        `__ZFS__ create $fspart`;
866      }
867  }
868}
869
870sub zfs_full_backup($$$) {
871  my ($host, $fs, $store) = @_;
872
873  # Translate into a proper dumpname
874  my $point = time();
875  my $efs = dir_encode($fs);
876  my $dumpname = "$point.$efs.full";
877
878  zfs_do_backup($host, $fs, 'f', $point, $store, $dumpname);
879}
880
881sub zfs_incremental_backup($$$$) {
882  my ($host, $fs, $base, $store) = @_;
883  my $agent = config_get($host, 'agent');
884
885  # Translate into a proper dumpname
886  my $point = time();
887  my $efs = dir_encode($fs);
888  my $dumpname = "$point.$efs.incremental.$base";
889
890  zfs_do_backup($host, $fs, 'i', $point, $store, $dumpname, $base);
891}
892
893sub zfs_dataset_backup($$$$) {
894  my ($host, $fs, $base, $store) = @_;
895  my $agent = config_get($host, 'agent');
896
897  my $point = time();
898  my $dumpname = "$fs\@$point";
899
900  zfs_do_backup($host, $fs, 's', $point, $store, $dumpname, $base);
901}
902
903sub perform_retention($) {
904  my ($host) = @_;
905  my $now = time();
906
907  if ($DEBUG) {
908    print "Performing retention for $host\n";
909  }
910
911  foreach my $class (get_classes()) {
912    if ($DEBUG) {
913      if ($class) {
914        print "=> Class: $class\n" if $class;
915      } else {
916        print "=> Class: (none)\n";
917      }
918    }
919    my $retention = config_get($host, 'retention', $class);
920    my $store = get_store($host, $class);
921    my $backup_info = scan_for_backups($store);
922    foreach my $disk (sort keys %{$backup_info}) {
923      my $info = $backup_info->{$disk};
924      next unless(ref($info) eq 'HASH');
925      my %must_save;
926
927      if ($DEBUG) {
928        print "   $disk\n";
929      }
930
931      # Get a list of all the full and incrementals, sorts newest to oldest
932      my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
933      @backup_points = sort { $b <=> $a } @backup_points;
934
935      # We _cannot_ throw away _all_ our backups,
936      # so save the most recent incremental and full no matter what
937      push(@{$must_save{$backup_points[0]}}, "most recent backup");
938      my @fulls = grep { exists($info->{full}->{$_}) } @backup_points;
939      push(@{$must_save{$fulls[0]}}, "most recent full");
940
941      # Process retention policy
942      my @parts = split(/;/, $retention);
943      my %retention_map;
944      foreach (@parts) {
945        my ($period, $amount) = split(/,/);
946        if (!defined($amount)) {
947          $amount = -1;
948        }
949        $retention_map{$period} = $amount;
950      }
951      my @periods = sort { $a <=> $b } keys(%retention_map);
952      my %backup_bins;
953      foreach(@periods) {
954        $backup_bins{$_} = ();
955      }
956      my $cutoff = $now - $periods[0];
957      # Sort backups into time period sections
958      foreach (@backup_points) {
959        # @backup_points is in descending order (newest first)
960        while ($_ <= $cutoff) {
961          # Move to the next largest bin if the current backup is not in the
962          # current bin. However, if there is no larger bin, then don't
963          shift(@periods);
964          if (@periods) {
965            $cutoff = $now - $periods[0];
966          } else {
967            last;
968          }
969        }
970        # Throw away all backups older than the largest time period specified
971        if (!@periods) {
972          last;
973        }
974        push(@{$backup_bins{$periods[0]}}, $_);
975      }
976      foreach (keys(%backup_bins)) {
977        my $keep = $retention_map{$_}; # How many backups to keep
978        if ($backup_bins{$_}) {
979          my @backups = @{$backup_bins{$_}};
980          my $total = @backups;  # How many backups we have
981          # If we didn't specify how many to keep, keep them all
982          if ($keep == -1) { $keep = $total };
983          # If we have less backups than we should keep, keep them all
984          if ($total < $keep) { $keep = $total };
985          for (my $i = 1; $i <= $keep; $i++) {
986            my $idx = int(($i * $total) / $keep) - 1;
987            push(@{$must_save{$backups[$idx]}}, "retention policy - $_");
988          }
989        }
990      }
991      if ($DEBUG) {
992        print "    => Backup bins:\n";
993        foreach my $a (keys(%backup_bins)) {
994          print "      => $a\n";
995          foreach my $i (@{$backup_bins{$a}}) {
996            my $trans = $now - $i;
997            print "         => $i ($trans seconds old)";
998            if (exists($must_save{$i})) { print " => keep" };
999            print "\n";
1000          }
1001        }
1002      }
1003
1004      # Look for dependencies
1005      foreach (@backup_points) {
1006        if(exists($info->{incremental}->{$_})) {
1007          print "   => $_ depends on $info->{incremental}->{$_}->{depends}\n" if($DEBUG);
1008          if (exists($must_save{$_})) {
1009            push(@{$must_save{$info->{incremental}->{$_}->{depends}}},
1010              "dependency");
1011          }
1012        }
1013      }
1014
1015      my @removals = grep { !exists($must_save{$_}) } @backup_points;
1016      if($DEBUG) {
1017        my $tf = config_get($host, 'time_format');
1018        print "    => Candidates for removal:\n";
1019        foreach (@backup_points) {
1020          print "      => ". strftime($tf, localtime($_));
1021          print " ($_)";
1022          print " [". (exists($info->{full}->{$_}) ? "full":"incremental") ."]";
1023          if (exists($must_save{$_})) {
1024            my $reason = join(", ", @{$must_save{$_}});
1025            print " => keep ($reason)";
1026          } else {
1027            print " => remove";
1028          }
1029          print "\n";
1030        }
1031      }
1032      foreach (@removals) {
1033        my $efs = dir_encode($disk);
1034        my $filename;
1035        my $dataset;
1036        if(exists($info->{full}->{$_}->{file})) {
1037          $filename = $info->{full}->{$_}->{file};
1038        } elsif(exists($info->{incremental}->{$_}->{file})) {
1039          $filename = $info->{incremental}->{$_}->{file};
1040        } elsif(exists($info->{full}->{$_}->{dataset})) {
1041          $dataset = $info->{full}->{$_}->{dataset};
1042        } elsif(exists($info->{incremental}->{$_}->{dataset})) {
1043          $dataset = $info->{incremental}->{$_}->{dataset};
1044        } else {
1045          print "ERROR: We tried to expunge $host $disk [$_], but couldn't find it.\n";
1046        }
1047        print "    => expunging ${filename}${dataset}\n" if($DEBUG);
1048        unless($NEUTERED) {
1049          if ($filename) {
1050            unlink($filename) || print "ERROR: unlink $filename: $?\n";
1051          } elsif ($dataset) {
1052            `__ZFS__ destroy $dataset`;
1053            if ($?) {
1054              print "ERROR: zfs destroy $dataset: $?\n";
1055            }
1056          }
1057        }
1058      }
1059    }
1060  }
1061}
1062
1063sub __default_sort($$) { return $_[0] cmp $_[1]; }
1064
1065sub choose($$;$$) {
1066  my($name, $obj, $many, $sort) = @_;
1067  $sort ||= \&__default_sort;;
1068  my @list;
1069  my $hash;
1070  if(ref $obj eq 'ARRAY') {
1071    @list = sort { $sort->($a,$b); } (@$obj);
1072    map { $hash->{$_} = $_; } @list;
1073  }
1074  elsif(ref $obj eq 'HASH') {
1075    @list = sort { $sort->($a,$b); } (keys %$obj);
1076    $hash = $obj;
1077  }
1078  else {
1079    die "choose passed bad object: " . ref($obj) . "\n";
1080  }
1081  return \@list if(scalar(@list) == 1) && $many;
1082  return $list[0] if(scalar(@list) == 1) && !$many;
1083  print "\n";
1084  my $i = 1;
1085  for (@list) {
1086    printf " %3d) $hash->{$_}\n", $i++;
1087  }
1088  if ($many) {
1089    my @selection;
1090    my $range;
1091    while(1) {
1092      print "$name: ";
1093      chomp($range = <>);
1094      next if ($range !~ /^[\d,-]+$/);
1095      my @parts = split(',', $range);
1096      foreach my $part (@parts) {
1097          my ($from, $to) = ($part =~ /(\d+)(?:-(\d+))?/);
1098          if ($from < 1 || $to > scalar(@list)) {
1099              print "Invalid range: $from-$to\n";
1100              @selection = ();
1101              last;
1102          }
1103          if ($to) {
1104            push @selection, @list[$from - 1 .. $to - 1];
1105          } else {
1106            push @selection, @list[$from - 1];
1107          }
1108      }
1109      if (@selection) {
1110          last;
1111      }
1112    }
1113    return \@selection;
1114  } else {
1115    my $selection = 0;
1116    while($selection !~ /^\d+$/ or
1117          $selection < 1 or
1118          $selection >= $i) {
1119      print "$name: ";
1120      chomp($selection = <>);
1121    }
1122    return $list[$selection - 1];
1123  }
1124}
1125
1126sub backup_chain($$) {
1127  my ($info, $ts) = @_;
1128  my @list;
1129  push @list, $info->{full}->{$ts} if(exists($info->{full}->{$ts}));
1130  if(exists($info->{incremental}->{$ts})) {
1131    push @list, $info->{incremental}->{$ts};
1132    push @list, backup_chain($info, $info->{incremental}->{$ts}->{depends});
1133  }
1134  return @list;
1135}
1136
1137sub get_fs_from_mountpoint($) {
1138    my ($mountpoint) = @_;
1139    my $fs;
1140    my $rv = open(ZFSLIST, "__ZFS__ list -t filesystem -H |");
1141    die "Unable to determine zfs filesystem for $mountpoint" unless $rv;
1142    while (<ZFSLIST>) {
1143        my @F = split(' ');
1144        if ($F[-1] eq $mountpoint) {
1145            $fs = $F[0];
1146            last;
1147        }
1148    }
1149    close(ZFSLIST);
1150    die "Unable to determine zfs filesystem for $mountpoint" unless $fs;
1151    return $fs;
1152}
1153
1154sub perform_restore() {
1155  my (%source, %classmap);
1156
1157  foreach my $host (grep { $_ ne "default" && $conf{$_}->{"type"} ne "class"}
1158      keys %conf) {
1159    # If -h was specific, we will skip this host if the arg isn't
1160    # an exact match or a pattern match
1161    if($HOST &&
1162       !(($HOST eq $host) ||
1163         ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
1164      next;
1165    }
1166
1167    foreach my $class (get_classes()) {
1168      if ($DEBUG) {
1169        if ($class) {
1170          print "=> Class: $class\n" if $class;
1171        } else {
1172          print "=> Class: (none)\n";
1173        }
1174      }
1175      my $store = get_store($host, $class);
1176      my $backup_info = scan_for_backups($store);
1177      foreach my $disk (sort keys %{$backup_info}) {
1178        my $info = $backup_info->{$disk};
1179        next unless(ref($info) eq 'HASH');
1180        next
1181          if($ZFS &&      # if the pattern was specified it could
1182            !($disk eq $ZFS ||        # be a specific match or a
1183              ($ZFS =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
1184        # We want to see this one
1185        my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
1186        my @source_points;
1187        foreach (@backup_points) {
1188          push @source_points, $_ if(!$TIMESTAMP || $TIMESTAMP == $_)
1189        }
1190        if(@source_points) {
1191          $source{$host}->{$disk} = \@source_points;
1192          $classmap{$host}->{$disk} = $class;
1193        }
1194      }
1195    }
1196  }
1197
1198  if(! keys %source) {
1199    print "No matching backups found\n";
1200    return;
1201  }
1202
1203  # Here goes the possibly interactive dialog
1204  my $host = choose("Restore from host",  [keys %source]);
1205  my $disks = choose("Restore from ZFS", [keys %{$source{$host}}], 1);
1206
1207  if (scalar(@$disks) > 1) {
1208    # We selected multiple backups, only the latest backup of each should be
1209    # used
1210    print "Multiple filesystems selected, choosing latest backup for each\n";
1211    my $backup_list = {};
1212    foreach my $disk (@$disks) {
1213      my $store = get_store($host, $classmap{$host}->{$disk});
1214      my $backup_info = scan_for_backups($store);
1215      $backup_list->{$disk} = [ reverse backup_chain($backup_info->{$disk},
1216          $backup_info->{$disk}->{last_backup}) ];
1217    }
1218
1219    if(!$RESTORE_HOST) {
1220      print "Restore to host [$host]:";
1221      chomp(my $input = <>);
1222      $RESTORE_HOST = length($input) ? $input : $host;
1223    }
1224    if(!$RESTORE_ZFS) {
1225      print "Restore at base zfs (filesystem must exist) []:";
1226      chomp(my $input = <>);
1227      $RESTORE_ZFS = $input;
1228    }
1229
1230    # show intentions
1231    print "Going to restore:\n";
1232    print "\tfrom: $host\n";
1233    foreach my $disk (@$disks) {
1234      print "\tfrom: $disk\n";
1235    }
1236    print "\t  to: $RESTORE_HOST\n";
1237    print "\t  at base zfs: $RESTORE_ZFS\n";
1238    print "\n";
1239
1240    foreach my $disk (@$disks) {
1241      print "Restoring: $disk\n";
1242      foreach(@{$backup_list->{$disk}}) {
1243        my $restore_dataset = $disk;
1244        if ($RESTORE_ZFS) {
1245          $restore_dataset = "$RESTORE_ZFS/$restore_dataset";
1246        }
1247        $_->{success} = zfs_restore_part($RESTORE_HOST, $restore_dataset, $_->{file}, $_->{dataset}, $_->{depends});
1248      }
1249    }
1250  } else {
1251    my $disk = $disks->[0];
1252    # Times are special.  We build a human readable form and use a numerical
1253    # sort function instead of the default lexical one.
1254    my %times;
1255    my $tf = config_get($host, 'time_format');
1256    map { $times{$_} = strftime($tf, localtime($_)); } @{$source{$host}->{$disk}};
1257    my $timestamp = choose("Restore as of timestamp", \%times, 0,
1258                            sub { $_[0] <=> $_[1]; });
1259
1260    my $store = get_store($host, $classmap{$host}->{$disk});
1261    my $backup_info = scan_for_backups($store);
1262    my @backup_list = reverse backup_chain($backup_info->{$disk}, $timestamp);
1263
1264    if(!$RESTORE_HOST) {
1265      print "Restore to host [$host]:";
1266      chomp(my $input = <>);
1267      $RESTORE_HOST = length($input) ? $input : $host;
1268    }
1269    if(!$RESTORE_ZFS) {
1270      print "Restore to zfs [$disk]:";
1271      chomp(my $input = <>);
1272      $RESTORE_ZFS = length($input) ? $input : $disk;
1273    }
1274
1275    # show intentions
1276    print "Going to restore:\n";
1277    print "\tfrom: $host\n";
1278    print "\tfrom: $disk\n";
1279    print "\t  at: $timestamp [" . strftime($tf, localtime($timestamp)) . "]\n";
1280    print "\t  to: $RESTORE_HOST\n";
1281    print "\t  to: $RESTORE_ZFS\n";
1282    print "\n";
1283
1284    foreach(@backup_list) {
1285      $_->{success} = zfs_restore_part($RESTORE_HOST, $RESTORE_ZFS, $_->{file}, $_->{dataset}, $_->{depends});
1286    }
1287  }
1288
1289}
1290
1291sub zfs_restore_part($$$$;$) {
1292  my ($host, $fs, $file, $dataset, $dep) = @_;
1293  unless ($file || $dataset) {
1294    print STDERR "=> No dataset or filename given to restore. Bailing out.";
1295    return 1;
1296  }
1297  my $ssh_config = config_get($host, 'ssh_config');
1298  $ssh_config = "-F $ssh_config" if($ssh_config);
1299  print "Using custom ssh config file: $ssh_config\n" if($DEBUG);
1300  my $command;
1301  if(exists($conf{$host})) {
1302    my $agent = config_get($host, 'agent');
1303    $command = "$agent -r -z $fs";
1304    $command .= " -b $dep" if($dep);
1305  }
1306  else {
1307    $command = "__ZFS__ recv $fs";
1308  }
1309  if ($file) {
1310    print " => piping $file to $command\n" if($DEBUG);
1311    print "gzip -dfc $file | ssh $ssh_config $host $command\n" if ($DEBUG && $NEUTERED);
1312  } elsif ($dataset) {
1313    print " => piping $dataset to $command using zfs send\n" if ($DEBUG);
1314    print "zfs send $dataset | ssh $ssh_config $host $command\n" if ($DEBUG && $NEUTERED);
1315  }
1316  unless($NEUTERED) {
1317    if ($file) {
1318      open(DUMP, "gzip -dfc $file |");
1319    } elsif ($dataset) {
1320      open(DUMP, "__ZFS__ send $dataset |");
1321    }
1322    eval {
1323      open(RECEIVER, "| ssh $ssh_config $host $command");
1324      my $buffer;
1325      while(my $len = sysread(DUMP, $buffer, $BLOCKSIZE)) {
1326        if(syswrite(RECEIVER, $buffer, $len) != $len) {
1327          die "$!";
1328        }
1329      }
1330    };
1331    close(DUMP);
1332    close(RECEIVER);
1333  }
1334  return $?;
1335}
1336
1337sub pretty_print_backup($$$) {
1338  my ($info, $host, $point) = @_;
1339  my $tf = config_get($host, 'time_format');
1340  print "\t" . strftime($tf, localtime($point)) . " [$point] ";
1341  if(exists($info->{full}->{$point})) {
1342    if ($info->{full}->{$point}->{file}) {
1343      my @st = stat($info->{full}->{$point}->{file});
1344      print "FULL " . pretty_size($st[7]);
1345      print "\n\tfile: $info->{full}->{$point}->{file}" if($SHOW_FILENAMES);
1346    } elsif ($info->{full}->{$point}->{dataset}) {
1347      print "FULL $info->{full}->{$point}->{pretty_size}";
1348      print "\n\tdataset: $info->{full}->{$point}->{dataset}"
1349        if($SHOW_FILENAMES);
1350    }
1351  } else {
1352    my @st = stat($info->{incremental}->{$point}->{file});
1353    print "INCR from [$info->{incremental}->{$point}->{depends}] " . pretty_size($st[7]);
1354    print "\n\tfile: $info->{incremental}->{$point}->{file}" if($SHOW_FILENAMES);
1355  }
1356  print "\n";
1357}
1358
1359sub show_backups($$) {
1360  my ($host, $diskpat) = @_;
1361  my (@files, @datasets, %classmap);
1362  my $tf = config_get($host, 'time_format');
1363  foreach my $class (get_classes()) {
1364    if ($DEBUG) {
1365      if ($class) {
1366        print "=> Class: $class\n" if $class;
1367      } else {
1368        print "=> Class: (none)\n";
1369      }
1370    }
1371    my $store = get_store($host, $class);
1372    my $backup_info = scan_for_backups($store);
1373    foreach my $disk (sort keys %{$backup_info}) {
1374      my $info = $backup_info->{$disk};
1375      next unless(ref($info) eq 'HASH');
1376      next
1377        if($diskpat &&      # if the pattern was specified it could
1378          !($disk eq $diskpat ||        # be a specific match or a
1379            ($diskpat =~ /^\/(.+)\/$/ && $disk =~ /$1/))); # regex
1380
1381      my @backup_points = (keys %{$info->{full}}, keys %{$info->{incremental}});
1382      @backup_points = sort { $a <=> $b } @backup_points;
1383      @backup_points = (pop @backup_points) unless ($ARCHIVE || $SUMMARY_EXT);
1384
1385      # We want to see this one
1386      print "$host:$disk\n";
1387      next unless($SUMMARY || $SUMMARY_EXT || $ARCHIVE);
1388      if($SUMMARY_EXT) {
1389        print "\tLast Full: ". ($info->{last_full} ? strftime($tf, localtime($info->{last_full})) : "Never") . "\n";
1390        if($info->{last_full} < $info->{last_incremental}) {
1391          print "\tLast Incr: ". strftime($tf, localtime($info->{last_incremental})). "\n";
1392        }
1393      }
1394      foreach (@backup_points) {
1395        pretty_print_backup($info, $host, $_);
1396        if(exists($info->{full}->{$_}->{file})) {
1397          push @files, $info->{full}->{$_}->{file};
1398          $classmap{$info->{full}->{$_}->{file}} = $class;
1399        } elsif(exists($info->{incremental}->{$_}->{file})) {
1400          push @files, $info->{incremental}->{$_}->{file};
1401          $classmap{$info->{incremental}->{$_}->{file}} = $class;
1402        } elsif(exists($info->{full}->{$_}->{dataset})) {
1403          push @datasets, $info->{full}->{$_}->{dataset};
1404          $classmap{$info->{full}->{$_}->{dataset}} = $class;
1405        }
1406      }
1407      print "\n";
1408    }
1409  }
1410  if($ARCHIVE && (scalar(@files) || scalar(@datasets))) {
1411    print "\nAre you sure you would like to archive ".scalar(@files).
1412      " file(s) and ".scalar(@datasets)." dataset(s)? ";
1413    while(($_ = <>) !~ /(?:y|n|yes|no)$/i) {
1414      print "\nAre you sure you would like to archive ".scalar(@files).
1415        " file(s) and ".scalar(@datasets)." dataset(s)? ";
1416    }
1417    if(/^y/i) {
1418      if (@files) {
1419        my $archive = config_get($host, 'archive');
1420        $archive =~ s/%h/$host/g;
1421        if(! -d $archive) {
1422          mkdir $archive || die "Cannot mkdir($archive)\n";
1423        }
1424        foreach my $file (@files) {
1425          my $store = get_store($host, $classmap{$file});
1426          (my $afile = $file) =~ s/^$store/$archive/;
1427          move($file, $afile) || print "Error archiving $file: $!\n";
1428        }
1429      }
1430      if (@datasets) {
1431        my $archive = config_get($host, 'archive');
1432        (my $basearchive = $archive) =~ s/\/?%h//g;
1433        my $basearchivefs;
1434        eval {
1435          $basearchivefs = get_fs_from_mountpoint($basearchive);
1436        };
1437        die "Unable to find archive filesystem. The archive directory must be the root of a zfs filesystem to archive datasets." if $@;
1438        my $archivefs = "$basearchivefs/$host";
1439        `__ZFS__ create $archivefs`; # We don't care if this fails
1440        my %seen = ();
1441        foreach my $dataset (@datasets) {
1442          my $store = get_store($host, $classmap{$dataset});
1443          my $storefs = get_fs_from_mountpoint($store);
1444          $dataset =~ s/@.*$//; # Only rename filesystems, not snapshots
1445          next if $seen{$dataset}++; # Only rename a filesystem once
1446          (my $adataset = $dataset) =~ s/^$storefs/$archivefs/;
1447          `__ZFS__ rename $dataset $adataset`;
1448          if ($?) {
1449            print "Error archiving $dataset\n";
1450          }
1451        }
1452      }
1453    }
1454  }
1455}
1456
1457sub show_violators($$) {
1458  my ($host, $diskpat) = @_;
1459  my $host_store = get_store($host);
1460  my $filesystems = {};
1461  if (open (my $fh, "$host_store/.fslist")) {
1462    while (<$fh>) {
1463      chomp;
1464      $filesystems->{$_} = 1;
1465    }
1466    close($fh);
1467  } elsif ($DEBUG) {
1468    print "=> $host_store/.fslist not present, skipping missing FS detection\n";
1469  }
1470  foreach my $class (get_classes()) {
1471    if ($DEBUG) {
1472      if ($class) {
1473        print "=> Class: $class\n" if $class;
1474      } else {
1475        print "=> Class: (none)\n";
1476      }
1477    }
1478    my $store = get_store($host, $class);
1479    my $backup_info = scan_for_backups($store);
1480    foreach my $disk (sort keys %{$backup_info}) {
1481      my $info = $backup_info->{$disk};
1482      next unless(ref($info) eq 'HASH');
1483      next if (
1484        $diskpat &&      # if the pattern was specified it could
1485        !(
1486          $disk eq $diskpat ||                        # be a specific match
1487          ($diskpat =~ /^\/(.+)\/$/ && $disk =~ /$1/) # or a regex
1488        )
1489      ); # regex
1490      # Backups for filesystems that no longer exist aren't violators
1491      if (!$SUMMARY_VIOLATORS_VERBOSE && %{$filesystems} &&
1492        !defined $filesystems->{$disk}) {
1493        print "=> $disk doesn't exist on server, not marking as violator\n"
1494          if ($DEBUG);
1495        next;
1496      }
1497
1498
1499      my @violators = ();
1500
1501      # No recent full
1502      if (time() > $info->{last_full} +
1503          config_get($host, 'full_interval', $class) +
1504          config_get($host, 'violator_grace_period', $class)) {
1505        push @violators, {
1506          "host" => $host, "disk" => $disk,
1507          "reason" => "No recent full backup",
1508          "backup" => "full"
1509        }
1510      }
1511
1512      # No recent incremental
1513      if (time() > $info->{last_backup} +
1514          config_get($host, 'backup_interval', $class) +
1515          config_get($host, 'violator_grace_period', $class)) {
1516        push @violators, {
1517          "host" => $host, "disk" => $disk,
1518          "reason" => "No recent incremental backup",
1519          "backup" => "backup"
1520        }
1521      }
1522
1523      for my $v (@violators) {
1524        print "$v->{host}:$v->{disk} - $v->{reason}\n";
1525        pretty_print_backup($info, $host, $info->{"last_$v->{'backup'}"});
1526      }
1527    }
1528  }
1529}
1530
1531sub plan_and_run($$) {
1532  my ($host, $diskpat) = @_;
1533  my $store;
1534  my $ssh_config = config_get($host, 'ssh_config');
1535  $ssh_config = "-F $ssh_config" if($ssh_config);
1536  my %suppress;
1537  print "Planning '$host'\n" if($DEBUG);
1538  my $agent = config_get($host, 'agent');
1539  my $took_action = 1;
1540  while($took_action) {
1541    $took_action = 0;
1542    my @disklist;
1543
1544    # We need a lock for the listing.
1545    return unless(lock($host, ".list"));
1546
1547    # Get list of zfs filesystems from the agent
1548    open(SILENT, ">&", \*STDERR);
1549    close(STDERR);
1550    my $rv = open(ZFSLIST, "ssh $ssh_config $host $agent -l |");
1551    open(STDERR, ">&", \*SILENT);
1552    close(SILENT);
1553    next unless $rv;
1554    @disklist = grep { chomp } (<ZFSLIST>);
1555    close(ZFSLIST);
1556    # Write the filesystem list out to a file for use by the violators list
1557    my $store = get_store($host); # Don't take classes into account - not needed
1558    mkpath($store) if(! -d $store);
1559    open(my $fh, ">$store/.fslist");
1560    foreach my $diskline (@disklist) {
1561      # Get only the filesystem and not the snapshots/classes
1562      (my $filesystem = $diskline) =~ s/ \[.*//;
1563      print $fh "$filesystem\n";
1564    }
1565    close($fh);
1566    if ($DEBUG) {
1567      print " => Filesystems for $host (zetaback_agent -l output)\n";
1568      foreach my $diskline (@disklist) {
1569        print "    $diskline\n";
1570      }
1571    }
1572
1573    foreach my $diskline (@disklist) {
1574      chomp($diskline);
1575      next unless($diskline =~ /^(\S+) \[([^\]]*)\](?: \{([^}]*)\})?/);
1576      my $diskname = $1;
1577      my %snaps;
1578      map { $snaps{$_} = 1 } (split(/,/, $2));
1579      my $class = $3;
1580
1581      # We've just done this.
1582      next if($suppress{"$host:$diskname"});
1583      # If we are being selective (via -z) now is the time.
1584      next
1585        if($diskpat &&          # if the pattern was specified it could
1586           !($diskname eq $diskpat ||        # be a specific match or a
1587             ($diskpat =~ /^\/(.+)\/$/ && $diskname =~ /$1/))); # regex
1588
1589      $store = get_store($host, $class);
1590      if ($DEBUG) {
1591        if ($class) {
1592            print STDERR "=> Class is $class\n";
1593        } else {
1594            print STDERR "=> No/default class\n";
1595        }
1596      }
1597      print " => Scanning '$store' for old backups of '$diskname'.\n" if($DEBUG);
1598
1599      # Make directory on demand
1600      mkpath($store) if(! -d $store);
1601      my $backup_info = scan_for_backups($store);
1602      # That gave us info on all backups, we just want this disk
1603      $backup_info = $backup_info->{$diskname} || {};
1604
1605      # Should we do a backup?
1606      my $backup_type = 'no';
1607      if(time() > $backup_info->{last_backup} + config_get($host,
1608          'backup_interval', $class)) {
1609        $backup_type = 'incremental';
1610      }
1611      if(time() > $backup_info->{last_full} + config_get($host,
1612          'full_interval', $class)) {
1613        $backup_type = 'full';
1614      }
1615      # If we want an incremental, but have no full, then we need to upgrade to full
1616      if($backup_type eq 'incremental') {
1617        my $have_full_locally = 0;
1618        # For each local full backup, see if the full backup still exists on the other end.
1619        foreach (keys %{$backup_info->{'full'}}) {
1620          $have_full_locally = 1 if(exists($snaps{'__zb_full_' . $_}));
1621        }
1622        $backup_type = 'full' unless($have_full_locally);
1623      }
1624      $backup_type = 'full' if($FORCE_FULL);
1625      $backup_type = 'incremental' if($FORCE_INC);
1626      $backup_type = 'dataset' if(config_get($host, 'dataset_backup', $class)
1627        eq 1 && $backup_type ne 'no');
1628
1629      print " => doing $backup_type backup\n" if($DEBUG);
1630      # We need to drop a __zb_base snap or a __zb_incr snap before we proceed
1631      unless($NEUTERED || $backup_type eq 'no') {
1632        # attempt to lock this action, if it fails, skip -- someone else is working it.
1633        next unless(lock($host, dir_encode($diskname), 1));
1634        unlock($host, '.list');
1635
1636        if($backup_type eq 'full') {
1637          eval { zfs_full_backup($host, $diskname, $store); };
1638          if ($@) {
1639            chomp(my $err = $@);
1640            print " => failure $err\n";
1641          }
1642          else {
1643            # Unless there was an error backing up, remove all the other full snaps
1644            foreach (keys %snaps) {
1645              zfs_remove_snap($host, $diskname, $_) if(/^__zb_full_(\d+)/)
1646            }
1647          }
1648          $took_action = 1;
1649        }
1650        if($backup_type eq 'incremental') {
1651          eval {
1652            zfs_remove_snap($host, $diskname, '__zb_incr') if($snaps{'__zb_incr'});
1653            # Find the newest full from which to do an incremental (NOTE: reverse numeric sort)
1654            my @fulls = sort { $b <=> $a } (keys %{$backup_info->{'full'}});
1655            zfs_incremental_backup($host, $diskname, $fulls[0], $store);
1656          };
1657          if ($@) {
1658            chomp(my $err = $@);
1659            print " => failure $err\n";
1660          }
1661          else {
1662            $took_action = 1;
1663          }
1664        }
1665        if($backup_type eq 'dataset') {
1666          my @backups = sort { $b <=> $a } (keys %{$backup_info->{'full'}});
1667          eval { zfs_dataset_backup($host, $diskname, $backups[0], $store); };
1668          if ($@) {
1669            chomp(my $err = $@);
1670            print " => failure $err\n";
1671          }
1672          else {
1673            # Unless there was an error backing up, remove all the other dset snaps
1674            foreach (keys %snaps) {
1675              zfs_remove_snap($host, $diskname, $_) if(/^__zb_dset_(\d+)/)
1676            }
1677          }
1678          $took_action = 1;
1679        }
1680        unlock($host, dir_encode($diskname), 1);
1681      }
1682      $suppress{"$host:$diskname"} = 1;
1683      last if($took_action);
1684    }
1685    unlock($host, '.list');
1686  }
1687}
1688
1689## Start of main program
1690limit_running_processes;
1691
1692if($RESTORE) {
1693  perform_restore();
1694}
1695else {
1696  foreach my $host (grep { $_ ne "default" && $conf{$_}->{"type"} ne "class"}
1697      keys %conf) {
1698    # If -h was specific, we will skip this host if the arg isn't
1699    # an exact match or a pattern match
1700    if($HOST &&
1701       !(($HOST eq $host) ||
1702         ($HOST =~ /^\/(.*)\/$/ && $host =~ /$1/))) {
1703      next;
1704    }
1705
1706    # Skip if the host is marked as 'offline' and we are not listing backups
1707    if (config_get($host, 'offline') == 1 &&
1708        !$LIST && !$SUMMARY && !$SUMMARY_EXT && !$ARCHIVE) {
1709      next;
1710    }
1711
1712    if($LIST || $SUMMARY || $SUMMARY_EXT || $ARCHIVE) {
1713      show_backups($host, $ZFS);
1714    }
1715    if ($SUMMARY_VIOLATORS || $SUMMARY_VIOLATORS_VERBOSE) {
1716      show_violators($host, $ZFS);
1717    }
1718    if($BACKUP) {
1719      plan_and_run($host, $ZFS);
1720    }
1721    if($EXPUNGE) {
1722      perform_retention($host);
1723    }
1724  }
1725}
1726
1727exit 0;
1728
1729=pod
1730
1731=head1 FILES
1732
1733=over
1734
1735=item zetaback.conf
1736
1737The main zetaback configuration file.  The location of the file can be
1738specified on the command line with the -c flag.  The prefix of this
1739file may also be specified as an argument to the configure script.
1740
1741=back
1742
1743=head1 SEE ALSO
1744
1745zetaback_agent(1)
1746
1747=cut
1748