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
{
112 push @$cmd, 'ssh', "root\@$target->{ip}", '--';
114 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
128 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
131 if ($text !~ $TARGETRE) {
135 $target->{ip
} = $1 if $1;
136 my @parts = split('/', $2);
138 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
140 my $pool = $target->{pool
} = shift(@parts);
141 die "$errstr\n" if !$pool;
143 if ($pool =~ m/^\d+$/) {
144 $target->{vmid
} = $pool;
145 delete $target->{pool
};
148 return $target if (@parts == 0);
149 $target->{last_part
} = pop(@parts);
155 $target->{path
} = join('/', @parts);
163 #This is for the first use to init file;
165 my $new_fh = IO
::File-
>new("> $CRONJOBS");
166 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
171 my $fh = IO
::File-
>new("< $CRONJOBS");
172 die "Could not open file $CRONJOBS: $!\n" if !$fh;
178 return encode_cron
(@text);
185 $param->{dest
} = undef;
186 $param->{source
} = undef;
187 $param->{verbose
} = undef;
188 $param->{limit
} = undef;
189 $param->{maxsnap
} = undef;
190 $param->{name
} = undef;
191 $param->{skip
} = undef;
192 $param->{method} = undef;
194 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
195 'dest=s' => \
$param->{dest
},
196 'source=s' => \
$param->{source
},
197 'verbose' => \
$param->{verbose
},
198 'limit=i' => \
$param->{limit
},
199 'maxsnap=i' => \
$param->{maxsnap
},
200 'name=s' => \
$param->{name
},
201 'skip' => \
$param->{skip
},
202 'method=s' => \
$param->{method});
205 die "can't parse options\n";
208 $param->{name
} = "default" if !$param->{name
};
209 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
210 $param->{method} = "ssh" if !$param->{method};
215 sub add_state_to_job
{
218 my $states = read_state
();
219 my $state = $states->{$job->{source
}}->{$job->{name
}};
221 $job->{state} = $state->{state};
222 $job->{lsync
} = $state->{lsync
};
223 $job->{vm_type
} = $state->{vm_type
};
225 for (my $i = 0; $state->{"snap$i"}; $i++) {
226 $job->{"snap$i"} = $state->{"snap$i"};
237 while (my $line = shift(@text)) {
239 my @arg = split('\s', $line);
240 my $param = parse_argv
(@arg);
242 if ($param->{source
} && $param->{dest
}) {
243 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
246 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
247 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
248 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
260 my $source = parse_target
($param->{source
});
261 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
263 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
264 $job->{dest
} = $param->{dest
} if $param->{dest
};
265 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
266 $job->{method} = "ssh" if !$job->{method};
267 $job->{limit
} = $param->{limit
};
268 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
269 $job->{source
} = $param->{source
};
277 make_path
$CONFIG_PATH;
278 my $new_fh = IO
::File-
>new("> $STATE");
279 die "Could not create $STATE: $!\n" if !$new_fh;
285 my $fh = IO
::File-
>new("< $STATE");
286 die "Could not open file $STATE: $!\n" if !$fh;
289 my $states = decode_json
($text);
303 $in_fh = IO
::File-
>new("< $STATE");
304 die "Could not open file $STATE: $!\n" if !$in_fh;
309 my $out_fh = IO
::File-
>new("> $STATE.new");
310 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
315 $states = decode_json
($text);
316 $state = $states->{$job->{source
}}->{$job->{name
}};
319 if ($job->{state} ne "del") {
320 $state->{state} = $job->{state};
321 $state->{lsync
} = $job->{lsync
};
322 $state->{vm_type
} = $job->{vm_type
};
324 for (my $i = 0; $job->{"snap$i"} ; $i++) {
325 $state->{"snap$i"} = $job->{"snap$i"};
327 $states->{$job->{source
}}->{$job->{name
}} = $state;
330 delete $states->{$job->{source
}}->{$job->{name
}};
331 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
334 $text = encode_json
($states);
338 move
("$STATE.new", $STATE);
353 my $header = "SHELL=/bin/sh\n";
354 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
356 my $fh = IO
::File-
>new("< $CRONJOBS");
357 die "Could not open file $CRONJOBS: $!\n" if !$fh;
362 while (my $line = shift(@test)) {
364 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
366 next if $job->{state} eq "del";
367 $text .= format_job
($job, $line);
369 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
378 $text = "$header$text";
382 $text .= format_job
($job);
384 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
385 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
387 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
390 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
395 my ($job, $line) = @_;
398 if ($job->{state} eq "stopped") {
402 $line =~ /^#*(.+) root/;
405 $text .= "*/$INTERVAL * * * *";
408 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
409 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
410 $text .= " --limit $job->{limit}" if $job->{limit
};
411 $text .= " --method $job->{method}";
412 $text .= " --verbose" if $job->{verbose
};
420 my $cfg = read_cron
();
422 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
424 my $states = read_state
();
425 foreach my $source (sort keys%{$cfg}) {
426 foreach my $name (sort keys%{$cfg->{$source}}) {
427 $list .= sprintf("%-25s", cut_target_width
($source, 25));
428 $list .= sprintf("%-25s", cut_target_width
($name, 25));
429 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
430 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
431 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
432 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
442 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
446 return undef if !defined($target->{vmid
});
448 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
450 return "qemu" if $res;
452 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
454 return "lxc" if $res;
462 my $cfg = read_cron
();
464 my $job = param_to_job
($param);
466 $job->{state} = "ok";
469 my $source = parse_target
($param->{source
});
470 my $dest = parse_target
($param->{dest
});
472 if (my $ip = $dest->{ip
}) {
473 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
476 if (my $ip = $source->{ip
}) {
477 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
480 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
482 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
484 die "Pool $source->{path} does not exists\n" if undef($check);
486 my $vm_type = vm_exists
($source);
487 $job->{vm_type
} = $vm_type;
488 $source->{vm_type
} = $vm_type;
490 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
492 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
494 #check if vm has zfs disks if not die;
495 get_disks
($source, 1) if $source->{vmid
};
501 sync
($param) if !$param->{skip
};
512 my $cfg = read_cron
();
514 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
515 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
517 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
518 $job->{name
} = $param->{name
};
519 $job->{source
} = $param->{source
};
520 $job = add_state_to_job
($job);
528 my $job = get_job
($param);
529 $job->{state} = "del";
538 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
539 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
542 my $date = get_date
();
545 $job = get_job
($param);
548 if ($job && $job->{state} eq "syncing") {
549 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
552 my $dest = parse_target
($param->{dest
});
553 my $source = parse_target
($param->{source
});
555 my $sync_path = sub {
556 my ($source, $dest, $job, $param, $date) = @_;
558 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
560 snapshot_add
($source, $dest, $param->{name
}, $date);
562 send_image
($source, $dest, $param);
564 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
568 my $vm_type = vm_exists
($source);
569 $source->{vm_type
} = $vm_type;
572 $job->{state} = "syncing";
573 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
578 if ($source->{vmid
}) {
579 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
580 my $disks = get_disks
($source);
582 foreach my $disk (sort keys %{$disks}) {
583 $source->{all
} = $disks->{$disk}->{all
};
584 $source->{pool
} = $disks->{$disk}->{pool
};
585 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
586 $source->{last_part
} = $disks->{$disk}->{last_part
};
587 &$sync_path($source, $dest, $job, $param, $date);
589 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
590 send_config
($source, $dest,'ssh');
592 send_config
($source, $dest,'local');
595 &$sync_path($source, $dest, $job, $param, $date);
600 $job->{state} = "error";
604 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
610 $job->{state} = "ok";
611 $job->{lsync
} = $date;
620 my ($source, $dest, $max_snap, $name) = @_;
623 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
624 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
625 push @$cmd, $source->{all
};
627 my $raw = run_cmd
($cmd);
630 my $last_snap = undef;
633 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
635 if ($line =~ m/(rep_$name.*)$/) {
637 $last_snap = $1 if (!$last_snap);
640 if ($index == $max_snap) {
641 $source->{destroy
} = 1;
647 return ($old_snap, $last_snap) if $last_snap;
653 my ($source, $dest, $name, $date) = @_;
655 my $snap_name = "rep_$name\_".$date;
657 $source->{new_snap
} = $snap_name;
659 my $path = "$source->{all}\@$snap_name";
662 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
663 push @$cmd, 'zfs', 'snapshot', $path;
669 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
677 my $text = "SHELL=/bin/sh\n";
678 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
680 my $fh = IO
::File-
>new("> $CRONJOBS");
681 die "Could not open file: $!\n" if !$fh;
683 foreach my $source (sort keys%{$cfg}) {
684 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
685 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
686 $text .= "$PROG_PATH sync";
687 $text .= " -source ";
688 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
689 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
690 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
692 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
693 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
694 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
697 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
698 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
699 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
700 $text .= " -name $sync_name ";
701 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
702 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
706 die "Can't write to cron\n" if (!print($fh $text));
711 my ($target, $get_err) = @_;
714 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
716 if ($target->{vm_type
} eq 'qemu') {
717 push @$cmd, 'qm', 'config', $target->{vmid
};
718 } elsif ($target->{vm_type
} eq 'lxc') {
719 push @$cmd, 'pct', 'config', $target->{vmid
};
721 die "VM Type unknown\n";
724 my $res = run_cmd
($cmd);
726 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
733 print "Start CMD\n" if $DEBUG;
734 print Dumper
$cmd if $DEBUG;
735 if (ref($cmd) eq 'ARRAY') {
736 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
738 my $output = `$cmd 2>&1`;
740 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
743 print Dumper
$output if $DEBUG;
744 print "END CMD\n" if $DEBUG;
749 my ($text, $ip, $vm_type, $get_err) = @_;
754 while ($text && $text =~ s/^(.*?)(\n|$)//) {
756 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
758 next if $line =~ /cdrom|none/;
759 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
761 #QEMU if backup is not set include in sync
762 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
764 #LXC if backup is not set do no in sync
765 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
769 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
770 my @parameter = split(/,/,$1);
772 foreach my $opt (@parameter) {
773 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
781 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
786 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
787 push @$cmd, 'pvesm', 'path', "$stor$disk";
788 my $path = run_cmd($cmd);
790 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
792 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
794 my @array = split('/', $1);
795 $disks->{$num}->{pool} = shift(@array);
796 $disks->{$num}->{all} = $disks->{$num}->{pool};
798 $disks->{$num}->{path} = join('/', @array);
799 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
801 $disks->{$num}->{last_part} = $disk;
802 $disks->{$num}->{all} .= "\
/$disk";
805 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
807 $disks->{$num}->{pool} = $1;
808 $disks->{$num}->{all} = $disks->{$num}->{pool};
811 $disks->{$num}->{path} = $3;
812 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
815 $disks->{$num}->{last_part} = $disk;
816 $disks->{$num}->{all} .= "\
/$disk";
821 die "ERROR
: in path
\n";
825 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
829 sub snapshot_destroy {
830 my ($source, $dest, $method, $snap) = @_;
832 my @zfscmd = ('zfs', 'destroy');
833 my $snapshot = "$source->{all
}\
@$snap";
836 if($source->{ip} && $method eq 'ssh'){
837 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
839 run_cmd([@zfscmd, $snapshot]);
846 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
848 my $path = "$dest->{all
}\
/$source->{last_part
}";
851 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
860 my ($source , $dest, $method) = @_;
863 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
864 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
865 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
868 eval {$text =run_cmd($cmd);};
874 while ($text && $text =~ s/^(.*?)(\n|$)//) {
876 return 1 if $line =~ m/^.*$source->{old_snap}$/;
881 my ($source, $dest, $param) = @_;
885 push @$cmd, 'ssh', "root\
@$source->{ip
}", '--' if $source->{ip};
886 push @$cmd, 'zfs', 'send';
887 push @$cmd, '-v' if $param->{verbose};
889 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
890 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
892 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
894 if ($param->{limit}){
895 my $bwl = $param->{limit}*1024;
896 push @$cmd, \'|', 'cstream', '-t', $bwl;
898 my $target = "$dest->{all
}/$source->{last_part
}";
902 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
903 push @$cmd, 'zfs', 'recv', '-F', '--';
904 push @$cmd, "$target";
911 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
918 my ($source, $dest, $method) = @_;
920 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
921 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
923 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
925 $dest_target_new = $config_dir.'/'.$dest_target_new;
927 if ($method eq 'ssh'){
928 if ($dest->{ip} && $source->{ip}) {
929 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
930 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
931 } elsif ($dest->{ip}) {
932 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
933 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
934 } elsif ($source->{ip}) {
935 run_cmd(['mkdir', '-p', '--', $config_dir]);
936 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
939 if ($source->{destroy}){
940 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
942 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
944 run_cmd(['rm', '-f', '--', $dest_target_old]);
947 } elsif ($method eq 'local') {
948 run_cmd(['mkdir', '-p', '--', $config_dir]);
949 run_cmd(['cp', $source_target, $dest_target_new]);
954 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
955 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
961 my $cfg = read_cron();
963 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
965 my $states = read_state();
967 foreach my $source (sort keys%{$cfg}) {
968 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
969 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
970 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
971 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
981 my $job = get_job($param);
982 $job->{state} = "ok
";
990 my $job = get_job($param);
991 $job->{state} = "stopped
";
996 my $command = $ARGV[0];
998 my $commands = {'destroy' => 1,
1007 if (!$command || !$commands->{$command}) {
1012 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1013 \twill sync one
time\n
1015 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1017 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1018 \t-maxsnap
\tinteger
\n
1019 \t\thow much snapshots will be kept before get erased
, default 1/n
1021 \t\tname of the sync job
, if not set it
is default.
1022 \tIt
is only necessary
if scheduler allready contains this source
.\n
1024 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1026 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1027 \tCreate a sync Job
\n
1029 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1031 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1032 \t-maxsnap
\tstring
\n
1033 \t\thow much snapshots will be kept before get erased
, default 1\n
1035 \t\tname of the sync job
, if not set it
is default\n
1037 \t\tif this flag
is set it will skip the first sync
\n
1039 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1041 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1042 \tremove a sync Job from the scheduler
\n
1044 \t\tname of the sync job
, if not set it
is default\n
1046 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1048 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1049 \tGet help about specified command
.\n
1052 \t-verbose
\tboolean
\n
1053 \t\tVerbose output format
.\n";
1055 my $help_list = "$PROGNAME list
\n
1056 \tGet a List of all scheduled Sync Jobs
\n";
1058 my $help_status = "$PROGNAME status
\n
1059 \tGet the status of all scheduled Sync Jobs
\n";
1061 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1062 \tenable a syncjob
and reset error
\n
1064 \t\tname of the sync job
, if not set it
is default\n
1066 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1068 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1071 \t\tname of the sync job
, if not set it
is default\n
1073 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1089 die "$help_destroy\n";
1093 die "$help_create\n";
1101 die "$help_status\n";
1105 die "$help_enable\n";
1109 die "$help_enable\n";
1116 my $param = parse_argv(@arg);
1122 die "$help_destroy\n" if !$param->{source};
1123 check_target($param->{source});
1124 destroy_job($param);
1128 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1129 check_target($param->{source});
1130 check_target($param->{dest});
1135 die "$help_create\n" if !$param->{source} || !$param->{dest};
1136 check_target($param->{source});
1137 check_target($param->{dest});
1150 my $help_command = $ARGV[1];
1151 if ($help_command && $commands->{$help_command}) {
1152 print help($help_command);
1154 if ($param->{verbose} == 1){
1155 exec("man
$PROGNAME");
1162 die "$help_enable\n" if !$param->{source};
1163 check_target($param->{source});
1168 die "$help_disable\n" if !$param->{source};
1169 check_target($param->{source});
1170 disable_job($param);
1177 print("ERROR
:\tno command specified
\n") if !$help;
1178 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1179 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1180 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1181 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1182 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1183 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1184 print("\t$PROGNAME list
\n");
1185 print("\t$PROGNAME status
\n");
1186 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1191 parse_target($target);
1198 pve-zsync - PVE ZFS Replication Manager
1202 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1204 pve-zsync help <cmd> [OPTIONS]
1206 Get help about specified command.
1214 Verbose output format.
1216 pve-zsync create -dest <string> -source <string> [OPTIONS]
1222 the destination target is like [IP]:<Pool>[/Path]
1226 max sync speed in kBytes/s, default unlimited
1230 how much snapshots will be kept before get erased, default 1
1234 name of the sync job, if not set it is default
1238 if this flag is set it will skip the first sync
1242 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1244 pve-zsync destroy -source <string> [OPTIONS]
1246 remove a sync Job from the scheduler
1250 name of the sync job, if not set it is default
1254 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1256 pve-zsync disable -source <string> [OPTIONS]
1262 name of the sync job, if not set it is default
1266 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1268 pve-zsync enable -source <string> [OPTIONS]
1270 enable a syncjob and reset error
1274 name of the sync job, if not set it is default
1278 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1281 Get a List of all scheduled Sync Jobs
1285 Get the status of all scheduled Sync Jobs
1287 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1293 the destination target is like [IP:]<Pool>[/Path]
1297 max sync speed in kBytes/s, default unlimited
1301 how much snapshots will be kept before get erased, default 1
1305 name of the sync job, if not set it is default.
1306 It is only necessary if scheduler allready contains this source.
1310 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1314 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1315 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1316 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.
1317 To config cron see man crontab.
1319 =head2 PVE ZFS Storage sync Tool
1321 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1325 add sync job from local VM to remote ZFS Server
1326 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1328 =head1 IMPORTANT FILES
1330 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1332 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1334 =head1 COPYRIGHT AND DISCLAIMER
1336 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1338 This program is free software: you can redistribute it and/or modify it
1339 under the terms of the GNU Affero General Public License as published
1340 by the Free Software Foundation, either version 3 of the License, or
1341 (at your option) any later version.
1343 This program is distributed in the hope that it will be useful, but
1344 WITHOUT ANY WARRANTY; without even the implied warranty of
1345 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1346 Affero General Public License for more details.
1348 You should have received a copy of the GNU Affero General Public
1349 License along with this program. If not, see
1350 <http://www.gnu.org/licenses/>.