#!/usr/pkg/bin/perl

use lib qw(); # PERL5LIB
use lib qw(/usr/pkg/lib); # LIBDIR

use Getopt::Long qw(:config posix_default no_ignore_case pass_through);
use Pod::Usage;
use File::Temp qw(tempfile);
use Scalar::Util qw(blessed);
use Mojo::Base -strict;
use Mojo::Log;
use ZnapZend::Config;
my $VERSION = '0.dev'; #VERSION

my @ROOT_EXEC_OPTIONS = qw(pfexec sudo rootExec=s debug features=s);
my %featureMap = map { $_ => 1 } qw(pfexec sudo lowmemRecurse zfsGetType);

sub dumpProperties {
    my $props = shift;

    for my $backupSet (@$props){
        print "*** backup plan: $backupSet->{src} ***\n";
        for my $opt (sort keys %$backupSet){
            #singular plural check on backup plans. if value is 1, remove trailing 's'
            $opt =~ /^(?:src|dst_[^_]+)_plan$/
                && $backupSet->{$opt} =~ s/(\d+)([a-z]+)s/$1 eq '1' ? "$1$2" : "$1$2s"/eg;

            #print warning if destination does not exist, print property otherwise
            if (my ($dst) = $opt =~ /^(dst_[^_]+)_valid$/){
                $backupSet->{$opt} || do {
                    warn "*** WARNING: destination '$backupSet->{$dst}'"
                        . " does not exist, will be ignored! ***\n\n";
                }
            }
            else{
                printf("%16s = %s\n",$opt,$backupSet->{$opt});
            }
        }
        print "\n";
    }
}

sub dumpPropertiesRaw {
    my $backupSet = shift;
    my @dump;

    for my $prop (sort keys %$backupSet){
        #don't export valid flag for destinations as this will be evaluated on demand
        next if $prop =~ /^dst_[^_]+_valid/;

        push @dump, "$prop=$backupSet->{$prop}";
    }
    return \@dump;
}

sub parseArguments {
    my $args = shift;
    my %backupSet;
    my $key = 0;
    my $state = '';

    for (@$args){
        #keyword SRC detected. start setting up source filesystem and plan
        /^SRC$/ && do {
            $state = 'src';
            next;
        };
        #keyword DST detected. start setting up destination filesystem and plan
        /^DST(?::([-_.a-zA-Z0-9]+))?/ && do {
            $state = 'dst';
            $key = $1 // $key++;
            next;
        };
        #option contains '=>' so it must be a backup plan
        /=>/ && do {
            $backupSet{src_plan} = $_ and next if $state eq 'src';
            $backupSet{'dst_' . $key . '_plan'} = $_ and next if $state eq 'dst';
            die "ERROR: backup plan $_ is not related do a source or destination dataset. check the syntax\n";
        };
        #option must be a dataset or invalid
        $state eq 'src' && do {
            $backupSet{src} = $_;
            $state = '';
            next;
        };
        #destination may be followed by a pre and potentially a post send/receive command
        $state eq 'dst' && do {
            $backupSet{"dst_$key"} = $_;
            $state = 'pre';
            next;
        };
        #catch pre-command if any
        $state eq 'pre' && do {
            $backupSet{'dst_' . $key . '_precmd'} = $_;
            $state = 'pst';
            next;
        };
        #catch post-command if any
        $state eq 'pst' && do {
            $backupSet{'dst_' . $key . '_pstcmd'} = $_;
            $state = '';
            next;
        };
        die "ERROR: dont know what to do with $_. check the syntax\n";
    }

    #check if we have a valid source as this is crucial
    $backupSet{src} or die "ERROR: no source dataset given. check the syntax\n";

    return \%backupSet;
}

