]>
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 $PVE_DIR = "/etc/pve/local/";
21 my $QEMU_CONF = "${PVE_DIR}qemu-server/";
22 my $LXC_CONF = "${PVE_DIR}lxc/";
23 my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
24 my $PROG_PATH = "$PATH${PROGNAME}";
28 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
29 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
30 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
31 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
34 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
35 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
36 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
42 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
44 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
45 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
46 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
47 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
48 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
50 check_bin
('cstream');
58 foreach my $p (split (/:/, $ENV{PATH
})) {
65 die "unable to find command '$bin'\n";
68 sub cut_target_width
{
69 my ($target, $max) = @_;
71 return $target if (length($target) <= $max);
72 my @spl = split('/', $target);
74 my $count = length($spl[@spl-1]);
75 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
77 $count += length($spl[0]) if @spl > 1;
78 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
81 $rest = $max-$count if ($max-$count > 0);
83 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
88 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
93 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
97 my ($source, $name, $status) = @_;
99 if ($status->{$source->{all
}}->{$name}->{status
}) {
106 sub check_pool_exists
{
110 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
111 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
125 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
128 if ($text !~ $TARGETRE) {
132 $target->{ip
} = $1 if $1;
133 my @parts = split('/', $2);
135 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
137 my $pool = $target->{pool
} = shift(@parts);
138 die "$errstr\n" if !$pool;
140 if ($pool =~ m/^\d+$/) {
141 $target->{vmid
} = $pool;
142 delete $target->{pool
};
145 return $target if (@parts == 0);
146 $target->{last_part
} = pop(@parts);
152 $target->{path
} = join('/', @parts);
160 #This is for the first use to init file;
162 my $new_fh = IO
::File-
>new("> $CRONJOBS");
163 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
168 my $fh = IO
::File-
>new("< $CRONJOBS");
169 die "Could not open file $CRONJOBS: $!\n" if !$fh;
175 return encode_cron
(@text);
182 $param->{dest
} = undef;
183 $param->{source
} = undef;
184 $param->{verbose
} = undef;
185 $param->{limit
} = undef;
186 $param->{maxsnap
} = undef;
187 $param->{name
} = undef;
188 $param->{skip
} = undef;
189 $param->{method} = undef;
191 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
192 'dest=s' => \
$param->{dest
},
193 'source=s' => \
$param->{source
},
194 'verbose' => \
$param->{verbose
},
195 'limit=i' => \
$param->{limit
},
196 'maxsnap=i' => \
$param->{maxsnap
},
197 'name=s' => \
$param->{name
},
198 'skip' => \
$param->{skip
},
199 'method=s' => \
$param->{method});
202 die "can't parse options\n";
205 $param->{name
} = "default" if !$param->{name
};
206 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
207 $param->{method} = "ssh" if !$param->{method};
212 sub add_state_to_job
{
215 my $states = read_state
();
216 my $state = $states->{$job->{source
}}->{$job->{name
}};
218 $job->{state} = $state->{state};
219 $job->{lsync
} = $state->{lsync
};
221 for (my $i = 0; $state->{"snap$i"}; $i++) {
222 $job->{"snap$i"} = $state->{"snap$i"};
233 while (my $line = shift(@text)) {
235 my @arg = split('\s', $line);
236 my $param = parse_argv
(@arg);
238 if ($param->{source
} && $param->{dest
}) {
239 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
240 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
256 my $source = parse_target
($param->{source
});
257 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
259 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
260 $job->{dest
} = $param->{dest
} if $param->{dest
};
261 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
262 $job->{method} = "ssh" if !$job->{method};
263 $job->{limit
} = $param->{limit
};
264 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
265 $job->{source
} = $param->{source
};
273 make_path
$CONFIG_PATH;
274 my $new_fh = IO
::File-
>new("> $STATE");
275 die "Could not create $STATE: $!\n" if !$new_fh;
281 my $fh = IO
::File-
>new("< $STATE");
282 die "Could not open file $STATE: $!\n" if !$fh;
285 my $states = decode_json
($text);
299 $in_fh = IO
::File-
>new("< $STATE");
300 die "Could not open file $STATE: $!\n" if !$in_fh;
305 my $out_fh = IO
::File-
>new("> $STATE.new");
306 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
311 $states = decode_json
($text);
312 $state = $states->{$job->{source
}}->{$job->{name
}};
315 if ($job->{state} ne "del") {
316 $state->{state} = $job->{state};
317 $state->{lsync
} = $job->{lsync
};
319 for (my $i = 0; $job->{"snap$i"} ; $i++) {
320 $state->{"snap$i"} = $job->{"snap$i"};
322 $states->{$job->{source
}}->{$job->{name
}} = $state;
325 delete $states->{$job->{source
}}->{$job->{name
}};
326 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
329 $text = encode_json
($states);
333 move
("$STATE.new", $STATE);
348 my $header = "SHELL=/bin/sh\n";
349 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
351 my $fh = IO
::File-
>new("< $CRONJOBS");
352 die "Could not open file $CRONJOBS: $!\n" if !$fh;
357 while (my $line = shift(@test)) {
359 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
361 next if $job->{state} eq "del";
362 $text .= format_job
($job, $line);
364 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
373 $text = "$header$text";
377 $text .= format_job
($job);
379 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
380 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
382 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
385 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
390 my ($job, $line) = @_;
393 if ($job->{state} eq "stopped") {
397 $line =~ /^#*(.+) root/;
400 $text .= "*/$INTERVAL * * * *";
403 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
404 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
405 $text .= " --method $job->{method}";
406 $text .= " --verbose" if $job->{verbose
};
414 my $cfg = read_cron
();
416 my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE");
418 my $states = read_state
();
419 foreach my $source (sort keys%{$cfg}) {
420 foreach my $name (sort keys%{$cfg->{$source}}) {
421 $list .= sprintf("%-25s", cut_target_width
($source, 25));
422 $list .= sprintf("%-15s", cut_target_width
($name, 15));
423 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
424 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync
});
425 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
435 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
439 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF$target->{vmid}.conf"]) };
441 return "qemu" if $res;
443 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF$target->{vmid}.conf"]) };
445 return "lxc" if $res;
453 my $cfg = read_cron
();
455 my $job = param_to_job
($param);
457 $job->{state} = "ok";
460 my $source = parse_target
($param->{source
});
461 my $dest = parse_target
($param->{dest
});
463 if (my $ip = $dest->{ip
}) {
464 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
467 if (my $ip = $source->{ip
}) {
468 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
471 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
473 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
475 die "Pool $source->{path} does not exists\n" if undef($check);
477 my $vm_type = vm_exists
($source);
479 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !$vm_type;
481 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
487 sync
($param) if !$param->{skip
};
498 my $cfg = read_cron
();
500 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
501 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
503 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
504 $job->{name
} = $param->{name
};
505 $job->{source
} = $param->{source
};
506 $job = add_state_to_job
($job);
514 my $job = get_job
($param);
515 $job->{state} = "del";
524 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
525 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
528 my $date = get_date
();
531 $job = get_job
($param);
534 if ($job && $job->{state} eq "syncing") {
535 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
538 my $dest = parse_target
($param->{dest
});
539 my $source = parse_target
($param->{source
});
541 my $sync_path = sub {
542 my ($source, $dest, $job, $param, $date) = @_;
544 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
546 snapshot_add
($source, $dest, $param->{name
}, $date);
548 send_image
($source, $dest, $param);
550 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
554 my $vm_type = vm_exists
($source);
555 $source->{vm_type
} = $vm_type;
558 $job->{state} = "syncing";
559 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
564 if ($source->{vmid
}) {
565 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
566 my $disks = get_disks
($source);
568 foreach my $disk (sort keys %{$disks}) {
569 $source->{all
} = $disks->{$disk}->{all
};
570 $source->{pool
} = $disks->{$disk}->{pool
};
571 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
572 $source->{last_part
} = $disks->{$disk}->{last_part
};
573 &$sync_path($source, $dest, $job, $param, $date);
575 if ($param->{method} eq "ssh") {
576 send_config
($source, $dest,'ssh');
579 &$sync_path($source, $dest, $job, $param, $date);
584 $job->{state} = "error";
588 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
594 $job->{state} = "ok";
595 $job->{lsync
} = $date;
604 my ($source, $dest, $max_snap, $name) = @_;
607 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
608 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
609 push @$cmd, $source->{all
};
611 my $raw = run_cmd
($cmd);
614 my $last_snap = undef;
617 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
619 if ($line =~ m/(rep_$name.*)$/) {
620 $last_snap = $1 if (!$last_snap);
623 if ($index == $max_snap) {
624 $source->{destroy
} = 1;
630 return ($old_snap, $last_snap) if $last_snap;
636 my ($source, $dest, $name, $date) = @_;
638 my $snap_name = "rep_$name\_".$date;
640 $source->{new_snap
} = $snap_name;
642 my $path = "$source->{all}\@$snap_name";
645 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
646 push @$cmd, 'zfs', 'snapshot', $path;
652 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
660 my $text = "SHELL=/bin/sh\n";
661 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
663 my $fh = IO
::File-
>new("> $CRONJOBS");
664 die "Could not open file: $!\n" if !$fh;
666 foreach my $source (sort keys%{$cfg}) {
667 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
668 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
669 $text .= "$PROG_PATH sync";
670 $text .= " -source ";
671 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
672 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
673 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
675 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
676 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
677 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
680 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
681 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
682 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
683 $text .= " -name $sync_name ";
684 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
685 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
689 die "Can't write to cron\n" if (!print($fh $text));
697 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
698 push @$cmd, 'qm', 'config', $target->{vmid
};
700 my $res = run_cmd
($cmd);
702 my $disks = parse_disks
($res, $target->{ip
});
709 print "Start CMD\n" if $DEBUG;
710 print Dumper
$cmd if $DEBUG;
711 if (ref($cmd) eq 'ARRAY') {
712 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
714 my $output = `$cmd 2>&1`;
716 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
719 print Dumper
$output if $DEBUG;
720 print "END CMD\n" if $DEBUG;
725 my ($text, $ip) = @_;
730 while ($text && $text =~ s/^(.*?)(\n|$)//) {
733 next if $line =~ /cdrom|none/;
734 next if $line !~ m/^(?:virtio|ide|scsi|sata)\d+: /;
738 if($line =~ m/^(?:virtio|ide|scsi|sata)\d+: (.+:)([A-Za-z0-9\-]+),(.*)$/) {
742 die "disk is not on ZFS Storage\n";
746 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
747 push @$cmd, 'pvesm', 'path', "$stor$disk";
748 my $path = run_cmd
($cmd);
750 if ($path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
752 my @array = split('/', $1);
753 $disks->{$num}->{pool
} = shift(@array);
754 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
756 $disks->{$num}->{path
} = join('/', @array);
757 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
759 $disks->{$num}->{last_part
} = $disk;
760 $disks->{$num}->{all
} .= "\/$disk";
765 die "ERROR: in path\n";
772 sub snapshot_destroy
{
773 my ($source, $dest, $method, $snap) = @_;
775 my @zfscmd = ('zfs', 'destroy');
776 my $snapshot = "$source->{all}\@$snap";
779 if($source->{ip
} && $method eq 'ssh'){
780 run_cmd
(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
782 run_cmd
([@zfscmd, $snapshot]);
789 my @ssh = $dest->{ip
} ?
('ssh', "root\@$dest->{ip}", '--') : ();
791 my $path = "$dest->{all}\/$source->{last_part}";
794 run_cmd
([@ssh, @zfscmd, "$path\@$snap"]);
803 my ($source ,$dest, $method) = @_;
806 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
807 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
808 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
811 eval {$text =run_cmd
($cmd);};
817 while ($text && $text =~ s/^(.*?)(\n|$)//) {
819 return 1 if $line =~ m/^.*$source->{old_snap}$/;
824 my ($source, $dest, $param) = @_;
828 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip
};
829 push @$cmd, 'zfs', 'send';
830 push @$cmd, '-v' if $param->{verbose
};
832 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
833 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
835 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
837 if ($param->{limit
}){
838 my $bwl = $param->{limit
}*1024;
839 push @$cmd, \'|', 'cstream
', '-t
', $bwl;
841 my $target = "$dest->{all}/$source->{last_part}";
845 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
846 push @$cmd, 'zfs', 'recv', '-F', '--';
847 push @$cmd, "$target";
854 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
861 my ($source, $dest, $method) = @_;
863 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
864 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
866 if ($method eq 'ssh'){
867 if ($dest->{ip
} && $source->{ip
}) {
868 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
869 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
870 } elsif ($dest->{ip
}) {
871 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
872 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
873 } elsif ($source->{ip
}) {
874 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
875 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
878 if ($source->{destroy
}){
879 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
881 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
883 run_cmd
(['rm', '-f', '--', $dest_target_old]);
890 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
891 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
897 my $cfg = read_cron
();
899 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
901 my $states = read_state
();
903 foreach my $source (sort keys%{$cfg}) {
904 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
905 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
906 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
907 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
917 my $job = get_job
($param);
918 $job->{state} = "ok";
926 my $job = get_job
($param);
927 $job->{state} = "stopped";
932 my $command = $ARGV[0];
934 my $commands = {'destroy' => 1,
943 if (!$command || !$commands->{$command}) {
948 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
949 \twill sync one time\n
951 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
953 \t\tmax sync speed in kBytes/s, default unlimited\n
954 \t-maxsnap\tinteger\n
955 \t\thow much snapshots will be kept before get erased, default 1/n
957 \t\tname of the sync job, if not set it is default.
958 \tIt is only necessary if scheduler allready contains this source.\n
960 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
962 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
963 \tCreate a sync Job\n
965 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
967 \t\tmax sync speed in kBytes/s, default unlimited\n
969 \t\thow much snapshots will be kept before get erased, default 1\n
971 \t\tname of the sync job, if not set it is default\n
973 \t\tif this flag is set it will skip the first sync\n
975 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
977 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
978 \tremove a sync Job from the scheduler\n
980 \t\tname of the sync job, if not set it is default\n
982 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
984 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
985 \tGet help about specified command.\n
988 \t-verbose\tboolean\n
989 \t\tVerbose output format.\n";
991 my $help_list = "$PROGNAME list\n
992 \tGet a List of all scheduled Sync Jobs\n";
994 my $help_status = "$PROGNAME status\n
995 \tGet the status of all scheduled Sync Jobs\n";
997 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
998 \tenable a syncjob and reset error\n
1000 \t\tname of the sync job, if not set it is default\n
1002 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1004 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
1007 \t\tname of the sync job, if not set it is default\n
1009 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1025 die "$help_destroy\n";
1029 die "$help_create\n";
1037 die "$help_status\n";
1041 die "$help_enable\n";
1045 die "$help_enable\n";
1052 my $param = parse_argv
(@arg);
1058 die "$help_destroy\n" if !$param->{source
};
1059 check_target
($param->{source
});
1060 destroy_job
($param);
1064 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1065 check_target
($param->{source
});
1066 check_target
($param->{dest
});
1071 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1072 check_target
($param->{source
});
1073 check_target
($param->{dest
});
1086 my $help_command = $ARGV[1];
1087 if ($help_command && $commands->{$help_command}) {
1088 print help
($help_command);
1090 if ($param->{verbose
} == 1){
1091 exec("man $PROGNAME");
1098 die "$help_enable\n" if !$param->{source
};
1099 check_target
($param->{source
});
1104 die "$help_disable\n" if !$param->{source
};
1105 check_target
($param->{source
});
1106 disable_job
($param);
1113 print("ERROR:\tno command specified\n") if !$help;
1114 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1115 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1116 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1117 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1118 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1119 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1120 print("\t$PROGNAME list\n");
1121 print("\t$PROGNAME status\n");
1122 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1127 parse_target
($target);
1134 pve-zsync - PVE ZFS Replication Manager
1138 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1140 pve-zsync help <cmd> [OPTIONS]
1142 Get help about specified command.
1150 Verbose output format.
1152 pve-zsync create -dest <string> -source <string> [OPTIONS]
1158 the destination target is like [IP]:<Pool>[/Path]
1162 max sync speed in kBytes/s, default unlimited
1166 how much snapshots will be kept before get erased, default 1
1170 name of the sync job, if not set it is default
1174 if this flag is set it will skip the first sync
1178 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1180 pve-zsync destroy -source <string> [OPTIONS]
1182 remove a sync Job from the scheduler
1186 name of the sync job, if not set it is default
1190 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1192 pve-zsync disable -source <string> [OPTIONS]
1198 name of the sync job, if not set it is default
1202 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1204 pve-zsync enable -source <string> [OPTIONS]
1206 enable a syncjob and reset error
1210 name of the sync job, if not set it is default
1214 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1217 Get a List of all scheduled Sync Jobs
1221 Get the status of all scheduled Sync Jobs
1223 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1229 the destination target is like [IP:]<Pool>[/Path]
1233 max sync speed in kBytes/s, default unlimited
1237 how much snapshots will be kept before get erased, default 1
1241 name of the sync job, if not set it is default.
1242 It is only necessary if scheduler allready contains this source.
1246 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1250 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1251 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1252 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.
1253 To config cron see man crontab.
1255 =head2 PVE ZFS Storage sync Tool
1257 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1261 add sync job from local VM to remote ZFS Server
1262 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1264 =head1 IMPORTANT FILES
1266 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1268 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1270 =head1 COPYRIGHT AND DISCLAIMER
1272 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1274 This program is free software: you can redistribute it and/or modify it
1275 under the terms of the GNU Affero General Public License as published
1276 by the Free Software Foundation, either version 3 of the License, or
1277 (at your option) any later version.
1279 This program is distributed in the hope that it will be useful, but
1280 WITHOUT ANY WARRANTY; without even the implied warranty of
1281 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1282 Affero General Public License for more details.
1284 You should have received a copy of the GNU Affero General Public
1285 License along with this program. If not, see
1286 <http://www.gnu.org/licenses/>.