]>
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";
708 $cmd .= "ssh root\@$ip " if $ip;
709 $cmd .= "pvesm path $stor$disk";
710 my $path = run_cmd
($cmd);
712 if ($path =~ m/^\/dev\
/zvol\/(\w
+).*(\
/$disk)$/) {
714 my @array = split('/', $1);
715 $disks->{$num}->{pool
} = pop(@array);
716 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
718 $disks->{$num}->{path
} = join('/', @array);
719 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
721 $disks->{$num}->{last_part
} = $disk;
722 $disks->{$num}->{all
} .= "\/$disk";
727 die "ERROR: in path\n";
735 sub snapshot_destroy
{
736 my ($source, $dest, $method, $snap) = @_;
738 my $zfscmd = "zfs destroy ";
739 my $snapshot = "$source->{all}\@$snap";
742 if($source->{ip
} && $method eq 'ssh'){
743 run_cmd
("ssh root\@$source->{ip} $zfscmd $snapshot");
745 run_cmd
("$zfscmd $snapshot");
752 my $ssh = $dest->{ip
} ?
"ssh root\@$dest->{ip}" : "";
754 my $path = "$dest->{all}\/$source->{last_part}";
757 run_cmd
("$ssh $zfscmd $path\@$snap ");
766 my ($source ,$dest, $method) = @_;
769 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip
};
770 $cmd .= "zfs list -rt snapshot -Ho name $dest->{all}";
771 $cmd .= "\/$source->{last_part}\@$source->{old_snap}";
774 eval {$text =run_cmd
($cmd);};
780 while ($text && $text =~ s/^(.*?)(\n|$)//) {
782 return 1 if $line =~ m/^.*$source->{old_snap}$/;
787 my ($source, $dest, $param) = @_;
791 $cmd .= "ssh root\@$source->{ip} " if $source->{ip
};
793 $cmd .= "-v " if $param->{verbose
};
795 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
796 $cmd .= "-i $source->{all}\@$source->{last_snap} $source->{all}\@$source->{new_snap} ";
798 $cmd .= "$source->{all}\@$source->{new_snap} ";
801 if ($param->{limit
}){
802 my $bwl = $param->{limit
}*1024;
803 $cmd .= "| cstream -t $bwl";
806 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip
};
807 $cmd .= "zfs recv $dest->{all}";
808 $cmd .= "\/$source->{last_part}\@$source->{new_snap}";
815 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
822 my ($source, $dest, $method) = @_;
824 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
825 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
827 if ($method eq 'ssh'){
828 if ($dest->{ip
} && $source->{ip
}) {
829 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
830 run_cmd
("scp root\@$source->{ip}:$source_target root\@$dest->{ip}:$dest_target_new");
831 } elsif ($dest->{ip
}) {
832 run_cmd
("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p");
833 run_cmd
("scp $source_target root\@$dest->{ip}:$dest_target_new");
834 } elsif ($source->{ip
}) {
835 run_cmd
("mkdir $CONFIG_PATH -p");
836 run_cmd
("scp root\@$source->{ip}:$source_target $dest_target_new");
839 if ($source->{destroy
}){
840 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
842 run_cmd
("ssh root\@$dest->{ip} rm -f $dest_target_old");
844 run_cmd
("rm -f $dest_target_old");
851 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
852 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
858 my $cfg = read_cron
();
860 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
862 my $states = read_state
();
864 foreach my $source (sort keys%{$cfg}) {
865 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
866 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
867 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
868 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
878 my $job = get_job
($param);
879 $job->{state} = "ok";
887 my $job = get_job
($param);
888 $job->{state} = "stopped";
893 my $command = $ARGV[0];
895 my $commands = {'destroy' => 1,
904 if (!$command || !$commands->{$command}) {
909 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
910 \twill sync one time\n
912 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
914 \t\tmax sync speed in kBytes/s, default unlimited\n
915 \t-maxsnap\tinteger\n
916 \t\thow much snapshots will be kept before get erased, default 1/n
918 \t\tname of the sync job, if not set it is default.
919 \tIt is only necessary if scheduler allready contains this source.\n
921 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
923 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
924 \tCreate a sync Job\n
926 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
928 \t\tmax sync speed in kBytes/s, default unlimited\n
930 \t\thow much snapshots will be kept before get erased, default 1\n
932 \t\tname of the sync job, if not set it is default\n
934 \t\tif this flag is set it will skip the first sync\n
936 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
938 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
939 \tremove a sync Job from the scheduler\n
941 \t\tname of the sync job, if not set it is default\n
943 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
945 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
946 \tGet help about specified command.\n
949 \t-verbose\tboolean\n
950 \t\tVerbose output format.\n";
952 my $help_list = "$PROGNAME list\n
953 \tGet a List of all scheduled Sync Jobs\n";
955 my $help_status = "$PROGNAME status\n
956 \tGet the status of all scheduled Sync Jobs\n";
958 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
959 \tenable a syncjob and reset error\n
961 \t\tname of the sync job, if not set it is default\n
963 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
965 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
968 \t\tname of the sync job, if not set it is default\n
970 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
986 die "$help_destroy\n";
990 die "$help_create\n";
998 die "$help_status\n";
1002 die "$help_enable\n";
1006 die "$help_enable\n";
1013 my $param = parse_argv
(@arg);
1019 die "$help_destroy\n" if !$param->{source
};
1020 check_target
($param->{source
});
1021 destroy_job
($param);
1025 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1026 check_target
($param->{source
});
1027 check_target
($param->{dest
});
1032 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1033 check_target
($param->{source
});
1034 check_target
($param->{dest
});
1047 my $help_command = $ARGV[1];
1048 if ($help_command && $commands->{$help_command}) {
1049 print help
($help_command);
1051 if ($param->{verbose
} == 1){
1052 exec("man $PROGNAME");
1059 die "$help_enable\n" if !$param->{source
};
1060 check_target
($param->{source
});
1065 die "$help_disable\n" if !$param->{source
};
1066 check_target
($param->{source
});
1067 disable_job
($param);
1074 print("ERROR:\tno command specified\n") if !$help;
1075 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1076 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1077 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1078 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1079 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1080 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1081 print("\t$PROGNAME list\n");
1082 print("\t$PROGNAME status\n");
1083 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1091 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\
/.+)?/) {
1092 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1102 pve-zsync - PVE ZFS Replication Manager
1106 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1108 pve-zsync help <cmd> [OPTIONS]
1110 Get help about specified command.
1118 Verbose output format.
1120 pve-zsync create -dest <string> -source <string> [OPTIONS]
1126 the destination target is like [IP]:<Pool>[/Path]
1130 max sync speed in kBytes/s, default unlimited
1134 how much snapshots will be kept before get erased, default 1
1138 name of the sync job, if not set it is default
1142 if this flag is set it will skip the first sync
1146 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1148 pve-zsync destroy -source <string> [OPTIONS]
1150 remove a sync Job from the scheduler
1154 name of the sync job, if not set it is default
1158 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1160 pve-zsync disable -source <string> [OPTIONS]
1166 name of the sync job, if not set it is default
1170 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1172 pve-zsync enable -source <string> [OPTIONS]
1174 enable a syncjob and reset error
1178 name of the sync job, if not set it is default
1182 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1185 Get a List of all scheduled Sync Jobs
1189 Get the status of all scheduled Sync Jobs
1191 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1197 the destination target is like [IP:]<Pool>[/Path]
1201 max sync speed in kBytes/s, default unlimited
1205 how much snapshots will be kept before get erased, default 1
1209 name of the sync job, if not set it is default.
1210 It is only necessary if scheduler allready contains this source.
1214 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1218 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1219 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1220 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.
1221 To config cron see man crontab.
1223 =head2 PVE ZFS Storage sync Tool
1225 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1229 add sync job from local VM to remote ZFS Server
1230 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1232 =head1 IMPORTANT FILES
1234 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1236 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1238 =head1 COPYRIGHT AND DISCLAIMER
1240 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1242 This program is free software: you can redistribute it and/or modify it
1243 under the terms of the GNU Affero General Public License as published
1244 by the Free Software Foundation, either version 3 of the License, or
1245 (at your option) any later version.
1247 This program is distributed in the hope that it will be useful, but
1248 WITHOUT ANY WARRANTY; without even the implied warranty of
1249 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1250 Affero General Public License for more details.
1252 You should have received a copy of the GNU Affero General Public
1253 License along with this program. If not, see
1254 <http://www.gnu.org/licenses/>.