]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
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 $QEMU_CONF = "/etc/pve/local/qemu-server/";
21 my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
22 my $PROG_PATH = "$PATH${PROGNAME}";
26 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
27 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
28 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
29 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
32 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
33 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
34 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
35 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
42 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
43 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
44 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
45 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
46 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
48 check_bin
('cstream');
56 foreach my $p (split (/:/, $ENV{PATH
})) {
63 die "unable to find command '$bin'\n";
66 sub cut_target_width
{
67 my ($target, $max) = @_;
69 return $target if (length($target) <= $max);
70 my @spl = split('/', $target);
72 my $count = length($spl[@spl-1]);
73 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
75 $count += length($spl[0]) if @spl > 1;
76 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
79 $rest = $max-$count if ($max-$count > 0);
81 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
86 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
91 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
95 my ($source, $name, $status) = @_;
97 if ($status->{$source->{all
}}->{$name}->{status
}) {
104 sub check_pool_exists
{
108 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
109 $cmd .= "zfs list $target->{all} -H";
123 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
126 if ($text !~ $TARGETRE) {
130 $target->{ip
} = $1 if $1;
131 my @parts = split('/', $2);
133 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
135 my $pool = $target->{pool
} = shift(@parts);
136 die "$errstr\n" if !$pool;
138 if ($pool =~ m/^\d+$/) {
139 $target->{vmid
} = $pool;
140 delete $target->{pool
};
143 return $target if (@parts == 0);
144 $target->{last_part
} = pop(@parts);
150 $target->{path
} = join('/', @parts);
158 #This is for the first use to init file;
160 my $new_fh = IO
::File-
>new("> $CRONJOBS");
161 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
166 my $fh = IO
::File-
>new("< $CRONJOBS");
167 die "Could not open file $CRONJOBS: $!\n" if !$fh;
173 return encode_cron
(@text);
180 $param->{dest
} = undef;
181 $param->{source
} = undef;
182 $param->{verbose
} = undef;
183 $param->{limit
} = undef;
184 $param->{maxsnap
} = undef;
185 $param->{name
} = undef;
186 $param->{skip
} = undef;
187 $param->{method} = undef;
189 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
190 'dest=s' => \
$param->{dest
},
191 'source=s' => \
$param->{source
},
192 'verbose' => \
$param->{verbose
},
193 'limit=i' => \
$param->{limit
},
194 'maxsnap=i' => \
$param->{maxsnap
},
195 'name=s' => \
$param->{name
},
196 'skip' => \
$param->{skip
},
197 'method=s' => \
$param->{method});
200 die "can't parse options\n";
203 $param->{name
} = "default" if !$param->{name
};
204 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
205 $param->{method} = "ssh" if !$param->{method};
210 sub add_state_to_job
{
213 my $states = read_state
();
214 my $state = $states->{$job->{source
}}->{$job->{name
}};
216 $job->{state} = $state->{state};
217 $job->{lsync
} = $state->{lsync
};
219 for (my $i = 0; $state->{"snap$i"}; $i++) {
220 $job->{"snap$i"} = $state->{"snap$i"};
231 while (my $line = shift(@text)) {
233 my @arg = split('\s', $line);
234 my $param = parse_argv
(@arg);
236 if ($param->{source
} && $param->{dest
}) {
237 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
238 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
239 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
240 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
254 my $source = parse_target
($param->{source
});
255 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
257 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
258 $job->{dest
} = $param->{dest
} if $param->{dest
};
259 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
260 $job->{method} = "ssh" if !$job->{method};
261 $job->{limit
} = $param->{limit
};
262 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
263 $job->{source
} = $param->{source
};
271 make_path
$CONFIG_PATH;
272 my $new_fh = IO
::File-
>new("> $STATE");
273 die "Could not create $STATE: $!\n" if !$new_fh;
279 my $fh = IO
::File-
>new("< $STATE");
280 die "Could not open file $STATE: $!\n" if !$fh;
283 my $states = decode_json
($text);
297 $in_fh = IO
::File-
>new("< $STATE");
298 die "Could not open file $STATE: $!\n" if !$in_fh;
303 my $out_fh = IO
::File-
>new("> $STATE.new");
304 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
309 $states = decode_json
($text);
310 $state = $states->{$job->{source
}}->{$job->{name
}};
313 if ($job->{state} ne "del") {
314 $state->{state} = $job->{state};
315 $state->{lsync
} = $job->{lsync
};
317 for (my $i = 0; $job->{"snap$i"} ; $i++) {
318 $state->{"snap$i"} = $job->{"snap$i"};
320 $states->{$job->{source
}}->{$job->{name
}} = $state;
323 delete $states->{$job->{source
}}->{$job->{name
}};
324 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
327 $text = encode_json
($states);
331 move
("$STATE.new", $STATE);
346 my $header = "SHELL=/bin/sh\n";
347 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
349 my $fh = IO
::File-
>new("< $CRONJOBS");
350 die "Could not open file $CRONJOBS: $!\n" if !$fh;
355 while (my $line = shift(@test)) {
357 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
359 next if $job->{state} eq "del";
360 $text .= format_job
($job, $line);
362 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
371 $text = "$header$text";
375 $text .= format_job
($job);
377 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
378 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
380 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
383 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
388 my ($job, $line) = @_;
391 if ($job->{state} eq "stopped") {
395 $line =~ /^#*(.+) root/;
398 $text .= "*/$INTERVAL * * * *";
401 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
402 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
403 $text .= " --method $job->{method}";
404 $text .= " --verbose" if $job->{verbose
};
412 my $cfg = read_cron
();
414 my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE");
416 my $states = read_state
();
417 foreach my $source (sort keys%{$cfg}) {
418 foreach my $name (sort keys%{$cfg->{$source}}) {
419 $list .= sprintf("%-25s", cut_target_width
($source, 25));
420 $list .= sprintf("%-15s", cut_target_width
($name, 15));
421 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
422 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync
});
423 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
434 $cmd = "ssh root\@$target->{ip} " if ($target->{ip
});
435 $cmd .= "qm status $target->{vmid}";
437 my $res = run_cmd
($cmd);
439 return 1 if ($res =~ m/^status.*$/);
446 my $cfg = read_cron
();
448 my $job = param_to_job
($param);
450 $job->{state} = "ok";
453 my $source = parse_target
($param->{source
});
454 my $dest = parse_target
($param->{dest
});
456 if (my $ip = $dest->{ip
}) {
457 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
460 if (my $ip = $source->{ip
}) {
461 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
464 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
466 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
468 die "Pool $source->{path} does not exists\n" if undef($check);
470 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !vm_exists
($source);
472 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
478 sync
($param) if !$param->{skip
};
489 my $cfg = read_cron
();
491 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
492 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
494 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
495 $job->{name
} = $param->{name
};
496 $job->{source
} = $param->{source
};
497 $job = add_state_to_job
($job);
505 my $job = get_job
($param);
506 $job->{state} = "del";
515 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
516 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
519 my $date = get_date
();
522 $job = get_job
($param);
525 if ($job && $job->{state} eq "syncing") {
526 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
529 my $dest = parse_target
($param->{dest
});
530 my $source = parse_target
($param->{source
});
532 my $sync_path = sub {
533 my ($source, $dest, $job, $param, $date) = @_;
535 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
537 snapshot_add
($source, $dest, $param->{name
}, $date);
539 send_image
($source, $dest, $param);
541 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
546 $job->{state} = "syncing";
551 if ($source->{vmid
}) {
552 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
553 my $disks = get_disks
($source);
555 foreach my $disk (sort keys %{$disks}) {
556 $source->{all
} = $disks->{$disk}->{all
};
557 $source->{pool
} = $disks->{$disk}->{pool
};
558 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
559 $source->{last_part
} = $disks->{$disk}->{last_part
};
560 &$sync_path($source, $dest, $job, $param, $date);
562 if ($param->{method} eq "ssh") {
563 send_config
($source, $dest,'ssh');
566 &$sync_path($source, $dest, $job, $param, $date);
571 $job->{state} = "error";
575 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
581 $job->{state} = "ok";
582 $job->{lsync
} = $date;
591 my ($source, $dest, $max_snap, $name) = @_;
593 my $cmd = "zfs list -r -t snapshot -Ho name, -S creation ";
595 $cmd .= $source->{all
};
596 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
598 my $raw = run_cmd
($cmd);
601 my $last_snap = undef;
604 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
606 if ($line =~ m/(rep_$name.*)$/) {
607 $last_snap = $1 if (!$last_snap);
610 if ($index == $max_snap) {
611 $source->{destroy
} = 1;
617 return ($old_snap, $last_snap) if $last_snap;
623 my ($source, $dest, $name, $date) = @_;
625 my $snap_name = "rep_$name\_".$date;
627 $source->{new_snap
} = $snap_name;
629 my $path = "$source->{all}\@$snap_name";
631 my $cmd = "zfs snapshot $path";
632 $cmd = "ssh root\@$source->{ip} $cmd" if $source->{ip
};
639 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
647 my $text = "SHELL=/bin/sh\n";
648 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
650 my $fh = IO
::File-
>new("> $CRONJOBS");
651 die "Could not open file: $!\n" if !$fh;
653 foreach my $source (sort keys%{$cfg}) {
654 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
655 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
656 $text .= "$PROG_PATH sync";
657 $text .= " -source ";
658 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
659 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
660 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
662 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
663 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
664 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
667 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
668 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
669 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
670 $text .= " -name $sync_name ";
671 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
672 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
676 die "Can't write to cron\n" if (!print($fh $text));
684 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
685 $cmd .= "qm config $target->{vmid}";
687 my $res = run_cmd
($cmd);
689 my $disks = parse_disks
($res, $target->{ip
});
696 print "Start CMD\n" if $DEBUG;
697 print Dumper
$cmd if $DEBUG;
698 if (ref($cmd) eq 'ARRAY') {
699 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
701 my $output = `$cmd 2>&1`;
703 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
706 print Dumper
$output if $DEBUG;
707 print "END CMD\n" if $DEBUG;
712 my ($text, $ip) = @_;
717 while ($text && $text =~ s/^(.*?)(\n|$)//) {
720 next if $line =~ /cdrom|none/;
721 next if $line !~ m/^(?:virtio|ide|scsi|sata)\d+: /;
725 if($line =~ m/^(?:virtio|ide|scsi|sata)\d+: (.+:)([A-Za-z0-9\-]+),(.*)$/) {
729 die "disk is not on ZFS Storage\n";
733 $cmd .= "ssh root\@$ip " if $ip;
734 $cmd .= "pvesm path $stor$disk";
735 my $path = run_cmd
($cmd);
737 if ($path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
739 my @array = split('/', $1);
740 $disks->{$num}->{pool
} = shift(@array);
741 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
743 $disks->{$num}->{path
} = join('/', @array);
744 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
746 $disks->{$num}->{last_part
} = $disk;
747 $disks->{$num}->{all
} .= "\/$disk";
752 die "ERROR: in path\n";
759 sub snapshot_destroy
{
760 my ($source, $dest, $method, $snap) = @_;
762 my $zfscmd = "zfs destroy ";
763 my $snapshot = "$source->{all}\@$snap";
766 if($source->{ip
} && $method eq 'ssh'){
767 run_cmd
("ssh root\@$source->{ip} $zfscmd $snapshot");
769 run_cmd
("$zfscmd $snapshot");
776 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
778 my $path = "$dest->{all}\/$source->{last_part}";
781 run_cmd
("$ssh $zfscmd $path\@$snap ");
790 my ($source ,$dest, $method) = @_;
793 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
794 $cmd .= "zfs list -rt snapshot -Ho name $dest->{all}";
795 $cmd .= "\/$source->{last_part}\@$source->{old_snap}";
798 eval {$text =run_cmd
($cmd);};
804 while ($text && $text =~ s/^(.*?)(\n|$)//) {
806 return 1 if $line =~ m/^.*$source->{old_snap}$/;
811 my ($source, $dest, $param) = @_;
815 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
817 $cmd .= "-v " if $param->{verbose
};
819 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
820 $cmd .= "-i $source->{all}\@$source->{last_snap} $source->{all}\@$source->{new_snap} ";
822 $cmd .= "$source->{all}\@$source->{new_snap} ";
825 if ($param->{limit
}){
826 my $bwl = $param->{limit
}*1024;
827 $cmd .= "| cstream -t $bwl";
830 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
831 $cmd .= "zfs recv $dest->{all}";
832 $cmd .= "\/$source->{last_part}\@$source->{new_snap}";
839 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
846 my ($source, $dest, $method) = @_;
848 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
849 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
851 if ($method eq 'ssh'){
852 if ($dest->{ip
} && $source->{ip
}) {
853 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
854 run_cmd
("scp root\@$source->{ip}:$source_target root\@$dest->{ip}:$dest_target_new");
855 } elsif ($dest->{ip
}) {
856 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
857 run_cmd
("scp $source_target root\@$dest->{ip}:$dest_target_new");
858 } elsif ($source->{ip
}) {
859 run_cmd
("mkdir $CONFIG_PATH -p");
860 run_cmd
("scp root\@$source->{ip}:$source_target $dest_target_new");
863 if ($source->{destroy
}){
864 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
866 run_cmd
("ssh root\@$dest->{ip} rm -f $dest_target_old");
868 run_cmd
("rm -f $dest_target_old");
875 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
876 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
882 my $cfg = read_cron
();
884 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
886 my $states = read_state
();
888 foreach my $source (sort keys%{$cfg}) {
889 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
890 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
891 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
892 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
902 my $job = get_job
($param);
903 $job->{state} = "ok";
911 my $job = get_job
($param);
912 $job->{state} = "stopped";
917 my $command = $ARGV[0];
919 my $commands = {'destroy' => 1,
928 if (!$command || !$commands->{$command}) {
933 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
934 \twill sync one time\n
936 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
938 \t\tmax sync speed in kBytes/s, default unlimited\n
939 \t-maxsnap\tinteger\n
940 \t\thow much snapshots will be kept before get erased, default 1/n
942 \t\tname of the sync job, if not set it is default.
943 \tIt is only necessary if scheduler allready contains this source.\n
945 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
947 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
948 \tCreate a sync Job\n
950 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
952 \t\tmax sync speed in kBytes/s, default unlimited\n
954 \t\thow much snapshots will be kept before get erased, default 1\n
956 \t\tname of the sync job, if not set it is default\n
958 \t\tif this flag is set it will skip the first sync\n
960 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
962 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
963 \tremove a sync Job from the scheduler\n
965 \t\tname of the sync job, if not set it is default\n
967 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
969 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
970 \tGet help about specified command.\n
973 \t-verbose\tboolean\n
974 \t\tVerbose output format.\n";
976 my $help_list = "$PROGNAME list\n
977 \tGet a List of all scheduled Sync Jobs\n";
979 my $help_status = "$PROGNAME status\n
980 \tGet the status of all scheduled Sync Jobs\n";
982 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
983 \tenable a syncjob and reset error\n
985 \t\tname of the sync job, if not set it is default\n
987 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
989 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
992 \t\tname of the sync job, if not set it is default\n
994 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1010 die "$help_destroy\n";
1014 die "$help_create\n";
1022 die "$help_status\n";
1026 die "$help_enable\n";
1030 die "$help_enable\n";
1037 my $param = parse_argv
(@arg);
1043 die "$help_destroy\n" if !$param->{source
};
1044 check_target
($param->{source
});
1045 destroy_job
($param);
1049 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1050 check_target
($param->{source
});
1051 check_target
($param->{dest
});
1056 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1057 check_target
($param->{source
});
1058 check_target
($param->{dest
});
1071 my $help_command = $ARGV[1];
1072 if ($help_command && $commands->{$help_command}) {
1073 print help
($help_command);
1075 if ($param->{verbose
} == 1){
1076 exec("man $PROGNAME");
1083 die "$help_enable\n" if !$param->{source
};
1084 check_target
($param->{source
});
1089 die "$help_disable\n" if !$param->{source
};
1090 check_target
($param->{source
});
1091 disable_job
($param);
1098 print("ERROR:\tno command specified\n") if !$help;
1099 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1100 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1101 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1102 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1103 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1104 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1105 print("\t$PROGNAME list\n");
1106 print("\t$PROGNAME status\n");
1107 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1112 parse_target
($target);
1119 pve-zsync - PVE ZFS Replication Manager
1123 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1125 pve-zsync help <cmd> [OPTIONS]
1127 Get help about specified command.
1135 Verbose output format.
1137 pve-zsync create -dest <string> -source <string> [OPTIONS]
1143 the destination target is like [IP]:<Pool>[/Path]
1147 max sync speed in kBytes/s, default unlimited
1151 how much snapshots will be kept before get erased, default 1
1155 name of the sync job, if not set it is default
1159 if this flag is set it will skip the first sync
1163 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1165 pve-zsync destroy -source <string> [OPTIONS]
1167 remove a sync Job from the scheduler
1171 name of the sync job, if not set it is default
1175 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1177 pve-zsync disable -source <string> [OPTIONS]
1183 name of the sync job, if not set it is default
1187 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1189 pve-zsync enable -source <string> [OPTIONS]
1191 enable a syncjob and reset error
1195 name of the sync job, if not set it is default
1199 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1202 Get a List of all scheduled Sync Jobs
1206 Get the status of all scheduled Sync Jobs
1208 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1214 the destination target is like [IP:]<Pool>[/Path]
1218 max sync speed in kBytes/s, default unlimited
1222 how much snapshots will be kept before get erased, default 1
1226 name of the sync job, if not set it is default.
1227 It is only necessary if scheduler allready contains this source.
1231 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1235 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1236 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1237 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.
1238 To config cron see man crontab.
1240 =head2 PVE ZFS Storage sync Tool
1242 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1246 add sync job from local VM to remote ZFS Server
1247 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1249 =head1 IMPORTANT FILES
1251 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1253 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1255 =head1 COPYRIGHT AND DISCLAIMER
1257 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1259 This program is free software: you can redistribute it and/or modify it
1260 under the terms of the GNU Affero General Public License as published
1261 by the Free Software Foundation, either version 3 of the License, or
1262 (at your option) any later version.
1264 This program is distributed in the hope that it will be useful, but
1265 WITHOUT ANY WARRANTY; without even the implied warranty of
1266 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1267 Affero General Public License for more details.
1269 You should have received a copy of the GNU Affero General Public
1270 License along with this program. If not, see
1271 <http://www.gnu.org/licenses/>.