#!/usr/local/bin/perl

# Copyright (c) 2007 OmniTI Computer Consulting, Inc. All rights reserved.
# For information on licensing see:
#   https://labs.omniti.com/zetaback/trunk/LICENSE

use strict;
use Getopt::Long;
use POSIX qw/mkfifo/;
use Data::Dumper;

use vars qw/%conf $version_string
            $PREFIX $CONF $LIST $FULL $SNAP $ZFS $BASE $RESTORE $VERSION
            $BUG_6343779 $NEEDSFD $DSET/;
$version_string = '1.0.6';
$PREFIX = q^/usr/local^;
$CONF = qq^$PREFIX/etc/zetaback_agent.conf^;
$NEEDSFD = ($^O eq 'darwin') ? 1 : 0;

=pod

=head1 NAME

zetaback_agent - client-side component of zetaback.

=head1 SYNOPSIS

  zetaback_agent -v

  zetaback_agent -l [-c conf]

  zetaback_agent -r [-b <timestamp>] [-c conf] [-z zfs]

  zetaback -f <timestamp> [-c conf] [-z zfs]

  zetaback -i <timestamp> [-c conf] [-z zfs]

  zetaback -d <snap> -z <zfs> [-c conf]

=cut

GetOptions(
  "c=s" => \$CONF,
  "l"   => \$LIST,
  "r"   => \$RESTORE,
  "z=s" => \$ZFS,
  "d=s" => \$SNAP,
  "f=s" => \$FULL,
  "i=s" => \$BASE,
  "s=s" => \$DSET,
  "b=s" => \$BUG_6343779,
  "v"   => \$VERSION,
);

=pod

=head1 DESCRIPTION

B<zetaback_agent> handles requests from zetaback and performs the requested
operations on a host.  Normally B<zetaback_agent> is only called by
zetaback and should never need to be invoked directly.

=head1 OPTIONS

The following options are available:

=over

=item -c <conf>

Use the specified file as the configuration file.  The default file, if
none is specified is /usr/local/etc/zetaback_agent.conf.  The prefix of this
file may also be specified as an argument to the configure script.

=item -d <snap>

Delete the specified snapshot.  Requires the use of -z to specify the
ZFS filesystem.

=item -f <timestamp>

Perform a full backup.  The name of the backup will include <timestamp>,
which is provided by the backup server.

=item -i <timestamp>

Perform an incremental backup.  The name of the backup will include 
<timestamp>, which is provided by the backup server.

=item -s <timestamp>

Perform a dataset backup.  The name of the backup will include 
<timestamp>, which is provided by the backup server. This requires the -i
option to specify the base dataset the expected by the backup server.

=item -l

List ZFS filesystems.

=item -r

Perform a restore.

=item -b

When performing a restore, if -b is specified, it informs the agent that
the receive command is an incremental based of the full snapshot with the
timestamp specified.  The agent will unmount and rollback the filesystem
prior to applying the incremental in order to work around bug 6343779.

=item -v

Print the version number and exit.

=item -z

Specify a ZFS filesystem to backup, restore, or delete.

=back

=cut

if($VERSION) {
  print "zetaback_agent: $version_string\n";
  exit 0;
}

=pod

=head1 CONFIGURATION

The zetaback_agent configuration file contains a pattern list of ZFS 
filesystems to be backed up.  The pattern list is a Perl-compatible 
regular expression (PCRE).  Only one 'pattern=' line is permitted.

The pattern acts as a filter to reduce the list of filesystems to
be backed up.  Further excludes from this list are possible by setting
a user property on any filesystem that should not be backed up, even
if it matches the pattern:

  zfs set com.omniti.labs.zetaback:exclude=on pool/fs

User properties are available on Solaris 10 8/07 and newer, and on
Solaris Express build 48 and newer.

Once a pattern and/or exclude properties have been configured on a host,
the list of remaining filesystems can be validated by invoking 
zetaback_agent with the -l option.

