]>
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);
13 use String
::ShellQuote
'shell_quote';
15 my $PROGNAME = "pve-zsync";
16 my $CONFIG_PATH = "/var/lib/${PROGNAME}/";
17 my $STATE = "${CONFIG_PATH}sync_state";
18 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
19 my $PATH = "/usr/sbin/";
20 my $QEMU_CONF = "/etc/pve/local/qemu-server/";
21 my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
22 my $PROG_PATH = "$PATH${PROGNAME}";
26 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
27 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
28 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
29 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
32 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
33 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
34 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
35 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
42 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
43 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
44 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
45 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
46 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
48 check_bin
('cstream');
56 foreach my $p (split (/:/, $ENV{PATH
})) {
63 die "unable to find command '$bin'\n";
66 sub cut_target_width
{
67 my ($target, $max) = @_;
69 return $target if (length($target) <= $max);
70 my @spl = split('/', $target);
72 my $count = length($spl[@spl-1]);
73 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
75 $count += length($spl[0]) if @spl > 1;
76 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
79 $rest = $max-$count if ($max-$count > 0);
81 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
86 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
91 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
95 my ($source, $name, $status) = @_;
97 if ($status->{$source->{all
}}->{$name}->{status
}) {
104 sub check_pool_exists
{
108 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
109 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
123 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
126 if ($text !~ $TARGETRE) {
130 $target->{ip
} = $1 if $1;
131 my @parts = split('/', $2);
133 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
135 my $pool = $target->{pool
} = shift(@parts);
136 die "$errstr\n" if !$pool;
138 if ($pool =~ m/^\d+$/) {
139 $target->{vmid
} = $pool;
140 delete $target->{pool
};
143 return $target if (@parts == 0);
144 $target->{last_part
} = pop(@parts);
150 $target->{path
} = join('/', @parts);
158 #This is for the first use to init file;
160 my $new_fh = IO
::File-
>new("> $CRONJOBS");
161 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
166 my $fh = IO
::File-
>new("< $CRONJOBS");
167 die "Could not open file $CRONJOBS: $!\n" if !$fh;
173 return encode_cron
(@text);
180 $param->{dest
} = undef;
181 $param->{source
} = undef;
182 $param->{verbose
} = undef;
183 $param->{limit
} = undef;
184 $param->{maxsnap
} = undef;
185 $param->{name
} = undef;
186 $param->{skip
} = undef;
187 $param->{method} = undef;
189 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
190 'dest=s' => \
$param->{dest
},
191 'source=s' => \
$param->{source
},
192 'verbose' => \
$param->{verbose
},
193 'limit=i' => \
$param->{limit
},
194 'maxsnap=i' => \
$param->{maxsnap
},
195 'name=s' => \
$param->{name
},
196 'skip' => \
$param->{skip
},
197 'method=s' => \
$param->{method});
200 die "can't parse options\n";
203 $param->{name
} = "default" if !$param->{name
};
204 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
205 $param->{method} = "ssh" if !$param->{method};
210 sub add_state_to_job
{
213 my $states = read_state
();
214 my $state = $states->{$job->{source
}}->{$job->{name
}};
216 $job->{state} = $state->{state};
217 $job->{lsync
} = $state->{lsync
};
219 for (my $i = 0; $state->{"snap$i"}; $i++) {
220 $job->{"snap$i"} = $state->{"snap$i"};
231 while (my $line = shift(@text)) {
233 my @arg = split('\s', $line);
234 my $param = parse_argv
(@arg);
236 if ($param->{source
} && $param->{dest
}) {
237 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
238 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
239 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
240 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
254 my $source = parse_target
($param->{source
});
255 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
257 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
258 $job->{dest
} = $param->{dest
} if $param->{dest
};
259 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
260 $job->{method} = "ssh" if !$job->{method};
261 $job->{limit
} = $param->{limit
};
262 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
263 $job->{source
} = $param->{source
};
271 make_path
$CONFIG_PATH;
272 my $new_fh = IO
::File-
>new("> $STATE");
273 die "Could not create $STATE: $!\n" if !$new_fh;
279 my $fh = IO
::File-
>new("< $STATE");
280 die "Could not open file $STATE: $!\n" if !$fh;
283 my $states = decode_json
($text);
297 $in_fh = IO
::File-
>new("< $STATE");
298 die "Could not open file $STATE: $!\n" if !$in_fh;
303 my $out_fh = IO
::File-
>new("> $STATE.new");
304 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
309 $states = decode_json
($text);
310 $state = $states->{$job->{source
}}->{$job->{name
}};
313 if ($job->{state} ne "del") {
314 $state->{state} = $job->{state};
315 $state->{lsync
} = $job->{lsync
};
317 for (my $i = 0; $job->{"snap$i"} ; $i++) {
318 $state->{"snap$i"} = $job->{"snap$i"};
320 $states->{$job->{source
}}->{$job->{name
}} = $state;
323 delete $states->{$job->{source
}}->{$job->{name
}};
324 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
327 $text = encode_json
($states);
331 move
("$STATE.new", $STATE);
346 my $header = "SHELL=/bin/sh\n";
347 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
349 my $fh = IO
::File-
>new("< $CRONJOBS");
350 die "Could not open file $CRONJOBS: $!\n" if !$fh;
355 while (my $line = shift(@test)) {
357 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
359 next if $job->{state} eq "del";
360 $text .= format_job
($job, $line);
362 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
371 $text = "$header$text";
375 $text .= format_job
($job);
377 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
378 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
380 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
383 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
388 my ($job, $line) = @_;
391 if ($job->{state} eq "stopped") {
395 $line =~ /^#*(.+) root/;
398 $text .= "*/$INTERVAL * * * *";
401 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
402 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
403 $text .= " --method $job->{method}";
404 $text .= " --verbose" if $job->{verbose
};
412 my $cfg = read_cron
();
414 my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE");
416 my $states = read_state
();
417 foreach my $source (sort keys%{$cfg}) {
418 foreach my $name (sort keys%{$cfg->{$source}}) {
419 $list .= sprintf("%-25s", cut_target_width
($source, 25));
420 $list .= sprintf("%-15s", cut_target_width
($name, 15));
421 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
422 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync
});
423 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
434 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
435 push @$cmd, 'qm', 'status', $target->{vmid
};
437 my $res = run_cmd
($cmd);
439 return 1 if ($res =~ m/^status.*$/);
446 my $cfg = read_cron
();
448 my $job = param_to_job
($param);
450 $job->{state} = "ok";
453 my $source = parse_target
($param->{source
});
454 my $dest = parse_target
($param->{dest
});
456 if (my $ip = $dest->{ip
}) {
457 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
460 if (my $ip = $source->{ip
}) {
461 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
464 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
466 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
468 die "Pool $source->{path} does not exists\n" if undef($check);
470 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !vm_exists
($source);
472 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
478 sync
($param) if !$param->{skip
};
489 my $cfg = read_cron
();
491 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
492 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
494 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
495 $job->{name
} = $param->{name
};
496 $job->{source
} = $param->{source
};
497 $job = add_state_to_job
($job);
505 my $job = get_job
($param);
506 $job->{state} = "del";
515 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
516 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
519 my $date = get_date
();
522 $job = get_job
($param);
525 if ($job && $job->{state} eq "syncing") {
526 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
529 my $dest = parse_target
($param->{dest
});
530 my $source = parse_target
($param->{source
});
532 my $sync_path = sub {
533 my ($source, $dest, $job, $param, $date) = @_;
535 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
537 snapshot_add
($source, $dest, $param->{name
}, $date);
539 send_image
($source, $dest, $param);
541 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
546 $job->{state} = "syncing";
551 if ($source->{vmid
}) {
552 die "VM $source->{vmid} doesn't exist\n" if !vm_exists
($source);
553 my $disks = get_disks
($source);
555 foreach my $disk (sort keys %{$disks}) {
556 $source->{all
} = $disks->{$disk}->{all
};
557 $source->{pool
} = $disks->{$disk}->{pool
};
558 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
559 $source->{last_part
} = $disks->{$disk}->{last_part
};
560 &$sync_path($source, $dest, $job, $param, $date);
562 if ($param->{method} eq "ssh") {
563 send_config
($source, $dest,'ssh');
566 &$sync_path($source, $dest, $job, $param, $date);
571 $job->{state} = "error";
575 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
581 $job->{state} = "ok";
582 $job->{lsync
} = $date;
591 my ($source, $dest, $max_snap, $name) = @_;
594 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
595 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
596 push @$cmd, $source->{all
};
598 my $raw = run_cmd
($cmd);
601 my $last_snap = undef;
604 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
606 if ($line =~ m/(rep_$name.*)$/) {
607 $last_snap = $1 if (!$last_snap);
610 if ($index == $max_snap) {
611 $source->{destroy
} = 1;
617 return ($old_snap, $last_snap) if $last_snap;
623 my ($source, $dest, $name, $date) = @_;
625 my $snap_name = "rep_$name\_".$date;
627 $source->{new_snap
} = $snap_name;
629 my $path = "$source->{all}\@$snap_name";
632 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
633 push @$cmd, 'zfs', 'snapshot', $path;
639 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
647 my $text = "SHELL=/bin/sh\n";
648 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
650 my $fh = IO
::File-
>new("> $CRONJOBS");
651 die "Could not open file: $!\n" if !$fh;
653 foreach my $source (sort keys%{$cfg}) {
654 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
655 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
656 $text .= "$PROG_PATH sync";
657 $text .= " -source ";
658 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
659 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
660 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
662 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
663 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
664 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
667 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
668 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
669 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
670 $text .= " -name $sync_name ";
671 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
672 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
676 die "Can't write to cron\n" if (!print($fh $text));
684 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
685 push @$cmd, 'qm', 'config', $target->{vmid
};
687 my $res = run_cmd
($cmd);
689 my $disks = parse_disks
($res, $target->{ip
});
696 print "Start CMD\n" if $DEBUG;
697 print Dumper
$cmd if $DEBUG;
698 if (ref($cmd) eq 'ARRAY') {
699 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
701 my $output = `$cmd 2>&1`;
703 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
706 print Dumper
$output if $DEBUG;
707 print "END CMD\n" if $DEBUG;
712 my ($text, $ip) = @_;
717 while ($text && $text =~ s/^(.*?)(\n|$)//) {
720 next if $line =~ /cdrom|none/;
721 next if $line !~ m/^(?:virtio|ide|scsi|sata)\d+: /;
725 if($line =~ m/^(?:virtio|ide|scsi|sata)\d+: (.+:)([A-Za-z0-9\-]+),(.*)$/) {
729 die "disk is not on ZFS Storage\n";
733 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
734 push @$cmd, 'pvesm', 'path', "$stor$disk";
735 my $path = run_cmd
($cmd);
737 if ($path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
739 my @array = split('/', $1);
740 $disks->{$num}->{pool
} = shift(@array);
741 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
743 $disks->{$num}->{path
} = join('/', @array);
744 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
746 $disks->{$num}->{last_part
} = $disk;
747 $disks->{$num}->{all
} .= "\/$disk";
752 die "ERROR: in path\n";
759 sub snapshot_destroy
{
760 my ($source, $dest, $method, $snap) = @_;
762 my @zfscmd = ('zfs', 'destroy');
763 my $snapshot = "$source->{all}\@$snap";
766 if($source->{ip
} && $method eq 'ssh'){
767 run_cmd
(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
769 run_cmd
([@zfscmd, $snapshot]);
776 my @ssh = $dest->{ip
} ?
('ssh', "root\@$dest->{ip}", '--') : ();
778 my $path = "$dest->{all}\/$source->{last_part}";
781 run_cmd
([@ssh, @zfscmd, "$path\@$snap"]);
790 my ($source ,$dest, $method) = @_;
793 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
794 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
795 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
798 eval {$text =run_cmd
($cmd);};
804 while ($text && $text =~ s/^(.*?)(\n|$)//) {
806 return 1 if $line =~ m/^.*$source->{old_snap}$/;
811 my ($source, $dest, $param) = @_;
815 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip
};
816 push @$cmd, 'zfs', 'send';
817 push @$cmd, '-v' if $param->{verbose
};
819 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
820 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
822 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
824 if ($param->{limit
}){
825 my $bwl = $param->{limit
}*1024;
826 push @$cmd, \'|', 'cstream
', '-t
', $bwl;
829 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
830 push @$cmd, 'zfs', 'recv', '--';
831 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{new_snap}";
838 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
845 my ($source, $dest, $method) = @_;
847 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
848 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
850 if ($method eq 'ssh'){
851 if ($dest->{ip
} && $source->{ip
}) {
852 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
853 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
854 } elsif ($dest->{ip
}) {
855 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
856 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
857 } elsif ($source->{ip
}) {
858 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
859 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
862 if ($source->{destroy
}){
863 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
865 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
867 run_cmd
(['rm', '-f', '--', $dest_target_old]);
874 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
875 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
881 my $cfg = read_cron
();
883 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
885 my $states = read_state
();
887 foreach my $source (sort keys%{$cfg}) {
888 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
889 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
890 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
891 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
901 my $job = get_job
($param);
902 $job->{state} = "ok";
910 my $job = get_job
($param);
911 $job->{state} = "stopped";
916 my $command = $ARGV[0];
918 my $commands = {'destroy' => 1,
927 if (!$command || !$commands->{$command}) {
932 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
933 \twill sync one time\n
935 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
937 \t\tmax sync speed in kBytes/s, default unlimited\n
938 \t-maxsnap\tinteger\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.
942 \tIt is only necessary if scheduler allready contains this source.\n
944 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
946 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
947 \tCreate a sync Job\n
949 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
951 \t\tmax sync speed in kBytes/s, default unlimited\n
953 \t\thow much snapshots will be kept before get erased, default 1\n
955 \t\tname of the sync job, if not set it is default\n
957 \t\tif this flag is set it will skip the first sync\n
959 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
961 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
962 \tremove a sync Job from the scheduler\n
964 \t\tname of the sync job, if not set it is default\n
966 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
968 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
969 \tGet help about specified command.\n
972 \t-verbose\tboolean\n
973 \t\tVerbose output format.\n";
975 my $help_list = "$PROGNAME list\n
976 \tGet a List of all scheduled Sync Jobs\n";
978 my $help_status = "$PROGNAME status\n
979 \tGet the status of all scheduled Sync Jobs\n";
981 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
982 \tenable a syncjob and reset error\n
984 \t\tname of the sync job, if not set it is default\n
986 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
988 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
991 \t\tname of the sync job, if not set it is default\n
993 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1009 die "$help_destroy\n";
1013 die "$help_create\n";
1021 die "$help_status\n";
1025 die "$help_enable\n";
1029 die "$help_enable\n";
1036 my $param = parse_argv
(@arg);
1042 die "$help_destroy\n" if !$param->{source
};
1043 check_target
($param->{source
});
1044 destroy_job
($param);
1048 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1049 check_target
($param->{source
});
1050 check_target
($param->{dest
});
1055 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1056 check_target
($param->{source
});
1057 check_target
($param->{dest
});
1070 my $help_command = $ARGV[1];
1071 if ($help_command && $commands->{$help_command}) {
1072 print help
($help_command);
1074 if ($param->{verbose
} == 1){
1075 exec("man $PROGNAME");
1082 die "$help_enable\n" if !$param->{source
};
1083 check_target
($param->{source
});
1088 die "$help_disable\n" if !$param->{source
};
1089 check_target
($param->{source
});
1090 disable_job
($param);
1097 print("ERROR:\tno command specified\n") if !$help;
1098 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1099 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1100 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1101 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1102 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1103 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1104 print("\t$PROGNAME list\n");
1105 print("\t$PROGNAME status\n");
1106 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1111 parse_target
($target);
1118 pve-zsync - PVE ZFS Replication Manager
1122 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1124 pve-zsync help <cmd> [OPTIONS]
1126 Get help about specified command.
1134 Verbose output format.
1136 pve-zsync create -dest <string> -source <string> [OPTIONS]
1142 the destination target is like [IP]:<Pool>[/Path]
1146 max sync speed in kBytes/s, default unlimited
1150 how much snapshots will be kept before get erased, default 1
1154 name of the sync job, if not set it is default
1158 if this flag is set it will skip the first sync
1162 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1164 pve-zsync destroy -source <string> [OPTIONS]
1166 remove a sync Job from the scheduler
1170 name of the sync job, if not set it is default
1174 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1176 pve-zsync disable -source <string> [OPTIONS]
1182 name of the sync job, if not set it is default
1186 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1188 pve-zsync enable -source <string> [OPTIONS]
1190 enable a syncjob and reset error
1194 name of the sync job, if not set it is default
1198 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1201 Get a List of all scheduled Sync Jobs
1205 Get the status of all scheduled Sync Jobs
1207 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1213 the destination target is like [IP:]<Pool>[/Path]
1217 max sync speed in kBytes/s, default unlimited
1221 how much snapshots will be kept before get erased, default 1
1225 name of the sync job, if not set it is default.
1226 It is only necessary if scheduler allready contains this source.
1230 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1234 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1235 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1236 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.
1237 To config cron see man crontab.
1239 =head2 PVE ZFS Storage sync Tool
1241 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1245 add sync job from local VM to remote ZFS Server
1246 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1248 =head1 IMPORTANT FILES
1250 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1252 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1254 =head1 COPYRIGHT AND DISCLAIMER
1256 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1258 This program is free software: you can redistribute it and/or modify it
1259 under the terms of the GNU Affero General Public License as published
1260 by the Free Software Foundation, either version 3 of the License, or
1261 (at your option) any later version.
1263 This program is distributed in the hope that it will be useful, but
1264 WITHOUT ANY WARRANTY; without even the implied warranty of
1265 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1266 Affero General Public License for more details.
1268 You should have received a copy of the GNU Affero General Public
1269 License along with this program. If not, see
1270 <http://www.gnu.org/licenses/>.