5 use Data
::Dumper
qw(Dumper);
6 use Fcntl
qw(:flock SEEK_END);
7 use Getopt
::Long
qw(GetOptionsFromArray);
8 use File
::Copy
qw(move);
9 use File
::Path
qw(make_path);
13 use String
::ShellQuote
'shell_quote';
15 my $PROGNAME = "pve-zsync";
16 my $CONFIG_PATH = "/var/lib/${PROGNAME}";
17 my $STATE = "${CONFIG_PATH}/sync_state";
18 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
19 my $PATH = "/usr/sbin";
20 my $PVE_DIR = "/etc/pve/local";
21 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
22 my $LXC_CONF = "${PVE_DIR}/lxc";
23 my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
24 my $PROG_PATH = "$PATH/${PROGNAME}";
28 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
29 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
30 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
31 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
34 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
35 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
36 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
42 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
44 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
45 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
46 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
47 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
48 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
50 check_bin
('cstream');
58 foreach my $p (split (/:/, $ENV{PATH
})) {
65 die "unable to find command '$bin'\n";
68 sub cut_target_width
{
69 my ($target, $max) = @_;
71 return $target if (length($target) <= $max);
72 my @spl = split('/', $target);
74 my $count = length($spl[@spl-1]);
75 return "..\/".substr($spl[@spl-1],($count-$max)+3 , $count) if $count > $max;
77 $count += length($spl[0]) if @spl > 1;
78 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
81 $rest = $max-$count if ($max-$count > 0);
83 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
88 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
93 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
97 my ($source, $name, $status) = @_;
99 if ($status->{$source->{all
}}->{$name}->{status
}) {
106 sub check_pool_exists
{
110 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
111 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
125 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
128 if ($text !~ $TARGETRE) {
132 $target->{ip
} = $1 if $1;
133 my @parts = split('/', $2);
135 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
137 my $pool = $target->{pool
} = shift(@parts);
138 die "$errstr\n" if !$pool;
140 if ($pool =~ m/^\d+$/) {
141 $target->{vmid
} = $pool;
142 delete $target->{pool
};
145 return $target if (@parts == 0);
146 $target->{last_part
} = pop(@parts);
152 $target->{path
} = join('/', @parts);
160 #This is for the first use to init file;
162 my $new_fh = IO
::File-
>new("> $CRONJOBS");
163 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
168 my $fh = IO
::File-
>new("< $CRONJOBS");
169 die "Could not open file $CRONJOBS: $!\n" if !$fh;
175 return encode_cron
(@text);
182 $param->{dest
} = undef;
183 $param->{source
} = undef;
184 $param->{verbose
} = undef;
185 $param->{limit
} = undef;
186 $param->{maxsnap
} = undef;
187 $param->{name
} = undef;
188 $param->{skip
} = undef;
189 $param->{method} = undef;
191 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
192 'dest=s' => \
$param->{dest
},
193 'source=s' => \
$param->{source
},
194 'verbose' => \
$param->{verbose
},
195 'limit=i' => \
$param->{limit
},
196 'maxsnap=i' => \
$param->{maxsnap
},
197 'name=s' => \
$param->{name
},
198 'skip' => \
$param->{skip
},
199 'method=s' => \
$param->{method});
202 die "can't parse options\n";
205 $param->{name
} = "default" if !$param->{name
};
206 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
207 $param->{method} = "ssh" if !$param->{method};
212 sub add_state_to_job
{
215 my $states = read_state
();
216 my $state = $states->{$job->{source
}}->{$job->{name
}};
218 $job->{state} = $state->{state};
219 $job->{lsync
} = $state->{lsync
};
220 $job->{vm_type
} = $state->{vm_type
};
222 for (my $i = 0; $state->{"snap$i"}; $i++) {
223 $job->{"snap$i"} = $state->{"snap$i"};
234 while (my $line = shift(@text)) {
236 my @arg = split('\s', $line);
237 my $param = parse_argv
(@arg);
239 if ($param->{source
} && $param->{dest
}) {
240 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
257 my $source = parse_target
($param->{source
});
258 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
260 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
261 $job->{dest
} = $param->{dest
} if $param->{dest
};
262 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
263 $job->{method} = "ssh" if !$job->{method};
264 $job->{limit
} = $param->{limit
};
265 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
266 $job->{source
} = $param->{source
};
274 make_path
$CONFIG_PATH;
275 my $new_fh = IO
::File-
>new("> $STATE");
276 die "Could not create $STATE: $!\n" if !$new_fh;
282 my $fh = IO
::File-
>new("< $STATE");
283 die "Could not open file $STATE: $!\n" if !$fh;
286 my $states = decode_json
($text);
300 $in_fh = IO
::File-
>new("< $STATE");
301 die "Could not open file $STATE: $!\n" if !$in_fh;
306 my $out_fh = IO
::File-
>new("> $STATE.new");
307 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
312 $states = decode_json
($text);
313 $state = $states->{$job->{source
}}->{$job->{name
}};
316 if ($job->{state} ne "del") {
317 $state->{state} = $job->{state};
318 $state->{lsync
} = $job->{lsync
};
319 $state->{vm_type
} = $job->{vm_type
};
321 for (my $i = 0; $job->{"snap$i"} ; $i++) {
322 $state->{"snap$i"} = $job->{"snap$i"};
324 $states->{$job->{source
}}->{$job->{name
}} = $state;
327 delete $states->{$job->{source
}}->{$job->{name
}};
328 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
331 $text = encode_json
($states);
335 move
("$STATE.new", $STATE);
350 my $header = "SHELL=/bin/sh\n";
351 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
353 my $fh = IO
::File-
>new("< $CRONJOBS");
354 die "Could not open file $CRONJOBS: $!\n" if !$fh;
359 while (my $line = shift(@test)) {
361 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
363 next if $job->{state} eq "del";
364 $text .= format_job
($job, $line);
366 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
375 $text = "$header$text";
379 $text .= format_job
($job);
381 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
382 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
384 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
387 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
392 my ($job, $line) = @_;
395 if ($job->{state} eq "stopped") {
399 $line =~ /^#*(.+) root/;
402 $text .= "*/$INTERVAL * * * *";
405 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
406 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
407 $text .= " --limit $job->{limit}" if $job->{limit
};
408 $text .= " --method $job->{method}";
409 $text .= " --verbose" if $job->{verbose
};
417 my $cfg = read_cron
();
419 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
421 my $states = read_state
();
422 foreach my $source (sort keys%{$cfg}) {
423 foreach my $name (sort keys%{$cfg->{$source}}) {
424 $list .= sprintf("%-25s", cut_target_width
($source, 25));
425 $list .= sprintf("%-25s", cut_target_width
($name, 25));
426 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
427 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
428 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
429 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
439 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
443 return undef if !defined($target->{vmid
});
445 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
447 return "qemu" if $res;
449 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
451 return "lxc" if $res;
459 my $cfg = read_cron
();
461 my $job = param_to_job
($param);
463 $job->{state} = "ok";
466 my $source = parse_target
($param->{source
});
467 my $dest = parse_target
($param->{dest
});
469 if (my $ip = $dest->{ip
}) {
470 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
473 if (my $ip = $source->{ip
}) {
474 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
477 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
479 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
481 die "Pool $source->{path} does not exists\n" if undef($check);
483 my $vm_type = vm_exists
($source);
484 $job->{vm_type
} = $vm_type;
485 $source->{vm_type
} = $vm_type;
487 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
489 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
491 #check if vm has zfs disks if not die;
492 get_disks
($source, 1) if $source->{vmid
};
498 sync
($param) if !$param->{skip
};
509 my $cfg = read_cron
();
511 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
512 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
514 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
515 $job->{name
} = $param->{name
};
516 $job->{source
} = $param->{source
};
517 $job = add_state_to_job
($job);
525 my $job = get_job
($param);
526 $job->{state} = "del";
535 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
536 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
539 my $date = get_date
();
542 $job = get_job
($param);
545 if ($job && $job->{state} eq "syncing") {
546 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
549 my $dest = parse_target
($param->{dest
});
550 my $source = parse_target
($param->{source
});
552 my $sync_path = sub {
553 my ($source, $dest, $job, $param, $date) = @_;
555 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
557 snapshot_add
($source, $dest, $param->{name
}, $date);
559 send_image
($source, $dest, $param);
561 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
565 my $vm_type = vm_exists
($source);
566 $source->{vm_type
} = $vm_type;
569 $job->{state} = "syncing";
570 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
575 if ($source->{vmid
}) {
576 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
577 my $disks = get_disks
($source);
579 foreach my $disk (sort keys %{$disks}) {
580 $source->{all
} = $disks->{$disk}->{all
};
581 $source->{pool
} = $disks->{$disk}->{pool
};
582 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
583 $source->{last_part
} = $disks->{$disk}->{last_part
};
584 &$sync_path($source, $dest, $job, $param, $date);
586 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
587 send_config
($source, $dest,'ssh');
589 send_config
($source, $dest,'local');
592 &$sync_path($source, $dest, $job, $param, $date);
597 $job->{state} = "error";
601 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
607 $job->{state} = "ok";
608 $job->{lsync
} = $date;
617 my ($source, $dest, $max_snap, $name) = @_;
620 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
621 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
622 push @$cmd, $source->{all
};
624 my $raw = run_cmd
($cmd);
627 my $last_snap = undef;
630 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
632 if ($line =~ m/(rep_$name.*)$/) {
634 $last_snap = $1 if (!$last_snap);
637 if ($index == $max_snap) {
638 $source->{destroy
} = 1;
644 return ($old_snap, $last_snap) if $last_snap;
650 my ($source, $dest, $name, $date) = @_;
652 my $snap_name = "rep_$name\_".$date;
654 $source->{new_snap
} = $snap_name;
656 my $path = "$source->{all}\@$snap_name";
659 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
660 push @$cmd, 'zfs', 'snapshot', $path;
666 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
674 my $text = "SHELL=/bin/sh\n";
675 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
677 my $fh = IO
::File-
>new("> $CRONJOBS");
678 die "Could not open file: $!\n" if !$fh;
680 foreach my $source (sort keys%{$cfg}) {
681 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
682 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
683 $text .= "$PROG_PATH sync";
684 $text .= " -source ";
685 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
686 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
687 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
689 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
690 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
691 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
694 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
695 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
696 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
697 $text .= " -name $sync_name ";
698 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
699 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
703 die "Can't write to cron\n" if (!print($fh $text));
708 my ($target, $get_err) = @_;
711 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
713 if ($target->{vm_type
} eq 'qemu') {
714 push @$cmd, 'qm', 'config', $target->{vmid
};
715 } elsif ($target->{vm_type
} eq 'lxc') {
716 push @$cmd, 'pct', 'config', $target->{vmid
};
718 die "VM Type unknown\n";
721 my $res = run_cmd
($cmd);
723 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
730 print "Start CMD\n" if $DEBUG;
731 print Dumper
$cmd if $DEBUG;
732 if (ref($cmd) eq 'ARRAY') {
733 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
735 my $output = `$cmd 2>&1`;
737 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
740 print Dumper
$output if $DEBUG;
741 print "END CMD\n" if $DEBUG;
746 my ($text, $ip, $vm_type, $get_err) = @_;
751 while ($text && $text =~ s/^(.*?)(\n|$)//) {
753 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
755 next if $line =~ /cdrom|none/;
756 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
758 #QEMU if backup is not set include in sync
759 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
761 #LXC if backup is not set do no in sync
762 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
766 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
767 my @parameter = split(/,/,$1);
769 foreach my $opt (@parameter) {
770 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
778 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
783 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
784 push @$cmd, 'pvesm', 'path', "$stor$disk";
785 my $path = run_cmd($cmd);
787 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
789 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
791 my @array = split('/', $1);
792 $disks->{$num}->{pool} = shift(@array);
793 $disks->{$num}->{all} = $disks->{$num}->{pool};
795 $disks->{$num}->{path} = join('/', @array);
796 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
798 $disks->{$num}->{last_part} = $disk;
799 $disks->{$num}->{all} .= "\
/$disk";
802 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
804 $disks->{$num}->{pool} = $1;
805 $disks->{$num}->{all} = $disks->{$num}->{pool};
808 $disks->{$num}->{path} = $3;
809 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
812 $disks->{$num}->{last_part} = $disk;
813 $disks->{$num}->{all} .= "\
/$disk";
818 die "ERROR
: in path
\n";
822 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
826 sub snapshot_destroy {
827 my ($source, $dest, $method, $snap) = @_;
829 my @zfscmd = ('zfs', 'destroy');
830 my $snapshot = "$source->{all
}\
@$snap";
833 if($source->{ip} && $method eq 'ssh'){
834 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
836 run_cmd([@zfscmd, $snapshot]);
843 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
845 my $path = "$dest->{all
}\
/$source->{last_part
}";
848 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
857 my ($source , $dest, $method) = @_;
860 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
861 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
862 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
865 eval {$text =run_cmd($cmd);};
871 while ($text && $text =~ s/^(.*?)(\n|$)//) {
873 return 1 if $line =~ m/^.*$source->{old_snap}$/;
878 my ($source, $dest, $param) = @_;
882 push @$cmd, 'ssh', "root\
@$source->{ip
}", '--' if $source->{ip};
883 push @$cmd, 'zfs', 'send';
884 push @$cmd, '-v' if $param->{verbose};
886 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
887 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
889 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
891 if ($param->{limit}){
892 my $bwl = $param->{limit}*1024;
893 push @$cmd, \'|', 'cstream', '-t', $bwl;
895 my $target = "$dest->{all
}/$source->{last_part
}";
899 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
900 push @$cmd, 'zfs', 'recv', '-F', '--';
901 push @$cmd, "$target";
908 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
915 my ($source, $dest, $method) = @_;
917 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
918 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
920 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
922 $dest_target_new = $config_dir.'/'.$dest_target_new;
924 if ($method eq 'ssh'){
925 if ($dest->{ip} && $source->{ip}) {
926 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
927 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
928 } elsif ($dest->{ip}) {
929 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
930 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
931 } elsif ($source->{ip}) {
932 run_cmd(['mkdir', '-p', '--', $config_dir]);
933 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
936 if ($source->{destroy}){
937 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
939 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
941 run_cmd(['rm', '-f', '--', $dest_target_old]);
944 } elsif ($method eq 'local') {
945 run_cmd(['mkdir', '-p', '--', $config_dir]);
946 run_cmd(['cp', $source_target, $dest_target_new]);
951 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
952 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
958 my $cfg = read_cron();
960 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
962 my $states = read_state();
964 foreach my $source (sort keys%{$cfg}) {
965 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
966 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
967 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
968 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
978 my $job = get_job($param);
979 $job->{state} = "ok
";
987 my $job = get_job($param);
988 $job->{state} = "stopped
";
993 my $command = $ARGV[0];
995 my $commands = {'destroy' => 1,
1004 if (!$command || !$commands->{$command}) {
1009 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1010 \twill sync one
time\n
1012 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1014 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1015 \t-maxsnap
\tinteger
\n
1016 \t\thow much snapshots will be kept before get erased
, default 1/n
1018 \t\tname of the sync job
, if not set it
is default.
1019 \tIt
is only necessary
if scheduler allready contains this source
.\n
1021 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1023 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1024 \tCreate a sync Job
\n
1026 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1028 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1029 \t-maxsnap
\tstring
\n
1030 \t\thow much snapshots will be kept before get erased
, default 1\n
1032 \t\tname of the sync job
, if not set it
is default\n
1034 \t\tif this flag
is set it will skip the first sync
\n
1036 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1038 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1039 \tremove a sync Job from the scheduler
\n
1041 \t\tname of the sync job
, if not set it
is default\n
1043 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1045 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1046 \tGet help about specified command
.\n
1049 \t-verbose
\tboolean
\n
1050 \t\tVerbose output format
.\n";
1052 my $help_list = "$PROGNAME list
\n
1053 \tGet a List of all scheduled Sync Jobs
\n";
1055 my $help_status = "$PROGNAME status
\n
1056 \tGet the status of all scheduled Sync Jobs
\n";
1058 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1059 \tenable a syncjob
and reset error
\n
1061 \t\tname of the sync job
, if not set it
is default\n
1063 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1065 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1068 \t\tname of the sync job
, if not set it
is default\n
1070 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1086 die "$help_destroy\n";
1090 die "$help_create\n";
1098 die "$help_status\n";
1102 die "$help_enable\n";
1106 die "$help_enable\n";
1113 my $param = parse_argv(@arg);
1119 die "$help_destroy\n" if !$param->{source};
1120 check_target($param->{source});
1121 destroy_job($param);
1125 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1126 check_target($param->{source});
1127 check_target($param->{dest});
1132 die "$help_create\n" if !$param->{source} || !$param->{dest};
1133 check_target($param->{source});
1134 check_target($param->{dest});
1147 my $help_command = $ARGV[1];
1148 if ($help_command && $commands->{$help_command}) {
1149 print help($help_command);
1151 if ($param->{verbose} == 1){
1152 exec("man
$PROGNAME");
1159 die "$help_enable\n" if !$param->{source};
1160 check_target($param->{source});
1165 die "$help_disable\n" if !$param->{source};
1166 check_target($param->{source});
1167 disable_job($param);
1174 print("ERROR
:\tno command specified
\n") if !$help;
1175 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1176 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1177 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1178 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1179 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1180 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1181 print("\t$PROGNAME list
\n");
1182 print("\t$PROGNAME status
\n");
1183 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1188 parse_target($target);
1195 pve-zsync - PVE ZFS Replication Manager
1199 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1201 pve-zsync help <cmd> [OPTIONS]
1203 Get help about specified command.
1211 Verbose output format.
1213 pve-zsync create -dest <string> -source <string> [OPTIONS]
1219 the destination target is like [IP]:<Pool>[/Path]
1223 max sync speed in kBytes/s, default unlimited
1227 how much snapshots will be kept before get erased, default 1
1231 name of the sync job, if not set it is default
1235 if this flag is set it will skip the first sync
1239 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1241 pve-zsync destroy -source <string> [OPTIONS]
1243 remove a sync Job from the scheduler
1247 name of the sync job, if not set it is default
1251 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1253 pve-zsync disable -source <string> [OPTIONS]
1259 name of the sync job, if not set it is default
1263 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1265 pve-zsync enable -source <string> [OPTIONS]
1267 enable a syncjob and reset error
1271 name of the sync job, if not set it is default
1275 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1278 Get a List of all scheduled Sync Jobs
1282 Get the status of all scheduled Sync Jobs
1284 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1290 the destination target is like [IP:]<Pool>[/Path]
1294 max sync speed in kBytes/s, default unlimited
1298 how much snapshots will be kept before get erased, default 1
1302 name of the sync job, if not set it is default.
1303 It is only necessary if scheduler allready contains this source.
1307 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1311 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1312 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1313 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.
1314 To config cron see man crontab.
1316 =head2 PVE ZFS Storage sync Tool
1318 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1322 add sync job from local VM to remote ZFS Server
1323 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1325 =head1 IMPORTANT FILES
1327 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1329 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1331 =head1 COPYRIGHT AND DISCLAIMER
1333 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1335 This program is free software: you can redistribute it and/or modify it
1336 under the terms of the GNU Affero General Public License as published
1337 by the Free Software Foundation, either version 3 of the License, or
1338 (at your option) any later version.
1340 This program is distributed in the hope that it will be useful, but
1341 WITHOUT ANY WARRANTY; without even the implied warranty of
1342 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1343 Affero General Public License for more details.
1345 You should have received a copy of the GNU Affero General Public
1346 License along with this program. If not, see
1347 <http://www.gnu.org/licenses/>.