]>
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);
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/;
695 next if $line !~ m/^(?:virtio|ide|scsi|sata)\d+: /;
699 if($line =~ m/^(?:virtio|ide|scsi|sata)\d+: (.+:)([A-Za-z0-9\-]+),(.*)$/) {
703 die "disk is not on ZFS Storage\n";
707 $cmd .= "ssh root\@$ip " if $ip;
708 $cmd .= "pvesm path $stor$disk";
709 my $path = run_cmd
($cmd);
711 if ($path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
713 my @array = split('/', $1);
714 $disks->{$num}->{pool
} = shift(@array);
715 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
717 $disks->{$num}->{path
} = join('/', @array);
718 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
720 $disks->{$num}->{last_part
} = $disk;
721 $disks->{$num}->{all
} .= "\/$disk";
726 die "ERROR: in path\n";
733 sub snapshot_destroy
{
734 my ($source, $dest, $method, $snap) = @_;
736 my $zfscmd = "zfs destroy ";
737 my $snapshot = "$source->{all}\@$snap";
740 if($source->{ip
} && $method eq 'ssh'){
741 run_cmd
("ssh root\@$source->{ip} $zfscmd $snapshot");
743 run_cmd
("$zfscmd $snapshot");
750 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
752 my $path = "$dest->{all}\/$source->{last_part}";
755 run_cmd
("$ssh $zfscmd $path\@$snap ");
764 my ($source ,$dest, $method) = @_;
767 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
768 $cmd .= "zfs list -rt snapshot -Ho name $dest->{all}";
769 $cmd .= "\/$source->{last_part}\@$source->{old_snap}";
772 eval {$text =run_cmd
($cmd);};
778 while ($text && $text =~ s/^(.*?)(\n|$)//) {
780 return 1 if $line =~ m/^.*$source->{old_snap}$/;
785 my ($source, $dest, $param) = @_;
789 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
791 $cmd .= "-v " if $param->{verbose
};
793 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
794 $cmd .= "-i $source->{all}\@$source->{last_snap} $source->{all}\@$source->{new_snap} ";
796 $cmd .= "$source->{all}\@$source->{new_snap} ";
799 if ($param->{limit
}){
800 my $bwl = $param->{limit
}*1024;
801 $cmd .= "| cstream -t $bwl";
804 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
805 $cmd .= "zfs recv $dest->{all}";
806 $cmd .= "\/$source->{last_part}\@$source->{new_snap}";
813 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
820 my ($source, $dest, $method) = @_;
822 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
823 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
825 if ($method eq 'ssh'){
826 if ($dest->{ip
} && $source->{ip
}) {
827 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
828 run_cmd
("scp root\@$source->{ip}:$source_target root\@$dest->{ip}:$dest_target_new");
829 } elsif ($dest->{ip
}) {
830 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
831 run_cmd
("scp $source_target root\@$dest->{ip}:$dest_target_new");
832 } elsif ($source->{ip
}) {
833 run_cmd
("mkdir $CONFIG_PATH -p");
834 run_cmd
("scp root\@$source->{ip}:$source_target $dest_target_new");
837 if ($source->{destroy
}){
838 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
840 run_cmd
("ssh root\@$dest->{ip} rm -f $dest_target_old");
842 run_cmd
("rm -f $dest_target_old");
849 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
850 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
856 my $cfg = read_cron
();
858 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
860 my $states = read_state
();
862 foreach my $source (sort keys%{$cfg}) {
863 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
864 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
865 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
866 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
876 my $job = get_job
($param);
877 $job->{state} = "ok";
885 my $job = get_job
($param);
886 $job->{state} = "stopped";
891 my $command = $ARGV[0];
893 my $commands = {'destroy' => 1,
902 if (!$command || !$commands->{$command}) {
907 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
908 \twill sync one time\n
910 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
912 \t\tmax sync speed in kBytes/s, default unlimited\n
913 \t-maxsnap\tinteger\n
914 \t\thow much snapshots will be kept before get erased, default 1/n
916 \t\tname of the sync job, if not set it is default.
917 \tIt is only necessary if scheduler allready contains this source.\n
919 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
921 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
922 \tCreate a sync Job\n
924 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
926 \t\tmax sync speed in kBytes/s, default unlimited\n
928 \t\thow much snapshots will be kept before get erased, default 1\n
930 \t\tname of the sync job, if not set it is default\n
932 \t\tif this flag is set it will skip the first sync\n
934 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
936 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
937 \tremove a sync Job from the scheduler\n
939 \t\tname of the sync job, if not set it is default\n
941 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
943 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
944 \tGet help about specified command.\n
947 \t-verbose\tboolean\n
948 \t\tVerbose output format.\n";
950 my $help_list = "$PROGNAME list\n
951 \tGet a List of all scheduled Sync Jobs\n";
953 my $help_status = "$PROGNAME status\n
954 \tGet the status of all scheduled Sync Jobs\n";
956 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
957 \tenable a syncjob and reset error\n
959 \t\tname of the sync job, if not set it is default\n
961 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
963 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
966 \t\tname of the sync job, if not set it is default\n
968 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
984 die "$help_destroy\n";
988 die "$help_create\n";
996 die "$help_status\n";
1000 die "$help_enable\n";
1004 die "$help_enable\n";
1011 my $param = parse_argv
(@arg);
1017 die "$help_destroy\n" if !$param->{source
};
1018 check_target
($param->{source
});
1019 destroy_job
($param);
1023 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1024 check_target
($param->{source
});
1025 check_target
($param->{dest
});
1030 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1031 check_target
($param->{source
});
1032 check_target
($param->{dest
});
1045 my $help_command = $ARGV[1];
1046 if ($help_command && $commands->{$help_command}) {
1047 print help
($help_command);
1049 if ($param->{verbose
} == 1){
1050 exec("man $PROGNAME");
1057 die "$help_enable\n" if !$param->{source
};
1058 check_target
($param->{source
});
1063 die "$help_disable\n" if !$param->{source
};
1064 check_target
($param->{source
});
1065 disable_job
($param);
1072 print("ERROR:\tno command specified\n") if !$help;
1073 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1074 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1075 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1076 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1077 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1078 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1079 print("\t$PROGNAME list\n");
1080 print("\t$PROGNAME status\n");
1081 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1089 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\
/.+)?/) {
1090 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1100 pve-zsync - PVE ZFS Replication Manager
1104 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1106 pve-zsync help <cmd> [OPTIONS]
1108 Get help about specified command.
1116 Verbose output format.
1118 pve-zsync create -dest <string> -source <string> [OPTIONS]
1124 the destination target is like [IP]:<Pool>[/Path]
1128 max sync speed in kBytes/s, default unlimited
1132 how much snapshots will be kept before get erased, default 1
1136 name of the sync job, if not set it is default
1140 if this flag is set it will skip the first sync
1144 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1146 pve-zsync destroy -source <string> [OPTIONS]
1148 remove a sync Job from the scheduler
1152 name of the sync job, if not set it is default
1156 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1158 pve-zsync disable -source <string> [OPTIONS]
1164 name of the sync job, if not set it is default
1168 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1170 pve-zsync enable -source <string> [OPTIONS]
1172 enable a syncjob and reset error
1176 name of the sync job, if not set it is default
1180 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1183 Get a List of all scheduled Sync Jobs
1187 Get the status of all scheduled Sync Jobs
1189 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1195 the destination target is like [IP:]<Pool>[/Path]
1199 max sync speed in kBytes/s, default unlimited
1203 how much snapshots will be kept before get erased, default 1
1207 name of the sync job, if not set it is default.
1208 It is only necessary if scheduler allready contains this source.
1212 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1216 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1217 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1218 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.
1219 To config cron see man crontab.
1221 =head2 PVE ZFS Storage sync Tool
1223 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1227 add sync job from local VM to remote ZFS Server
1228 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1230 =head1 IMPORTANT FILES
1232 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1234 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1236 =head1 COPYRIGHT AND DISCLAIMER
1238 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1240 This program is free software: you can redistribute it and/or modify it
1241 under the terms of the GNU Affero General Public License as published
1242 by the Free Software Foundation, either version 3 of the License, or
1243 (at your option) any later version.
1245 This program is distributed in the hope that it will be useful, but
1246 WITHOUT ANY WARRANTY; without even the implied warranty of
1247 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1248 Affero General Public License for more details.
1250 You should have received a copy of the GNU Affero General Public
1251 License along with this program. If not, see
1252 <http://www.gnu.org/licenses/>.