sub main {
    my $mainOpt = shift;
    my $opts = {};
    my %cfg;
    my $ZNAPZEND_TESTING = 0;

### RM_COMM_4_TEST ###  # remove ### RM_COMM_4_TEST ### comments for testing purpose.
### RM_COMM_4_TEST ###  $ZNAPZEND_TESTING = 1;

    if (!defined $mainOpt) {
        pod2usage(-exitval => 'NOEXIT');
        die ("ERROR: Main option was not provided\n");
    }

    Getopt::Long::Configure(qw(posix_default no_ignore_case pass_through));
    GetOptions($opts, @ROOT_EXEC_OPTIONS) or exit 1;
    Getopt::Long::Configure(qw(posix_default no_ignore_case));

    #split all features into individual options
    if ($opts->{features}){
        for my $feature (split /,/, $opts->{features}){
            $featureMap{$feature} or die "ERROR: feature '$feature' not supported\n";
            if ($feature eq "pfexec" or $feature eq "sudo") {
                warn "--features=" . $feature . " is deprecated. Use --rootExec=" . $feature . " instead\n";
                $opts->{rootExec} = $feature;
            } else {
                $opts->{$feature} = 1;
            }
        }
        delete $opts->{features};
    }

    if ($opts->{pfexec}) {
        warn "--pfexec is deprecated. Use --rootExec=pfexec instead\n";
        $opts->{rootExec} = 'pfexec';
    } elsif ($opts->{sudo}) {
        warn "--sudo is deprecated. Use --rootExec=sudo instead\n";
        $opts->{rootExec} = 'sudo';
    }

    if (defined($opts->{debug})) {
        $opts->{debug} = ( $opts->{debug} eq 'off' ? 0 : 1 );
    } else {
        $opts->{debug} = 0;
    }

    if (!defined($opts->{lowmemRecurse})) {
        $opts->{lowmemRecurse} = 0;
    }

    if (!defined($opts->{zfsGetType})) {
        $opts->{zfsGetType} = 0;
    }

    my $zConfig = ZnapZend::Config->new( zLog => Mojo::Log->new(),
        rootExec => $opts->{rootExec}, debug => $opts->{debug},
        lowmemRecurse => $opts->{lowmemRecurse}, zfsGetType => $opts->{zfsGetType});

    for ($mainOpt){

    /^create$/ && do {
        GetOptions($opts, (@ROOT_EXEC_OPTIONS, qw(recursive|r donotask mbuffer=s mbuffersize=s),
            qw(tsformat=s pre-snap-command=s post-snap-command=s send-delay=i))) or exit 1;

        $cfg{enabled}       = 'on';
        $cfg{recursive}     = $opts->{recursive}            ? 'on' : 'off';
        $cfg{mbuffer}       = $opts->{mbuffer}             || 'off';
        $cfg{mbuffer_size}  = $opts->{mbuffersize}         || '1G';
        $cfg{tsformat}      = $opts->{tsformat}            || '%Y-%m-%d-%H%M%S';
        $cfg{pre_znap_cmd}  = $opts->{'pre-snap-command'}  || 'off';
        $cfg{post_znap_cmd} = $opts->{'post-snap-command'} || 'off';
        $cfg{zend_delay}    = $opts->{'send-delay'}        || 0;

        my $backupSet = parseArguments(\@ARGV);
        #merge arguments to our cfg
        for (keys %$backupSet){
            $cfg{$_} = $backupSet->{$_};
        }

        $zConfig->checkBackupSet(\%cfg);

        dumpProperties([\%cfg]);
        #ask user if backup plan shall be stored unless option 'donotask' is passed
        if (!$opts->{donotask}){
            print "Do you want to save this backup set [y/N]? ";
            #bypass check if testing
            my $check = $ZNAPZEND_TESTING ? 'y' : <STDIN>;
            #...and make sure test harness result is on its own line
            if ($ZNAPZEND_TESTING) { print "\n"; }
            chomp $check;
            if ($check =~ /^y(?:es)?$/i){
                $zConfig->setBackupSet(\%cfg)
                    or die "ERROR: cannot create backup config for $cfg{src}\n";
            }
        }
        else {
            $zConfig->setBackupSet(\%cfg)
                or die "ERROR: cannot create backup config for $cfg{src}\n";
        }

        last;
    };
    /^delete$/ && do {
        GetOptions($opts, @ROOT_EXEC_OPTIONS, qw(dst=s)) or exit 1;

        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }

        if ($opts->{dst}){
            $zConfig->deleteBackupDestination($opts->{src}, 'dst_' . $opts->{dst});
        }
        else {
            $zConfig->deleteBackupSet($opts->{src})
                or die "ERROR: cannot delete backup config for $opts->{src}\n";
        }

        last;
    };
    /^edit$/ && do {
        #only dataset given so we use the editor based edit mode
        my $ignoreOpts = $ROOT_EXEC_OPTIONS[0];
        map { $ignoreOpts .= '|' . $_ } @ROOT_EXEC_OPTIONS[1 .. $#ROOT_EXEC_OPTIONS];
        if ($#ARGV == grep { /^--(?:$ignoreOpts)$/ } @ARGV){
            $opts->{src} = pop @ARGV;

            my $backupSet = $zConfig->getBackupSet(0, 0, $opts->{src})->[0]
                or die "ERROR: cannot get backup config\n";

            my $props = dumpPropertiesRaw($backupSet);
            my ($fh, $filename) = tempfile();
            for (@$props){
                print $fh "$_\n";
            }
            close $fh;

            my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
            system ($editor, $filename) && die "ERROR: executing editor\n" if !$ZNAPZEND_TESTING;

            open $fh, "<$filename" or die "ERROR: cannot open temp file $filename\n";
            while (my $prop = <$fh>){
                chomp $prop;
                $prop =~ s/\r$//;
                my ($key, $value) = split /=/, $prop, 2;
                $cfg{$key} = $value;
            }
            close $fh;
            unlink $filename;

            #check new backup set
            $zConfig->checkBackupSet(\%cfg);

            dumpProperties([\%cfg]);

            print "Do you want to save this backup set [y/N]? ";
            #bypass check if testing
            my $check = $ZNAPZEND_TESTING ? 'y' : <STDIN>;
            #...and make sure test harness result is on its own line
            if ($ZNAPZEND_TESTING) { print "\n"; }
            chomp $check;
            if ($check =~ /^y(?:es)?$/i){
                $zConfig->setBackupSet(\%cfg)
                    or die "ERROR: cannot create backup config for $cfg{src}\n";
            }

            last;
        }

        GetOptions($opts, (@ROOT_EXEC_OPTIONS, qw(donotask recursive=s mbuffer=s mbuffersize=s),
            qw(tsformat=s pre-snap-command=s post-snap-command=s send-delay=i))) or exit 1;

        my $backupSet = parseArguments(\@ARGV);

        $backupSet->{recursive}     = $opts->{recursive}
            if $opts->{recursive} && $opts->{recursive} =~ /^(?:on|off)$/;
        $backupSet->{mbuffer}       = $opts->{mbuffer}             if $opts->{mbuffer};
        $backupSet->{mbuffer_size}  = $opts->{mbuffersize}         if $opts->{mbuffersize};
        $backupSet->{tsformat}      = $opts->{tsformat}            if $opts->{tsformat};
        $backupSet->{pre_znap_cmd}  = $opts->{'pre-snap-command'}  if $opts->{'pre-snap-command'};
        $backupSet->{post_znap_cmd} = $opts->{'post-snap-command'} if $opts->{'post-snap-command'};
        $backupSet->{zend_delay}    = $opts->{'send-delay'}        if $opts->{'send-delay'};

        #get active backup config
        my $backupSets = $zConfig->getBackupSet(0, 0, $backupSet->{src}) or die "ERROR: cannot get backup config\n";
        ($backupSets && @$backupSets) or die "ERROR: backup set for $backupSet->{src} does not exist\n";
        %cfg = %{$backupSets->[0]};

        #merge new config
        for (keys %$backupSet){
            $cfg{$_} = $backupSet->{$_};
        }
        #check new backup set
        $zConfig->checkBackupSet(\%cfg);

        dumpProperties([\%cfg]);
        #ask user if backup plan shall be stored
        if (!$opts->{donotask}){
            print "Do you want to save this backup set [y/N]? ";
            #bypass check if testing
            my $check = $ZNAPZEND_TESTING ? 'y' : <STDIN>;
            #...and make sure test harness result is on its own line
            if ($ZNAPZEND_TESTING) { print "\n"; }
            chomp $check;
            if ($check =~ /^y(?:es)?$/i){
                $zConfig->setBackupSet(\%cfg)
                    or die "ERROR: cannot create backup config for $cfg{src}\n";
            }
        }
        else {
            $zConfig->setBackupSet(\%cfg)
                or die "ERROR: cannot create backup config for $cfg{src}\n";
        }

        last;
    };
    /^enable$/ && do {
        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }

        $zConfig->enableBackupSet($opts->{src})
            or die "ERROR: cannot enable backup config for $opts->{src}. Did you create it?\n";

        last;
    };
    /^disable$/ && do {
        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }
        $zConfig->disableBackupSet($opts->{src})
            or die "ERROR: cannot disable backup config for $opts->{src}. Did you create it?\n";

        last;
    };
    /^enable-dst$/ && do {
        $opts->{dst} = pop @ARGV;
        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }
        if (!defined $opts->{dst}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: destination argument for option $mainOpt was not provided\n");
        }
        $zConfig->enableBackupSetDst($opts->{src}, $opts->{dst})
            or die "ERROR: cannot enable backup config for $opts->{src} destination $opts->{dst}. Did you create this config?\n";

        last;
    };
    /^disable-dst$/ && do {
        $opts->{dst} = pop @ARGV;
        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }
        if (!defined $opts->{dst}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: destination argument for option $mainOpt was not provided\n");
        }
        $zConfig->disableBackupSetDst($opts->{src}, $opts->{dst})
            or die "ERROR: cannot disable backup config for $opts->{src} destination $opts->{dst}. Did you create this config?\n";

        last;
    };
    /^list$/ && do {
        GetOptions($opts, (@ROOT_EXEC_OPTIONS, qw(recursive|r inherited))) or exit 1;

        my $recurse = ( defined($opts->{recursive}) );
        my $inherit = ( defined($opts->{inherited}) );

        my $backupSets;
        if (! ($backupSets = $zConfig->getBackupSet($recurse, $inherit, @ARGV) ) ) {
            die "ERROR: cannot list backup config\n";
        }

        dumpProperties($backupSets);
        last;
    };
    /^export$/ && do {
        $opts->{src} = pop @ARGV;
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: source argument for option $mainOpt was not provided\n");
        }
        # TODO: support --recursive? Does not make much sense "as is" for import then

        my $backupSets;
        if (! ($backupSets = $zConfig->getBackupSet(0, 0, $opts->{src}) ) ) {
            die "ERROR: cannot get backup config\n";
        }
        my $backupSet;
        if (! ($backupSet = $backupSets->[0]) ) {
            die "ERROR: cannot get backup config\n";
        }

        my $props = dumpPropertiesRaw($backupSet);
        for (@$props){
            print "$_\n";
        }

        last;
    };
    /^import$/ && do {
        my @props;
        my $fh;

        GetOptions($opts, @ROOT_EXEC_OPTIONS, qw(write|w), 'prop=s' => \@props) or exit 1;

        if ($#ARGV){
            my $filename = pop @ARGV;
            open $fh, "<$filename" or die "ERROR: cannot open file $filename\n";
        }
        else{
            $fh = *STDIN;
        }

        while (my $prop = <$fh>){
            chomp $prop;
            $prop =~ s/\r$//;
            my ($key, $value) = split /=/, $prop, 2;
            $cfg{$key} = $value;
        }

        $cfg{src} = pop @ARGV;

        #merge properties by CLI
        for my $prop (@props){
            my ($key, $value) = split /=/, $prop, 2;
            $cfg{$key} = $value;
        }

        #check new backup set
        $zConfig->checkBackupSet(\%cfg);

        dumpProperties([\%cfg]);

        if ($opts->{write}){
            $zConfig->setBackupSet(\%cfg)
                or die "ERROR: cannot create backup config for $cfg{src}\n";
        }
        else{
            print "backup set not written to dataset $cfg{src}. use option --write to store it\n";
        }

        last;
    };
    /^man$/ && do {
        pod2usage(-exitstatus => 0, -verbose => 2);

        last;
    };
    /^help$/ && do {
        pod2usage(-exitval => 'NOEXIT');

        last;
    };
        #main argument invalid
        if (!defined $opts->{src}) {
            pod2usage(-exitval => 'NOEXIT');
            die ("ERROR: Unsupported main option $mainOpt was provided\n");
        }
    }
    
    if ($^O eq 'darwin') {
        print STDERR <<NOTE;
NOTE: if you have modified your configuration, send a HUP signal
(sudo launchctl kill -HUP system/org.znapzend) to your znapzend daemon for it to notice the change.
NOTE
            } 
    else    {
        print STDERR <<NOTE;
NOTE: if you have modified your configuration, send a HUP signal
(pkill -HUP znapzend) to your znapzend daemon for it to notice the change.
NOTE
            }
    return 1;
    }

