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);
12 use String
::ShellQuote
'shell_quote';
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 $PVE_DIR = "/etc/pve/local";
20 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
21 my $LXC_CONF = "${PVE_DIR}/lxc";
22 my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
23 my $PROG_PATH = "$PATH/${PROGNAME}";
27 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
28 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
29 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
30 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
33 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
34 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
35 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
43 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
44 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
45 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
46 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
47 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
49 check_bin
('cstream');
54 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} =
56 die "Signal aborting sync\n";
62 foreach my $p (split (/:/, $ENV{PATH
})) {
69 die "unable to find command '$bin'\n";
72 sub cut_target_width
{
73 my ($path, $maxlen) = @_;
76 return $path if length($path) <= $maxlen;
78 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
80 $path =~ s
@/([^/]+/?
)$@@;
83 if (length($tail)+3 == $maxlen) {
85 } elsif (length($tail)+2 >= $maxlen) {
86 return '..'.substr($tail, -$maxlen+2)
89 $path =~ s
@(/[^/]+)(?
:/|$)@@;
91 my $both = length($head) + length($tail);
92 my $remaining = $maxlen-$both-4; # -4 for "/../"
95 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
98 substr($path, ($remaining/2), (length($path)-$remaining), '..');
99 return "$head/" . $path . "/$tail";
104 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
109 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
113 my ($source, $name, $status) = @_;
115 if ($status->{$source->{all
}}->{$name}->{status
}) {
122 sub check_pool_exists
{
128 push @$cmd, 'ssh', "root\@$target->{ip}", '--';
130 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
144 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
147 if ($text !~ $TARGETRE) {
151 $target->{ip
} = $1 if $1;
152 my @parts = split('/', $2);
154 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
156 my $pool = $target->{pool
} = shift(@parts);
157 die "$errstr\n" if !$pool;
159 if ($pool =~ m/^\d+$/) {
160 $target->{vmid
} = $pool;
161 delete $target->{pool
};
164 return $target if (@parts == 0);
165 $target->{last_part
} = pop(@parts);
171 $target->{path
} = join('/', @parts);
179 #This is for the first use to init file;
181 my $new_fh = IO
::File-
>new("> $CRONJOBS");
182 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
187 my $fh = IO
::File-
>new("< $CRONJOBS");
188 die "Could not open file $CRONJOBS: $!\n" if !$fh;
194 return encode_cron
(@text);
201 $param->{dest
} = undef;
202 $param->{source
} = undef;
203 $param->{verbose
} = undef;
204 $param->{limit
} = undef;
205 $param->{maxsnap
} = undef;
206 $param->{name
} = undef;
207 $param->{skip
} = undef;
208 $param->{method} = undef;
210 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
211 'dest=s' => \
$param->{dest
},
212 'source=s' => \
$param->{source
},
213 'verbose' => \
$param->{verbose
},
214 'limit=i' => \
$param->{limit
},
215 'maxsnap=i' => \
$param->{maxsnap
},
216 'name=s' => \
$param->{name
},
217 'skip' => \
$param->{skip
},
218 'method=s' => \
$param->{method});
221 die "can't parse options\n";
224 $param->{name
} = "default" if !$param->{name
};
225 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
226 $param->{method} = "ssh" if !$param->{method};
231 sub add_state_to_job
{
234 my $states = read_state
();
235 my $state = $states->{$job->{source
}}->{$job->{name
}};
237 $job->{state} = $state->{state};
238 $job->{lsync
} = $state->{lsync
};
239 $job->{vm_type
} = $state->{vm_type
};
241 for (my $i = 0; $state->{"snap$i"}; $i++) {
242 $job->{"snap$i"} = $state->{"snap$i"};
253 while (my $line = shift(@text)) {
255 my @arg = split('\s', $line);
256 my $param = parse_argv
(@arg);
258 if ($param->{source
} && $param->{dest
}) {
259 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
260 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
261 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
262 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
263 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
264 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
276 my $source = parse_target
($param->{source
});
277 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
279 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
280 $job->{dest
} = $param->{dest
} if $param->{dest
};
281 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
282 $job->{method} = "ssh" if !$job->{method};
283 $job->{limit
} = $param->{limit
};
284 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
285 $job->{source
} = $param->{source
};
293 make_path
$CONFIG_PATH;
294 my $new_fh = IO
::File-
>new("> $STATE");
295 die "Could not create $STATE: $!\n" if !$new_fh;
301 my $fh = IO
::File-
>new("< $STATE");
302 die "Could not open file $STATE: $!\n" if !$fh;
305 my $states = decode_json
($text);
319 $in_fh = IO
::File-
>new("< $STATE");
320 die "Could not open file $STATE: $!\n" if !$in_fh;
325 my $out_fh = IO
::File-
>new("> $STATE.new");
326 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
331 $states = decode_json
($text);
332 $state = $states->{$job->{source
}}->{$job->{name
}};
335 if ($job->{state} ne "del") {
336 $state->{state} = $job->{state};
337 $state->{lsync
} = $job->{lsync
};
338 $state->{vm_type
} = $job->{vm_type
};
340 for (my $i = 0; $job->{"snap$i"} ; $i++) {
341 $state->{"snap$i"} = $job->{"snap$i"};
343 $states->{$job->{source
}}->{$job->{name
}} = $state;
346 delete $states->{$job->{source
}}->{$job->{name
}};
347 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
350 $text = encode_json
($states);
354 move
("$STATE.new", $STATE);
369 my $header = "SHELL=/bin/sh\n";
370 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
372 my $fh = IO
::File-
>new("< $CRONJOBS");
373 die "Could not open file $CRONJOBS: $!\n" if !$fh;
378 while (my $line = shift(@test)) {
380 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
382 next if $job->{state} eq "del";
383 $text .= format_job
($job, $line);
385 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
394 $text = "$header$text";
398 $text .= format_job
($job);
400 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
401 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
403 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
406 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
411 my ($job, $line) = @_;
414 if ($job->{state} eq "stopped") {
418 $line =~ /^#*(.+) root/;
421 $text .= "*/$INTERVAL * * * *";
424 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
425 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
426 $text .= " --limit $job->{limit}" if $job->{limit
};
427 $text .= " --method $job->{method}";
428 $text .= " --verbose" if $job->{verbose
};
436 my $cfg = read_cron
();
438 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
440 my $states = read_state
();
441 foreach my $source (sort keys%{$cfg}) {
442 foreach my $name (sort keys%{$cfg->{$source}}) {
443 $list .= sprintf("%-25s", cut_target_width
($source, 25));
444 $list .= sprintf("%-25s", cut_target_width
($name, 25));
445 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
446 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
447 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
448 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
458 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
462 return undef if !defined($target->{vmid
});
464 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
466 return "qemu" if $res;
468 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
470 return "lxc" if $res;
478 my $cfg = read_cron
();
480 my $job = param_to_job
($param);
482 $job->{state} = "ok";
485 my $source = parse_target
($param->{source
});
486 my $dest = parse_target
($param->{dest
});
488 if (my $ip = $dest->{ip
}) {
489 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
492 if (my $ip = $source->{ip
}) {
493 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
496 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest);
498 if (!defined($source->{vmid
})) {
499 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source);
502 my $vm_type = vm_exists
($source);
503 $job->{vm_type
} = $vm_type;
504 $source->{vm_type
} = $vm_type;
506 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
508 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
510 #check if vm has zfs disks if not die;
511 get_disks
($source, 1) if $source->{vmid
};
517 sync
($param) if !$param->{skip
};
528 my $cfg = read_cron
();
530 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
531 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
533 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
534 $job->{name
} = $param->{name
};
535 $job->{source
} = $param->{source
};
536 $job = add_state_to_job
($job);
544 my $job = get_job
($param);
545 $job->{state} = "del";
554 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
555 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
558 my $date = get_date
();
561 $job = get_job
($param);
564 if ($job && $job->{state} eq "syncing") {
565 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
568 my $dest = parse_target
($param->{dest
});
569 my $source = parse_target
($param->{source
});
571 my $sync_path = sub {
572 my ($source, $dest, $job, $param, $date) = @_;
574 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
576 snapshot_add
($source, $dest, $param->{name
}, $date);
578 send_image
($source, $dest, $param);
580 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
584 my $vm_type = vm_exists
($source);
585 $source->{vm_type
} = $vm_type;
588 $job->{state} = "syncing";
589 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
594 if ($source->{vmid
}) {
595 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
596 my $disks = get_disks
($source);
598 foreach my $disk (sort keys %{$disks}) {
599 $source->{all
} = $disks->{$disk}->{all
};
600 $source->{pool
} = $disks->{$disk}->{pool
};
601 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
602 $source->{last_part
} = $disks->{$disk}->{last_part
};
603 &$sync_path($source, $dest, $job, $param, $date);
605 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
606 send_config
($source, $dest,'ssh');
608 send_config
($source, $dest,'local');
611 &$sync_path($source, $dest, $job, $param, $date);
616 $job->{state} = "error";
620 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
626 $job->{state} = "ok";
627 $job->{lsync
} = $date;
636 my ($source, $dest, $max_snap, $name) = @_;
639 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
640 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
641 push @$cmd, $source->{all
};
643 my $raw = run_cmd
($cmd);
646 my $last_snap = undef;
649 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
651 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
653 $last_snap = $1 if (!$last_snap);
656 if ($index == $max_snap) {
657 $source->{destroy
} = 1;
663 return ($old_snap, $last_snap) if $last_snap;
669 my ($source, $dest, $name, $date) = @_;
671 my $snap_name = "rep_$name\_".$date;
673 $source->{new_snap
} = $snap_name;
675 my $path = "$source->{all}\@$snap_name";
678 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
679 push @$cmd, 'zfs', 'snapshot', $path;
685 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
693 my $text = "SHELL=/bin/sh\n";
694 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
696 my $fh = IO
::File-
>new("> $CRONJOBS");
697 die "Could not open file: $!\n" if !$fh;
699 foreach my $source (sort keys%{$cfg}) {
700 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
701 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
702 $text .= "$PROG_PATH sync";
703 $text .= " -source ";
704 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
705 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
706 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
708 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
709 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
710 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
713 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
714 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
715 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
716 $text .= " -name $sync_name ";
717 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
718 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
722 die "Can't write to cron\n" if (!print($fh $text));
727 my ($target, $get_err) = @_;
730 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
732 if ($target->{vm_type
} eq 'qemu') {
733 push @$cmd, 'qm', 'config', $target->{vmid
};
734 } elsif ($target->{vm_type
} eq 'lxc') {
735 push @$cmd, 'pct', 'config', $target->{vmid
};
737 die "VM Type unknown\n";
740 my $res = run_cmd
($cmd);
742 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
749 print "Start CMD\n" if $DEBUG;
750 print Dumper
$cmd if $DEBUG;
751 if (ref($cmd) eq 'ARRAY') {
752 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
754 my $output = `$cmd 2>&1`;
756 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
759 print Dumper
$output if $DEBUG;
760 print "END CMD\n" if $DEBUG;
765 my ($text, $ip, $vm_type, $get_err) = @_;
770 while ($text && $text =~ s/^(.*?)(\n|$)//) {
772 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
774 next if $line =~ /cdrom|none/;
775 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
777 #QEMU if backup is not set include in sync
778 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
780 #LXC if backup is not set do no in sync
781 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
785 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
786 my @parameter = split(/,/,$1);
788 foreach my $opt (@parameter) {
789 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
797 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
802 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
803 push @$cmd, 'pvesm', 'path', "$stor$disk";
804 my $path = run_cmd($cmd);
806 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
808 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
810 my @array = split('/', $1);
811 $disks->{$num}->{pool} = shift(@array);
812 $disks->{$num}->{all} = $disks->{$num}->{pool};
814 $disks->{$num}->{path} = join('/', @array);
815 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
817 $disks->{$num}->{last_part} = $disk;
818 $disks->{$num}->{all} .= "\
/$disk";
821 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
823 $disks->{$num}->{pool} = $1;
824 $disks->{$num}->{all} = $disks->{$num}->{pool};
827 $disks->{$num}->{path} = $3;
828 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
831 $disks->{$num}->{last_part} = $disk;
832 $disks->{$num}->{all} .= "\
/$disk";
837 die "ERROR
: in path
\n";
841 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
845 sub snapshot_destroy {
846 my ($source, $dest, $method, $snap) = @_;
848 my @zfscmd = ('zfs', 'destroy');
849 my $snapshot = "$source->{all
}\
@$snap";
852 if($source->{ip} && $method eq 'ssh'){
853 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
855 run_cmd([@zfscmd, $snapshot]);
862 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
864 my $path = "$dest->{all
}\
/$source->{last_part
}";
867 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
876 my ($source , $dest, $method) = @_;
879 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
880 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
881 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
884 eval {$text =run_cmd($cmd);};
890 while ($text && $text =~ s/^(.*?)(\n|$)//) {
892 return 1 if $line =~ m/^.*$source->{old_snap}$/;
897 my ($source, $dest, $param) = @_;
901 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$source->{ip
}", '--' if $source->{ip};
902 push @$cmd, 'zfs', 'send';
903 push @$cmd, '-v' if $param->{verbose};
905 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
906 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
908 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
910 if ($param->{limit}){
911 my $bwl = $param->{limit}*1024;
912 push @$cmd, \'|', 'cstream', '-t', $bwl;
914 my $target = "$dest->{all
}/$source->{last_part
}";
918 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$dest->{ip
}", '--' if $dest->{ip};
919 push @$cmd, 'zfs', 'recv', '-F', '--';
920 push @$cmd, "$target";
927 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
934 my ($source, $dest, $method) = @_;
936 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
937 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
939 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
941 $dest_target_new = $config_dir.'/'.$dest_target_new;
943 if ($method eq 'ssh'){
944 if ($dest->{ip} && $source->{ip}) {
945 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
946 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
947 } elsif ($dest->{ip}) {
948 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
949 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
950 } elsif ($source->{ip}) {
951 run_cmd(['mkdir', '-p', '--', $config_dir]);
952 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
955 if ($source->{destroy}){
956 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
958 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
960 run_cmd(['rm', '-f', '--', $dest_target_old]);
963 } elsif ($method eq 'local') {
964 run_cmd(['mkdir', '-p', '--', $config_dir]);
965 run_cmd(['cp', $source_target, $dest_target_new]);
970 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
971 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
977 my $cfg = read_cron();
979 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
981 my $states = read_state();
983 foreach my $source (sort keys%{$cfg}) {
984 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
985 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
986 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
987 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
997 my $job = get_job($param);
998 $job->{state} = "ok
";
1006 my $job = get_job($param);
1007 $job->{state} = "stopped
";
1012 my $command = $ARGV[0];
1014 my $commands = {'destroy' => 1,
1023 if (!$command || !$commands->{$command}) {
1028 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1029 \twill sync one
time\n
1031 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1033 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1034 \t-maxsnap
\tinteger
\n
1035 \t\thow much snapshots will be kept before get erased
, default 1/n
1037 \t\tname of the sync job
, if not set it
is default.
1038 \tIt
is only necessary
if scheduler allready contains this source
.\n
1040 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1042 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1043 \tCreate a sync Job
\n
1045 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1047 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1048 \t-maxsnap
\tstring
\n
1049 \t\thow much snapshots will be kept before get erased
, default 1\n
1051 \t\tname of the sync job
, if not set it
is default\n
1053 \t\tif this flag
is set it will skip the first sync
\n
1055 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1057 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1058 \tremove a sync Job from the scheduler
\n
1060 \t\tname of the sync job
, if not set it
is default\n
1062 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1064 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1065 \tGet help about specified command
.\n
1068 \t-verbose
\tboolean
\n
1069 \t\tVerbose output format
.\n";
1071 my $help_list = "$PROGNAME list
\n
1072 \tGet a List of all scheduled Sync Jobs
\n";
1074 my $help_status = "$PROGNAME status
\n
1075 \tGet the status of all scheduled Sync Jobs
\n";
1077 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1078 \tenable a syncjob
and reset error
\n
1080 \t\tname of the sync job
, if not set it
is default\n
1082 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1084 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1087 \t\tname of the sync job
, if not set it
is default\n
1089 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1094 if ($command eq 'help') {
1097 } elsif ($command eq 'sync') {
1100 } elsif ($command eq 'destroy') {
1101 die "$help_destroy\n";
1103 } elsif ($command eq 'create') {
1104 die "$help_create\n";
1106 } elsif ($command eq 'list') {
1109 } elsif ($command eq 'status') {
1110 die "$help_status\n";
1112 } elsif ($command eq 'enable') {
1113 die "$help_enable\n";
1115 } elsif ($command eq 'disable') {
1116 die "$help_disable\n";
1123 my $param = parse_argv(@arg);
1125 if ($command eq 'destroy') {
1126 die "$help_destroy\n" if !$param->{source};
1128 check_target($param->{source});
1129 destroy_job($param);
1131 } elsif ($command eq 'sync') {
1132 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1134 check_target($param->{source});
1135 check_target($param->{dest});
1138 } elsif ($command eq 'create') {
1139 die "$help_create\n" if !$param->{source} || !$param->{dest};
1141 check_target($param->{source});
1142 check_target($param->{dest});
1145 } elsif ($command eq 'status') {
1148 } elsif ($command eq 'list') {
1151 } elsif ($command eq 'help') {
1152 my $help_command = $ARGV[1];
1154 if ($help_command && $commands->{$help_command}) {
1155 print help($help_command);
1158 if ($param->{verbose} == 1){
1159 exec("man
$PROGNAME");
1166 } elsif ($command eq 'enable') {
1167 die "$help_enable\n" if !$param->{source};
1169 check_target($param->{source});
1172 } elsif ($command eq 'disable') {
1173 die "$help_disable\n" if !$param->{source};
1175 check_target($param->{source});
1176 disable_job($param);
1183 print("ERROR
:\tno command specified
\n") if !$help;
1184 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1185 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1186 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1187 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1188 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1189 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1190 print("\t$PROGNAME list
\n");
1191 print("\t$PROGNAME status
\n");
1192 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1197 parse_target($target);
1204 pve-zsync - PVE ZFS Replication Manager
1208 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1210 pve-zsync help <cmd> [OPTIONS]
1212 Get help about specified command.
1220 Verbose output format.
1222 pve-zsync create -dest <string> -source <string> [OPTIONS]
1228 the destination target is like [IP]:<Pool>[/Path]
1232 max sync speed in kBytes/s, default unlimited
1236 how much snapshots will be kept before get erased, default 1
1240 name of the sync job, if not set it is default
1244 if this flag is set it will skip the first sync
1248 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1250 pve-zsync destroy -source <string> [OPTIONS]
1252 remove a sync Job from the scheduler
1256 name of the sync job, if not set it is default
1260 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1262 pve-zsync disable -source <string> [OPTIONS]
1268 name of the sync job, if not set it is default
1272 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1274 pve-zsync enable -source <string> [OPTIONS]
1276 enable a syncjob and reset error
1280 name of the sync job, if not set it is default
1284 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1287 Get a List of all scheduled Sync Jobs
1291 Get the status of all scheduled Sync Jobs
1293 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1299 the destination target is like [IP:]<Pool>[/Path]
1303 max sync speed in kBytes/s, default unlimited
1307 how much snapshots will be kept before get erased, default 1
1311 name of the sync job, if not set it is default.
1312 It is only necessary if scheduler allready contains this source.
1316 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1320 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1321 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1322 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.
1323 To config cron see man crontab.
1325 =head2 PVE ZFS Storage sync Tool
1327 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1331 add sync job from local VM to remote ZFS Server
1332 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1334 =head1 IMPORTANT FILES
1336 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1338 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1340 =head1 COPYRIGHT AND DISCLAIMER
1342 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1344 This program is free software: you can redistribute it and/or modify it
1345 under the terms of the GNU Affero General Public License as published
1346 by the Free Software Foundation, either version 3 of the License, or
1347 (at your option) any later version.
1349 This program is distributed in the hope that it will be useful, but
1350 WITHOUT ANY WARRANTY; without even the implied warranty of
1351 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1352 Affero General Public License for more details.
1354 You should have received a copy of the GNU Affero General Public
1355 License along with this program. If not, see
1356 <http://www.gnu.org/licenses/>.