]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
26b38c100fdeaf07d967261f39a6b10b3c5c6cb8
3 my $PROGNAME = "pve-zsync";
4 my $CONFIG_PATH = '/var/lib/'.$PROGNAME.'/';
5 my $CONFIG = "$PROGNAME.cfg";
6 my $CRONJOBS = '/etc/cron.d/'.$PROGNAME;
7 my $VMCONFIG = '/var/lib/'.$PROGNAME.'/';
8 my $PATH = "/usr/sbin/";
9 my $QEMU_CONF = '/etc/pve/local/qemu-server/';
14 use Data
::Dumper
qw(Dumper);
15 use Fcntl
qw(:flock SEEK_END);
19 check_bin
('cstream');
27 foreach my $p (split (/:/, $ENV{PATH
})) {
34 warn "unable to find command '$bin'\n";
38 my ($text, $max) = @_;
40 return $text if (length($text) <= $max);
41 my @spl = split('/', $text);
43 my $count = length($spl[@spl-1]);
44 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
46 $count += length($spl[0]) if @spl > 1;
47 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
50 $rest = $max-$count if ($max-$count > 0);
52 return "$spl[0]".substr($text, length($spl[0]), $rest)."..\/".$spl[@spl-1];
57 flock($fh, LOCK_EX
) or die "Cannot lock config - $!\n";
59 seek($fh, 0, SEEK_END
) or die "Cannot seek - $!\n";
64 flock($fh, LOCK_UN
) or die "Cannot unlock config- $!\n";
68 my ($source, $name, $cfg) = @_;
70 my $id = $source->{vmid
} ?
$source->{vmid
} : $source->{abs_path
};
71 my $status = $cfg->{$id}->{$name}->{status
};
73 if ($cfg->{$id}->{$name}->{status
}){
81 sub check_pool_exsits
{
85 $cmd = "ssh root\@$ip " if $ip;
86 $cmd .= "zfs list $pool -H";
100 open(my $fh, ">", "$CONFIG_PATH$CONFIG")
101 or die "cannot open >$CONFIG_PATH$CONFIG: $!\n";
103 my $text = decode_config
($cfg);
110 sub read_from_config
{
112 unless(-e
"$CONFIG_PATH$CONFIG") {
116 open(my $fh, "<", "$CONFIG_PATH$CONFIG")
117 or die "cannot open > $CONFIG_PATH$CONFIG: $!\n";
127 my $cfg = encode_config
($text);
135 foreach my $source (sort keys%{$cfg}){
136 foreach my $sync_name (sort keys%{$cfg->{$source}}){
137 $raw .= "$source: $sync_name\n";
138 foreach my $parameter (sort keys%{$cfg->{$source}->{$sync_name}}){
139 $raw .= "\t$parameter: $cfg->{$source}->{$sync_name}->{$parameter}\n";
153 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
156 next if $line =~ m/^\#/;
157 next if $line =~ m/^\s*$/;
159 if ($line =~ m/^(\t| )(\w+): (.+)/){
163 if ($par eq 'source_pool') {
164 $cfg->{$source}->{$sync_name}->{$par} = $value;
165 die "error in Config: SourcePool value doubled\n" if ($check & 1);
167 } elsif ($par eq 'source_ip') {
168 $cfg->{$source}->{$sync_name}->{$par} = $value;
169 die "error in Config: SourceIP value doubled\n" if ($check & 2);
171 } elsif ($par eq 'status') {
172 $cfg->{$source}->{$sync_name}->{$par} = $value;
173 die "error in Config: Status value doubled\n" if ($check & 4);
175 } elsif ($par eq 'method') {
176 $cfg -> {$source}->{$sync_name}->{$par} = $value;
177 die "error in Config: Method value doubled\n" if ($check & 8);
179 } elsif ($par eq 'interval') {
180 $cfg -> {$source}->{$sync_name}->{$par} = $value;
181 die "error in Config: Iterval value doubled\n" if ($check & 16);
183 } elsif ($par eq 'limit') {
184 $cfg -> {$source}->{$sync_name}->{$par} = $value;
185 die "error in Config: Limit value doubled\n" if ($check & 32);
187 } elsif ($par eq 'dest_pool') {
188 $cfg -> {$source}->{$sync_name}->{$par} = $value;
189 die "error in Config: DestPool value doubled\n" if ($check & 64);
191 } elsif ($par eq 'dest_ip') {
192 $cfg -> {$source}->{$sync_name}->{$par} = $value;
193 die "error in Config: DestIp value doubled\n" if ($check & 128);
195 } elsif ($par eq 'dest_path') {
196 $cfg -> {$source}->{$sync_name}->{$par} = $value;
197 die "error in Config: DestPath value doubled\n" if ($check & 256);
199 } elsif ($par eq 'source_path') {
200 $cfg -> {$source}->{$sync_name}->{$par} = $value;
201 die "error in Config: SourcePath value doubled\n" if ($check & 512);
203 } elsif ($par eq 'vmid') {
204 $cfg -> {$source}->{$sync_name}->{$par} = $value;
205 die "error in Config: Vmid value doubled\n" if ($check & 1024);
207 } elsif ($par =~ 'lsync') {
208 $cfg->{$source}->{$sync_name}->{$par} = $value;
209 die "error in Config: lsync value doubled\n" if ($check & 2048);
211 } elsif ($par =~ 'maxsnap') {
212 $cfg->{$source}->{$sync_name}->{$par} = $value;
213 die "error in Config: maxsnap value doubled\n" if ($check & 4096);
216 die "error in Config\n";
218 } elsif ($line =~ m/^((\d+.\d+.\d+.\d+):)?([\w\-\_\/]+): (.+){0,1}/){
220 $sync_name = $4 ?
$4 : 'default' ;
221 $cfg->{$source}->{$sync_name} = undef;
222 $cfg->{$source}->{$sync_name}->{source_ip
} = $2 if $2;
232 if ($text =~ m/^((\d+.\d+.\d+.\d+):)?((\w+)\/?
)([\w\
/\-\_]*)?$/) {
234 die "Input not valid\n" if !$3;
242 if ($tmp =~ m/^(\d\d\d+)$/){
243 $target->{vmid
} = $tmp;
245 $target->{pool
} = $4;
248 $target->{path
} = "\/$5";
251 $target->{abs_path
} = $abs_path;
256 die "Input not valid\n";
261 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
263 my $list = sprintf("%-25s%-15s%-7s%-20s%-10s%-5s\n" , "SOURCE", "NAME", "ACTIVE", "LAST SYNC", "INTERVAL", "TYPE");
265 foreach my $source (sort keys%{$cfg}){
266 foreach my $sync_name (sort keys%{$cfg->{$source}}){
267 my $source_name = $source;
268 $source_name = $cfg->{$source}->{$sync_name}->{source_ip
}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip
};
269 $list .= sprintf("%-25s%-15s", cut_to_width
($source_name,25), cut_to_width
($sync_name,15));
272 if($cfg->{$source}->{$sync_name}->{status
} eq 'syncing'){
278 $list .= sprintf("%-7s", $active);
279 $list .= sprintf("%-20s",$cfg->{$source}->{$sync_name}->{lsync
});
280 $list .= sprintf("%-10s",$cfg->{$source}->{$sync_name}->{interval
});
281 $list .= sprintf("%-5s\n",$cfg->{$source}->{$sync_name}->{method});
292 $cmd = "ssh root\@$target->{ip} " if ($target->{ip
});
293 $cmd .= "qm status $target->{vmid}";
295 my $res = run_cmd
($cmd);
297 return 1 if ($res =~ m/^status.*$/);
304 my $cfg = read_from_config
;
308 my $name = $param->{name
} ?
$param->{name
} : "default";
309 my $interval = $param->{interval
} ?
$param->{interval
} : 15;
311 my $source = parse_target
($param->{source
});
312 my $dest = parse_target
($param->{dest
});
314 $vm->{$name}->{dest_pool
} = $dest->{pool
};
315 $vm->{$name}->{dest_ip
} = $dest->{ip
} if $dest->{ip
};
316 $vm->{$name}->{dest_path
} = $dest->{path
} if $dest->{path
};
318 $param->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
319 $vm->{$name}->{status
} = "ok";
320 $vm->{$name}->{interval
} = $interval;
321 $vm->{$name}->{method} = $param->{method} ?
$param->{method} : "ssh";
322 $vm->{$name}->{limit
} = $param->{limit
} if $param->{limit
};
323 $vm->{$name}->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
325 if ( my $ip = $vm->{$name}->{dest_ip
} ) {
326 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
329 if ( my $ip = $source->{ip
} ) {
330 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
333 die "Pool $dest->{abs_path} does not exists\n" if check_pool_exsits
($dest->{abs_path
}, $dest->{ip
});
335 my $check = check_pool_exsits
($source->{abs_path
}, $source->{ip
}) if !$source->{vmid
} && $source->{abs_path
};
337 die "Pool $source->{abs_path} does not exists\n" if undef($check);
340 my ($vm, $name) = @_;
343 if ($vm->{$name}->{vmid
}) {
344 $source = "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
345 $source .= $vm->{$name}->{vmid
};
347 $source = $vm->{$name}->{source_pool
};
348 $source .= $vm->{$name}->{source_path
} if $vm->{$name}->{source_path
};
350 die "Config already exists\n" if $cfg->{$source}->{$name};
354 $cfg->{$source}->{$name} = $vm->{$name};
356 write_to_config
($cfg);
359 if ($source->{vmid
}) {
360 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
361 my $disks = get_disks
($source);
362 $vm->{$name}->{vmid
} = $source->{vmid
};
363 $vm->{$name}->{lsync
} = 0;
364 $vm->{$name}->{source_ip
} = $source->{ip
} if $source->{ip
};
366 &$add_job($vm, $name);
369 $vm->{$name}->{source_pool
} = $source->{pool
};
370 $vm->{$name}->{source_ip
} = $source->{ip
} if $source->{ip
};
371 $vm->{$name}->{source_path
} = $source->{path
} if $source->{path
};
372 $vm->{$name}->{lsync
} = 0;
374 &$add_job($vm, $name);
377 eval {sync
($param) if !$param->{skip
};};
387 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
388 my $name = $param->{name
} ?
$param->{name
} : "default";
390 my $source = parse_target
($param->{source
});
392 my $delete_configs = sub {
393 my ($path, $name, $cfg) = @_;
395 die "Source does not exist!\n" unless $cfg->{$path} ;
397 die "Sync Name does not exist!\n" unless $cfg->{$path}->{$name};
399 my $source = $cfg->{$path}->{$name}->{source_ip
} ?
"$cfg->{$path}->{$name}->{source_ip}:" : '';
401 $source .= $cfg->{$path}->{$name}->{source_pool
} if $cfg->{$path}->{$name}->{source_pool
};
402 $source .= $cfg->{$path}->{$name}->{source_path
} ?
$cfg->{$path}->{$name}->{source_path
} :'';
404 my $dest = $cfg->{$path}->{$name}->{dest_ip
} ?
$cfg->{$path}->{$name}->{dest_ip
} :"";
405 $dest .= $cfg->{$path}->{$name}->{dest_pool
} if $cfg->{$path}->{$name}->{dest_pool
};
406 $dest .= $cfg->{$path}->{$name}->{dest_path
} ?
$cfg->{$path}->{$name}->{dest_path
} :'';
408 delete $cfg->{$path}->{$name};
410 delete $cfg->{$path} if keys%{$cfg->{$path}} == 0;
412 write_to_config
($cfg);
414 cron_del
($source, $dest, $name);
418 if ($source->{vmid
}) {
419 my $path = $source->{vmid
};
421 &$delete_configs($path, $name, $cfg)
425 my $path = $source->{pool
};
426 $path .= $source->{path
} if $source->{path
};
428 &$delete_configs($path, $name, $cfg);
435 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
437 my $name = $param->{name
} ?
$param->{name
} : "default";
438 my $max_snap = $param->{maxsnap
} ?
$param->{maxsnap
} : 1;
439 my $method = $param->{method} ?
$param->{method} : "ssh";
441 my $dest = parse_target
($param->{dest
});
442 my $source = parse_target
($param->{source
});
444 my $sync_path = sub {
445 my ($source, $name, $cfg, $max_snap, $dest, $method) = @_;
447 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $max_snap, $name);
449 my $job_status = check_config
($source, $name, $cfg) if $cfg;
450 die "VM Status: $job_status syncing will not done!\n" if ($job_status && $job_status ne "ok");
453 my $conf_name = $source->{abs_path
};
454 $conf_name = $source->{vmid
} if $source->{vmid
};
455 $cfg->{$conf_name}->{$name}->{status
} = "syncing";
456 write_to_config
($cfg);
462 $date = snapshot_add
($source, $dest, $name);
464 send_image
($source, $dest, $method, $param->{verbose
}, $param->{limit
});
466 snapshot_destroy
($source, $dest, $method, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
470 my $conf_name = $source->{abs_path
};
471 $conf_name = $source->{vmid
} if $source->{vmid
};
472 $cfg->{$conf_name}->{$name}->{status
} = "error";
473 write_to_config
($cfg);
474 my $source_target = $source->{ip
} ?
$source->{ip
}.":" : '';
475 $source_target .= $source->{vmid
} ?
$source->{vmid
} : $source->{abs_path
};
476 cron_del
($source_target, $dest->{abs_path
}, $name);
482 my $conf_name = $source->{abs_path
};
483 $conf_name = $source->{vmid
} if $source->{vmid
};
484 $cfg->{$conf_name}->{$name}->{status
} = "ok";
485 $cfg->{$conf_name}->{$name}->{lsync
} = $date;
486 write_to_config
($cfg);
490 $param->{method} = "ssh" if !$param->{method};
492 if ($source->{vmid
}) {
493 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
494 my $disks = get_disks
($source);
496 foreach my $disk (sort keys %{$disks}) {
497 $source->{abs_path
} = $disks->{$disk}->{pool
};
498 $source->{abs_path
} .= "\/$disks->{$disk}->{path}" if $disks->{$disk}->{path
};
500 $source->{pool
} = $disks->{$disk}->{pool
};
501 $source->{path
} = "\/$disks->{$disk}->{path}";
503 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
505 if ($method eq "ssh") {
506 send_config
($source, $dest,'ssh');
509 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
514 my ($source, $dest, $max_snap, $name) = @_;
516 my $cmd = "zfs list -r -t snapshot -Ho name, -S creation ";
518 $cmd .= $source->{abs_path
};
519 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
521 my $raw = run_cmd
($cmd);
524 my $last_snap = undef;
526 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
528 $last_snap = $line if $index == 1;
529 if ($index == $max_snap) {
530 $source->{destroy
} = 1;
536 $line =~ m/^(.+)\@(rep_$name\_.+)(\n|$)/;
537 return ($2, $last_snap) if $2;
543 my ($source, $dest, $name) = @_;
545 my $date = get_date
();
547 my $snap_name = "rep_$name\_".$date;
549 $source->{new_snap
} = $snap_name;
551 my $path = $source->{abs_path
}."\@".$snap_name;
553 my $cmd = "zfs snapshot $path";
554 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
561 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
572 unless(-e
$CRONJOBS){
573 $text .= 'SHELL=/bin/sh'."\n";
574 $text .= 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin'."\n ";
577 open(my $fh, '>>', "$CRONJOBS")
578 or die "Could not open file: $!\n";
580 foreach my $name (keys%{$vm}){
581 $text .= "*/$vm->{$name}->{interval} * * * * root ";
582 $text .= "$PATH$PROGNAME sync";
583 $text .= " -source ";
584 if ($vm->{$name}->{vmid
}) {
585 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
586 $text .= "$vm->{$name}->{vmid} ";
588 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
589 $text .= "$vm->{$name}->{source_pool}";
590 $text .= "$vm->{$name}->{source_path}" if $vm->{$name}->{source_path
};
593 $text .= "$vm->{$name}->{dest_ip}:" if $vm->{$name}->{dest_ip
};
594 $text .= "$vm->{$name}->{dest_pool}";
595 $text .= "$vm->{$name}->{dest_path}" if $vm->{$name}->{dest_path
};
596 $text .= " -name $name ";
597 $text .= " -limit $vm->{$name}->{limit}" if $vm->{$name}->{limit
};
598 $text .= " -maxsnap $vm->{$name}->{maxsnap}" if $vm->{$name}->{maxsnap
};
606 my ($source, $dest, $name) = @_;
608 open(my $fh, '<', "$CRONJOBS")
609 or die "Could not open file: $!\n";
616 while ($text && $text =~ s/^(.*?)(\n|$)//) {
618 if ($line !~ m/^.*root $PATH$PROGNAME sync -source $source.*-dest $dest.*-name $name.*$/){
622 open($fh, '>', "$CRONJOBS")
623 or die "Could not open file: $!\n";
632 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
633 $cmd .= "qm config $target->{vmid}";
635 my $res = run_cmd
($cmd);
637 my $disks = parse_disks
($res, $target->{ip
});
644 print "Start CMD\n" if $DEBUG;
645 print Dumper
$cmd if $DEBUG;
646 my $output = `$cmd 2>&1`;
648 die $output if 0 != $?;
651 print Dumper
$output if $DEBUG;
652 print "END CMD\n" if $DEBUG;
657 my ($text, $ip) = @_;
663 $cmd .= "ssh root\@$ip " if $ip;
664 $cmd .= "pvesm zfsscan";
665 my $zfs_pools = run_cmd
($cmd);
666 while ($text && $text =~ s/^(.*?)(\n|$)//) {
670 my $is_disk = $line =~ m/^(virtio|ide|scsi|sata){1}\d+: /;
671 if($line =~ m/^(virtio\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
674 } elsif($line =~ m/^(ide\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
677 } elsif($line =~ m/^(scsi\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
680 } elsif($line =~ m/^(sata\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
685 die "disk is not on ZFS Storage\n" if $is_disk && !$disk && $line !~ m/cdrom/;
687 if($disk && $line !~ m/none/ && $line !~ m/cdrom/ ) {
689 $cmd .= "ssh root\@$ip " if $ip;
690 $cmd .= "pvesm path $stor$disk";
691 my $path = run_cmd
($cmd);
693 if ($path =~ m/^\/dev\
/zvol\/(\w
+).*(\
/$disk)$/){
695 $disks->{$num}->{pool
} = $1;
696 $disks->{$num}->{path
} = $disk;
700 die "ERROR: in path\n";
707 sub snapshot_destroy
{
708 my ($source, $dest, $method, $snap) = @_;
710 my $zfscmd = "zfs destroy ";
711 my $name = "$source->{path}\@$snap";
714 if($source->{ip
} && $method eq 'ssh'){
715 run_cmd
("ssh root\@$source->{ip} $zfscmd $source->{pool}$name");
717 run_cmd
("$zfscmd $source->{pool}$name");
724 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
727 $path ="$dest->{path}" if $dest->{path
};
729 my @dir = split(/\//, $source->{path
});
731 run_cmd
("$ssh $zfscmd $dest->{pool}$path\/$dir[@dir-1]\@$snap ");
740 my ($source ,$dest, $method) = @_;
743 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
744 $cmd .= "zfs list -rt snapshot -Ho name $dest->{pool}";
745 $cmd .= "$dest->{path}" if $dest->{path
};
746 my @dir = split(/\//, $source->{path
});
747 $cmd .= "\/$dir[@dir-1]\@$source->{old_snap}";
750 eval {$text =run_cmd
($cmd);};
756 while ($text && $text =~ s/^(.*?)(\n|$)//) {
758 return 1 if $line =~ m/^.*$source->{old_snap}$/;
763 my ($source, $dest, $method, $verbose, $limit) = @_;
767 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
769 $cmd .= "-v " if $verbose;
771 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $method)) {
772 $cmd .= "-i $source->{abs_path}\@$source->{old_snap} $source->{abs_path}\@$source->{new_snap} ";
774 $cmd .= "$source->{abs_path}\@$source->{new_snap} ";
778 my $bwl = $limit*1024;
779 $cmd .= "| cstream -t $bwl";
782 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
783 $cmd .= "zfs recv $dest->{pool}";
784 $cmd .= "$dest->{path}" if $dest->{path
};
786 my @dir = split(/\//,$source->{path
});
787 $cmd .= "\/$dir[@dir-1]\@$source->{new_snap}";
794 snapshot_destroy
($source, undef, $method, $source->{new_snap
});
801 my ($source, $dest, $method) = @_;
803 if ($method eq 'ssh'){
804 if ($dest->{ip
} && $source->{ip
}) {
805 run_cmd
("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
806 run_cmd
("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
807 } elsif ($dest->{ip
}) {
808 run_cmd
("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
809 run_cmd
("scp $QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
810 } elsif ($source->{ip
}) {
811 run_cmd
("mkdir $VMCONFIG -p");
812 run_cmd
("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf $VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
815 if ($source->{destroy
}){
817 run_cmd
("ssh root\@$dest->{ip} rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
819 run_cmd
("rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
826 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
827 my $datestamp = sprintf ( "%04d-%02d-%02d_%02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec);
833 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
835 my $status_list = sprintf("%-25s%-15s%-10s\n","SOURCE","NAME","STATUS");
837 foreach my $source (sort keys%{$cfg}){
838 foreach my $sync_name (sort keys%{$cfg->{$source}}){
841 my $source_name = $source;
843 $source_name = $cfg->{$source}->{$sync_name}->{source_ip
}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip
};
845 $status = sprintf("%-10s",$cfg->{$source}->{$sync_name}->{status
});
847 $status_list .= sprintf("%-25s%-15s", cut_to_width
($source_name,25), cut_to_width
($sync_name,15));
848 $status_list .= "$status\n";
856 my $command = $ARGV[0];
858 my $commands = {'destroy' => 1,
865 if (!$command || !$commands->{$command}) {
879 my $help_sync = "zfs-zsync sync -dest <string> -source <string> [OPTIONS]\n
880 \twill sync one time\n
882 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
884 \t\tmax sync speed in kBytes/s, default unlimited\n
885 \t-maxsnap\tinteger\n
886 \t\thow much snapshots will be kept before get erased, default 1/n
888 \t\tname of the sync job, if not set it is default.
889 \tIt is only necessary if scheduler allready contains this source.\n
891 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
893 my $help_create = "zfs-zsync create -dest <string> -source <string> [OPTIONS]/n
894 \tCreate a sync Job\n
896 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
897 \t-interval\tinteger\n
898 \t\tthe interval in min in witch the zfs will sync,
899 \t\tdefault is 15 min\n
901 \t\tmax sync speed, default unlimited\n
903 \t\thow much snapshots will be kept before get erased, default 1\n
905 \t\tname of the sync job, if not set it is default\n
907 \t\tif this flag is set it will skip the first sync\n
909 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
911 my $help_destroy = "zfs-zsync destroy -source <string> [OPTIONS]\n
912 \tremove a sync Job from the scheduler\n
914 \t\tname of the sync job, if not set it is default\n
916 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
918 my $help_help = "zfs-zsync help <cmd> [OPTIONS]\n
919 \tGet help about specified command.\n
922 \t-verbose\tboolean\n
923 \t\tVerbose output format.\n";
925 my $help_list = "zfs-zsync list\n
926 \tGet a List of all scheduled Sync Jobs\n";
928 my $help_status = "zfs-zsync status\n
929 \tGet the status of all scheduled Sync Jobs\n";
945 die "$help_destroy\n";
949 die "$help_create\n";
957 die "$help_status\n";
963 my $err = GetOptions
('dest=s' => \
$dest,
964 'source=s' => \
$source,
965 'verbose' => \
$verbose,
966 'interval=i' => \
$interval,
967 'limit=i' => \
$limit,
968 'maxsnap=i' => \
$maxsnap,
973 die "can't parse options\n";
977 $param->{dest
} = $dest;
978 $param->{source
} = $source;
979 $param->{verbose
} = $verbose;
980 $param->{interval
} = $interval;
981 $param->{limit
} = $limit;
982 $param->{maxsnap
} = $maxsnap;
983 $param->{name
} = $name;
984 $param->{skip
} = $skip;
989 die "$help_destroy\n" if !$source;
990 check_target
($source);
995 die "$help_sync\n" if !$source || !$dest;
996 check_target
($source);
1002 die "$help_create\n" if !$source || !$dest;
1003 check_target
($source);
1004 check_target
($dest);
1017 my $help_command = $ARGV[1];
1018 if ($help_command && $commands->{$help_command}) {
1019 print help
($help_command);
1022 exec("man $PROGNAME");
1032 print("ERROR:\tno command specified\n") if !$help;
1033 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1034 print("\tpve-zsync help [<cmd>] [OPTIONS]\n\n");
1035 print("\tpve-zsync create -dest <string> -source <string> [OPTIONS]\n");
1036 print("\tpve-zsync destroy -source <string> [OPTIONS]\n");
1037 print("\tpve-zsync list\n");
1038 print("\tpve-zsync status\n");
1039 print("\tpve-zsync sync -dest <string> -source <string> [OPTIONS]\n");
1047 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\
/.+)?/){
1048 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1058 pve-zsync - PVE ZFS Replication Manager
1062 zfs-zsync <COMMAND> [ARGS] [OPTIONS]
1064 zfs-zsync help <cmd> [OPTIONS]
1066 Get help about specified command.
1074 Verbose output format.
1076 zfs-zsync create -dest <string> -source <string> [OPTIONS]
1082 the destination target is like [IP]:<Pool>[/Path]
1086 the interval in min in witch the zfs will sync, default is 15 min
1090 max sync speed, default unlimited
1094 how much snapshots will be kept before get erased, default 1
1098 name of the sync job, if not set it is default
1102 if this flag is set it will skip the first sync
1106 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1108 zfs-zsync destroy -source <string> [OPTIONS]
1110 remove a sync Job from the scheduler
1114 name of the sync job, if not set it is default
1118 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1122 Get a List of all scheduled Sync Jobs
1126 Get the status of all scheduled Sync Jobs
1128 zfs-zsync sync -dest <string> -source <string> [OPTIONS]
1134 the destination target is like [IP:]<Pool>[/Path]
1138 max sync speed in kBytes/s, default unlimited
1142 how much snapshots will be kept before get erased, default 1
1146 name of the sync job, if not set it is default.
1147 It is only necessary if scheduler allready contains this source.
1151 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1155 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1156 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1158 =head2 PVE ZFS Storage sync Tool
1160 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1164 add sync job from local VM to remote ZFS Server
1165 zfs-zsync -source=100 -dest=192.168.1.2:zfspool
1167 =head1 IMPORTANT FILES
1169 Where the cron jobs are stored /etc/cron.d/pve-zsync
1170 Where the VM config get copied on the destination machine /var/pve-zsync
1171 Where the config is stored /var/pve-zsync
1173 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1175 This program is free software: you can redistribute it and/or modify it
1176 under the terms of the GNU Affero General Public License as published
1177 by the Free Software Foundation, either version 3 of the License, or
1178 (at your option) any later version.
1180 This program is distributed in the hope that it will be useful, but
1181 WITHOUT ANY WARRANTY; without even the implied warranty of
1182 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1183 Affero General Public License for more details.
1185 You should have received a copy of the GNU Affero General Public
1186 License along with this program. If not, see
1187 <http://www.gnu.org/licenses/>.