{
    local $@;
    eval {
        local $SIG{__DIE__};
        main(shift);
    };
    if ($@){
        if (blessed $@ && $@->isa('Mojo::Exception')){
            print STDERR $@->message . "\n";
        }
        else{
            print STDERR $@ . "\n";
        }
    }
}

1;

__END__

=head1 NAME

znapzendzetup - znapzend setup utility

=head1 SYNOPSIS

B<znapzendzetup> I<command> [I<common-options...>] [I<command-options...>]

where 'common-options' (must precede command-options) is a mix of the
following:

            [--rootExec={pfexec|sudo}] \
            [--features={x,y,...}] \
                {lowmemRecurse zfsGetType} \
            [--debug]

and where 'command' and its unique options is one of the following:

    create  [--recursive] [--mbuffer=<path>[:<port>]] [--mbuffersize=<size>] \
            [--pre-snap-command=<command>] \
            [--post-snap-command=<command>] \
            [--tsformat=<format>] --donotask \
            [--send-delay=<time>] \
            SRC plan dataset \
            [ DST[:key] plan [[user@]host:]dataset
                [pre-send-command] [post-send-command] ]

    delete  [--dst=key] <src_dataset>

    edit    [--recursive=on|off] \
            [--mbuffer=<path>[:<port>]|off] [--mbuffersize=<size>] \
            [--pre-snap-command=<command>|off] \
            [--post-snap-command=<command>|off] \
            [--tsformat=<format>] --donotask \
            [--send-delay=<time>] \
            SRC [plan] dataset \
            [ DST:key [plan] [dataset] \
                [pre-send-command|off] [post-send-command|off] ]

    edit    <src_dataset>

    enable  <src_dataset>

    disable <src_dataset>

    enable-dst  <src_dataset> <DST_key>

    disable-dst <src_dataset> <DST_key>

    list    [--recursive] [--inherited] [src_dataset...]

    export  <src_dataset>

    import  [--write] [--prop <property>=<value>, [--prop ...] ...]
            <src_dataset> [<prop_dump_file>]

    help

    man

