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);
12 use String
::ShellQuote
'shell_quote';
14 my $PROGNAME = "pve-zsync";
15 my $CONFIG_PATH = "/var/lib/${PROGNAME}";
16 my $STATE = "${CONFIG_PATH}/sync_state";
17 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
18 my $PATH = "/usr/sbin";
19 my $PVE_DIR = "/etc/pve/local";
20 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
21 my $LXC_CONF = "${PVE_DIR}/lxc";
22 my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
23 my $PROG_PATH = "$PATH/${PROGNAME}";
27 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
28 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
29 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
30 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
33 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
34 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
35 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
43 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
44 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
45 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
46 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
47 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
49 check_bin
('cstream');
57 foreach my $p (split (/:/, $ENV{PATH
})) {
64 die "unable to find command '$bin'\n";
67 sub cut_target_width
{
68 my ($target, $max) = @_;
70 return $target if (length($target) <= $max);
71 my @spl = split('/', $target);
73 my $count = length($spl[@spl-1]);
74 return "..\/".substr($spl[@spl-1],($count-$max)+3 , $count) if $count > $max;
76 $count += length($spl[0]) if @spl > 1;
77 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
80 $rest = $max-$count if ($max-$count > 0);
82 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
87 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
92 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
96 my ($source, $name, $status) = @_;
98 if ($status->{$source->{all
}}->{$name}->{status
}) {
105 sub check_pool_exists
{
111 push @$cmd, 'ssh', "root\@$target->{ip}", '--';
113 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
127 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
130 if ($text !~ $TARGETRE) {
134 $target->{ip
} = $1 if $1;
135 my @parts = split('/', $2);
137 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
139 my $pool = $target->{pool
} = shift(@parts);
140 die "$errstr\n" if !$pool;
142 if ($pool =~ m/^\d+$/) {
143 $target->{vmid
} = $pool;
144 delete $target->{pool
};
147 return $target if (@parts == 0);
148 $target->{last_part
} = pop(@parts);
154 $target->{path
} = join('/', @parts);
162 #This is for the first use to init file;
164 my $new_fh = IO
::File-
>new("> $CRONJOBS");
165 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
170 my $fh = IO
::File-
>new("< $CRONJOBS");
171 die "Could not open file $CRONJOBS: $!\n" if !$fh;
177 return encode_cron
(@text);
184 $param->{dest
} = undef;
185 $param->{source
} = undef;
186 $param->{verbose
} = undef;
187 $param->{limit
} = undef;
188 $param->{maxsnap
} = undef;
189 $param->{name
} = undef;
190 $param->{skip
} = undef;
191 $param->{method} = undef;
193 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
194 'dest=s' => \
$param->{dest
},
195 'source=s' => \
$param->{source
},
196 'verbose' => \
$param->{verbose
},
197 'limit=i' => \
$param->{limit
},
198 'maxsnap=i' => \
$param->{maxsnap
},
199 'name=s' => \
$param->{name
},
200 'skip' => \
$param->{skip
},
201 'method=s' => \
$param->{method});
204 die "can't parse options\n";
207 $param->{name
} = "default" if !$param->{name
};
208 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
209 $param->{method} = "ssh" if !$param->{method};
214 sub add_state_to_job
{
217 my $states = read_state
();
218 my $state = $states->{$job->{source
}}->{$job->{name
}};
220 $job->{state} = $state->{state};
221 $job->{lsync
} = $state->{lsync
};
222 $job->{vm_type
} = $state->{vm_type
};
224 for (my $i = 0; $state->{"snap$i"}; $i++) {
225 $job->{"snap$i"} = $state->{"snap$i"};
236 while (my $line = shift(@text)) {
238 my @arg = split('\s', $line);
239 my $param = parse_argv
(@arg);
241 if ($param->{source
} && $param->{dest
}) {
242 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
246 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
247 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
259 my $source = parse_target
($param->{source
});
260 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
262 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
263 $job->{dest
} = $param->{dest
} if $param->{dest
};
264 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
265 $job->{method} = "ssh" if !$job->{method};
266 $job->{limit
} = $param->{limit
};
267 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
268 $job->{source
} = $param->{source
};
276 make_path
$CONFIG_PATH;
277 my $new_fh = IO
::File-
>new("> $STATE");
278 die "Could not create $STATE: $!\n" if !$new_fh;
284 my $fh = IO
::File-
>new("< $STATE");
285 die "Could not open file $STATE: $!\n" if !$fh;
288 my $states = decode_json
($text);
302 $in_fh = IO
::File-
>new("< $STATE");
303 die "Could not open file $STATE: $!\n" if !$in_fh;
308 my $out_fh = IO
::File-
>new("> $STATE.new");
309 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
314 $states = decode_json
($text);
315 $state = $states->{$job->{source
}}->{$job->{name
}};
318 if ($job->{state} ne "del") {
319 $state->{state} = $job->{state};
320 $state->{lsync
} = $job->{lsync
};
321 $state->{vm_type
} = $job->{vm_type
};
323 for (my $i = 0; $job->{"snap$i"} ; $i++) {
324 $state->{"snap$i"} = $job->{"snap$i"};
326 $states->{$job->{source
}}->{$job->{name
}} = $state;
329 delete $states->{$job->{source
}}->{$job->{name
}};
330 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
333 $text = encode_json
($states);
337 move
("$STATE.new", $STATE);
352 my $header = "SHELL=/bin/sh\n";
353 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
355 my $fh = IO
::File-
>new("< $CRONJOBS");
356 die "Could not open file $CRONJOBS: $!\n" if !$fh;
361 while (my $line = shift(@test)) {
363 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
365 next if $job->{state} eq "del";
366 $text .= format_job
($job, $line);
368 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
377 $text = "$header$text";
381 $text .= format_job
($job);
383 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
384 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
386 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
389 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
394 my ($job, $line) = @_;
397 if ($job->{state} eq "stopped") {
401 $line =~ /^#*(.+) root/;
404 $text .= "*/$INTERVAL * * * *";
407 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
408 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
409 $text .= " --limit $job->{limit}" if $job->{limit
};
410 $text .= " --method $job->{method}";
411 $text .= " --verbose" if $job->{verbose
};
419 my $cfg = read_cron
();
421 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
423 my $states = read_state
();
424 foreach my $source (sort keys%{$cfg}) {
425 foreach my $name (sort keys%{$cfg->{$source}}) {
426 $list .= sprintf("%-25s", cut_target_width
($source, 25));
427 $list .= sprintf("%-25s", cut_target_width
($name, 25));
428 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
429 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
430 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
431 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
441 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
445 return undef if !defined($target->{vmid
});
447 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
449 return "qemu" if $res;
451 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
453 return "lxc" if $res;
461 my $cfg = read_cron
();
463 my $job = param_to_job
($param);
465 $job->{state} = "ok";
468 my $source = parse_target
($param->{source
});
469 my $dest = parse_target
($param->{dest
});
471 if (my $ip = $dest->{ip
}) {
472 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
475 if (my $ip = $source->{ip
}) {
476 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
479 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest);
481 if (!defined($source->{vmid
})) {
482 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source);
485 my $vm_type = vm_exists
($source);
486 $job->{vm_type
} = $vm_type;
487 $source->{vm_type
} = $vm_type;
489 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
491 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
493 #check if vm has zfs disks if not die;
494 get_disks
($source, 1) if $source->{vmid
};
500 sync
($param) if !$param->{skip
};
511 my $cfg = read_cron
();
513 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
514 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
516 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
517 $job->{name
} = $param->{name
};
518 $job->{source
} = $param->{source
};
519 $job = add_state_to_job
($job);
527 my $job = get_job
($param);
528 $job->{state} = "del";
537 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
538 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
541 my $date = get_date
();
544 $job = get_job
($param);
547 if ($job && $job->{state} eq "syncing") {
548 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
551 my $dest = parse_target
($param->{dest
});
552 my $source = parse_target
($param->{source
});
554 my $sync_path = sub {
555 my ($source, $dest, $job, $param, $date) = @_;
557 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
559 snapshot_add
($source, $dest, $param->{name
}, $date);
561 send_image
($source, $dest, $param);
563 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
567 my $vm_type = vm_exists
($source);
568 $source->{vm_type
} = $vm_type;
571 $job->{state} = "syncing";
572 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
577 if ($source->{vmid
}) {
578 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
579 my $disks = get_disks
($source);
581 foreach my $disk (sort keys %{$disks}) {
582 $source->{all
} = $disks->{$disk}->{all
};
583 $source->{pool
} = $disks->{$disk}->{pool
};
584 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
585 $source->{last_part
} = $disks->{$disk}->{last_part
};
586 &$sync_path($source, $dest, $job, $param, $date);
588 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
589 send_config
($source, $dest,'ssh');
591 send_config
($source, $dest,'local');
594 &$sync_path($source, $dest, $job, $param, $date);
599 $job->{state} = "error";
603 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
609 $job->{state} = "ok";
610 $job->{lsync
} = $date;
619 my ($source, $dest, $max_snap, $name) = @_;
622 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
623 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
624 push @$cmd, $source->{all
};
626 my $raw = run_cmd
($cmd);
629 my $last_snap = undef;
632 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
634 if ($line =~ m/(rep_$name.*)$/) {
636 $last_snap = $1 if (!$last_snap);
639 if ($index == $max_snap) {
640 $source->{destroy
} = 1;
646 return ($old_snap, $last_snap) if $last_snap;
652 my ($source, $dest, $name, $date) = @_;
654 my $snap_name = "rep_$name\_".$date;
656 $source->{new_snap
} = $snap_name;
658 my $path = "$source->{all}\@$snap_name";
661 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
662 push @$cmd, 'zfs', 'snapshot', $path;
668 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
676 my $text = "SHELL=/bin/sh\n";
677 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
679 my $fh = IO
::File-
>new("> $CRONJOBS");
680 die "Could not open file: $!\n" if !$fh;
682 foreach my $source (sort keys%{$cfg}) {
683 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
684 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
685 $text .= "$PROG_PATH sync";
686 $text .= " -source ";
687 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
688 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
689 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
691 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
692 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
693 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
696 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
697 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
698 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
699 $text .= " -name $sync_name ";
700 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
701 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
705 die "Can't write to cron\n" if (!print($fh $text));
710 my ($target, $get_err) = @_;
713 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
715 if ($target->{vm_type
} eq 'qemu') {
716 push @$cmd, 'qm', 'config', $target->{vmid
};
717 } elsif ($target->{vm_type
} eq 'lxc') {
718 push @$cmd, 'pct', 'config', $target->{vmid
};
720 die "VM Type unknown\n";
723 my $res = run_cmd
($cmd);
725 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
732 print "Start CMD\n" if $DEBUG;
733 print Dumper
$cmd if $DEBUG;
734 if (ref($cmd) eq 'ARRAY') {
735 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
737 my $output = `$cmd 2>&1`;
739 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
742 print Dumper
$output if $DEBUG;
743 print "END CMD\n" if $DEBUG;
748 my ($text, $ip, $vm_type, $get_err) = @_;
753 while ($text && $text =~ s/^(.*?)(\n|$)//) {
755 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
757 next if $line =~ /cdrom|none/;
758 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
760 #QEMU if backup is not set include in sync
761 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
763 #LXC if backup is not set do no in sync
764 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
768 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
769 my @parameter = split(/,/,$1);
771 foreach my $opt (@parameter) {
772 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
780 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
785 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
786 push @$cmd, 'pvesm', 'path', "$stor$disk";
787 my $path = run_cmd($cmd);
789 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
791 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
793 my @array = split('/', $1);
794 $disks->{$num}->{pool} = shift(@array);
795 $disks->{$num}->{all} = $disks->{$num}->{pool};
797 $disks->{$num}->{path} = join('/', @array);
798 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
800 $disks->{$num}->{last_part} = $disk;
801 $disks->{$num}->{all} .= "\
/$disk";
804 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
806 $disks->{$num}->{pool} = $1;
807 $disks->{$num}->{all} = $disks->{$num}->{pool};
810 $disks->{$num}->{path} = $3;
811 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
814 $disks->{$num}->{last_part} = $disk;
815 $disks->{$num}->{all} .= "\
/$disk";
820 die "ERROR
: in path
\n";
824 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
828 sub snapshot_destroy {
829 my ($source, $dest, $method, $snap) = @_;
831 my @zfscmd = ('zfs', 'destroy');
832 my $snapshot = "$source->{all
}\
@$snap";
835 if($source->{ip} && $method eq 'ssh'){
836 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
838 run_cmd([@zfscmd, $snapshot]);
845 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
847 my $path = "$dest->{all
}\
/$source->{last_part
}";
850 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
859 my ($source , $dest, $method) = @_;
862 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
863 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
864 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
867 eval {$text =run_cmd($cmd);};
873 while ($text && $text =~ s/^(.*?)(\n|$)//) {
875 return 1 if $line =~ m/^.*$source->{old_snap}$/;
880 my ($source, $dest, $param) = @_;
884 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$source->{ip
}", '--' if $source->{ip};
885 push @$cmd, 'zfs', 'send';
886 push @$cmd, '-v' if $param->{verbose};
888 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
889 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
891 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
893 if ($param->{limit}){
894 my $bwl = $param->{limit}*1024;
895 push @$cmd, \'|', 'cstream', '-t', $bwl;
897 my $target = "$dest->{all
}/$source->{last_part
}";
901 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$dest->{ip
}", '--' if $dest->{ip};
902 push @$cmd, 'zfs', 'recv', '-F', '--';
903 push @$cmd, "$target";
910 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
917 my ($source, $dest, $method) = @_;
919 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
920 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
922 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
924 $dest_target_new = $config_dir.'/'.$dest_target_new;
926 if ($method eq 'ssh'){
927 if ($dest->{ip} && $source->{ip}) {
928 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
929 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
930 } elsif ($dest->{ip}) {
931 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
932 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
933 } elsif ($source->{ip}) {
934 run_cmd(['mkdir', '-p', '--', $config_dir]);
935 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
938 if ($source->{destroy}){
939 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
941 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
943 run_cmd(['rm', '-f', '--', $dest_target_old]);
946 } elsif ($method eq 'local') {
947 run_cmd(['mkdir', '-p', '--', $config_dir]);
948 run_cmd(['cp', $source_target, $dest_target_new]);
953 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
954 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
960 my $cfg = read_cron();
962 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
964 my $states = read_state();
966 foreach my $source (sort keys%{$cfg}) {
967 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
968 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
969 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
970 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
980 my $job = get_job($param);
981 $job->{state} = "ok
";
989 my $job = get_job($param);
990 $job->{state} = "stopped
";
995 my $command = $ARGV[0];
997 my $commands = {'destroy' => 1,
1006 if (!$command || !$commands->{$command}) {
1011 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1012 \twill sync one
time\n
1014 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1016 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1017 \t-maxsnap
\tinteger
\n
1018 \t\thow much snapshots will be kept before get erased
, default 1/n
1020 \t\tname of the sync job
, if not set it
is default.
1021 \tIt
is only necessary
if scheduler allready contains this source
.\n
1023 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1025 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1026 \tCreate a sync Job
\n
1028 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1030 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1031 \t-maxsnap
\tstring
\n
1032 \t\thow much snapshots will be kept before get erased
, default 1\n
1034 \t\tname of the sync job
, if not set it
is default\n
1036 \t\tif this flag
is set it will skip the first sync
\n
1038 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1040 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1041 \tremove a sync Job from the scheduler
\n
1043 \t\tname of the sync job
, if not set it
is default\n
1045 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1047 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1048 \tGet help about specified command
.\n
1051 \t-verbose
\tboolean
\n
1052 \t\tVerbose output format
.\n";
1054 my $help_list = "$PROGNAME list
\n
1055 \tGet a List of all scheduled Sync Jobs
\n";
1057 my $help_status = "$PROGNAME status
\n
1058 \tGet the status of all scheduled Sync Jobs
\n";
1060 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1061 \tenable a syncjob
and reset error
\n
1063 \t\tname of the sync job
, if not set it
is default\n
1065 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1067 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1070 \t\tname of the sync job
, if not set it
is default\n
1072 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1077 if ($command eq 'help') {
1080 } elsif ($command eq 'sync') {
1083 } elsif ($command eq 'destroy') {
1084 die "$help_destroy\n";
1086 } elsif ($command eq 'create') {
1087 die "$help_create\n";
1089 } elsif ($command eq 'list') {
1092 } elsif ($command eq 'status') {
1093 die "$help_status\n";
1095 } elsif ($command eq 'enable') {
1096 die "$help_enable\n";
1098 } elsif ($command eq 'disable') {
1099 die "$help_disable\n";
1106 my $param = parse_argv(@arg);
1108 if ($command eq 'destroy') {
1109 die "$help_destroy\n" if !$param->{source};
1111 check_target($param->{source});
1112 destroy_job($param);
1114 } elsif ($command eq 'sync') {
1115 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1117 check_target($param->{source});
1118 check_target($param->{dest});
1121 } elsif ($command eq 'create') {
1122 die "$help_create\n" if !$param->{source} || !$param->{dest};
1124 check_target($param->{source});
1125 check_target($param->{dest});
1128 } elsif ($command eq 'status') {
1131 } elsif ($command eq 'list') {
1134 } elsif ($command eq 'help') {
1135 my $help_command = $ARGV[1];
1137 if ($help_command && $commands->{$help_command}) {
1138 print help($help_command);
1141 if ($param->{verbose} == 1){
1142 exec("man
$PROGNAME");
1149 } elsif ($command eq 'enable') {
1150 die "$help_enable\n" if !$param->{source};
1152 check_target($param->{source});
1155 } elsif ($command eq 'disable') {
1156 die "$help_disable\n" if !$param->{source};
1158 check_target($param->{source});
1159 disable_job($param);
1166 print("ERROR
:\tno command specified
\n") if !$help;
1167 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1168 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1169 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1170 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1171 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1172 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1173 print("\t$PROGNAME list
\n");
1174 print("\t$PROGNAME status
\n");
1175 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1180 parse_target($target);
1187 pve-zsync - PVE ZFS Replication Manager
1191 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1193 pve-zsync help <cmd> [OPTIONS]
1195 Get help about specified command.
1203 Verbose output format.
1205 pve-zsync create -dest <string> -source <string> [OPTIONS]
1211 the destination target is like [IP]:<Pool>[/Path]
1215 max sync speed in kBytes/s, default unlimited
1219 how much snapshots will be kept before get erased, default 1
1223 name of the sync job, if not set it is default
1227 if this flag is set it will skip the first sync
1231 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1233 pve-zsync destroy -source <string> [OPTIONS]
1235 remove a sync Job from the scheduler
1239 name of the sync job, if not set it is default
1243 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1245 pve-zsync disable -source <string> [OPTIONS]
1251 name of the sync job, if not set it is default
1255 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1257 pve-zsync enable -source <string> [OPTIONS]
1259 enable a syncjob and reset error
1263 name of the sync job, if not set it is default
1267 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1270 Get a List of all scheduled Sync Jobs
1274 Get the status of all scheduled Sync Jobs
1276 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1282 the destination target is like [IP:]<Pool>[/Path]
1286 max sync speed in kBytes/s, default unlimited
1290 how much snapshots will be kept before get erased, default 1
1294 name of the sync job, if not set it is default.
1295 It is only necessary if scheduler allready contains this source.
1299 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1303 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1304 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1305 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.
1306 To config cron see man crontab.
1308 =head2 PVE ZFS Storage sync Tool
1310 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1314 add sync job from local VM to remote ZFS Server
1315 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1317 =head1 IMPORTANT FILES
1319 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1321 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1323 =head1 COPYRIGHT AND DISCLAIMER
1325 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1327 This program is free software: you can redistribute it and/or modify it
1328 under the terms of the GNU Affero General Public License as published
1329 by the Free Software Foundation, either version 3 of the License, or
1330 (at your option) any later version.
1332 This program is distributed in the hope that it will be useful, but
1333 WITHOUT ANY WARRANTY; without even the implied warranty of
1334 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1335 Affero General Public License for more details.
1337 You should have received a copy of the GNU Affero General Public
1338 License along with this program. If not, see
1339 <http://www.gnu.org/licenses/>.