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 .= " --limit $job->{limit}" if $job->{limit
};
408 $text .= " --method $job->{method}";
409 $text .= " --verbose" if $job->{verbose
};
417 my $cfg = read_cron
();
419 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
421 my $states = read_state
();
422 foreach my $source (sort keys%{$cfg}) {
423 foreach my $name (sort keys%{$cfg->{$source}}) {
424 $list .= sprintf("%-25s", cut_target_width
($source, 25));
425 $list .= sprintf("%-25s", cut_target_width
($name, 25));
426 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
427 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
428 $list .= sprintf("%-6s", $states->{$source}->{$name}->{vm_type
});
429 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
439 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
443 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
445 return "qemu" if $res;
447 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
449 return "lxc" if $res;
457 my $cfg = read_cron
();
459 my $job = param_to_job
($param);
461 $job->{state} = "ok";
464 my $source = parse_target
($param->{source
});
465 my $dest = parse_target
($param->{dest
});
467 if (my $ip = $dest->{ip
}) {
468 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
471 if (my $ip = $source->{ip
}) {
472 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
475 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
477 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
479 die "Pool $source->{path} does not exists\n" if undef($check);
481 my $vm_type = vm_exists
($source);
482 $job->{vm_type
} = $vm_type;
483 $source->{vm_type
} = $vm_type;
485 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !$vm_type;
487 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
489 #check if vm has zfs disks if not die;
490 get_disks
($source, 1) if $source->{vmid
};
496 sync
($param) if !$param->{skip
};
507 my $cfg = read_cron
();
509 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
510 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
512 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
513 $job->{name
} = $param->{name
};
514 $job->{source
} = $param->{source
};
515 $job = add_state_to_job
($job);
523 my $job = get_job
($param);
524 $job->{state} = "del";
533 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
534 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
537 my $date = get_date
();
540 $job = get_job
($param);
543 if ($job && $job->{state} eq "syncing") {
544 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
547 my $dest = parse_target
($param->{dest
});
548 my $source = parse_target
($param->{source
});
550 my $sync_path = sub {
551 my ($source, $dest, $job, $param, $date) = @_;
553 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
555 snapshot_add
($source, $dest, $param->{name
}, $date);
557 send_image
($source, $dest, $param);
559 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
563 my $vm_type = vm_exists
($source);
564 $source->{vm_type
} = $vm_type;
567 $job->{state} = "syncing";
568 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
573 if ($source->{vmid
}) {
574 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
575 my $disks = get_disks
($source);
577 foreach my $disk (sort keys %{$disks}) {
578 $source->{all
} = $disks->{$disk}->{all
};
579 $source->{pool
} = $disks->{$disk}->{pool
};
580 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
581 $source->{last_part
} = $disks->{$disk}->{last_part
};
582 &$sync_path($source, $dest, $job, $param, $date);
584 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
585 send_config
($source, $dest,'ssh');
587 send_config
($source, $dest,'local');
590 &$sync_path($source, $dest, $job, $param, $date);
595 $job->{state} = "error";
599 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
605 $job->{state} = "ok";
606 $job->{lsync
} = $date;
615 my ($source, $dest, $max_snap, $name) = @_;
618 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
619 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
620 push @$cmd, $source->{all
};
622 my $raw = run_cmd
($cmd);
625 my $last_snap = undef;
628 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
630 if ($line =~ m/(rep_$name.*)$/) {
632 $last_snap = $1 if (!$last_snap);
635 if ($index == $max_snap) {
636 $source->{destroy
} = 1;
642 return ($old_snap, $last_snap) if $last_snap;
648 my ($source, $dest, $name, $date) = @_;
650 my $snap_name = "rep_$name\_".$date;
652 $source->{new_snap
} = $snap_name;
654 my $path = "$source->{all}\@$snap_name";
657 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
658 push @$cmd, 'zfs', 'snapshot', $path;
664 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
672 my $text = "SHELL=/bin/sh\n";
673 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
675 my $fh = IO
::File-
>new("> $CRONJOBS");
676 die "Could not open file: $!\n" if !$fh;
678 foreach my $source (sort keys%{$cfg}) {
679 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
680 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
681 $text .= "$PROG_PATH sync";
682 $text .= " -source ";
683 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
684 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
685 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
687 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
688 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
689 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
692 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
693 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
694 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
695 $text .= " -name $sync_name ";
696 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
697 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
701 die "Can't write to cron\n" if (!print($fh $text));
706 my ($target, $get_err) = @_;
709 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
711 if ($target->{vm_type
} eq 'qemu') {
712 push @$cmd, 'qm', 'config', $target->{vmid
};
713 } elsif ($target->{vm_type
} eq 'lxc') {
714 push @$cmd, 'pct', 'config', $target->{vmid
};
716 die "VM Type unknown\n";
719 my $res = run_cmd
($cmd);
721 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
728 print "Start CMD\n" if $DEBUG;
729 print Dumper
$cmd if $DEBUG;
730 if (ref($cmd) eq 'ARRAY') {
731 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
733 my $output = `$cmd 2>&1`;
735 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
738 print Dumper
$output if $DEBUG;
739 print "END CMD\n" if $DEBUG;
744 my ($text, $ip, $vm_type, $get_err) = @_;
749 while ($text && $text =~ s/^(.*?)(\n|$)//) {
751 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
753 next if $line =~ /cdrom|none/;
754 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
756 #QEMU if backup is not set include in sync
757 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
759 #LXC if backup is not set do no in sync
760 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
764 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): ([^:]+:)([A-Za-z0-9\-]+),(.*)$/) {
768 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
773 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
774 push @$cmd, 'pvesm', 'path', "$stor$disk";
775 my $path = run_cmd($cmd);
777 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
779 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
781 my @array = split('/', $1);
782 $disks->{$num}->{pool} = shift(@array);
783 $disks->{$num}->{all} = $disks->{$num}->{pool};
785 $disks->{$num}->{path} = join('/', @array);
786 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
788 $disks->{$num}->{last_part} = $disk;
789 $disks->{$num}->{all} .= "\
/$disk";
792 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
794 $disks->{$num}->{pool} = $1;
795 $disks->{$num}->{all} = $disks->{$num}->{pool};
798 $disks->{$num}->{path} = $3;
799 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
802 $disks->{$num}->{last_part} = $disk;
803 $disks->{$num}->{all} .= "\
/$disk";
808 die "ERROR
: in path
\n";
812 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
816 sub snapshot_destroy {
817 my ($source, $dest, $method, $snap) = @_;
819 my @zfscmd = ('zfs', 'destroy');
820 my $snapshot = "$source->{all
}\
@$snap";
823 if($source->{ip} && $method eq 'ssh'){
824 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
826 run_cmd([@zfscmd, $snapshot]);
833 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
835 my $path = "$dest->{all
}\
/$source->{last_part
}";
838 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
847 my ($source , $dest, $method) = @_;
850 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
851 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
852 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
855 eval {$text =run_cmd($cmd);};
861 while ($text && $text =~ s/^(.*?)(\n|$)//) {
863 return 1 if $line =~ m/^.*$source->{old_snap}$/;
868 my ($source, $dest, $param) = @_;
872 push @$cmd, 'ssh', "root\
@$source->{ip
}", '--' if $source->{ip};
873 push @$cmd, 'zfs', 'send';
874 push @$cmd, '-v' if $param->{verbose};
876 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
877 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
879 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
881 if ($param->{limit}){
882 my $bwl = $param->{limit}*1024;
883 push @$cmd, \'|', 'cstream', '-t', $bwl;
885 my $target = "$dest->{all
}/$source->{last_part
}";
889 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
890 push @$cmd, 'zfs', 'recv', '-F', '--';
891 push @$cmd, "$target";
898 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
905 my ($source, $dest, $method) = @_;
907 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
908 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
910 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
912 $dest_target_new = $config_dir.'/'.$dest_target_new;
914 if ($method eq 'ssh'){
915 if ($dest->{ip} && $source->{ip}) {
916 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
917 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
918 } elsif ($dest->{ip}) {
919 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
920 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
921 } elsif ($source->{ip}) {
922 run_cmd(['mkdir', '-p', '--', $config_dir]);
923 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
926 if ($source->{destroy}){
927 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
929 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
931 run_cmd(['rm', '-f', '--', $dest_target_old]);
934 } elsif ($method eq 'local') {
935 run_cmd(['mkdir', '-p', '--', $config_dir]);
936 run_cmd(['cp', $source_target, $dest_target_new]);
941 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
942 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
948 my $cfg = read_cron();
950 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
952 my $states = read_state();
954 foreach my $source (sort keys%{$cfg}) {
955 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
956 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
957 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
958 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
968 my $job = get_job($param);
969 $job->{state} = "ok
";
977 my $job = get_job($param);
978 $job->{state} = "stopped
";
983 my $command = $ARGV[0];
985 my $commands = {'destroy' => 1,
994 if (!$command || !$commands->{$command}) {
999 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1000 \twill sync one
time\n
1002 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1004 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1005 \t-maxsnap
\tinteger
\n
1006 \t\thow much snapshots will be kept before get erased
, default 1/n
1008 \t\tname of the sync job
, if not set it
is default.
1009 \tIt
is only necessary
if scheduler allready contains this source
.\n
1011 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1013 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1014 \tCreate a sync Job
\n
1016 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1018 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1019 \t-maxsnap
\tstring
\n
1020 \t\thow much snapshots will be kept before get erased
, default 1\n
1022 \t\tname of the sync job
, if not set it
is default\n
1024 \t\tif this flag
is set it will skip the first sync
\n
1026 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1028 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1029 \tremove a sync Job from the scheduler
\n
1031 \t\tname of the sync job
, if not set it
is default\n
1033 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1035 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1036 \tGet help about specified command
.\n
1039 \t-verbose
\tboolean
\n
1040 \t\tVerbose output format
.\n";
1042 my $help_list = "$PROGNAME list
\n
1043 \tGet a List of all scheduled Sync Jobs
\n";
1045 my $help_status = "$PROGNAME status
\n
1046 \tGet the status of all scheduled Sync Jobs
\n";
1048 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1049 \tenable a syncjob
and reset error
\n
1051 \t\tname of the sync job
, if not set it
is default\n
1053 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1055 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1058 \t\tname of the sync job
, if not set it
is default\n
1060 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1076 die "$help_destroy\n";
1080 die "$help_create\n";
1088 die "$help_status\n";
1092 die "$help_enable\n";
1096 die "$help_enable\n";
1103 my $param = parse_argv(@arg);
1109 die "$help_destroy\n" if !$param->{source};
1110 check_target($param->{source});
1111 destroy_job($param);
1115 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1116 check_target($param->{source});
1117 check_target($param->{dest});
1122 die "$help_create\n" if !$param->{source} || !$param->{dest};
1123 check_target($param->{source});
1124 check_target($param->{dest});
1137 my $help_command = $ARGV[1];
1138 if ($help_command && $commands->{$help_command}) {
1139 print help($help_command);
1141 if ($param->{verbose} == 1){
1142 exec("man
$PROGNAME");
1149 die "$help_enable\n" if !$param->{source};
1150 check_target($param->{source});
1155 die "$help_disable\n" if !$param->{source};
1156 check_target($param->{source});
1157 disable_job($param);
1164 print("ERROR
:\tno command specified
\n") if !$help;
1165 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1166 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1167 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1168 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1169 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1170 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1171 print("\t$PROGNAME list
\n");
1172 print("\t$PROGNAME status
\n");
1173 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1178 parse_target($target);
1185 pve-zsync - PVE ZFS Replication Manager
1189 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1191 pve-zsync help <cmd> [OPTIONS]
1193 Get help about specified command.
1201 Verbose output format.
1203 pve-zsync create -dest <string> -source <string> [OPTIONS]
1209 the destination target is like [IP]:<Pool>[/Path]
1213 max sync speed in kBytes/s, default unlimited
1217 how much snapshots will be kept before get erased, default 1
1221 name of the sync job, if not set it is default
1225 if this flag is set it will skip the first sync
1229 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1231 pve-zsync destroy -source <string> [OPTIONS]
1233 remove a sync Job from the scheduler
1237 name of the sync job, if not set it is default
1241 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1243 pve-zsync disable -source <string> [OPTIONS]
1249 name of the sync job, if not set it is default
1253 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1255 pve-zsync enable -source <string> [OPTIONS]
1257 enable a syncjob and reset error
1261 name of the sync job, if not set it is default
1265 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1268 Get a List of all scheduled Sync Jobs
1272 Get the status of all scheduled Sync Jobs
1274 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1280 the destination target is like [IP:]<Pool>[/Path]
1284 max sync speed in kBytes/s, default unlimited
1288 how much snapshots will be kept before get erased, default 1
1292 name of the sync job, if not set it is default.
1293 It is only necessary if scheduler allready contains this source.
1297 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1301 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1302 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1303 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.
1304 To config cron see man crontab.
1306 =head2 PVE ZFS Storage sync Tool
1308 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1312 add sync job from local VM to remote ZFS Server
1313 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1315 =head1 IMPORTANT FILES
1317 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1319 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1321 =head1 COPYRIGHT AND DISCLAIMER
1323 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1325 This program is free software: you can redistribute it and/or modify it
1326 under the terms of the GNU Affero General Public License as published
1327 by the Free Software Foundation, either version 3 of the License, or
1328 (at your option) any later version.
1330 This program is distributed in the hope that it will be useful, but
1331 WITHOUT ANY WARRANTY; without even the implied warranty of
1332 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1333 Affero General Public License for more details.
1335 You should have received a copy of the GNU Affero General Public
1336 License along with this program. If not, see
1337 <http://www.gnu.org/licenses/>.