=head1 DESCRIPTION

Use znapzendsetup to configure your backup tasks. The cli is modled after
the zfs commandline.

After modifying the configuration, send a HUP signal to your znapzend daemon
for it to re-read the configuration.

Common options include:

=over

=item B<--features>=I<feature1>,I<feature2>,...

enables a few enhanced zfs module features to be on par with those used by
the znapzend command (but far fewer are relevant for the znapzendzetup)

Available features:

=over

=item lowmemRecurse

use 'lowmemRecurse' on systems where you have too many datasets,
so a recursive listing of attributes to find backup plans exhausts
the memory available to `znapzend(zetup)`: instead, go the slower
way to first list all impacted dataset names, and then query their
configs one by one.

=item zfsGetType

use 'zfsGetType' if your 'zfs get' supports a '-t' argument for
filtering by dataset type at all (e.g. one in Solaris 10 does not),
AND lists properties for snapshots by default when recursing (e.g.
the one in Solaris 10u8 already does), so that there is too much
data to process while searching for backup plans.

If these two conditions apply to your system, the time needed for
a '--recursive' listing of backup plans can literally differ by
hundreds of times (depending on the amount of snapshots in that
dataset tree... and a decent backup plan will ensure you have a
lot of those), so you would benefit from requesting this feature.

