]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
33962a6f7f20e160567d2e78e94144c0e762f7ac
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);
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 $QEMU_CONF = "/etc/pve/local/qemu-server/";
20 my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
21 my $PROG_PATH = "$PATH${PROGNAME}";
25 check_bin
('cstream');
33 foreach my $p (split (/:/, $ENV{PATH
})) {
40 die "unable to find command '$bin'\n";
43 sub cut_target_width
{
44 my ($target, $max) = @_;
46 return $target if (length($target) <= $max);
47 my @spl = split('/', $target);
49 my $count = length($spl[@spl-1]);
50 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
52 $count += length($spl[0]) if @spl > 1;
53 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
56 $rest = $max-$count if ($max-$count > 0);
58 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
63 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
68 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
72 my ($source, $name, $status) = @_;
74 if ($status->{$source->{all
}}->{$name}->{status
}) {
81 sub check_pool_exists
{
85 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
86 $cmd .= "zfs list $target->{all} -H";
100 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
103 $text =~ m/^((\d+.\d+.\d+.\d+):)?(.*)$/;
110 my @parts = split('/', $3);
112 die "$text $errstr\n" if !($target->{pool
} = shift(@parts));
113 die "$text $errstr\n" if $target->{pool
} =~ /^(\d+.\d+.\d+.\d+)$/;
115 if ($target->{pool
} =~ m/^\d+$/) {
116 $target->{vmid
} = $target->{pool
};
117 delete $target->{pool
};
120 return $target if (@parts == 0);
121 $target->{last_part
} = pop(@parts);
127 $target->{path
} = join('/', @parts);
135 #This is for the first use to init file;
137 my $new_fh = IO
::File-
>new("> $CRONJOBS");
138 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
143 my $fh = IO
::File-
>new("< $CRONJOBS");
144 die "Could not open file $CRONJOBS: $!\n" if !$fh;
150 return encode_cron
(@text);
157 $param->{dest
} = undef;
158 $param->{source
} = undef;
159 $param->{verbose
} = undef;
160 $param->{limit
} = undef;
161 $param->{maxsnap
} = undef;
162 $param->{name
} = undef;
163 $param->{skip
} = undef;
164 $param->{method} = undef;
166 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
167 'dest=s' => \
$param->{dest
},
168 'source=s' => \
$param->{source
},
169 'verbose' => \
$param->{verbose
},
170 'limit=i' => \
$param->{limit
},
171 'maxsnap=i' => \
$param->{maxsnap
},
172 'name=s' => \
$param->{name
},
173 'skip' => \
$param->{skip
},
174 'method=s' => \
$param->{method});
177 die "can't parse options\n";
180 $param->{name
} = "default" if !$param->{name
};
181 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
182 $param->{method} = "ssh" if !$param->{method};
187 sub add_state_to_job
{
190 my $states = read_state
();
191 my $state = $states->{$job->{source
}}->{$job->{name
}};
193 $job->{state} = $state->{state};
194 $job->{lsync
} = $state->{lsync
};
196 for (my $i = 0; $state->{"snap$i"}; $i++) {
197 $job->{"snap$i"} = $state->{"snap$i"};
208 while (my $line = shift(@text)) {
210 my @arg = split('\s', $line);
211 my $param = parse_argv
(@arg);
213 if ($param->{source
} && $param->{dest
}) {
214 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
215 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
216 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
217 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
218 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
219 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
231 my $source = parse_target
($param->{source
});
232 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
234 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
235 $job->{dest
} = $param->{dest
} if $param->{dest
};
236 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
237 $job->{method} = "ssh" if !$job->{method};
238 $job->{limit
} = $param->{limit
};
239 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
240 $job->{source
} = $param->{source
};
248 make_path
$CONFIG_PATH;
249 my $new_fh = IO
::File-
>new("> $STATE");
250 die "Could not create $STATE: $!\n" if !$new_fh;
256 my $fh = IO
::File-
>new("< $STATE");
257 die "Could not open file $STATE: $!\n" if !$fh;
260 my $states = decode_json
($text);
274 $in_fh = IO
::File-
>new("< $STATE");
275 die "Could not open file $STATE: $!\n" if !$in_fh;
280 my $out_fh = IO
::File-
>new("> $STATE.new");
281 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
286 $states = decode_json
($text);
287 $state = $states->{$job->{source
}}->{$job->{name
}};
290 if ($job->{state} ne "del") {
291 $state->{state} = $job->{state};
292 $state->{lsync
} = $job->{lsync
};
294 for (my $i = 0; $job->{"snap$i"} ; $i++) {
295 $state->{"snap$i"} = $job->{"snap$i"};
297 $states->{$job->{source
}}->{$job->{name
}} = $state;
300 delete $states->{$job->{source
}}->{$job->{name
}};
301 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
304 $text = encode_json
($states);
308 move
("$STATE.new", $STATE);
323 my $header = "SHELL=/bin/sh\n";
324 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
326 my $fh = IO
::File-
>new("< $CRONJOBS");
327 die "Could not open file $CRONJOBS: $!\n" if !$fh;
332 while (my $line = shift(@test)) {
334 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
336 next if $job->{state} eq "del";
337 $text .= format_job
($job, $line);
339 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
348 $text = "$header$text";
352 $text .= format_job
($job);
354 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
355 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
357 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
360 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
365 my ($job, $line) = @_;
368 if ($job->{state} eq "stopped") {
372 $line =~ /^#*(.+) root/;
375 $text .= "*/$INTERVAL * * * *";
378 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
379 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
380 $text .= " --method $job->{method}";
381 $text .= " --verbose" if $job->{verbose
};
389 my $cfg = read_cron
();
391 my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE");
393 my $states = read_state
();
394 foreach my $source (sort keys%{$cfg}) {
395 foreach my $name (sort keys%{$cfg->{$source}}) {
396 $list .= sprintf("%-25s", cut_target_width
($source, 25));
397 $list .= sprintf("%-15s", cut_target_width
($name, 15));
398 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
399 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync
});
400 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
411 $cmd = "ssh root\@$target->{ip} " if ($target->{ip
});
412 $cmd .= "qm status $target->{vmid}";
414 my $res = run_cmd
($cmd);
416 return 1 if ($res =~ m/^status.*$/);
423 my $cfg = read_cron
();
425 my $job = param_to_job
($param);
427 $job->{state} = "ok";
430 my $source = parse_target
($param->{source
});
431 my $dest = parse_target
($param->{dest
});
433 if (my $ip = $dest->{ip
}) {
434 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
437 if (my $ip = $source->{ip
}) {
438 run_cmd
("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip");
441 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
443 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
445 die "Pool $source->{path} does not exists\n" if undef($check);
447 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !vm_exists
($source);
449 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
455 sync
($param) if !$param->{skip
};
466 my $cfg = read_cron
();
468 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
469 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
471 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
472 $job->{name
} = $param->{name
};
473 $job->{source
} = $param->{source
};
474 $job = add_state_to_job
($job);
482 my $job = get_job
($param);
483 $job->{state} = "del";
492 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
493 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
496 my $date = get_date
();
499 $job = get_job
($param);
502 if ($job && $job->{state} eq "syncing") {
503 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
506 my $dest = parse_target
($param->{dest
});
507 my $source = parse_target
($param->{source
});
509 my $sync_path = sub {
510 my ($source, $dest, $job, $param, $date) = @_;
512 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
514 snapshot_add
($source, $dest, $param->{name
}, $date);
516 send_image
($source, $dest, $param);
518 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
523 $job->{state} = "syncing";
528 if ($source->{vmid
}) {
529 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
530 my $disks = get_disks
($source);
532 foreach my $disk (sort keys %{$disks}) {
533 $source->{all
} = $disks->{$disk}->{all
};
534 $source->{pool
} = $disks->{$disk}->{pool
};
535 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
536 $source->{last_part
} = $disks->{$disk}->{last_part
};
537 &$sync_path($source, $dest, $job, $param, $date);
539 if ($param->{method} eq "ssh") {
540 send_config
($source, $dest,'ssh');
543 &$sync_path($source, $dest, $job, $param, $date);
548 $job->{state} = "error";
552 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
558 $job->{state} = "ok";
559 $job->{lsync
} = $date;
568 my ($source, $dest, $max_snap, $name) = @_;
570 my $cmd = "zfs list -r -t snapshot -Ho name, -S creation ";
572 $cmd .= $source->{all
};
573 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip
};
575 my $raw = run_cmd
($cmd);
578 my $last_snap = undef;
581 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
583 if ($line =~ m/(rep_$name.*)$/) {
584 $last_snap = $1 if (!$last_snap);
587 if ($index == $max_snap) {
588 $source->{destroy
} = 1;
594 return ($old_snap, $last_snap) if $last_snap;
600 my ($source, $dest, $name, $date) = @_;
602 my $snap_name = "rep_$name\_".$date;
604 $source->{new_snap
} = $snap_name;
606 my $path = "$source->{all}\@$snap_name";
608 my $cmd = "zfs snapshot $path";
609 $cmd = "ssh root\@$source->{ip} $cmd" if $source->{ip
};
616 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
624 my $text = "SHELL=/bin/sh\n";
625 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
627 my $fh = IO
::File-
>new("> $CRONJOBS");
628 die "Could not open file: $!\n" if !$fh;
630 foreach my $source (sort keys%{$cfg}) {
631 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
632 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
633 $text .= "$PROG_PATH sync";
634 $text .= " -source ";
635 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
636 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
637 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
639 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
640 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
641 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
644 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
645 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
646 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
647 $text .= " -name $sync_name ";
648 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
649 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
653 die "Can't write to cron\n" if (!print($fh $text));
661 $cmd = "ssh root\@$target->{ip} " if $target->{ip
};
662 $cmd .= "qm config $target->{vmid}";
664 my $res = run_cmd
($cmd);
666 my $disks = parse_disks
($res, $target->{ip
});
673 print "Start CMD\n" if $DEBUG;
674 print Dumper
$cmd if $DEBUG;
675 my $output = `$cmd 2>&1`;
677 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
680 print Dumper
$output if $DEBUG;
681 print "END CMD\n" if $DEBUG;
686 my ($text, $ip) = @_;
691 while ($text && $text =~ s/^(.*?)(\n|$)//) {
694 next if $line =~ /cdrom|none/;
698 my $is_disk = $line =~ m/^(virtio|ide|scsi|sata){1}\d+: /;
699 if($line =~ m/^(virtio\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
702 } elsif($line =~ m/^(ide\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
705 } elsif($line =~ m/^(scsi\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
708 } elsif($line =~ m/^(sata\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
713 die "disk is not on ZFS Storage\n" if $is_disk && !$disk;
717 $cmd .= "ssh root\@$ip " if $ip;
718 $cmd .= "pvesm path $stor$disk";
719 my $path = run_cmd
($cmd);
721 if ($path =~ m/^\/dev\
/zvol\/(\w
+).*(\
/$disk)$/) {
723 my @array = split('/', $1);
724 $disks->{$num}->{pool
} = pop(@array);
725 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
727 $disks->{$num}->{path
} = join('/', @array);
728 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
730 $disks->{$num}->{last_part
} = $disk;
731 $disks->{$num}->{all
} .= "\/$disk";
736 die "ERROR: in path\n";
744 sub snapshot_destroy
{
745 my ($source, $dest, $method, $snap) = @_;
747 my $zfscmd = "zfs destroy ";
748 my $snapshot = "$source->{all}\@$snap";
751 if($source->{ip
} && $method eq 'ssh'){
752 run_cmd
("ssh root\@$source->{ip} $zfscmd $snapshot");
754 run_cmd
("$zfscmd $snapshot");
761 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
763 my $path = "$dest->{all}\/$source->{last_part}";
766 run_cmd
("$ssh $zfscmd $path\@$snap ");
775 my ($source ,$dest, $method) = @_;
778 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
779 $cmd .= "zfs list -rt snapshot -Ho name $dest->{all}";
780 $cmd .= "\/$source->{last_part}\@$source->{old_snap}";
783 eval {$text =run_cmd
($cmd);};
789 while ($text && $text =~ s/^(.*?)(\n|$)//) {
791 return 1 if $line =~ m/^.*$source->{old_snap}$/;
796 my ($source, $dest, $param) = @_;
800 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
802 $cmd .= "-v " if $param->{verbose
};
804 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
805 $cmd .= "-i $source->{all}\@$source->{last_snap} $source->{all}\@$source->{new_snap} ";
807 $cmd .= "$source->{all}\@$source->{new_snap} ";
810 if ($param->{limit
}){
811 my $bwl = $param->{limit
}*1024;
812 $cmd .= "| cstream -t $bwl";
815 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
816 $cmd .= "zfs recv $dest->{all}";
817 $cmd .= "\/$source->{last_part}\@$source->{new_snap}";
824 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
831 my ($source, $dest, $method) = @_;
833 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
834 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
836 if ($method eq 'ssh'){
837 if ($dest->{ip
} && $source->{ip
}) {
838 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
839 run_cmd
("scp root\@$source->{ip}:$source_target root\@$dest->{ip}:$dest_target_new");
840 } elsif ($dest->{ip
}) {
841 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
842 run_cmd
("scp $source_target root\@$dest->{ip}:$dest_target_new");
843 } elsif ($source->{ip
}) {
844 run_cmd
("mkdir $CONFIG_PATH -p");
845 run_cmd
("scp root\@$source->{ip}:$source_target $dest_target_new");
848 if ($source->{destroy
}){
849 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
851 run_cmd
("ssh root\@$dest->{ip} rm -f $dest_target_old");
853 run_cmd
("rm -f $dest_target_old");
860 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
861 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
867 my $cfg = read_cron
();
869 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
871 my $states = read_state
();
873 foreach my $source (sort keys%{$cfg}) {
874 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
875 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
876 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
877 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
887 my $job = get_job
($param);
888 $job->{state} = "ok";
896 my $job = get_job
($param);
897 $job->{state} = "stopped";
902 my $command = $ARGV[0];
904 my $commands = {'destroy' => 1,
913 if (!$command || !$commands->{$command}) {
918 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
919 \twill sync one time\n
921 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
923 \t\tmax sync speed in kBytes/s, default unlimited\n
924 \t-maxsnap\tinteger\n
925 \t\thow much snapshots will be kept before get erased, default 1/n
927 \t\tname of the sync job, if not set it is default.
928 \tIt is only necessary if scheduler allready contains this source.\n
930 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
932 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
933 \tCreate a sync Job\n
935 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
937 \t\tmax sync speed in kBytes/s, default unlimited\n
939 \t\thow much snapshots will be kept before get erased, default 1\n
941 \t\tname of the sync job, if not set it is default\n
943 \t\tif this flag is set it will skip the first sync\n
945 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
947 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
948 \tremove a sync Job from the scheduler\n
950 \t\tname of the sync job, if not set it is default\n
952 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
954 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
955 \tGet help about specified command.\n
958 \t-verbose\tboolean\n
959 \t\tVerbose output format.\n";
961 my $help_list = "$PROGNAME list\n
962 \tGet a List of all scheduled Sync Jobs\n";
964 my $help_status = "$PROGNAME status\n
965 \tGet the status of all scheduled Sync Jobs\n";
967 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
968 \tenable a syncjob and reset error\n
970 \t\tname of the sync job, if not set it is default\n
972 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
974 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
977 \t\tname of the sync job, if not set it is default\n
979 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
995 die "$help_destroy\n";
999 die "$help_create\n";
1007 die "$help_status\n";
1011 die "$help_enable\n";
1015 die "$help_enable\n";
1022 my $param = parse_argv
(@arg);
1028 die "$help_destroy\n" if !$param->{source
};
1029 check_target
($param->{source
});
1030 destroy_job
($param);
1034 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1035 check_target
($param->{source
});
1036 check_target
($param->{dest
});
1041 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1042 check_target
($param->{source
});
1043 check_target
($param->{dest
});
1056 my $help_command = $ARGV[1];
1057 if ($help_command && $commands->{$help_command}) {
1058 print help
($help_command);
1060 if ($param->{verbose
} == 1){
1061 exec("man $PROGNAME");
1068 die "$help_enable\n" if !$param->{source
};
1069 check_target
($param->{source
});
1074 die "$help_disable\n" if !$param->{source
};
1075 check_target
($param->{source
});
1076 disable_job
($param);
1083 print("ERROR:\tno command specified\n") if !$help;
1084 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1085 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1086 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1087 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1088 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1089 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1090 print("\t$PROGNAME list\n");
1091 print("\t$PROGNAME status\n");
1092 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1100 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\
/.+)?/) {
1101 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1111 pve-zsync - PVE ZFS Replication Manager
1115 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1117 pve-zsync help <cmd> [OPTIONS]
1119 Get help about specified command.
1127 Verbose output format.
1129 pve-zsync create -dest <string> -source <string> [OPTIONS]
1135 the destination target is like [IP]:<Pool>[/Path]
1139 max sync speed in kBytes/s, default unlimited
1143 how much snapshots will be kept before get erased, default 1
1147 name of the sync job, if not set it is default
1151 if this flag is set it will skip the first sync
1155 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1157 pve-zsync destroy -source <string> [OPTIONS]
1159 remove a sync Job from the scheduler
1163 name of the sync job, if not set it is default
1167 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1169 pve-zsync disable -source <string> [OPTIONS]
1175 name of the sync job, if not set it is default
1179 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1181 pve-zsync enable -source <string> [OPTIONS]
1183 enable a syncjob and reset error
1187 name of the sync job, if not set it is default
1191 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1194 Get a List of all scheduled Sync Jobs
1198 Get the status of all scheduled Sync Jobs
1200 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1206 the destination target is like [IP:]<Pool>[/Path]
1210 max sync speed in kBytes/s, default unlimited
1214 how much snapshots will be kept before get erased, default 1
1218 name of the sync job, if not set it is default.
1219 It is only necessary if scheduler allready contains this source.
1223 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1227 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1228 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1229 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.
1230 To config cron see man crontab.
1232 =head2 PVE ZFS Storage sync Tool
1234 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1238 add sync job from local VM to remote ZFS Server
1239 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1241 =head1 IMPORTANT FILES
1243 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1245 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1247 =head1 COPYRIGHT AND DISCLAIMER
1249 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1251 This program is free software: you can redistribute it and/or modify it
1252 under the terms of the GNU Affero General Public License as published
1253 by the Free Software Foundation, either version 3 of the License, or
1254 (at your option) any later version.
1256 This program is distributed in the hope that it will be useful, but
1257 WITHOUT ANY WARRANTY; without even the implied warranty of
1258 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1259 Affero General Public License for more details.
1261 You should have received a copy of the GNU Affero General Public
1262 License along with this program. If not, see
1263 <http://www.gnu.org/licenses/>.