]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
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 if ($source->{vmid
} && $cfg->{$source->{vmid
}}->{$name}->{locked
}){
71 return "active" if $cfg->{$source->{vmid
}}->{$name}->{locked
} eq 'yes';
72 return "exist" if $cfg->{$source->{vmid
}}->{$name}->{locked
} eq 'no';
73 } elsif ($cfg->{$source->{abs_path
}}->{$name}->{locked
}) {
74 return "active" if $cfg->{$source->{abs_path
}}->{$name}->{locked
} eq 'yes';
75 return "exist" if $cfg->{$source->{abs_path
}}->{$name}->{locked
} eq 'no';
82 sub check_pool_exsits
{
86 $cmd = "ssh root\@$ip " if $ip;
87 $cmd .= "zfs list $pool -H";
101 open(my $fh, ">", "$CONFIG_PATH$CONFIG")
102 or die "cannot open >$CONFIG_PATH$CONFIG: $!\n";
104 my $text = decode_config
($cfg);
111 sub read_from_config
{
113 unless(-e
"$CONFIG_PATH$CONFIG") {
117 open(my $fh, "<", "$CONFIG_PATH$CONFIG")
118 or die "cannot open > $CONFIG_PATH$CONFIG: $!\n";
128 my $cfg = encode_config
($text);
136 foreach my $source (sort keys%{$cfg}){
137 foreach my $sync_name (sort keys%{$cfg->{$source}}){
138 $raw .= "$source: $sync_name\n";
139 foreach my $parameter (sort keys%{$cfg->{$source}->{$sync_name}}){
140 $raw .= "\t$parameter: $cfg->{$source}->{$sync_name}->{$parameter}\n";
154 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
157 next if $line =~ m/^\#/;
158 next if $line =~ m/^\s*$/;
160 if ($line =~ m/^(\t| )(\w+): (.+)/){
164 if ($par eq 'source_pool') {
165 $cfg->{$source}->{$sync_name}->{$par} = $value;
166 die "error in Config: SourcePool value doubled\n" if ($check & 1);
168 } elsif ($par eq 'source_ip') {
169 $cfg->{$source}->{$sync_name}->{$par} = $value;
170 die "error in Config: SourceIP value doubled\n" if ($check & 2);
172 } elsif ($par eq 'locked') {
173 $cfg->{$source}->{$sync_name}->{$par} = $value;
174 die "error in Config: Locked value doubled\n" if ($check & 4);
176 } elsif ($par eq 'method') {
177 $cfg -> {$source}->{$sync_name}->{$par} = $value;
178 die "error in Config: Method value doubled\n" if ($check & 8);
180 } elsif ($par eq 'interval') {
181 $cfg -> {$source}->{$sync_name}->{$par} = $value;
182 die "error in Config: Iterval value doubled\n" if ($check & 16);
184 } elsif ($par eq 'limit') {
185 $cfg -> {$source}->{$sync_name}->{$par} = $value;
186 die "error in Config: Limit value doubled\n" if ($check & 32);
188 } elsif ($par eq 'dest_pool') {
189 $cfg -> {$source}->{$sync_name}->{$par} = $value;
190 die "error in Config: DestPool value doubled\n" if ($check & 64);
192 } elsif ($par eq 'dest_ip') {
193 $cfg -> {$source}->{$sync_name}->{$par} = $value;
194 die "error in Config: DestIp value doubled\n" if ($check & 128);
196 } elsif ($par eq 'dest_path') {
197 $cfg -> {$source}->{$sync_name}->{$par} = $value;
198 die "error in Config: DestPath value doubled\n" if ($check & 256);
200 } elsif ($par eq 'source_path') {
201 $cfg -> {$source}->{$sync_name}->{$par} = $value;
202 die "error in Config: SourcePath value doubled\n" if ($check & 512);
204 } elsif ($par eq 'vmid') {
205 $cfg -> {$source}->{$sync_name}->{$par} = $value;
206 die "error in Config: Vmid value doubled\n" if ($check & 1024);
208 } elsif ($par =~ 'lsync') {
209 $cfg->{$source}->{$sync_name}->{$par} = $value;
210 die "error in Config: lsync value doubled\n" if ($check & 2048);
212 } elsif ($par =~ 'maxsnap') {
213 $cfg->{$source}->{$sync_name}->{$par} = $value;
214 die "error in Config: maxsnap value doubled\n" if ($check & 4096);
217 die "error in Config\n";
219 } elsif ($line =~ m/^((\d+.\d+.\d+.\d+):)?([\w\-\_\/]+): (.+){0,1}/){
221 $sync_name = $4 ?
$4 : 'default' ;
222 $cfg->{$source}->{$sync_name} = undef;
223 $cfg->{$source}->{$sync_name}->{source_ip
} = $2 if $2;
233 if ($text =~ m/^((\d+.\d+.\d+.\d+):)?((\w+)\/?
)([\w\
/\-\_]*)?$/) {
235 die "Input not valid\n" if !$3;
243 if ($tmp =~ m/^(\d\d\d+)$/){
244 $target->{vmid
} = $tmp;
246 $target->{pool
} = $4;
249 $target->{path
} = "\/$5";
252 $target->{abs_path
} = $abs_path;
257 die "Input not valid\n";
262 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
264 my $list = sprintf("%-25s%-15s%-7s%-20s%-10s%-5s\n" , "SOURCE", "NAME", "ACTIVE", "LAST SYNC", "INTERVAL", "TYPE");
266 foreach my $source (sort keys%{$cfg}){
267 foreach my $sync_name (sort keys%{$cfg->{$source}}){
268 my $source_name = $source;
269 $source_name = $cfg->{$source}->{$sync_name}->{source_ip
}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip
};
270 $list .= sprintf("%-25s%-15s", cut_to_width
($source_name,25), cut_to_width
($sync_name,15));
271 $list .= sprintf("%-7s",$cfg->{$source}->{$sync_name}->{locked
});
272 $list .= sprintf("%-20s",$cfg->{$source}->{$sync_name}->{lsync
});
273 $list .= sprintf("%-10s",$cfg->{$source}->{$sync_name}->{interval
});
274 $list .= sprintf("%-5s\n",$cfg->{$source}->{$sync_name}->{method});
285 $cmd = "ssh root\@$target->{ip} " if ($target->{ip
});
286 $cmd .= "qm status $target->{vmid}";
288 my $res = run_cmd
($cmd);
290 return 1 if ($res =~ m/^status.*$/);
297 my $cfg = read_from_config
;
301 my $name = $param->{name
} ?
$param->{name
} : "default";
302 my $interval = $param->{interval
} ?
$param->{interval
} : 15;
304 my $source = parse_target
($param->{source
});
305 my $dest = parse_target
($param->{dest
});
307 $vm->{$name}->{dest_pool
} = $dest->{pool
};
308 $vm->{$name}->{dest_ip
} = $dest->{ip
} if $dest->{ip
};
309 $vm->{$name}->{dest_path
} = $dest->{path
} if $dest->{path
};
311 $param->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
312 $vm->{$name}->{locked
} = "no";
313 $vm->{$name}->{interval
} = $interval;
314 $vm->{$name}->{method} = $param->{method} ?
$param->{method} : "ssh";
315 $vm->{$name}->{limit
} = $param->{limit
} if $param->{limit
};
316 $vm->{$name}->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
318 if ( my $ip = $vm->{$name}->{dest_ip
} ) {
319 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
322 if ( my $ip = $source->{ip
} ) {
323 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
326 die "Pool $dest->{abs_path} does not exists\n" if check_pool_exsits
($dest->{abs_path
}, $dest->{ip
});
328 my $check = check_pool_exsits
($source->{abs_path
}, $source->{ip
}) if !$source->{vmid
} && $source->{abs_path
};
330 die "Pool $source->{abs_path} does not exists\n" if undef($check);
333 my ($vm, $name) = @_;
336 if ($vm->{$name}->{vmid
}) {
337 $source = "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
338 $source .= $vm->{$name}->{vmid
};
340 $source = $vm->{$name}->{source_pool
};
341 $source .= $vm->{$name}->{source_path
} if $vm->{$name}->{source_path
};
343 die "Config already exists\n" if $cfg->{$source}->{$name};
347 $cfg->{$source}->{$name} = $vm->{$name};
349 write_to_config
($cfg);
352 if ($source->{vmid
}) {
353 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
354 my $disks = get_disks
($source);
355 $vm->{$name}->{vmid
} = $source->{vmid
};
356 $vm->{$name}->{lsync
} = 0;
357 $vm->{$name}->{source_ip
} = $source->{ip
} if $source->{ip
};
359 &$add_job($vm, $name);
362 $vm->{$name}->{source_pool
} = $source->{pool
};
363 $vm->{$name}->{source_ip
} = $source->{ip
} if $source->{ip
};
364 $vm->{$name}->{source_path
} = $source->{path
} if $source->{path
};
365 $vm->{$name}->{lsync
} = 0;
367 &$add_job($vm, $name);
370 eval {sync
($param) if !$param->{skip
};};
380 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
381 my $name = $param->{name
} ?
$param->{name
} : "default";
383 my $source = parse_target
($param->{source
});
385 my $delete_cron = sub {
386 my ($path, $name, $cfg) = @_;
388 die "Source does not exist!\n" unless $cfg->{$path} ;
390 die "Sync Name does not exist!\n" unless $cfg->{$path}->{$name};
392 my $source .= $cfg->{$path}->{$name}->{source_ip
} ?
"$cfg->{$path}->{$name}->{source_ip}:" : '';
394 $source .= $cfg->{$path}->{$name}->{source_pool
};
395 $source .= $cfg->{$path}->{$name}->{source_path
} ?
$cfg->{$path}->{$name}->{source_path
} :'';
397 my $dest = $cfg->{$path}->{$name}->{dest_ip
} ?
$cfg->{$path}->{$name}->{dest_ip
} :"";
398 $dest .= $cfg->{$path}->{$name}->{dest_pool
};
399 $dest .= $cfg->{$path}->{$name}->{dest_path
} ?
$cfg->{$path}->{$name}->{dest_path
} :'';
401 delete $cfg->{$path}->{$name};
403 delete $cfg->{$path} if keys%{$cfg->{$path}} == 0;
405 write_to_config
($cfg);
407 cron_del
($source, $dest, $name);
411 if ($source->{vmid
}) {
412 my $path = $source->{vmid
};
414 &$delete_cron($path, $name, $cfg)
418 my $path = $source->{pool
};
419 $path .= $source->{path
} if $source->{path
};
421 &$delete_cron($path, $name, $cfg);
428 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
430 my $name = $param->{name
} ?
$param->{name
} : "default";
431 my $max_snap = $param->{maxsnap
} ?
$param->{maxsnap
} : 1;
432 my $method = $param->{method} ?
$param->{method} : "ssh";
434 my $dest = parse_target
($param->{dest
});
435 my $source = parse_target
($param->{source
});
437 my $sync_path = sub {
438 my ($source, $name, $cfg, $max_snap, $dest, $method) = @_;
440 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $max_snap, $name);
442 my $job_status = check_config
($source, $name, $cfg) if $cfg;
443 die "VM syncing at the moment!\n" if ($job_status && $job_status eq "active");
445 if ($job_status && $job_status eq "exist") {
446 my $conf_name = $source->{abs_path
};
447 $conf_name = $source->{vmid
} if $source->{vmid
};
448 $cfg->{$conf_name}->{$name}->{locked
} = "yes";
449 write_to_config
($cfg);
452 my $date = snapshot_add
($source, $dest, $name);
454 send_image
($source, $dest, $method, $param->{verbose
}, $param->{limit
});
456 snapshot_destroy
($source, $dest, $method, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
459 if ($job_status && $job_status eq "exist") {
460 my $conf_name = $source->{abs_path
};
461 $conf_name = $source->{vmid
} if $source->{vmid
};
462 $cfg->{$conf_name}->{$name}->{locked
} = "no";
463 $cfg->{$conf_name}->{$name}->{lsync
} = $date;
464 write_to_config
($cfg);
468 $param->{method} = "ssh" if !$param->{method};
470 if ($source->{vmid
}) {
471 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
472 my $disks = get_disks
($source);
474 foreach my $disk (sort keys %{$disks}) {
475 $source->{abs_path
} = $disks->{$disk}->{pool
};
476 $source->{abs_path
} .= "\/$disks->{$disk}->{path}" if $disks->{$disk}->{path
};
478 $source->{pool
} = $disks->{$disk}->{pool
};
479 $source->{path
} = "\/$disks->{$disk}->{path}";
481 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
484 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
489 my ($source, $dest, $max_snap, $name) = @_;
491 my $cmd = "zfs list -r -t snapshot -Ho name, -S creation ";
493 $cmd .= $source->{abs_path
};
494 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
496 my $raw = run_cmd
($cmd);
499 my $last_snap = undef;
501 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
503 $last_snap = $line if $index == 1;
504 if ($index == $max_snap) {
505 $source->{destroy
} = 1;
511 $line =~ m/^(.+)\@(rep_$name\_.+)(\n|$)/;
512 return ($2, $last_snap) if $2;
518 my ($source, $dest, $name) = @_;
520 my $date = get_date
();
522 my $snap_name = "rep_$name\_".$date;
524 $source->{new_snap
} = $snap_name;
526 my $path = $source->{abs_path
}."\@".$snap_name;
528 my $cmd = "zfs snapshot $path";
529 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
536 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
545 open(my $fh, '>>', "$CRONJOBS")
546 or die "Could not open file: $!\n";
548 foreach my $name (keys%{$vm}){
549 my $text = "*/$vm->{$name}->{interval} * * * * root ";
550 $text .= "$PATH$PROGNAME sync";
551 $text .= " -source ";
552 if ($vm->{$name}->{vmid
}) {
553 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
554 $text .= "$vm->{$name}->{vmid} ";
556 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip
};
557 $text .= "$vm->{$name}->{source_pool}";
558 $text .= "$vm->{$name}->{source_path}" if $vm->{$name}->{source_path
};
561 $text .= "$vm->{$name}->{dest_ip}:" if $vm->{$name}->{dest_ip
};
562 $text .= "$vm->{$name}->{dest_pool}";
563 $text .= "$vm->{$name}->{dest_path}" if $vm->{$name}->{dest_path
};
564 $text .= " -name $name ";
565 $text .= " -limit $vm->{$name}->{limit}" if $vm->{$name}->{limit
};
566 $text .= " -maxsnap $vm->{$name}->{maxsnap}" if $vm->{$name}->{maxsnap
};
574 my ($source, $dest, $name) = @_;
576 open(my $fh, '<', "$CRONJOBS")
577 or die "Could not open file: $!\n";
584 while ($text && $text =~ s/^(.*?)(\n|$)//) {
586 if ($line !~ m/^.*root $PATH$PROGNAME sync -source $source.*-dest $dest.*-name $name.*$/){
590 open($fh, '>', "$CRONJOBS")
591 or die "Could not open file: $!\n";
600 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
601 $cmd .= "qm config $target->{vmid}";
603 my $res = run_cmd
($cmd);
605 my $disks = parse_disks
($res, $target->{ip
});
612 print "Start CMD\n" if $DEBUG;
613 print Dumper
$cmd if $DEBUG;
614 my $output = `$cmd 2>&1`;
616 die $output if 0 != $?;
619 print Dumper
$output if $DEBUG;
620 print "END CMD\n" if $DEBUG;
625 my ($text, $ip) = @_;
631 $cmd .= "ssh root\@$ip " if $ip;
632 $cmd .= "pvesm zfsscan";
633 my $zfs_pools = run_cmd
($cmd);
634 while ($text && $text =~ s/^(.*?)(\n|$)//) {
638 my $is_disk = $line =~ m/^(virtio|ide|scsi|sata){1}\d+: /;
639 if($line =~ m/^(virtio\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
642 } elsif($line =~ m/^(ide\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
645 } elsif($line =~ m/^(scsi\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
648 } elsif($line =~ m/^(sata\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
653 die "disk is not on ZFS Storage\n" if $is_disk && !$disk && $line !~ m/cdrom/;
655 if($disk && $line !~ m/none/ && $line !~ m/cdrom/ ) {
657 $cmd .= "ssh root\@$ip " if $ip;
658 $cmd .= "pvesm path $stor$disk";
659 my $path = run_cmd
($cmd);
661 if ($path =~ m/^\/dev\
/zvol\/(\w
+).*(\
/$disk)$/){
663 $disks->{$num}->{pool
} = $1;
664 $disks->{$num}->{path
} = $disk;
668 die "ERROR: in path\n";
675 sub snapshot_destroy
{
676 my ($source, $dest, $method, $snap) = @_;
678 my $zfscmd = "zfs destroy ";
679 my $name = "$source->{path}\@$snap";
682 if($source->{ip
} && $method eq 'ssh'){
683 run_cmd
("ssh root\@$source->{ip} $zfscmd $source->{pool}$name");
685 run_cmd
("$zfscmd $source->{pool}$name");
692 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
695 $path ="$dest->{path}" if $dest->{path
};
697 my @dir = split(/\//, $source->{path
});
699 run_cmd
("$ssh $zfscmd $dest->{pool}$path\/$dir[@dir-1]\@$snap ");
708 my ($source ,$dest, $method) = @_;
711 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
712 $cmd .= "zfs list -rt snapshot -Ho name $dest->{pool}";
713 $cmd .= "$dest->{path}" if $dest->{path
};
714 my @dir = split(/\//, $source->{path
});
715 $cmd .= "\/$dir[@dir-1]\@$source->{old_snap}";
718 eval {$text =run_cmd
($cmd);};
724 while ($text && $text =~ s/^(.*?)(\n|$)//) {
726 return 1 if $line =~ m/^.*$source->{old_snap}$/;
731 my ($source, $dest, $method, $verbose, $limit) = @_;
735 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
737 $cmd .= "-v " if $verbose;
739 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $method)) {
740 $cmd .= "-i $source->{abs_path}\@$source->{old_snap} $source->{abs_path}\@$source->{new_snap} ";
742 $cmd .= "$source->{abs_path}\@$source->{new_snap} ";
746 my $bwl = $limit*1024;
747 $cmd .= "| cstream -t $bwl";
750 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
751 $cmd .= "zfs recv $dest->{pool}";
752 $cmd .= "$dest->{path}" if $dest->{path
};
754 my @dir = split(/\//,$source->{path
});
755 $cmd .= "\/$dir[@dir-1]\@$source->{new_snap}";
762 snapshot_destroy
($source, undef, $method, $source->{new_snap
});
766 if ($source->{vmid
}) {
767 if ($method eq "ssh") {
768 send_config
($source, $dest,'ssh');
775 my ($source, $dest, $method) = @_;
777 if ($method eq 'ssh'){
778 if ($dest->{ip
} && $source->{ip
}) {
779 run_cmd
("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
780 run_cmd
("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
781 } elsif ($dest->{ip
}) {
782 run_cmd
("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
783 run_cmd
("scp $QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
784 } elsif ($source->{ip
}) {
785 run_cmd
("mkdir $VMCONFIG -p");
786 run_cmd
("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf $VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
789 if ($source->{destroy
}){
791 run_cmd
("ssh root\@$dest->{ip} rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
793 run_cmd
("rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
800 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
801 my $datestamp = sprintf ( "%04d-%02d-%02d_%02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec);
807 my $cfg = read_from_config
("$CONFIG_PATH$CONFIG");
809 my $status_list = sprintf("%-25s%-15s%-10s\n","SOURCE","NAME","STATUS");
811 foreach my $source (sort keys%{$cfg}){
812 foreach my $sync_name (sort keys%{$cfg->{$source}}){
815 my $source_name = $source;
817 $source_name = $cfg->{$source}->{$sync_name}->{source_ip
}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip
};
819 if ($cfg->{$source}->{$sync_name}->{locked
} eq 'no'){
820 $status = sprintf("%-10s","OK");
821 } elsif ($cfg->{$source}->{$sync_name}->{locked
} eq 'yes' &&
822 $cfg->{$source}->{$sync_name}->{failure
}) {
823 $status = sprintf("%-10s","sync error");
825 $status = sprintf("%-10s","syncing");
828 $status_list .= sprintf("%-25s%-15s", cut_to_width
($source_name,25), cut_to_width
($sync_name,15));
829 $status_list .= "$status\n";
837 my $command = $ARGV[0];
839 my $commands = {'destroy' => 1,
846 if (!$command || !$commands->{$command}) {
860 my $help_sync = "zfs-zsync sync -dest <string> -source <string> [OPTIONS]\n
861 \twill sync one time\n
863 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
865 \t\tmax sync speed in kBytes/s, default unlimited\n
866 \t-maxsnap\tinteger\n
867 \t\thow much snapshots will be kept before get erased, default 1/n
869 \t\tname of the sync job, if not set it is default.
870 \tIt is only necessary if scheduler allready contains this source.\n
872 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
874 my $help_create = "zfs-zsync create -dest <string> -source <string> [OPTIONS]/n
875 \tCreate a sync Job\n
877 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
878 \t-interval\tinteger\n
879 \t\tthe interval in min in witch the zfs will sync,
880 \t\tdefault is 15 min\n
882 \t\tmax sync speed, default unlimited\n
884 \t\thow much snapshots will be kept before get erased, default 1\n
886 \t\tname of the sync job, if not set it is default\n
888 \t\tif this flag is set it will skip the first sync\n
890 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
892 my $help_destroy = "zfs-zsync destroy -source <string> [OPTIONS]\n
893 \tremove a sync Job from the scheduler\n
895 \t\tname of the sync job, if not set it is default\n
897 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
899 my $help_help = "zfs-zsync help <cmd> [OPTIONS]\n
900 \tGet help about specified command.\n
903 \t-verbose\tboolean\n
904 \t\tVerbose output format.\n";
906 my $help_list = "zfs-zsync list\n
907 \tGet a List of all scheduled Sync Jobs\n";
909 my $help_status = "zfs-zsync status\n
910 \tGet the status of all scheduled Sync Jobs\n";
926 die "$help_destroy\n";
930 die "$help_create\n";
938 die "$help_status\n";
944 my $err = GetOptions
('dest=s' => \
$dest,
945 'source=s' => \
$source,
946 'verbose' => \
$verbose,
947 'interval=i' => \
$interval,
948 'limit=i' => \
$limit,
949 'maxsnap=i' => \
$maxsnap,
954 die "can't parse options\n";
958 $param->{dest
} = $dest;
959 $param->{source
} = $source;
960 $param->{verbose
} = $verbose;
961 $param->{interval
} = $interval;
962 $param->{limit
} = $limit;
963 $param->{maxsnap
} = $maxsnap;
964 $param->{name
} = $name;
965 $param->{skip
} = $skip;
970 die "$help_destroy\n" if !$source;
971 check_target
($source);
976 die "$help_sync\n" if !$source || !$dest;
977 check_target
($source);
983 die "$help_create\n" if !$source || !$dest;
984 check_target
($source);
998 my $help_command = $ARGV[1];
999 if ($help_command && $commands->{$help_command}) {
1000 print help
($help_command);
1003 exec("man $PROGNAME");
1013 print("ERROR:\tno command specified\n") if !$help;
1014 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1015 print("\tpve-zsync help [<cmd>] [OPTIONS]\n\n");
1016 print("\tpve-zsync create -dest <string> -source <string> [OPTIONS]\n");
1017 print("\tpve-zsync destroy -source <string> [OPTIONS]\n");
1018 print("\tpve-zsync list\n");
1019 print("\tpve-zsync status\n");
1020 print("\tpve-zsync sync -dest <string> -source <string> [OPTIONS]\n");
1028 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\
/.+)?/){
1029 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1039 pve-zsync - PVE ZFS Replication Manager
1043 zfs-zsync <COMMAND> [ARGS] [OPTIONS]
1045 zfs-zsync help <cmd> [OPTIONS]
1047 Get help about specified command.
1055 Verbose output format.
1057 zfs-zsync create -dest <string> -source <string> [OPTIONS]
1063 the destination target is like [IP]:<Pool>[/Path]
1067 the interval in min in witch the zfs will sync, default is 15 min
1071 max sync speed, default unlimited
1075 how much snapshots will be kept before get erased, default 1
1079 name of the sync job, if not set it is default
1083 if this flag is set it will skip the first sync
1087 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1089 zfs-zsync destroy -source <string> [OPTIONS]
1091 remove a sync Job from the scheduler
1095 name of the sync job, if not set it is default
1099 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1103 Get a List of all scheduled Sync Jobs
1107 Get the status of all scheduled Sync Jobs
1109 zfs-zsync sync -dest <string> -source <string> [OPTIONS]
1115 the destination target is like [IP:]<Pool>[/Path]
1119 max sync speed in kBytes/s, default unlimited
1123 how much snapshots will be kept before get erased, default 1
1127 name of the sync job, if not set it is default.
1128 It is only necessary if scheduler allready contains this source.
1132 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1136 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1137 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1139 =head2 PVE ZFS Storage sync Tool
1141 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1145 add sync job from local VM to remote ZFS Server
1146 zfs-zsync -source=100 -dest=192.168.1.2:zfspool
1148 =head1 IMPORTANT FILES
1150 Where the cron jobs are stored /etc/cron.d/pve-zsync
1151 Where the VM config get copied on the destination machine /var/pve-zsync
1152 Where the config is stored /var/pve-zsync
1154 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1156 This program is free software: you can redistribute it and/or modify it
1157 under the terms of the GNU Affero General Public License as published
1158 by the Free Software Foundation, either version 3 of the License, or
1159 (at your option) any later version.
1161 This program is distributed in the hope that it will be useful, but
1162 WITHOUT ANY WARRANTY; without even the implied warranty of
1163 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1164 Affero General Public License for more details.
1166 You should have received a copy of the GNU Affero General Public
1167 License along with this program. If not, see
1168 <http://www.gnu.org/licenses/>.