This feature should not impact the default (non- '--recursive')
listings however.

=back

=back


Below a few notes on main commands.

=head2 B<create>

The heart of the znapzend backup is the plan. The plan specifies how often
to backup and for how long to keep the backups. A plan is required both for
the source and the destination datasets.

The plan consists of a series of retention periods to interval
associations:

  retA=>intA,retB=>intB,...

Both intervals and retention periods are expressed in standard units of time
or multiples of them. You can use both the full name or a shortcut according
to the following table:

 second|sec|s
 minute|min
 hour|h
 day|d
 week|w
 month|mon|m
 year|y

To keep one copy every 30 minutes for one week, specify:

 1week=>30min

To keep one copy every two days for 10 years:

 10year=>2day

In a minimal setup, you just specify a plan for the B<SRC> fileset. This
will cause snapshots to be taken and destroyed according to the plan. You
can then add one or several destinations (B<DST>) both local (preferably on
a different pool) or remote.

When adding multiple B<DST> entries, each will get labeled for later
identification, optionally you can specify your own label.

=over

=item B<--tsformat>=I<limited-strftime-format>

The B<--tsformat> option specifies how the names of the snapshots are
constructed.

The syntax is L<strftime>-like. The string must consist of the mandatory

 %Y %m %d %H %M %S %z

