]>
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;
828 my $target = "$dest->{all}/$source->{last_part}";
832 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
833 push @$cmd, 'zfs', 'recv', '-F', '--';
834 push @$cmd, "$target";
841 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
848 my ($source, $dest, $method) = @_;
850 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
851 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
853 if ($method eq 'ssh'){
854 if ($dest->{ip
} && $source->{ip
}) {
855 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
856 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
857 } elsif ($dest->{ip
}) {
858 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
859 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
860 } elsif ($source->{ip
}) {
861 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
862 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
865 if ($source->{destroy
}){
866 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
868 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
870 run_cmd
(['rm', '-f', '--', $dest_target_old]);
877 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
878 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
884 my $cfg = read_cron
();
886 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
888 my $states = read_state
();
890 foreach my $source (sort keys%{$cfg}) {
891 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
892 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
893 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
894 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
904 my $job = get_job
($param);
905 $job->{state} = "ok";
913 my $job = get_job
($param);
914 $job->{state} = "stopped";
919 my $command = $ARGV[0];
921 my $commands = {'destroy' => 1,
930 if (!$command || !$commands->{$command}) {
935 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
936 \twill sync one time\n
938 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
940 \t\tmax sync speed in kBytes/s, default unlimited\n
941 \t-maxsnap\tinteger\n
942 \t\thow much snapshots will be kept before get erased, default 1/n
944 \t\tname of the sync job, if not set it is default.
945 \tIt is only necessary if scheduler allready contains this source.\n
947 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
949 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
950 \tCreate a sync Job\n
952 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
954 \t\tmax sync speed in kBytes/s, default unlimited\n
956 \t\thow much snapshots will be kept before get erased, default 1\n
958 \t\tname of the sync job, if not set it is default\n
960 \t\tif this flag is set it will skip the first sync\n
962 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
964 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
965 \tremove a sync Job from the scheduler\n
967 \t\tname of the sync job, if not set it is default\n
969 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
971 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
972 \tGet help about specified command.\n
975 \t-verbose\tboolean\n
976 \t\tVerbose output format.\n";
978 my $help_list = "$PROGNAME list\n
979 \tGet a List of all scheduled Sync Jobs\n";
981 my $help_status = "$PROGNAME status\n
982 \tGet the status of all scheduled Sync Jobs\n";
984 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
985 \tenable a syncjob and reset error\n
987 \t\tname of the sync job, if not set it is default\n
989 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
991 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
994 \t\tname of the sync job, if not set it is default\n
996 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1012 die "$help_destroy\n";
1016 die "$help_create\n";
1024 die "$help_status\n";
1028 die "$help_enable\n";
1032 die "$help_enable\n";
1039 my $param = parse_argv
(@arg);
1045 die "$help_destroy\n" if !$param->{source
};
1046 check_target
($param->{source
});
1047 destroy_job
($param);
1051 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1052 check_target
($param->{source
});
1053 check_target
($param->{dest
});
1058 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1059 check_target
($param->{source
});
1060 check_target
($param->{dest
});
1073 my $help_command = $ARGV[1];
1074 if ($help_command && $commands->{$help_command}) {
1075 print help
($help_command);
1077 if ($param->{verbose
} == 1){
1078 exec("man $PROGNAME");
1085 die "$help_enable\n" if !$param->{source
};
1086 check_target
($param->{source
});
1091 die "$help_disable\n" if !$param->{source
};
1092 check_target
($param->{source
});
1093 disable_job
($param);
1100 print("ERROR:\tno command specified\n") if !$help;
1101 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1102 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1103 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1104 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1105 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1106 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1107 print("\t$PROGNAME list\n");
1108 print("\t$PROGNAME status\n");
1109 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1114 parse_target
($target);
1121 pve-zsync - PVE ZFS Replication Manager
1125 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1127 pve-zsync help <cmd> [OPTIONS]
1129 Get help about specified command.
1137 Verbose output format.
1139 pve-zsync create -dest <string> -source <string> [OPTIONS]
1145 the destination target is like [IP]:<Pool>[/Path]
1149 max sync speed in kBytes/s, default unlimited
1153 how much snapshots will be kept before get erased, default 1
1157 name of the sync job, if not set it is default
1161 if this flag is set it will skip the first sync
1165 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1167 pve-zsync destroy -source <string> [OPTIONS]
1169 remove a sync Job from the scheduler
1173 name of the sync job, if not set it is default
1177 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1179 pve-zsync disable -source <string> [OPTIONS]
1185 name of the sync job, if not set it is default
1189 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1191 pve-zsync enable -source <string> [OPTIONS]
1193 enable a syncjob and reset error
1197 name of the sync job, if not set it is default
1201 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1204 Get a List of all scheduled Sync Jobs
1208 Get the status of all scheduled Sync Jobs
1210 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1216 the destination target is like [IP:]<Pool>[/Path]
1220 max sync speed in kBytes/s, default unlimited
1224 how much snapshots will be kept before get erased, default 1
1228 name of the sync job, if not set it is default.
1229 It is only necessary if scheduler allready contains this source.
1233 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1237 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1238 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1239 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.
1240 To config cron see man crontab.
1242 =head2 PVE ZFS Storage sync Tool
1244 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1248 add sync job from local VM to remote ZFS Server
1249 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1251 =head1 IMPORTANT FILES
1253 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1255 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1257 =head1 COPYRIGHT AND DISCLAIMER
1259 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1261 This program is free software: you can redistribute it and/or modify it
1262 under the terms of the GNU Affero General Public License as published
1263 by the Free Software Foundation, either version 3 of the License, or
1264 (at your option) any later version.
1266 This program is distributed in the hope that it will be useful, but
1267 WITHOUT ANY WARRANTY; without even the implied warranty of
1268 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1269 Affero General Public License for more details.
1271 You should have received a copy of the GNU Affero General Public
1272 License along with this program. If not, see
1273 <http://www.gnu.org/licenses/>.