]> git.proxmox.com Git - pve-zsync.git/blobdiff - pve-zsync
introduce and use read_file helper
[pve-zsync.git] / pve-zsync
old mode 100644 (file)
new mode 100755 (executable)
index 0d8088f..76e12ce
--- a/pve-zsync
+++ b/pve-zsync
@@ -2,28 +2,34 @@
 
 use strict;
 use warnings;
-use Data::Dumper qw(Dumper);
+
 use Fcntl qw(:flock SEEK_END);
 use Getopt::Long qw(GetOptionsFromArray);
-use File::Copy qw(move);
 use File::Path qw(make_path);
-use Switch;
 use JSON;
 use IO::File;
 use String::ShellQuote 'shell_quote';
 
 my $PROGNAME = "pve-zsync";
-my $CONFIG_PATH = "/var/lib/${PROGNAME}/";
-my $STATE = "${CONFIG_PATH}sync_state";
+my $CONFIG_PATH = "/var/lib/${PROGNAME}";
+my $STATE = "${CONFIG_PATH}/sync_state";
 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
-my $PATH = "/usr/sbin/";
-my $PVE_DIR = "/etc/pve/local/";
-my $QEMU_CONF = "${PVE_DIR}qemu-server/";
-my $LXC_CONF = "${PVE_DIR}lxc/";
-my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
-my $PROG_PATH = "$PATH${PROGNAME}";
+my $PATH = "/usr/sbin";
+my $PVE_DIR = "/etc/pve/local";
+my $QEMU_CONF = "${PVE_DIR}/qemu-server";
+my $LXC_CONF = "${PVE_DIR}/lxc";
+my $PROG_PATH = "$PATH/${PROGNAME}";
 my $INTERVAL = 15;
-my $DEBUG = 0;
+my $DEBUG;
+
+BEGIN {
+    $DEBUG = 0; # change default here. not above on declaration!
+    $DEBUG ||= $ENV{ZSYNC_DEBUG};
+    if ($DEBUG) {
+       require Data::Dumper;
+       Data::Dumper->import();
+    }
+}
 
 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
@@ -47,10 +53,20 @@ my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])";       # ipv6 must always be in brac
 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
 