Optionally,

- _ . :

characters as well as any alphanumeric character are allowed.

If not specified, B<--tsformat> defaults to C<%Y-%m-%d-%H%M%S>.

If B<--tsformat> string is suffixed by a 'Z', times will be in UTC. E.g.:

 --tsformat='%Y-%m-%dT%H:%M:%SZ'

NOTE: that windoz will probably not like the C<:> characters. So if you
intend to browse the snapshots with windoz, you may want to use a different
separator.

=item B<--mbuffer>=I</usr/bin/mbuffer>

Specify the path to your copy of the mbuffer utility.

=item B<--mbuffer>=I</usr/bin/mbuffer:31337>

Specify the path to your copy of the mbuffer utility and the port used
on the destination. Caution: znapzend will send the data directly
from source mbuffer to destination mbuffer, thus data stream is B<not>
encrypted.

=item B<--mbuffersize>=I<number>{B<b>|B<k>|B<M>|B<G>}

The size of the mbuffer can be set with the B<--mbuffersize> option.  It
supports the following units:

 b, k, M, G

To specify a mbuffer size of 100MB:

 --mbuffersize=100M

If not set, the buffer size defaults to 1GB.

=item B<--donotask>

Apply changes immediately. Without being asked if the config is as you
intended it to be.

=item B<--pre-snap-command>=I</path/bin args>, B<--post-snap-command>=I</path/bin args>

Run commands/scripts before and after snapshots are taken on source.
e.g. for database locking/flushing (pre) and unlocking (post).

If you deal with a mariadb/mysql database, you can use

  pre-snap-command  = /opt/oep/mariadb/bin/mysql -e "set autocommit=0;flush tables with read lock;\\! /bin/sleep 600" &  /usr/bin/echo $! > /tmp/mariadblock.pid ; sleep 10
  post-snap-command = /usr/bin/kill `/usr/bin/cat /tmp/mariadblock.pid`;/usr/bin/rm /tmp/mariadblock.pid

