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
};
220 $job->{vm_type
} = $state->{vm_type
};
222 for (my $i = 0; $state->{"snap$i"}; $i++) {
223 $job->{"snap$i"} = $state->{"snap$i"};
234 while (my $line = shift(@text)) {
236 my @arg = split('\s', $line);
237 my $param = parse_argv
(@arg);
239 if ($param->{source
} && $param->{dest
}) {
240 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
257 my $source = parse_target
($param->{source
});
258 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
260 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
261 $job->{dest
} = $param->{dest
} if $param->{dest
};
262 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
263 $job->{method} = "ssh" if !$job->{method};
264 $job->{limit
} = $param->{limit
};
265 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
266 $job->{source
} = $param->{source
};
274 make_path
$CONFIG_PATH;
275 my $new_fh = IO
::File-
>new("> $STATE");
276 die "Could not create $STATE: $!\n" if !$new_fh;
282 my $fh = IO
::File-
>new("< $STATE");
283 die "Could not open file $STATE: $!\n" if !$fh;
286 my $states = decode_json
($text);
300 $in_fh = IO
::File-
>new("< $STATE");
301 die "Could not open file $STATE: $!\n" if !$in_fh;
306 my $out_fh = IO
::File-
>new("> $STATE.new");
307 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
312 $states = decode_json
($text);
313 $state = $states->{$job->{source
}}->{$job->{name
}};
316 if ($job->{state} ne "del") {
317 $state->{state} = $job->{state};
318 $state->{lsync
} = $job->{lsync
};
319 $state->{vm_type
} = $job->{vm_type
};
321 for (my $i = 0; $job->{"snap$i"} ; $i++) {
322 $state->{"snap$i"} = $job->{"snap$i"};
324 $states->{$job->{source
}}->{$job->{name
}} = $state;
327 delete $states->{$job->{source
}}->{$job->{name
}};
328 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
331 $text = encode_json
($states);
335 move
("$STATE.new", $STATE);
350 my $header = "SHELL=/bin/sh\n";
351 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
353 my $fh = IO
::File-
>new("< $CRONJOBS");
354 die "Could not open file $CRONJOBS: $!\n" if !$fh;
359 while (my $line = shift(@test)) {
361 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
363 next if $job->{state} eq "del";
364 $text .= format_job
($job, $line);
366 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
375 $text = "$header$text";
379 $text .= format_job
($job);
381 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
382 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
384 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
387 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
392 my ($job, $line) = @_;
395 if ($job->{state} eq "stopped") {
399 $line =~ /^#*(.+) root/;
402 $text .= "*/$INTERVAL * * * *";
405 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
406 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
407 $text .= " --method $job->{method}";
408 $text .= " --verbose" if $job->{verbose
};
416 my $cfg = read_cron
();
418 my $list = sprintf("%-25s%-10s%-7s%-20s%-5s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
420 my $states = read_state
();
421 foreach my $source (sort keys%{$cfg}) {
422 foreach my $name (sort keys%{$cfg->{$source}}) {
423 $list .= sprintf("%-25s", cut_target_width
($source, 25));
424 $list .= sprintf("%-10s", cut_target_width
($name, 10));
425 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
426 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
427 $list .= sprintf("%-5s", $states->{$source}->{$name}->{vm_type
});
428 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
438 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
442 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
444 return "qemu" if $res;
446 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
448 return "lxc" if $res;
456 my $cfg = read_cron
();
458 my $job = param_to_job
($param);
460 $job->{state} = "ok";
463 my $source = parse_target
($param->{source
});
464 my $dest = parse_target
($param->{dest
});
466 if (my $ip = $dest->{ip
}) {
467 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
470 if (my $ip = $source->{ip
}) {
471 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
474 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
476 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
478 die "Pool $source->{path} does not exists\n" if undef($check);
480 my $vm_type = vm_exists
($source);
481 $job->{vm_type
} = $vm_type;
482 $source->{vm_type
} = $vm_type;
484 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !$vm_type;
486 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
488 #check if vm has zfs disks if not die;
489 get_disks
($source, 1) if $source->{vmid
};
495 sync
($param) if !$param->{skip
};
506 my $cfg = read_cron
();
508 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
509 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
511 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
512 $job->{name
} = $param->{name
};
513 $job->{source
} = $param->{source
};
514 $job = add_state_to_job
($job);
522 my $job = get_job
($param);
523 $job->{state} = "del";
532 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
533 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
536 my $date = get_date
();
539 $job = get_job
($param);
542 if ($job && $job->{state} eq "syncing") {
543 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
546 my $dest = parse_target
($param->{dest
});
547 my $source = parse_target
($param->{source
});
549 my $sync_path = sub {
550 my ($source, $dest, $job, $param, $date) = @_;
552 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
554 snapshot_add
($source, $dest, $param->{name
}, $date);
556 send_image
($source, $dest, $param);
558 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
562 my $vm_type = vm_exists
($source);
563 $source->{vm_type
} = $vm_type;
566 $job->{state} = "syncing";
567 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
572 if ($source->{vmid
}) {
573 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
574 my $disks = get_disks
($source);
576 foreach my $disk (sort keys %{$disks}) {
577 $source->{all
} = $disks->{$disk}->{all
};
578 $source->{pool
} = $disks->{$disk}->{pool
};
579 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
580 $source->{last_part
} = $disks->{$disk}->{last_part
};
581 &$sync_path($source, $dest, $job, $param, $date);
583 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
584 send_config
($source, $dest,'ssh');
586 send_config
($source, $dest,'local');
589 &$sync_path($source, $dest, $job, $param, $date);
594 $job->{state} = "error";
598 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
604 $job->{state} = "ok";
605 $job->{lsync
} = $date;
614 my ($source, $dest, $max_snap, $name) = @_;
617 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
618 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
619 push @$cmd, $source->{all
};
621 my $raw = run_cmd
($cmd);
624 my $last_snap = undef;
627 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
629 if ($line =~ m/(rep_$name.*)$/) {
631 $last_snap = $1 if (!$last_snap);
634 if ($index == $max_snap) {
635 $source->{destroy
} = 1;
641 return ($old_snap, $last_snap) if $last_snap;
647 my ($source, $dest, $name, $date) = @_;
649 my $snap_name = "rep_$name\_".$date;
651 $source->{new_snap
} = $snap_name;
653 my $path = "$source->{all}\@$snap_name";
656 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
657 push @$cmd, 'zfs', 'snapshot', $path;
663 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
671 my $text = "SHELL=/bin/sh\n";
672 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
674 my $fh = IO
::File-
>new("> $CRONJOBS");
675 die "Could not open file: $!\n" if !$fh;
677 foreach my $source (sort keys%{$cfg}) {
678 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
679 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
680 $text .= "$PROG_PATH sync";
681 $text .= " -source ";
682 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
683 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
684 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
686 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
687 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
688 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
691 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
692 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
693 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
694 $text .= " -name $sync_name ";
695 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
696 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
700 die "Can't write to cron\n" if (!print($fh $text));
705 my ($target, $get_err) = @_;
708 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
710 if ($target->{vm_type
} eq 'qemu') {
711 push @$cmd, 'qm', 'config', $target->{vmid
};
712 } elsif ($target->{vm_type
} eq 'lxc') {
713 push @$cmd, 'pct', 'config', $target->{vmid
};
715 die "VM Type unknown\n";
718 my $res = run_cmd
($cmd);
720 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
727 print "Start CMD\n" if $DEBUG;
728 print Dumper
$cmd if $DEBUG;
729 if (ref($cmd) eq 'ARRAY') {
730 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
732 my $output = `$cmd 2>&1`;
734 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
737 print Dumper
$output if $DEBUG;
738 print "END CMD\n" if $DEBUG;
743 my ($text, $ip, $vm_type, $get_err) = @_;
748 while ($text && $text =~ s/^(.*?)(\n|$)//) {
750 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
752 next if $line =~ /cdrom|none/;
753 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
755 #QEMU if backup is not set include in sync
756 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
758 #LXC if backup is not set do no in sync
759 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
763 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
767 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
772 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
773 push @$cmd, 'pvesm', 'path', "$stor$disk";
774 my $path = run_cmd($cmd);
776 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
778 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
780 my @array = split('/', $1);
781 $disks->{$num}->{pool} = shift(@array);
782 $disks->{$num}->{all} = $disks->{$num}->{pool};
784 $disks->{$num}->{path} = join('/', @array);
785 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
787 $disks->{$num}->{last_part} = $disk;
788 $disks->{$num}->{all} .= "\
/$disk";
791 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
793 $disks->{$num}->{pool} = $1;
794 $disks->{$num}->{all} = $disks->{$num}->{pool};
797 $disks->{$num}->{path} = $3;
798 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
801 $disks->{$num}->{last_part} = $disk;
802 $disks->{$num}->{all} .= "\
/$disk";
807 die "ERROR
: in path
\n";
811 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
815 sub snapshot_destroy {
816 my ($source, $dest, $method, $snap) = @_;
818 my @zfscmd = ('zfs', 'destroy');
819 my $snapshot = "$source->{all
}\
@$snap";
822 if($source->{ip} && $method eq 'ssh'){
823 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
825 run_cmd([@zfscmd, $snapshot]);
832 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
834 my $path = "$dest->{all
}\
/$source->{last_part
}";
837 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
846 my ($source , $dest, $method) = @_;
849 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
850 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
851 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
854 eval {$text =run_cmd($cmd);};
860 while ($text && $text =~ s/^(.*?)(\n|$)//) {
862 return 1 if $line =~ m/^.*$source->{old_snap}$/;
867 my ($source, $dest, $param) = @_;
871 push @$cmd, 'ssh', "root\
@$source->{ip
}", '--' if $source->{ip};
872 push @$cmd, 'zfs', 'send';
873 push @$cmd, '-v' if $param->{verbose};
875 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
876 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
878 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
880 if ($param->{limit}){
881 my $bwl = $param->{limit}*1024;
882 push @$cmd, \'|', 'cstream', '-t', $bwl;
884 my $target = "$dest->{all
}/$source->{last_part
}";
888 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
889 push @$cmd, 'zfs', 'recv', '-F', '--';
890 push @$cmd, "$target";
897 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
904 my ($source, $dest, $method) = @_;
906 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
907 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
909 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
911 $dest_target_new = $config_dir.'/'.$dest_target_new;
913 if ($method eq 'ssh'){
914 if ($dest->{ip} && $source->{ip}) {
915 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
916 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
917 } elsif ($dest->{ip}) {
918 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
919 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
920 } elsif ($source->{ip}) {
921 run_cmd(['mkdir', '-p', '--', $config_dir]);
922 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
925 if ($source->{destroy}){
926 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
928 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
930 run_cmd(['rm', '-f', '--', $dest_target_old]);
933 } elsif ($method eq 'local') {
934 run_cmd(['mkdir', '-p', '--', $config_dir]);
935 run_cmd(['cp', $source_target, $dest_target_new]);
940 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
941 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
947 my $cfg = read_cron();
949 my $status_list = sprintf("%-25s
%-15s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
951 my $states = read_state();
953 foreach my $source (sort keys%{$cfg}) {
954 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
955 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
956 $status_list .= sprintf("%-15s
", cut_target_width($sync_name, 25));
957 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
967 my $job = get_job($param);
968 $job->{state} = "ok
";
976 my $job = get_job($param);
977 $job->{state} = "stopped
";
982 my $command = $ARGV[0];
984 my $commands = {'destroy' => 1,
993 if (!$command || !$commands->{$command}) {
998 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
999 \twill sync one
time\n
1001 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1003 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1004 \t-maxsnap
\tinteger
\n
1005 \t\thow much snapshots will be kept before get erased
, default 1/n
1007 \t\tname of the sync job
, if not set it
is default.
1008 \tIt
is only necessary
if scheduler allready contains this source
.\n
1010 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1012 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1013 \tCreate a sync Job
\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
\tstring
\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\n
1023 \t\tif this flag
is set it will skip the first sync
\n
1025 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1027 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1028 \tremove a sync Job from the scheduler
\n
1030 \t\tname of the sync job
, if not set it
is default\n
1032 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1034 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1035 \tGet help about specified command
.\n
1038 \t-verbose
\tboolean
\n
1039 \t\tVerbose output format
.\n";
1041 my $help_list = "$PROGNAME list
\n
1042 \tGet a List of all scheduled Sync Jobs
\n";
1044 my $help_status = "$PROGNAME status
\n
1045 \tGet the status of all scheduled Sync Jobs
\n";
1047 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1048 \tenable a syncjob
and reset error
\n
1050 \t\tname of the sync job
, if not set it
is default\n
1052 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1054 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1057 \t\tname of the sync job
, if not set it
is default\n
1059 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1075 die "$help_destroy\n";
1079 die "$help_create\n";
1087 die "$help_status\n";
1091 die "$help_enable\n";
1095 die "$help_enable\n";
1102 my $param = parse_argv(@arg);
1108 die "$help_destroy\n" if !$param->{source};
1109 check_target($param->{source});
1110 destroy_job($param);
1114 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1115 check_target($param->{source});
1116 check_target($param->{dest});
1121 die "$help_create\n" if !$param->{source} || !$param->{dest};
1122 check_target($param->{source});
1123 check_target($param->{dest});
1136 my $help_command = $ARGV[1];
1137 if ($help_command && $commands->{$help_command}) {
1138 print help($help_command);
1140 if ($param->{verbose} == 1){
1141 exec("man
$PROGNAME");
1148 die "$help_enable\n" if !$param->{source};
1149 check_target($param->{source});
1154 die "$help_disable\n" if !$param->{source};
1155 check_target($param->{source});
1156 disable_job($param);
1163 print("ERROR
:\tno command specified
\n") if !$help;
1164 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1165 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1166 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1167 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1168 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1169 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1170 print("\t$PROGNAME list
\n");
1171 print("\t$PROGNAME status
\n");
1172 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1177 parse_target($target);
1184 pve-zsync - PVE ZFS Replication Manager
1188 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1190 pve-zsync help <cmd> [OPTIONS]
1192 Get help about specified command.
1200 Verbose output format.
1202 pve-zsync create -dest <string> -source <string> [OPTIONS]
1208 the destination target is like [IP]:<Pool>[/Path]
1212 max sync speed in kBytes/s, default unlimited
1216 how much snapshots will be kept before get erased, default 1
1220 name of the sync job, if not set it is default
1224 if this flag is set it will skip the first sync
1228 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1230 pve-zsync destroy -source <string> [OPTIONS]
1232 remove a sync Job from the scheduler
1236 name of the sync job, if not set it is default
1240 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1242 pve-zsync disable -source <string> [OPTIONS]
1248 name of the sync job, if not set it is default
1252 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1254 pve-zsync enable -source <string> [OPTIONS]
1256 enable a syncjob and reset error
1260 name of the sync job, if not set it is default
1264 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1267 Get a List of all scheduled Sync Jobs
1271 Get the status of all scheduled Sync Jobs
1273 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1279 the destination target is like [IP:]<Pool>[/Path]
1283 max sync speed in kBytes/s, default unlimited
1287 how much snapshots will be kept before get erased, default 1
1291 name of the sync job, if not set it is default.
1292 It is only necessary if scheduler allready contains this source.
1296 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1300 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1301 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1302 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.
1303 To config cron see man crontab.
1305 =head2 PVE ZFS Storage sync Tool
1307 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1311 add sync job from local VM to remote ZFS Server
1312 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1314 =head1 IMPORTANT FILES
1316 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1318 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1320 =head1 COPYRIGHT AND DISCLAIMER
1322 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1324 This program is free software: you can redistribute it and/or modify it
1325 under the terms of the GNU Affero General Public License as published
1326 by the Free Software Foundation, either version 3 of the License, or
1327 (at your option) any later version.
1329 This program is distributed in the hope that it will be useful, but
1330 WITHOUT ANY WARRANTY; without even the implied warranty of
1331 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1332 Affero General Public License for more details.
1334 You should have received a copy of the GNU Affero General Public
1335 License along with this program. If not, see
1336 <http://www.gnu.org/licenses/>.