=head2 Excluding inactive boot environments

The zetaback_agent configuration file also has an option to not back up filesystems that are part of an alternate/inactive boot environment. To enable this option, add the following to the configuration file:

  exclude_inactive_be=1

=head1 CONFIGURATION EXAMPLES

=head2 All ZFS filesystems

This pattern matches all ZFS filesystems.

  pattern=.

=head2 Substring match

This will match anywhere in the name of the ZFS filesystem.  This is
helpful for catching all ZFS filesystems in a particular zpool, while
excluding any others.

  pattern=zones

=head2 Left-anchored names

This pattern matches all ZFS filesystems whose names begin with 'www'.

  pattern=^www

=head2 Specific ZFS filesystems

This pattern matches specific ZFS filesystems.

  pattern=(?:data|mirrors|www)

=head2 Combining with property-based exclude

All filesystems in pool 'zones' except 'foo'

  pattern=^zones

  (At a root shell or with pfexec/sudo):
  zfs set com.omniti.labs.zetaback:exclude=on zones/foo

=cut

# Read our config in
$conf{pattern} = '.';
$conf{exclude_inactive_be} = '0';
open(CONF, "<$CONF");
while(<CONF>) { /^\s*([^#](?:\S*)?)\s*=\s*(\S+)/ && ($conf{lc($1)} = $2); }
close(CONF);

sub zfs_agent_remove_snap {
  my $target = $ZFS . '@';
  die "zfs_agent_remove_snap: insufficient args\n" unless($ZFS && $SNAP);
  if($SNAP eq '__zb_incr' or
     $SNAP =~ /__zb_full_\d+/ or
     $SNAP =~ /__zb_dset_\d+/) {
    $target .= $SNAP;
  }
  else {
    die "zfs_agent_remove_snap: illegal snap: $SNAP\n";
  }
  `/usr/sbin/zfs destroy $target`;
}

sub zfs_agent_perform_full {
  my $target = $ZFS . '@__zb_full_' . $FULL;
  unless($ZFS && $FULL =~ /^\d+$/) {
    die "zfs_agent_perform_full: bad fs or snap name\n"
  }
  `/usr/sbin/zfs snapshot $target`;
  my @cmd = ("/usr/sbin/zfs", "send", $target);
  if($NEEDSFD) {
    fifo_exec(@cmd);
  } else {
    exec { $cmd[0] } @cmd;
  }
  exit;
}

sub zfs_agent_perform_incremental {
  my $target = $ZFS . '@__zb_incr';
  my $base = $ZFS . '@__zb_full_' . $BASE;
  unless($ZFS && $BASE) {
    die "zfs_agent_perform_incremental: bad args\n"
  }
  `/usr/sbin/zfs snapshot $target`;
  my @cmd = ("/usr/sbin/zfs", "send", "-i", $base, $target);
  if($NEEDSFD) {
    fifo_exec(@cmd);
  } else {
    exec { $cmd[0] } @cmd;
  }
  exit;
}

sub zfs_agent_perform_dataset {
  my $target = $ZFS . '@__zb_dset_' . $DSET;
  my $base = $ZFS . '@__zb_dset_' . $BASE;
  unless($ZFS && $DSET) {
    die "zfs_agent_perform_dataset: bad args\n"
  }
  `/usr/sbin/zfs snapshot $target`;
  # $BASE (the base snapshot) is optional. If provided, send an incremental
  # snapshot
  my @cmd;
  if ($BASE) {
    @cmd = ("/usr/sbin/zfs", "send", "-i", $base, $target);
  } else {
    @cmd = ("/usr/sbin/zfs", "send", $target);
  }
  if($NEEDSFD) {
    fifo_exec(@cmd);
  } else {
    exec { $cmd[0] } @cmd;
  }
  exit;
}

sub zfs_agent_list {
  my (%zfs, %storageclass);
  open(ZFSLIST, "/usr/sbin/zfs list -H -t snapshot,filesystem,volume -o name,com.omniti.labs.zetaback:exclude,com.omniti.labs.zetaback:class,org.opensolaris.libbe:parentbe,org.opensolaris.libbe:uuid |");
  # Get the UUID (if any) of the current BE, should return blank on systems
  # where beadm isn't present
  my $currentbe = "";
  if($conf{exclude_inactive_be} eq '1') {
    $currentbe = (split(/;/,`/sbin/beadm list -H 2>&1 | grep ';N'`))[1];
  }
  while(<ZFSLIST>) {
    chomp;
    my @line = split /\t/;
    (my $fs = $line[0]) =~ s/\@.+//;
    my $excl = $line[1];
    my $class = $line[2];
    my $parentbe = $line[3];
    my $beuuid = $line[4];
    if($conf{exclude_inactive_be} eq '1') {
        next if ($parentbe ne '-' && $parentbe ne $currentbe);
        next if ($beuuid ne '-' && $beuuid ne $currentbe);
    }
    if(($excl ne "on") && ($fs =~ /$conf{pattern}/)) {
      if($line[0] =~ /(\S+)\@([^\@]+)$/) {
        $zfs{$1} ||= [];
        push @{$zfs{$1}}, $2;
        if ($class ne "-" && $class ne "") {
            $storageclass{$1} = $class;
        }
      }
      else {
        $zfs{$line[0]} ||= [];
        if ($class ne "-" && $class ne "") {
            $storageclass{$line[0]} = $class;
        }
      }
    }
  }
  close(ZFSLIST);

  foreach my $fs (sort keys %zfs) {
    print "$fs [".join(',',@{$zfs{$fs}})."]";
    if ($storageclass{$fs} ne "") {
        print " {$storageclass{$fs}}";
    }
    print "\n";
  }
}

sub zfs_agent_perform_restore {
  unless($ZFS && $RESTORE) {
    die "zfs_agent_perform_restore: bad state\n";
  }
  if($BUG_6343779) {
    # Optionally work around Solaris bug: 6343779
    my $base = $ZFS . '@__zb_full_' . $BUG_6343779;
    `/usr/sbin/zfs unmount $ZFS`;
    `/usr/sbin/zfs rollback $base`;
  }
  my @cmd = ("/usr/sbin/zfs", "recv", $ZFS);
  exec { $cmd[0] } @cmd;
  exit;
}

sub fifo_exec {
  my @cmd = @_;
  my $rv = -1;
  my $fifo = "zetaback_${$}_${FULL}${BASE}.fifo";
  mkfifo($fifo, 0600) || die "Could not create fifo: $!";
  my $pid = fork();
  if($pid == 0) {
    close(STDOUT);
    open(STDOUT, ">$fifo") || die "Could not open fifo: $!";
    exec { $cmd[0] } @cmd;
    exit;
  }
  open(FIFO, "<$fifo");
  unlink($fifo);
  my $buf;
  while(my $len = sysread(FIFO, $buf, 1024*64)) {
    syswrite(STDOUT, $buf, $len);
  }
  waitpid($pid, 0);
}

if($LIST) { zfs_agent_list(); exit; }
if($ZFS && $SNAP) { zfs_agent_remove_snap(); exit; }
if($ZFS && $RESTORE) { zfs_agent_perform_restore(); exit; }
if($ZFS && $FULL) { zfs_agent_perform_full(); exit; }
if($ZFS && $DSET) { zfs_agent_perform_dataset(); exit; }
if($ZFS && $BASE) { zfs_agent_perform_incremental(); exit; }

=pod

=head1 FILES

=over

=item zetaback_agent.conf

The zetaback_agent configuration file.  The location of the file can be
specified on the command line with the -c flag.  The prefix of this
file may also be specified as an argument to the configure script.

=back

=head1 SEE ALSO

zetaback(1)

=cut