to make sure that the on-disk data is consistent when snapshotting. The
lock stays in place only for the duration of the lingering connection
to mysql we need to employ, or until the snapshotting attempt times out.
For this to work, add the root password of your mariadb/mysql database
setup into ~root/.my.cnf and make sure the file permissions are tight ...

The pre and post snapshot commands can find the name and time of the
snapshot in the environment variables I<ZNAP_NAME> and I<ZNAP_TIME>.

=item B<--send-delay>

Specify delay (in seconds) before sending snaps to the destination.
May be useful if you want to control sending time.

=item B<pre-send-command>=I</path/bin args>, B<post-send-command>=I</path/bin args>

Run command/script before and after sending the snapshot to the destination.
Intended to run a remote script via ssh on the destination, e.g. to bring
up a backup disk or server. Or to put a zpool online/offline:

  "ssh root@bserv zpool import -Nf tank"
  "ssh root@bserv zpool export tank"

=back

=head2 B<delete>

to remove configuration from a dataset just give its name

 znapzendzetup delete I<dataset>

the B<delete> function understands the following options:

=over

=item B<--dst>=I<key>

to only remove a destination, specify the key of the destination. Use the
B<list> function to see the keys.

=back

=head2 B<edit>

modify the configuration of a dataset. See the descriptions in the B<create>
function for details.

If B<edit> is used with a source dataset as single argument, properties
can be edited in an editor.

=head2 B<export>

dumps the backup configuration of a dataset

 znapzendzetup export I<dataset>

=head2 B<import>

reads configuration data from a file or STDIN and prints it content

=over

=item B<--write>

actually store the new configuration into the dataset given on the
command line.

=item B<--prop> I<key>="I<value>" [ B<--prop> ... ]

may be called multiple times to override properties in the imported config.

=back

=head1 EXAMPLES

create a complex backup task

    znapzendzetup create --recursive --mbuffer=/opt/omni/bin/mbuffer \
       --mbuffersize=1G --tsformat='%Y-%m-%d-%H%M%S' \
       --pre-snap-command="/bin/sh /usr/local/bin/lock_flush_db.sh" \
       --post-snap-command="/bin/sh /usr/local/bin/unlock_db.sh" \
       SRC '7d=>1h,30d=>4h,90d=>1d' tank/home \
       DST:a '7d=>1h,30d=>4h,90d=>1d,1y=>1w,10y=>1month' backup/home \
       DST:b '7d=>1h,30d=>4h,90d=>1d,1y=>1w,10y=>1month' \
          root@bserv:backup/home \
          "/root/znapzend.sh dst_b pool on" \
          "/root/znapzend.sh dst_b pool off"


copy the setup from one fileset to another

    znapzendzetup export tank/home | znapzendzetup import --write tank/new_home

=head1 RUNNING AS AN UNPRIVILEGED USER

In order to allow a non-privileged user to use it, the following
permissions are required on the ZFS filesystems:

Sending end: I<destroy,hold,mount,send,snapshot,userprop>
Receiving end: I<create,mount,receive,userprop>

=head1 COPYRIGHT

Copyright (c) 2014 by OETIKER+PARTNER AG. All rights reserved.

=head1 LICENSE

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
this program. If not, see L<http://www.gnu.org/licenses/>.

=head1 AUTHOR

S<Tobias Oetiker E<lt>tobi@oetiker.chE<gt>>
S<Dominik Hassler E<lt>hadfl@cpan.orgE<gt>>

=head1 HISTORY

2016-09-23 ron Destination pre and post send/receive commands
2014-07-22 had Pre and post snapshot commands
2014-06-29 had Flexible snapshot time format
2014-06-01 had Multi destination backup
2014-05-30 had Initial Version

=cut