-check_bin ('cstream');
-check_bin ('zfs');
-check_bin ('ssh');
-check_bin ('scp');
+my $DISK_KEY_RE = qr/^(?:(?:(?:virtio|ide|scsi|sata|efidisk|mp)\d+)|rootfs): /;
+
+my $command = $ARGV[0];
+
+if (defined($command) && $command ne 'help' && $command ne 'printpod') {
+    check_bin ('cstream');
+    check_bin ('zfs');
+    check_bin ('ssh');
+    check_bin ('scp');
+}
+
+$SIG{TERM} = $SIG{QUIT} = $SIG{PIPE} = $SIG{HUP} = $SIG{KILL} = $SIG{INT} =  sub {
+    die "Signaled, aborting sync: $!\n";
+};
 
 sub check_bin {
     my ($bin)  = @_;
@@ -65,32 +81,63 @@ sub check_bin {
     die "unable to find command '$bin'\n";
 }
 
+sub read_file {
+    my ($filename, $one_line_only) = @_;
+
+    my $fh = IO::File->new($filename, "r")
+       or die "Could not open file ${filename}: $!\n";
+
+    my $text = $one_line_only ? <$fh> : [ <$fh> ];
+
+    close($fh);
+
+    return $text;
+}
+
 sub cut_target_width {
-    my ($target, $max) = @_;
+    my ($path, $maxlen) = @_;
+    $path =~ s@/+@/@g;
+
+    return $path if length($path) <= $maxlen;
 
-    return  $target if (length($target) <= $max);
-    my @spl = split('/', $target);
+    return '..'.substr($path, -$maxlen+2) if $path !~ m@/@;
 
-    my $count = length($spl[@spl-1]);
-    return "..\/".substr($spl[@spl-1],($count-$max)+3 , $count) if $count > $max;
+    $path =~ s@/([^/]+/?)$@@;
+    my $tail = $1;
 
-    $count +=  length($spl[0]) if @spl > 1;
-    return substr($spl[0], 0, $max-4-length($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
+    if (length($tail)+3 == $maxlen) {
+       return "../$tail";
+    } elsif (length($tail)+2 >= $maxlen) {
+       return '..'.substr($tail, -$maxlen+2)
+    }
 
-    my $rest = 1;
-    $rest = $max-$count if ($max-$count > 0);
+    $path =~ s@(/[^/]+)(?:/|$)@@;
+    my $head = $1;
+    my $both = length($head) + length($tail);
+    my $remaining = $maxlen-$both-4; # -4 for "/../"
 
-    return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
-}
+    if ($remaining < 0) {
+       return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
+    }
 
-sub lock {
-    my ($fh) = @_;
-    flock($fh, LOCK_EX) || die "Can't lock config - $!\n";
+    substr($path, ($remaining/2), (length($path)-$remaining), '..');
+    return "$head/" . $path . "/$tail";
 }
 
-sub unlock {
-    my ($fh) = @_;
-    flock($fh, LOCK_UN) || die "Can't unlock config- $!\n";
+sub locked {
+    my ($lock_fn, $code) = @_;
+
+    my $lock_fh = IO::File->new("> $lock_fn");
+
+    flock($lock_fh, LOCK_EX) || die "Couldn't acquire lock - $!\n";
+    my $res = eval { $code->() };
+    my $err = $@;
+
+    flock($lock_fh, LOCK_UN) || warn "Error unlocking - $!\n";
+    die "$err" if $err;
+
+    close($lock_fh);
+    return $res;
 }
 
 sub get_status {
@@ -104,19 +151,22 @@ sub get_status {
 }
 
 sub check_pool_exists {
-    my ($target) = @_;
+    my ($target, $user) = @_;
 
     my $cmd = [];
-    push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
+
+    if ($target->{ip}) {
+       push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
+    }
     push @$cmd, 'zfs', 'list', '-H', '--', $target->{all};
     eval {
        run_cmd($cmd);
     };
 
     if ($@) {
-       return 1;
+       return 0;
     }
-    return undef;
+    return 1;
 }
 
 sub parse_target {
@@ -165,46 +215,52 @@ sub read_cron {
        return undef;
     }
 
-    my $fh = IO::File->new("< $CRONJOBS");
-    die "Could not open file $CRONJOBS: $!\n" if !$fh;
-
-    my @text = <$fh>;
-
-    close($fh);
+    my $text = read_file($CRONJOBS, 0);
 
-    return encode_cron(@text);
+    return encode_cron(@{$text});
 }
 
 sub parse_argv {
     my (@arg) = @_;
 
-    my $param = {};
-    $param->{dest} = undef;
-    $param->{source} = undef;
-    $param->{verbose} = undef;
-    $param->{limit} = undef;
-    $param->{maxsnap} = undef;
-    $param->{name} = undef;
-    $param->{skip} = undef;
-    $param->{method} = undef;
-
-    my ($ret, $ar) = GetOptionsFromArray(\@arg,
-                                        'dest=s' => \$param->{dest},
-                                        'source=s' => \$param->{source},
-                                        'verbose' => \$param->{verbose},
-                                        'limit=i' => \$param->{limit},
-                                        'maxsnap=i' => \$param->{maxsnap},
-                                        'name=s' => \$param->{name},
-                                        'skip' => \$param->{skip},
-                                        'method=s' => \$param->{method});
-
-    if ($ret == 0) {
-       die "can't parse options\n";
-    }
+    my $param = {
+       dest => undef,
+       source => undef,
+       verbose => undef,
+       limit => undef,
+       maxsnap => undef,
+       name => undef,
+       skip => undef,
+       method => undef,
+       source_user => undef,
+       dest_user => undef,
+       properties => undef,
+       dest_config_path => undef,
+    };
 
-    $param->{name} = "default" if !$param->{name};
-    $param->{maxsnap} = 1 if !$param->{maxsnap};
-    $param->{method} = "ssh" if !$param->{method};
+    my ($ret) = GetOptionsFromArray(
+       \@arg,
+       'dest=s' => \$param->{dest},
+       'source=s' => \$param->{source},
+       'verbose' => \$param->{verbose},
+       'limit=i' => \$param->{limit},
+       'maxsnap=i' => \$param->{maxsnap},
+       'name=s' => \$param->{name},
+       'skip' => \$param->{skip},
+       'method=s' => \$param->{method},
+       'source-user=s' => \$param->{source_user},
+       'dest-user=s' => \$param->{dest_user},
+       'properties' => \$param->{properties},
+       'dest-config-path=s' => \$param->{dest_config_path},
+    );
+
+    die "can't parse options\n" if $ret == 0;
+
+    $param->{name} //= "default";
+    $param->{maxsnap} //= 1;
+    $param->{method} //= "ssh";
+    $param->{source_user} //= "root";
+    $param->{dest_user} //= "root";
 
     return $param;
 }
@@ -237,12 +293,10 @@ sub encode_cron {
        my $param = parse_argv(@arg);
 
        if ($param->{source} && $param->{dest}) {
-           $cfg->{$param->{source}}->{$param->{name}}->{dest} = $param->{dest};
-           $cfg->{$param->{source}}->{$param->{name}}->{verbose} = $param->{verbose};
-           $cfg->{$param->{source}}->{$param->{name}}->{limit} = $param->{limit};
-           $cfg->{$param->{source}}->{$param->{name}}->{maxsnap} = $param->{maxsnap};
-           $cfg->{$param->{source}}->{$param->{name}}->{skip} = $param->{skip};
-           $cfg->{$param->{source}}->{$param->{name}}->{method} = $param->{method};
+           my $source = delete $param->{source};
+           my $name = delete $param->{name};
+
+           $cfg->{$source}->{$name} = $param;
        }
     }
 
@@ -264,6 +318,10 @@ sub param_to_job {
     $job->{limit} = $param->{limit};
     $job->{maxsnap} = $param->{maxsnap} if $param->{maxsnap};
     $job->{source} = $param->{source};
+    $job->{source_user} = $param->{source_user};
+    $job->{dest_user} = $param->{dest_user};
+    $job->{properties} = !!$param->{properties};
+    $job->{dest_config_path} = $param->{dest_config_path} if $param->{dest_config_path};
 
     return $job;
 }
@@ -279,29 +337,14 @@ sub read_state {
        return undef;
     }
 
-    my $fh = IO::File->new("< $STATE");
-    die "Could not open file $STATE: $!\n" if !$fh;
-
-    my $text = <$fh>;
-    my $states = decode_json($text);
-
-    close($fh);
-
-    return $states;
+    my $text = read_file($STATE, 1);
+    return decode_json($text);
 }
 
 sub update_state {
     my ($job) = @_;
-    my $text;
-    my $in_fh;
 
-    eval {
-
-       $in_fh = IO::File->new("< $STATE");
-       die "Could not open file $STATE: $!\n" if !$in_fh;
-       lock($in_fh);
-       $text = <$in_fh>;
-    };
+    my $text = eval { read_file($STATE, 1); };
 
     my $out_fh = IO::File->new("> $STATE.new");
     die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
@@ -332,10 +375,7 @@ sub update_state {
     print $out_fh $text;
 
     close($out_fh);
-    move("$STATE.new", $STATE);
-    eval {
-       close($in_fh);
-    };
+    rename "$STATE.new", $STATE;
 
     return $states;
 }
@@ -350,13 +390,9 @@ sub update_cron {
     my $header = "SHELL=/bin/sh\n";
     $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
 
-    my $fh = IO::File->new("< $CRONJOBS");
-    die "Could not open file $CRONJOBS: $!\n" if !$fh;
-    lock($fh);
-
-    my @test = <$fh>;
+    my $current = read_file($CRONJOBS, 0);
 
-    while (my $line = shift(@test)) {
+    foreach my $line (@{$current}) {
        chomp($line);
        if ($line =~ m/source $job->{source} .*name $job->{name} /) {
            $updated = 1;
@@ -384,8 +420,7 @@ sub update_cron {
     die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
     close ($new_fh);
 
-    die "can't move $CRONJOBS.new: $!\n" if !move("${CRONJOBS}.new", "$CRONJOBS");
-    close ($fh);
+    die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
 }
 
 sub format_job {
@@ -396,7 +431,7 @@ sub format_job {
        $text = "#";
     }
     if ($line) {
-       $line =~ /^#*(.+) root/;
+       $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
        $text .= $1;
     } else {
        $text .= "*/$INTERVAL * * * *";
@@ -404,8 +439,13 @@ sub format_job {
     $text .= " root";
     $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
     $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
+    $text .= " --limit $job->{limit}" if $job->{limit};
     $text .= " --method $job->{method}";
     $text .= " --verbose" if $job->{verbose};
+    $text .= " --source-user $job->{source_user}";
+    $text .= " --dest-user $job->{dest_user}";
+    $text .= " --properties" if $job->{properties};
+    $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path};
     $text .= "\n";
 
     return $text;
@@ -415,16 +455,16 @@ sub list {
 
     my $cfg = read_cron();
 
-    my $list = sprintf("%-25s%-10s%-7s%-20s%-5s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
+    my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
 
     my $states = read_state();
     foreach my $source (sort keys%{$cfg}) {
        foreach my $name (sort keys%{$cfg->{$source}}) {
            $list .= sprintf("%-25s", cut_target_width($source, 25));
-           $list .= sprintf("%-10s", cut_target_width($name, 10));
-           $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
+           $list .= sprintf("%-25s", cut_target_width($name, 25));
+           $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
            $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync});
-           $list .= sprintf("%-5s", $states->{$source}->{$name}->{vm_type});
+           $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type}) ? $states->{$source}->{$name}->{vm_type} : "undef");
            $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
        }
     }
@@ -433,19 +473,20 @@ sub list {
 }
 
 sub vm_exists {
-    my ($target) = @_;
-
-    my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip};
+    my ($target, $user) = @_;
 
-    my $res = undef;
+    return undef if !defined($target->{vmid});
 
-    eval { $res = run_cmd([@cmd, 'ls',  "$QEMU_CONF$target->{vmid}.conf"]) };
+    my $conf_fn = "$target->{vmid}.conf";
 
-    return "qemu" if $res;
-
-    eval { $res = run_cmd([@cmd, 'ls',  "$LXC_CONF$target->{vmid}.conf"]) };
-
-    return "lxc" if $res;
+    if ($target->{ip}) {
+       my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
+       return "qemu" if eval { run_cmd([@cmd, "$QEMU_CONF/$conf_fn"]) };
+       return "lxc" if  eval { run_cmd([@cmd, "$LXC_CONF/$conf_fn"]) };
+    } else {
+       return "qemu" if -f "$QEMU_CONF/$conf_fn";
+       return "lxc" if -f "$LXC_CONF/$conf_fn";
+    }
 
     return undef;
 }
@@ -453,44 +494,50 @@ sub vm_exists {
 sub init {
     my ($param) = @_;
 
-    my $cfg = read_cron();
+    locked("$CONFIG_PATH/cron_and_state.lock", sub {
+       my $cfg = read_cron();
+
+       my $job = param_to_job($param);
 
-    my $job = param_to_job($param);
+       $job->{state} = "ok";
+       $job->{lsync} = 0;
 
-    $job->{state} = "ok";
-    $job->{lsync} = 0;
+       my $source = parse_target($param->{source});
+       my $dest = parse_target($param->{dest});
 
-    my $source = parse_target($param->{source});
-    my $dest = parse_target($param->{dest});
+       if (my $ip =  $dest->{ip}) {
+           run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
+       }
 
-    if (my $ip =  $dest->{ip}) {
-       run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
-    }
+       if (my $ip =  $source->{ip}) {
+           run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
+       }
 
-    if (my $ip =  $source->{ip}) {
-       run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
-    }
+       die "Pool $dest->{all} does not exists\n" if !check_pool_exists($dest, $param->{dest_user});
 
-    die "Pool $dest->{all} does not exists\n" if check_pool_exists($dest);
+       if (!defined($source->{vmid})) {
+           die "Pool $source->{all} does not exists\n" if !check_pool_exists($source, $param->{source_user});
+       }
 
-    my $check = check_pool_exists($source->{path}, $source->{ip}) if !$source->{vmid} && $source->{path};
+       my $vm_type = vm_exists($source, $param->{source_user});
+       $job->{vm_type} = $vm_type;
+       $source->{vm_type} = $vm_type;
 
-    die "Pool $source->{path} does not exists\n" if undef($check);
+       die "VM $source->{vmid} doesn't exist\n" if $source->{vmid} && !$vm_type;
 
-    my $vm_type = vm_exists($source);
-    $job->{vm_type} = $vm_type;
+       die "Config already exists\n" if $cfg->{$job->{source}}->{$job->{name}};
 
-    die "VM $source->{vmid} doesn't exist\n" if $param->{vmid} && !$vm_type;
+       #check if vm has zfs disks if not die;
+       get_disks($source, $param->{source_user}) if $source->{vmid};
 
-    die "Config already exists\n" if $cfg->{$job->{source}}->{$job->{name}};
+       update_cron($job);
+       update_state($job);
+    }); #cron and state lock
 
-    update_cron($job);
-    update_state($job);
+    return if $param->{skip};
 
-    eval {
-       sync($param) if !$param->{skip};
-    };
-    if(my $err = $@) {
+    eval { sync($param) };
+    if (my $err = $@) {
        destroy_job($param);
        print $err;
     }
@@ -515,106 +562,141 @@ sub get_job {
 sub destroy_job {
     my ($param) = @_;
 
-    my $job = get_job($param);
-    $job->{state} = "del";
+    locked("$CONFIG_PATH/cron_and_state.lock", sub {
+       my $job = get_job($param);
+       $job->{state} = "del";
 
-    update_cron($job);
-    update_state($job);
+       update_cron($job);
+       update_state($job);
+    });
 }
 
 sub sync {
     my ($param) = @_;
 
-    my $lock_fh = IO::File->new("> $LOCKFILE");
-    die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
-    lock($lock_fh);
-
-    my $date = get_date();
     my $job;
-    eval {
-       $job = get_job($param);
-    };
 
-    if ($job && $job->{state} eq "syncing") {
-       die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
-    }
+    locked("$CONFIG_PATH/cron_and_state.lock", sub {
+       eval { $job = get_job($param) };
 
-    my $dest = parse_target($param->{dest});
-    my $source = parse_target($param->{source});
+       if ($job) {
+           if (defined($job->{state}) && ($job->{state} eq "syncing" || $job->{state} eq "waiting")) {
+               die "Job --source $param->{source} --name $param->{name} is already scheduled to sync\n";
+           }
 
-    my $sync_path = sub {
-       my ($source, $dest, $job, $param, $date) = @_;
+           $job->{state} = "waiting";
+           update_state($job);
+       }
+    });
 
-       ($source->{old_snap}, $source->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name});
+    locked("$CONFIG_PATH/sync.lock", sub {
 
-       snapshot_add($source, $dest, $param->{name}, $date);
+       my $date = get_date();
 
-       send_image($source, $dest, $param);
+       my $dest;
+       my $source;
+       my $vm_type;
 
-       snapshot_destroy($source, $dest, $param->{method}, $source->{old_snap}) if ($source->{destroy} && $source->{old_snap});
+       locked("$CONFIG_PATH/cron_and_state.lock", sub {
+           #job might've changed while we waited for the sync lock, but we can be sure it's not syncing
+           eval { $job = get_job($param); };
 
-    };
+           if ($job && defined($job->{state}) && $job->{state} eq "stopped") {
+               die "Job --source $param->{source} --name $param->{name} has been disabled\n";
+           }
 
-    my $vm_type = vm_exists($source);
-    $source->{vm_type} = $vm_type;
+           $dest = parse_target($param->{dest});
+           $source = parse_target($param->{source});
 
-    if ($job) {
-       $job->{state} = "syncing";
-       $job->{vm_type} = $vm_type if !$job->{vm_type};
-       update_state($job);
-    }
+           $vm_type = vm_exists($source, $param->{source_user});
+           $source->{vm_type} = $vm_type;
 
-    eval{
-       if ($source->{vmid}) {
-           die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
-           my $disks = get_disks($source);
-
-           foreach my $disk (sort keys %{$disks}) {
-               $source->{all} = $disks->{$disk}->{all};
-               $source->{pool} = $disks->{$disk}->{pool};
-               $source->{path} = $disks->{$disk}->{path} if $disks->{$disk}->{path};
-               $source->{last_part} = $disks->{$disk}->{last_part};
-               &$sync_path($source, $dest, $job, $param, $date);
+           if ($job) {
+               $job->{state} = "syncing";
+               $job->{vm_type} = $vm_type if !$job->{vm_type};
+               update_state($job);
            }
-           if ($param->{method} eq "ssh" && ($source->{ip} || $dest->{ip})) {
-               send_config($source, $dest,'ssh');
+       }); #cron and state lock
+
+       my $sync_path = sub {
+           my ($source, $dest, $job, $param, $date) = @_;
+
+           ($dest->{old_snap}, $dest->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name}, $param->{dest_user});
+
+           snapshot_add($source, $dest, $param->{name}, $date, $param->{source_user}, $param->{dest_user});
+
+           send_image($source, $dest, $param);
+
+           snapshot_destroy($source, $dest, $param->{method}, $dest->{old_snap}, $param->{source_user}, $param->{dest_user}) if ($source->{destroy} && $dest->{old_snap});
+
+       };
+
+       eval{
+           if ($source->{vmid}) {
+               die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
+               die "source-user has to be root for syncing VMs\n" if ($param->{source_user} ne "root");
+               my $disks = get_disks($source, $param->{source_user});
+
+               foreach my $disk (sort keys %{$disks}) {
+                   $source->{all} = $disks->{$disk}->{all};
+                   $source->{pool} = $disks->{$disk}->{pool};
+                   $source->{path} = $disks->{$disk}->{path} if $disks->{$disk}->{path};
+                   $source->{last_part} = $disks->{$disk}->{last_part};
+                   &$sync_path($source, $dest, $job, $param, $date);
+               }
+               if ($param->{method} eq "ssh" && ($source->{ip} || $dest->{ip})) {
+                   send_config($source, $dest,'ssh', $param->{source_user}, $param->{dest_user}, $param->{dest_config_path});
+               } else {
+                   send_config($source, $dest,'local', $param->{source_user}, $param->{dest_user}, $param->{dest_config_path});
+               }
            } else {
-               send_config($source, $dest,'local');
+               &$sync_path($source, $dest, $job, $param, $date);
            }
-       } else {
-           &$sync_path($source, $dest, $job, $param, $date);
-       }
-    };
-    if(my $err = $@) {
-       if ($job) {
-           $job->{state} = "error";
-           update_state($job);
-           unlock($lock_fh);
-           close($lock_fh);
+       };
+       if (my $err = $@) {
+           locked("$CONFIG_PATH/cron_and_state.lock", sub {
+               eval { $job = get_job($param); };
+               if ($job) {
+                   $job->{state} = "error";
+                   update_state($job);
+               }
+           });
            print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
+           die "$err\n";
        }
-       die "$err\n";
-    }
-
-    if ($job) {
-       $job->{state} = "ok";
-       $job->{lsync} = $date;
-       update_state($job);
-    }
 
-    unlock($lock_fh);
-    close($lock_fh);
+       locked("$CONFIG_PATH/cron_and_state.lock", sub {
+           eval { $job = get_job($param); };
+           if ($job) {
+               if (defined($job->{state}) && $job->{state} eq "stopped") {
+                   $job->{state} = "stopped";
+               } else {
+                   $job->{state} = "ok";
+               }
+               $job->{lsync} = $date;
+               update_state($job);
+           }
+       });
+    }); #sync lock
 }
 
 sub snapshot_get{
-    my ($source, $dest, $max_snap, $name) = @_;
+    my ($source, $dest, $max_snap, $name, $dest_user) = @_;
 
     my $cmd = [];
-    push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
+    push @$cmd, 'ssh', "$dest_user\@$dest->{ip}", '--', if $dest->{ip};
     push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
-    push @$cmd, $source->{all};
 
-    my $raw = run_cmd($cmd);
+    my $path = $dest->{all};
+    $path .= "/$source->{last_part}" if $source->{last_part};
+    push @$cmd, $path;
+
+    my $raw;
+    eval {$raw = run_cmd($cmd)};
+    if (my $erro =$@) { #this means the volume doesn't exist on dest yet
+       return undef;
+    }
+
     my $index = 0;
     my $line = "";
     my $last_snap = undef;
@@ -622,8 +704,10 @@ sub snapshot_get{
 
     while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
        $line = $1;
-       if ($line =~ m/(rep_$name.*)$/) {
+       if ($line =~ m/@(.*)$/) {
            $last_snap = $1 if (!$last_snap);
+       }
+       if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
            $old_snap = $1;
            $index++;
            if ($index == $max_snap) {
@@ -639,7 +723,7 @@ sub snapshot_get{
 }
 
 sub snapshot_add {
-    my ($source, $dest, $name, $date) = @_;
+    my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
 
     my $snap_name = "rep_$name\_".$date;
 
@@ -648,59 +732,23 @@ sub snapshot_add {
     my $path = "$source->{all}\@$snap_name";
 
     my $cmd = [];
-    push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
+    push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip};
     push @$cmd, 'zfs', 'snapshot', $path;
     eval{
        run_cmd($cmd);
     };
 
     if (my $err = $@) {
-       snapshot_destroy($source, $dest, 'ssh', $snap_name);
+       snapshot_destroy($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
        die "$err\n";
     }
 }
 
-sub write_cron {
-    my ($cfg) = @_;
-
-    my $text = "SHELL=/bin/sh\n";
-    $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
-
-    my $fh = IO::File->new("> $CRONJOBS");
-    die "Could not open file: $!\n" if !$fh;
-
-    foreach my $source (sort keys%{$cfg}) {
-       foreach my $sync_name (sort keys%{$cfg->{$source}}) {
-           next if $cfg->{$source}->{$sync_name}->{status} ne 'ok';
-           $text .= "$PROG_PATH sync";
-           $text .= " -source  ";
-           if ($cfg->{$source}->{$sync_name}->{vmid}) {
-               $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
-               $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
-           } else {
-               $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
-               $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
-               $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path};
-           }
-           $text .= " -dest  ";
-           $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip};
-           $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
-           $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path};
-           $text .= " -name $sync_name ";
-           $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit};
-           $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap};
-           $text .= "\n";
-       }
-    }
-    die "Can't write to cron\n" if (!print($fh $text));
-    close($fh);
-}
-
 sub get_disks {
-    my ($target) = @_;
+    my ($target, $user) = @_;
 
     my $cmd = [];
-    push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
+    push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip};
 
     if ($target->{vm_type} eq 'qemu') {
        push @$cmd, 'qm', 'config', $target->{vmid};
@@ -712,7 +760,7 @@ sub get_disks {
 
     my $res = run_cmd($cmd);
 
-    my $disks = parse_disks($res, $target->{ip}, $target->{vm_type});
+    my $disks = parse_disks($res, $target->{ip}, $target->{vm_type}, $user);
 
     return $disks;
 }
@@ -735,7 +783,7 @@ sub run_cmd {
 }
 
 sub parse_disks {
-    my ($text, $ip, $vm_type) = @_;
+    my ($text, $ip, $vm_type, $user) = @_;
 
     my $disks;
 
@@ -743,23 +791,40 @@ sub parse_disks {
     while ($text && $text =~ s/^(.*?)(\n|$)//) {
        my $line = $1;
 
-       next if $line =~ /cdrom|none/;
-       next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
+       next if $line =~ /media=cdrom/;
+       next if $line !~ m/$DISK_KEY_RE/;
+
+       #QEMU if backup is not set include in  sync
+       next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
+
+       #LXC if backup is not set do no in sync
+       next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
 
        my $disk = undef;
        my $stor = undef;
-       if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
-           $disk = $3;
-           $stor = $2;
-       } else {
-           die "disk is not on ZFS Storage\n";
+       if($line =~ m/$DISK_KEY_RE(.*)$/) {
+           my @parameter = split(/,/,$1);
+
+           foreach my $opt (@parameter) {
+               if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
+                   $disk = $2;
+                   $stor = $1;
+                   last;
+               }
+           }
+       }
+       if (!defined($disk) || !defined($stor)) {
+           print "Disk: \"$line\" has no valid zfs dataset format and will be skipped\n";
+           next;
        }
 
        my $cmd = [];
-       push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
+       push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
        push @$cmd, 'pvesm', 'path', "$stor$disk";
        my $path = run_cmd($cmd);
 
+       die "Get no path from pvesm path $stor$disk\n" if !$path;
        if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
 
            my @array = split('/', $1);
@@ -773,13 +838,13 @@ sub parse_disks {
            $disks->{$num}->{all} .= "\/$disk";
 
            $num++;
-       } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)\/(\w+.*)(\/$disk)$/) {
+       } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
 
            $disks->{$num}->{pool} = $1;
            $disks->{$num}->{all} = $disks->{$num}->{pool};
 
            if ($2) {
-               $disks->{$num}->{path} = $2;
+               $disks->{$num}->{path} = $3;
                $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
            }
 
@@ -793,18 +858,19 @@ sub parse_disks {
        }
     }
 
+    die "Vm include no disk on zfs.\n" if !$disks->{0};
     return $disks;
 }
 
 sub snapshot_destroy {
-    my ($source, $dest, $method, $snap) = @_;
+    my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
 
     my @zfscmd = ('zfs', 'destroy');
     my $snapshot = "$source->{all}\@$snap";
 
     eval {
        if($source->{ip} && $method eq 'ssh'){
-           run_cmd(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
+           run_cmd(['ssh', "$source_user\@$source->{ip}", '--', @zfscmd, $snapshot]);
        } else {
            run_cmd([@zfscmd, $snapshot]);
        }
@@ -813,9 +879,10 @@ sub snapshot_destroy {
        warn "WARN: $erro";
     }
     if ($dest) {
-       my @ssh = $dest->{ip} ? ('ssh', "root\@$dest->{ip}", '--') : ();
+       my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip}", '--') : ();
 
-       my $path = "$dest->{all}\/$source->{last_part}";
+       my $path = "$dest->{all}";
+       $path .= "/$source->{last_part}" if $source->{last_part};
 
        eval {
            run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
@@ -826,25 +893,25 @@ sub snapshot_destroy {
     }
 }
 
+# check if snapshot for incremental sync exist on source side
 sub snapshot_exist {
-    my ($source , $dest, $method) = @_;
+    my ($source , $dest, $method, $source_user) = @_;
 
     my $cmd = [];
-    push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
+    push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--' if $source->{ip};
     push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
-    push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
 
-    my $text = "";
-    eval {$text =run_cmd($cmd);};
+    my $path = $source->{all};
+    $path .= "\@$dest->{last_snap}";
+
+    push @$cmd, $path;
+
+    eval {run_cmd($cmd)};
     if (my $erro =$@) {
        warn "WARN: $erro";
        return undef;
     }
-
-    while ($text && $text =~ s/^(.*?)(\n|$)//) {
-       my $line =$1;
-       return 1 if $line =~ m/^.*$source->{old_snap}$/;
-    }
+    return 1;
 }
 
 sub send_image {
@@ -852,12 +919,13 @@ sub send_image {
 
     my $cmd = [];
 
-    push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip};
+    push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user}\@$source->{ip}", '--' if $source->{ip};
     push @$cmd, 'zfs', 'send';
+    push @$cmd, '-p', if $param->{properties};
     push @$cmd, '-v' if $param->{verbose};
 
-    if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
-       push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
+    if($dest->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
+       push @$cmd, '-i', "$source->{all}\@$dest->{last_snap}";
     }
     push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
 
@@ -865,457 +933,415 @@ sub send_image {
        my $bwl = $param->{limit}*1024;
        push @$cmd, \'|', 'cstream', '-t', $bwl;
     }
-    my $target = "$dest->{all}/$source->{last_part}";
+    my $target = "$dest->{all}";
+    $target .= "/$source->{last_part}" if $source->{last_part};
     $target =~ s!/+!/!g;
 
     push @$cmd, \'|';
-       push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
-       push @$cmd, 'zfs', 'recv', '-F', '--';
-       push @$cmd, "$target";
+    push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user}\@$dest->{ip}", '--' if $dest->{ip};
+    push @$cmd, 'zfs', 'recv', '-F', '--';
+    push @$cmd, "$target";
 
-       eval {
-           run_cmd($cmd)
-       };
+    eval {
+       run_cmd($cmd)
+    };
 
-       if (my $erro = $@) {
-           snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
-           die $erro;
-       };
-    }
+    if (my $erro = $@) {
+       snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
+       die $erro;
+    };
+}
 
 
-    sub send_config{
-       my ($source, $dest, $method) = @_;
+sub send_config{
+    my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
 
-       my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF$source->{vmid}.conf": "$LXC_CONF$source->{vmid}.conf";
-       my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}";
+    my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid}.conf": "$LXC_CONF/$source->{vmid}.conf";
+    my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}";
 
-       my $config_dir = $dest->{last_part} ?  "${CONFIG_PATH}$dest->{last_part}/" : $CONFIG_PATH;
+    my $config_dir = $dest_config_path // $CONFIG_PATH;
+    $config_dir .= "/$dest->{last_part}" if $dest->{last_part};
 
-       $dest_target_new = $config_dir.$dest_target_new;
+    $dest_target_new = $config_dir.'/'.$dest_target_new;
 
-       print Dumper $dest_target_new, $dest;
-       if ($method eq 'ssh'){
-           if ($dest->{ip} && $source->{ip}) {
-               run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
-               run_cmd(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
-           } elsif ($dest->{ip}) {
-               run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
-               run_cmd(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
-           } elsif ($source->{ip}) {
-               run_cmd(['mkdir', '-p', '--', $config_dir]);
-               run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
-           }
+    if ($method eq 'ssh'){
+       if ($dest->{ip} && $source->{ip}) {
+           run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
+           run_cmd(['scp', '--', "$source_user\@[$source->{ip}]:$source_target", "$dest_user\@[$dest->{ip}]:$dest_target_new"]);
+       } elsif ($dest->{ip}) {
+           run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
+           run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip}]:$dest_target_new"]);
+       } elsif ($source->{ip}) {
+           run_cmd(['mkdir', '-p', '--', $config_dir]);
+           run_cmd(['scp', '--', "$source_user\@[$source->{ip}]:$source_target", $dest_target_new]);
+       }
 
-           if ($source->{destroy}){
-               my $dest_target_old ="${config_dir}$source->{vmid}.conf.$source->{old_snap}";
-               if($dest->{ip}){
-                   run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
-               } else {
-                   run_cmd(['rm', '-f', '--', $dest_target_old]);
-               }
+       if ($source->{destroy}){
+           my $dest_target_old ="${config_dir}/$source->{vmid}.conf.$source->{vm_type}.$dest->{old_snap}";
+           if($dest->{ip}){
+               run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
+           } else {
+               run_cmd(['rm', '-f', '--', $dest_target_old]);
            }
-       } elsif ($method eq 'local') {
-           run_cmd(['mkdir', '-p', '--', $config_dir]);
-           run_cmd(['cp', $source_target, $dest_target_new]);
        }
+    } elsif ($method eq 'local') {
+       run_cmd(['mkdir', '-p', '--', $config_dir]);
+       run_cmd(['cp', $source_target, $dest_target_new]);
     }
+}
 
-    sub get_date {
-       my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
-       my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
+sub get_date {
+    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
+    my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
 
-       return $datestamp;
-    }
+    return $datestamp;
+}
 
-    sub status {
-       my $cfg = read_cron();
+sub status {
+    my $cfg = read_cron();
 
-       my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
+    my $status_list = sprintf("%-25s%-25s%-10s\n", "SOURCE", "NAME", "STATUS");
 
-       my $states = read_state();
+    my $states = read_state();
 
-       foreach my $source (sort keys%{$cfg}) {
-           foreach my $sync_name (sort keys%{$cfg->{$source}}) {
-               $status_list .= sprintf("%-25s", cut_target_width($source, 25));
-               $status_list .= sprintf("%-15s", cut_target_width($sync_name, 25));
-               $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
-           }
+    foreach my $source (sort keys%{$cfg}) {
+       foreach my $sync_name (sort keys%{$cfg->{$source}}) {
+           $status_list .= sprintf("%-25s", cut_target_width($source, 25));
+           $status_list .= sprintf("%-25s", cut_target_width($sync_name, 25));
+           $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
        }
-
-       return $status_list;
     }
 
-    sub enable_job {
-       my ($param) = @_;
+    return $status_list;
+}
+
+sub enable_job {
+    my ($param) = @_;
 
+    locked("$CONFIG_PATH/cron_and_state.lock", sub {
        my $job = get_job($param);
        $job->{state} = "ok";
        update_state($job);
        update_cron($job);
-    }
+    });
+}
 
-    sub disable_job {
-       my ($param) = @_;
+sub disable_job {
+    my ($param) = @_;
 
+    locked("$CONFIG_PATH/cron_and_state.lock", sub {
        my $job = get_job($param);
        $job->{state} = "stopped";
        update_state($job);
        update_cron($job);
-    }
+    });
+}
 
-    my $command = $ARGV[0];
+my $cmd_help = {
+    destroy => qq{
+$PROGNAME destroy -source <string> [OPTIONS]
 
-    my $commands = {'destroy' => 1,
-                   'create' => 1,
-                   'sync' => 1,
-                   'list' => 1,
-                   'status' => 1,
-                   'help' => 1,
-                   'enable' => 1,
-                   'disable' => 1};
+        remove a sync Job from the scheduler
 
-    if (!$command || !$commands->{$command}) {
-       usage();
-       die "\n";
-    }
+        -name      string
 
-    my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
-\twill sync one time\n
-\t-dest\tstring\n
-\t\tthe destination target is like [IP:]<Pool>[/Path]\n
-\t-limit\tinteger\n
-\t\tmax sync speed in kBytes/s, default unlimited\n
-\t-maxsnap\tinteger\n
-\t\thow much snapshots will be kept before get erased, default 1/n
-\t-name\tstring\n
-\t\tname of the sync job, if not set it is default.
-\tIt is only necessary if scheduler allready contains this source.\n
-\t-source\tstring\n
-\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
-
-    my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
-\tCreate a sync Job\n
-\t-dest\tstringn\n
-\t\tthe destination target is like [IP]:<Pool>[/Path]\n
-\t-limit\tinteger\n
-\t\tmax sync speed in kBytes/s, default unlimited\n
-\t-maxsnap\tstring\n
-\t\thow much snapshots will be kept before get erased, default 1\n
-\t-name\tstring\n
-\t\tname of the sync job, if not set it is default\n
-\t-skip\tboolean\n
-\t\tif this flag is set it will skip the first sync\n
-\t-source\tstring\n
-\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
-
-    my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
-\tremove a sync Job from the scheduler\n
-\t-name\tstring\n
-\t\tname of the sync job, if not set it is default\n
-\t-source\tstring\n
-\t\tthe source can be an  <VMID> or [IP:]<ZFSPool>[/Path]\n";
-
-    my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
-\tGet help about specified command.\n
-\t<cmd>\tstring\n
-\t\tCommand name\n
-\t-verbose\tboolean\n
-\t\tVerbose output format.\n";
-
-    my $help_list = "$PROGNAME list\n
-\tGet a List of all scheduled Sync Jobs\n";
-
-    my $help_status = "$PROGNAME status\n
-\tGet the status of all scheduled Sync Jobs\n";
-
-    my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
-\tenable a syncjob and reset error\n
-\t-name\tstring\n
-\t\tname of the sync job, if not set it is default\n
-\t-source\tstring\n
-\t\tthe source can be an  <VMID> or [IP:]<ZFSPool>[/Path]\n";
-
-    my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
-\tpause a syncjob\n
-\t-name\tstring\n
-\t\tname of the sync job, if not set it is default\n
-\t-source\tstring\n
-\t\tthe source can be an  <VMID> or [IP:]<ZFSPool>[/Path]\n";
-
-    sub help {
-       my ($command) = @_;
-
-       switch($command){
-           case 'help'
-           {
-               die "$help_help\n";
-           }
-           case 'sync'
-           {
-               die "$help_sync\n";
-           }
-           case 'destroy'
-           {
-               die "$help_destroy\n";
-           }
-           case 'create'
-           {
-               die "$help_create\n";
-           }
-           case 'list'
-           {
-               die "$help_list\n";
-           }
-           case 'status'
-           {
-               die "$help_status\n";
-           }
-           case 'enable'
-           {
-               die "$help_enable\n";
-           }
-           case 'disable'
-           {
-               die "$help_enable\n";
-           }
-       }
+               name of the sync job, if not set it is default
 
-    }
+        -source    string
 
-    my @arg = @ARGV;
-    my $param = parse_argv(@arg);
+                the source can be an  <VMID> or [IP:]<ZFSPool>[/Path]
+    },
+    create => qq{
+$PROGNAME create -dest <string> -source <string> [OPTIONS]
 
+        Create a sync Job
 
-    switch($command) {
-       case "destroy"
-       {
-           die "$help_destroy\n" if !$param->{source};
-           check_target($param->{source});
-           destroy_job($param);
-       }
-       case "sync"
-       {
-           die "$help_sync\n" if !$param->{source} || !$param->{dest};
-           check_target($param->{source});
-           check_target($param->{dest});
-           sync($param);
-       }
-       case "create"
-       {
-           die "$help_create\n" if !$param->{source} || !$param->{dest};
-           check_target($param->{source});
-           check_target($param->{dest});
-           init($param);
-       }
-       case "status"
-       {
-           print status();
-       }
-       case "list"
-       {
-           print list();
-       }
-       case "help"
-       {
-           my $help_command = $ARGV[1];
-           if ($help_command && $commands->{$help_command}) {
-               print help($help_command);
-           }
-           if ($param->{verbose} == 1){
-               exec("man $PROGNAME");
-           } else {
-               usage(1);
-           }
-       }
-       case "enable"
-       {
-           die "$help_enable\n" if !$param->{source};
-           check_target($param->{source});
-           enable_job($param);
-       }
-       case "disable"
-       {
-           die "$help_disable\n" if !$param->{source};
-           check_target($param->{source});
-           disable_job($param);
-       }
-    }
+        -dest      string
 
-    sub usage {
-       my ($help) = @_;
-
-       print("ERROR:\tno command specified\n") if !$help;
-       print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
-       print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
-       print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
-       print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
-       print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
-       print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
-       print("\t$PROGNAME list\n");
-       print("\t$PROGNAME status\n");
-       print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
-    }
+               the destination target is like [IP]:<Pool>[/Path]
 
-    sub check_target {
-       my ($target) = @_;
-       parse_target($target);
-    }
+        -dest-user string
 
-    __END__
+               name of the user on the destination target, root by default
 
-       =head1 NAME
+        -limit     integer
 
-       pve-zsync - PVE ZFS Replication Manager
+               max sync speed in kBytes/s, default unlimited
 
-       =head1 SYNOPSIS
+        -maxsnap   string
 
-       pve-zsync <COMMAND> [ARGS] [OPTIONS]
+               how much snapshots will be kept before get erased, default 1
 
-       pve-zsync help <cmd> [OPTIONS]
+        -name      string
 
-       Get help about specified command.
+               name of the sync job, if not set it is default
 
-       <cmd>      string
+        -skip      boolean
 
-       Command name
+               if this flag is set it will skip the first sync
 
-       -verbose   boolean
+        -source    string
+
+               the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
+
+        -source-user    string
+
+               name of the user on the source target, root by default
+
+       -properties     boolean
+
+               Include the dataset's properties in the stream.
+
+       -dest-config-path    string
+
+               specify a custom config path on the destination target. default is /var/lib/pve-zsync
+    },
+    sync => qq{
+$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
 
-       Verbose output format.
+       will sync one time
 
-       pve-zsync create -dest <string> -source <string> [OPTIONS]
+        -dest      string
 
-       Create a sync Job
+               the destination target is like [IP:]<Pool>[/Path]
 
-       -dest      string
+        -dest-user string
 
-       the destination target is like [IP]:<Pool>[/Path]
+               name of the user on the destination target, root by default
 
        -limit     integer
 
                max sync speed in kBytes/s, default unlimited
 
-                                                  -maxsnap   string
+        -maxsnap   integer
 
-                                                  how much snapshots will be kept before get erased, default 1
+               how much snapshots will be kept before get erased, default 1
 
-                                                  -name      string
+        -name      string
 
-                                                  name of the sync job, if not set it is default
+               name of the sync job, if not set it is default.
+               It is only necessary if scheduler allready contains this source.
 
-                                                  -skip      boolean
+        -source    string
 
-                                                  if this flag is set it will skip the first sync
+               the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
 
-                                                  -source    string
+        -source-user    string
 
-                                                  the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
+               name of the user on the source target, root by default
 
-pve-zsync destroy -source <string> [OPTIONS]
+       -verbose   boolean
 
-       remove a sync Job from the scheduler
+               print out the sync progress.
 
-       -name      string
+       -properties     boolean
 
-               name of the sync job, if not set it is default
+               Include the dataset's properties in the stream.
 
-       -source    string
+       -dest-config-path    string
 
-               the source can be an  <VMID> or [IP:]<ZFSPool>[/Path]
+               specify a custom config path on the destination target. default is /var/lib/pve-zsync
+    },
+    list => qq{
+$PROGNAME list
 
-                                                  pve-zsync disable -source <string> [OPTIONS]
+       Get a List of all scheduled Sync Jobs
+    },
+    status => qq{
+$PROGNAME status
 
-                                                  pause a sync job
+       Get the status of all scheduled Sync Jobs
+    },
+    help => qq{
+$PROGNAME help <cmd> [OPTIONS]
 
-                                                  -name      string
+       Get help about specified command.
 
-                                                  name of the sync job, if not set it is default
+        <cmd>      string
 
-                                                  -source    string
+               Command name
 
-                                                  the source can be an  <VMID> or [IP:]<ZFSPool>[/Path]
+       -verbose   boolean
 
-pve-zsync enable -source <string> [OPTIONS]
+               Verbose output format.
+    },
+    enable => qq{
+$PROGNAME enable -source <string> [OPTIONS]
 
-       enable a syncjob and reset error
+        enable a syncjob and reset error
 
-       -name      string
+        -name      string
 
                name of the sync job, if not set it is default
 
-       -source    string
+        -source    string
 
-       the source can be an  <VMID> or [IP:]<ZFSPool>[/Path]
-                                                  pve-zsync list
+                the source can be an  <VMID> or [IP:]<ZFSPool>[/Path]
+    },
+    disable => qq{
+$PROGNAME disable -source <string> [OPTIONS]
 
-                                                  Get a List of all scheduled Sync Jobs
+        pause a sync job
 
-                                                  pve-zsync status
+        -name      string
 
-                                                  Get the status of all scheduled Sync Jobs
+               name of the sync job, if not set it is default
 
-                                                  pve-zsync sync -dest <string> -source <string> [OPTIONS]
+        -source    string
 
-                                                  will sync one time
+                the source can be an  <VMID> or [IP:]<ZFSPool>[/Path] 
+    },
+    printpod => 'internal command',
 
-                                                  -dest      string
+};
 
-                                                  the destination target is like [IP:]<Pool>[/Path]
+if (!$command) {
+    usage(); die "\n";
+} elsif (!$cmd_help->{$command}) {
+    print "ERROR: unknown command '$command'";
+    usage(1); die "\n";
+}
 
-       -limit     integer
+my @arg = @ARGV;
+my $param = parse_argv(@arg);
 
-               max sync speed in kBytes/s, default unlimited
+sub check_params {
+    for (@_) {
+       die "$cmd_help->{$command}\n" if !$param->{$_};
+    }
+}
+
+if ($command eq 'destroy') {
+    check_params(qw(source));
+
+    check_target($param->{source});
+    destroy_job($param);
+
+} elsif ($command eq 'sync') {
+    check_params(qw(source dest));
+
+    check_target($param->{source});
+    check_target($param->{dest});
+    sync($param);
+
+} elsif ($command eq 'create') {
+    check_params(qw(source dest));
+
+    check_target($param->{source});
+    check_target($param->{dest});
+    init($param);
+
+} elsif ($command eq 'status') {
+    print status();
+
+} elsif ($command eq 'list') {
+    print list();
+
+} elsif ($command eq 'help') {
+    my $help_command = $ARGV[1];
+
+    if ($help_command && $cmd_help->{$help_command}) {
+       die "$cmd_help->{$help_command}\n";
+
+    }
+    if ($param->{verbose}) {
+       exec("man $PROGNAME");
+
+    } else {
+       usage(1);
 
-                                                                                             -maxsnap   integer
+    }
+
+} elsif ($command eq 'enable') {
+    check_params(qw(source));
+
+    check_target($param->{source});
+    enable_job($param);
+
+} elsif ($command eq 'disable') {
+    check_params(qw(source));
+
+    check_target($param->{source});
+    disable_job($param);
+
+} elsif ($command eq 'printpod') {
+    print_pod();
+}
+
+sub usage {
+    my ($help) = @_;
+
+    print("ERROR:\tno command specified\n") if !$help;
+    print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
+    print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
+    print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
+    print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
+    print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
+    print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
+    print("\t$PROGNAME list\n");
+    print("\t$PROGNAME status\n");
+    print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
+}
+
+sub check_target {
+    my ($target) = @_;
+    parse_target($target);
+}
 
-                                                                                             how much snapshots will be kept before get erased, default 1
+sub print_pod {
 
-                                                                                             -name      string
+    my $synopsis = join("\n", sort values %$cmd_help);
 
-                                                                                             name of the sync job, if not set it is default.
-                                                                                             It is only necessary if scheduler allready contains this source.
+    print <<EOF;
+=head1 NAME
 
-                                                                                             -source    string
+pve-zsync - PVE ZFS Replication Manager
 
-                                                                                             the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
+=head1 SYNOPSIS
+
+pve-zsync <COMMAND> [ARGS] [OPTIONS]
+
+$synopsis
 
 =head1 DESCRIPTION
 
 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
 This tool also has the capability to add jobs to cron so the sync will be automatically done.
 The default syncing interval is set to 15 min, if you want to change this value you can do this in /etc/cron.d/pve-zsync.
-                                                                                                                                           To config cron see man crontab.
+To config cron see man crontab.
 
-                                                                                                                                           =head2 PVE ZFS Storage sync Tool
+=head2 PVE ZFS Storage sync Tool
 
-                                                                                                                                           This Tool can get remote pool on other PVE or send Pool to others ZFS machines
+This Tool can get remote pool on other PVE or send Pool to others ZFS machines
 
-                                                                                                                                           =head1 EXAMPLES
+=head1 EXAMPLES
 
-                                                                                                                                           add sync job from local VM to remote ZFS Server
-                                                                                                                                           pve-zsync create -source=100 -dest=192.168.1.2:zfspool
+add sync job from local VM to remote ZFS Server
+pve-zsync create -source=100 -dest=192.168.1.2:zfspool
 
-                                                                                                                                           =head1 IMPORTANT FILES
+=head1 IMPORTANT FILES
 
-                                                                                                                                           Cron jobs and config are stored at                      /etc/cron.d/pve-zsync
+Cron jobs and config are stored at                      /etc/cron.d/pve-zsync
 
-                                                                                                                                           The VM config get copied on the destination machine to  /var/lib/pve-zsync/
+The VM config get copied on the destination machine to  /var/lib/pve-zsync/
 
-                                                                                                                                           =head1 COPYRIGHT AND DISCLAIMER
+=head1 COPYRIGHT AND DISCLAIMER
 
-                                                                                                                                           Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
+Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
 
-                                                                                                                                           This program is free software: you can redistribute it and/or modify it
+This program is free software: you can redistribute it and/or modify it
 under the terms of the GNU Affero 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
-    Affero General Public License for more details.
+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
+Affero General Public License for more details.
 
-    You should have received a copy of the GNU Affero General Public
-    License along with this program.  If not, see
-    <http://www.gnu.org/licenses/>.
+You should have received a copy of the GNU Affero General Public
+License along with this program.  If not, see
+<http://www.gnu.org/licenses/>.
+
+EOF
+}