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 ($target, $max) = @_;
75 return $target if (length($target) <= $max);
76 my @spl = split('/', $target);
78 my $count = length($spl[@spl-1]);
79 return "..\/".substr($spl[@spl-1],($count-$max)+3 , $count) if $count > $max;
81 $count += length($spl[0]) if @spl > 1;
82 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
85 $rest = $max-$count if ($max-$count > 0);
87 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
92 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
97 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
101 my ($source, $name, $status) = @_;
103 if ($status->{$source->{all
}}->{$name}->{status
}) {
110 sub check_pool_exists
{
116 push @$cmd, 'ssh', "root\@$target->{ip}", '--';
118 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
132 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
135 if ($text !~ $TARGETRE) {
139 $target->{ip
} = $1 if $1;
140 my @parts = split('/', $2);
142 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
144 my $pool = $target->{pool
} = shift(@parts);
145 die "$errstr\n" if !$pool;
147 if ($pool =~ m/^\d+$/) {
148 $target->{vmid
} = $pool;
149 delete $target->{pool
};
152 return $target if (@parts == 0);
153 $target->{last_part
} = pop(@parts);
159 $target->{path
} = join('/', @parts);
167 #This is for the first use to init file;
169 my $new_fh = IO
::File-
>new("> $CRONJOBS");
170 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
175 my $fh = IO
::File-
>new("< $CRONJOBS");
176 die "Could not open file $CRONJOBS: $!\n" if !$fh;
182 return encode_cron
(@text);
189 $param->{dest
} = undef;
190 $param->{source
} = undef;
191 $param->{verbose
} = undef;
192 $param->{limit
} = undef;
193 $param->{maxsnap
} = undef;
194 $param->{name
} = undef;
195 $param->{skip
} = undef;
196 $param->{method} = undef;
198 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
199 'dest=s' => \
$param->{dest
},
200 'source=s' => \
$param->{source
},
201 'verbose' => \
$param->{verbose
},
202 'limit=i' => \
$param->{limit
},
203 'maxsnap=i' => \
$param->{maxsnap
},
204 'name=s' => \
$param->{name
},
205 'skip' => \
$param->{skip
},
206 'method=s' => \
$param->{method});
209 die "can't parse options\n";
212 $param->{name
} = "default" if !$param->{name
};
213 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
214 $param->{method} = "ssh" if !$param->{method};
219 sub add_state_to_job
{
222 my $states = read_state
();
223 my $state = $states->{$job->{source
}}->{$job->{name
}};
225 $job->{state} = $state->{state};
226 $job->{lsync
} = $state->{lsync
};
227 $job->{vm_type
} = $state->{vm_type
};
229 for (my $i = 0; $state->{"snap$i"}; $i++) {
230 $job->{"snap$i"} = $state->{"snap$i"};
241 while (my $line = shift(@text)) {
243 my @arg = split('\s', $line);
244 my $param = parse_argv
(@arg);
246 if ($param->{source
} && $param->{dest
}) {
247 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
248 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
249 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
250 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
251 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
252 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
264 my $source = parse_target
($param->{source
});
265 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
267 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
268 $job->{dest
} = $param->{dest
} if $param->{dest
};
269 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
270 $job->{method} = "ssh" if !$job->{method};
271 $job->{limit
} = $param->{limit
};
272 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
273 $job->{source
} = $param->{source
};
281 make_path
$CONFIG_PATH;
282 my $new_fh = IO
::File-
>new("> $STATE");
283 die "Could not create $STATE: $!\n" if !$new_fh;
289 my $fh = IO
::File-
>new("< $STATE");
290 die "Could not open file $STATE: $!\n" if !$fh;
293 my $states = decode_json
($text);
307 $in_fh = IO
::File-
>new("< $STATE");
308 die "Could not open file $STATE: $!\n" if !$in_fh;
313 my $out_fh = IO
::File-
>new("> $STATE.new");
314 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
319 $states = decode_json
($text);
320 $state = $states->{$job->{source
}}->{$job->{name
}};
323 if ($job->{state} ne "del") {
324 $state->{state} = $job->{state};
325 $state->{lsync
} = $job->{lsync
};
326 $state->{vm_type
} = $job->{vm_type
};
328 for (my $i = 0; $job->{"snap$i"} ; $i++) {
329 $state->{"snap$i"} = $job->{"snap$i"};
331 $states->{$job->{source
}}->{$job->{name
}} = $state;
334 delete $states->{$job->{source
}}->{$job->{name
}};
335 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
338 $text = encode_json
($states);
342 move
("$STATE.new", $STATE);
357 my $header = "SHELL=/bin/sh\n";
358 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
360 my $fh = IO
::File-
>new("< $CRONJOBS");
361 die "Could not open file $CRONJOBS: $!\n" if !$fh;
366 while (my $line = shift(@test)) {
368 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
370 next if $job->{state} eq "del";
371 $text .= format_job
($job, $line);
373 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
382 $text = "$header$text";
386 $text .= format_job
($job);
388 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
389 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
391 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
394 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
399 my ($job, $line) = @_;
402 if ($job->{state} eq "stopped") {
406 $line =~ /^#*(.+) root/;
409 $text .= "*/$INTERVAL * * * *";
412 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
413 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
414 $text .= " --limit $job->{limit}" if $job->{limit
};
415 $text .= " --method $job->{method}";
416 $text .= " --verbose" if $job->{verbose
};
424 my $cfg = read_cron
();
426 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
428 my $states = read_state
();
429 foreach my $source (sort keys%{$cfg}) {
430 foreach my $name (sort keys%{$cfg->{$source}}) {
431 $list .= sprintf("%-25s", cut_target_width
($source, 25));
432 $list .= sprintf("%-25s", cut_target_width
($name, 25));
433 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
434 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
435 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
436 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
446 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
450 return undef if !defined($target->{vmid
});
452 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
454 return "qemu" if $res;
456 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
458 return "lxc" if $res;
466 my $cfg = read_cron
();
468 my $job = param_to_job
($param);
470 $job->{state} = "ok";
473 my $source = parse_target
($param->{source
});
474 my $dest = parse_target
($param->{dest
});
476 if (my $ip = $dest->{ip
}) {
477 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
480 if (my $ip = $source->{ip
}) {
481 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
484 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest);
486 if (!defined($source->{vmid
})) {
487 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source);
490 my $vm_type = vm_exists
($source);
491 $job->{vm_type
} = $vm_type;
492 $source->{vm_type
} = $vm_type;
494 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
496 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
498 #check if vm has zfs disks if not die;
499 get_disks
($source, 1) if $source->{vmid
};
505 sync
($param) if !$param->{skip
};
516 my $cfg = read_cron
();
518 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
519 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
521 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
522 $job->{name
} = $param->{name
};
523 $job->{source
} = $param->{source
};
524 $job = add_state_to_job
($job);
532 my $job = get_job
($param);
533 $job->{state} = "del";
542 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
543 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
546 my $date = get_date
();
549 $job = get_job
($param);
552 if ($job && $job->{state} eq "syncing") {
553 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
556 my $dest = parse_target
($param->{dest
});
557 my $source = parse_target
($param->{source
});
559 my $sync_path = sub {
560 my ($source, $dest, $job, $param, $date) = @_;
562 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
564 snapshot_add
($source, $dest, $param->{name
}, $date);
566 send_image
($source, $dest, $param);
568 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
572 my $vm_type = vm_exists
($source);
573 $source->{vm_type
} = $vm_type;
576 $job->{state} = "syncing";
577 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
582 if ($source->{vmid
}) {
583 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
584 my $disks = get_disks
($source);
586 foreach my $disk (sort keys %{$disks}) {
587 $source->{all
} = $disks->{$disk}->{all
};
588 $source->{pool
} = $disks->{$disk}->{pool
};
589 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
590 $source->{last_part
} = $disks->{$disk}->{last_part
};
591 &$sync_path($source, $dest, $job, $param, $date);
593 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
594 send_config
($source, $dest,'ssh');
596 send_config
($source, $dest,'local');
599 &$sync_path($source, $dest, $job, $param, $date);
604 $job->{state} = "error";
608 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
614 $job->{state} = "ok";
615 $job->{lsync
} = $date;
624 my ($source, $dest, $max_snap, $name) = @_;
627 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
628 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
629 push @$cmd, $source->{all
};
631 my $raw = run_cmd
($cmd);
634 my $last_snap = undef;
637 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
639 if ($line =~ m/(rep_$name.*)$/) {
641 $last_snap = $1 if (!$last_snap);
644 if ($index == $max_snap) {
645 $source->{destroy
} = 1;
651 return ($old_snap, $last_snap) if $last_snap;
657 my ($source, $dest, $name, $date) = @_;
659 my $snap_name = "rep_$name\_".$date;
661 $source->{new_snap
} = $snap_name;
663 my $path = "$source->{all}\@$snap_name";
666 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
667 push @$cmd, 'zfs', 'snapshot', $path;
673 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
681 my $text = "SHELL=/bin/sh\n";
682 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
684 my $fh = IO
::File-
>new("> $CRONJOBS");
685 die "Could not open file: $!\n" if !$fh;
687 foreach my $source (sort keys%{$cfg}) {
688 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
689 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
690 $text .= "$PROG_PATH sync";
691 $text .= " -source ";
692 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
693 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
694 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
696 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
697 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
698 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
701 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
702 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
703 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
704 $text .= " -name $sync_name ";
705 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
706 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
710 die "Can't write to cron\n" if (!print($fh $text));
715 my ($target, $get_err) = @_;
718 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
720 if ($target->{vm_type
} eq 'qemu') {
721 push @$cmd, 'qm', 'config', $target->{vmid
};
722 } elsif ($target->{vm_type
} eq 'lxc') {
723 push @$cmd, 'pct', 'config', $target->{vmid
};
725 die "VM Type unknown\n";
728 my $res = run_cmd
($cmd);
730 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $get_err);
737 print "Start CMD\n" if $DEBUG;
738 print Dumper
$cmd if $DEBUG;
739 if (ref($cmd) eq 'ARRAY') {
740 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
742 my $output = `$cmd 2>&1`;
744 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
747 print Dumper
$output if $DEBUG;
748 print "END CMD\n" if $DEBUG;
753 my ($text, $ip, $vm_type, $get_err) = @_;
758 while ($text && $text =~ s/^(.*?)(\n|$)//) {
760 my $error = $vm_type eq 'qemu' ?
1 : 0 ;
762 next if $line =~ /cdrom|none/;
763 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
765 #QEMU if backup is not set include in sync
766 next if $vm_type eq 'qemu && ($line =~ m/backup=(?i:0|no|off|false)/)';
768 #LXC if backup is not set do no in sync
769 $error = ($line =~ m/backup=(?i:1|yes|on|true)/) if $vm_type eq 'lxc';
773 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
774 my @parameter = split(/,/,$1);
776 foreach my $opt (@parameter) {
777 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
785 print "Disk: \"$line\" will
not include
in pve-sync
\n" if $get_err || $error;
790 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
791 push @$cmd, 'pvesm', 'path', "$stor$disk";
792 my $path = run_cmd($cmd);
794 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
796 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
798 my @array = split('/', $1);
799 $disks->{$num}->{pool} = shift(@array);
800 $disks->{$num}->{all} = $disks->{$num}->{pool};
802 $disks->{$num}->{path} = join('/', @array);
803 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
805 $disks->{$num}->{last_part} = $disk;
806 $disks->{$num}->{all} .= "\
/$disk";
809 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
811 $disks->{$num}->{pool} = $1;
812 $disks->{$num}->{all} = $disks->{$num}->{pool};
815 $disks->{$num}->{path} = $3;
816 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
819 $disks->{$num}->{last_part} = $disk;
820 $disks->{$num}->{all} .= "\
/$disk";
825 die "ERROR
: in path
\n";
829 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
833 sub snapshot_destroy {
834 my ($source, $dest, $method, $snap) = @_;
836 my @zfscmd = ('zfs', 'destroy');
837 my $snapshot = "$source->{all
}\
@$snap";
840 if($source->{ip} && $method eq 'ssh'){
841 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
843 run_cmd([@zfscmd, $snapshot]);
850 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
852 my $path = "$dest->{all
}\
/$source->{last_part
}";
855 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
864 my ($source , $dest, $method) = @_;
867 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
868 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
869 push @$cmd, "$dest->{all
}/$source->{last_part
}\
@$source->{old_snap
}";
872 eval {$text =run_cmd($cmd);};
878 while ($text && $text =~ s/^(.*?)(\n|$)//) {
880 return 1 if $line =~ m/^.*$source->{old_snap}$/;
885 my ($source, $dest, $param) = @_;
889 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$source->{ip
}", '--' if $source->{ip};
890 push @$cmd, 'zfs', 'send';
891 push @$cmd, '-v' if $param->{verbose};
893 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
894 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
896 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
898 if ($param->{limit}){
899 my $bwl = $param->{limit}*1024;
900 push @$cmd, \'|', 'cstream', '-t', $bwl;
902 my $target = "$dest->{all
}/$source->{last_part
}";
906 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$dest->{ip
}", '--' if $dest->{ip};
907 push @$cmd, 'zfs', 'recv', '-F', '--';
908 push @$cmd, "$target";
915 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
922 my ($source, $dest, $method) = @_;
924 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
925 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
927 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
929 $dest_target_new = $config_dir.'/'.$dest_target_new;
931 if ($method eq 'ssh'){
932 if ($dest->{ip} && $source->{ip}) {
933 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
934 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
935 } elsif ($dest->{ip}) {
936 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
937 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
938 } elsif ($source->{ip}) {
939 run_cmd(['mkdir', '-p', '--', $config_dir]);
940 run_cmd(['scp', '--', "root\
@$source->{ip
}:$source_target", $dest_target_new]);
943 if ($source->{destroy}){
944 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
946 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
948 run_cmd(['rm', '-f', '--', $dest_target_old]);
951 } elsif ($method eq 'local') {
952 run_cmd(['mkdir', '-p', '--', $config_dir]);
953 run_cmd(['cp', $source_target, $dest_target_new]);
958 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
959 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
965 my $cfg = read_cron();
967 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
969 my $states = read_state();
971 foreach my $source (sort keys%{$cfg}) {
972 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
973 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
974 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
975 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
985 my $job = get_job($param);
986 $job->{state} = "ok
";
994 my $job = get_job($param);
995 $job->{state} = "stopped
";
1000 my $command = $ARGV[0];
1002 my $commands = {'destroy' => 1,
1011 if (!$command || !$commands->{$command}) {
1016 my $help_sync = "$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n
1017 \twill sync one
time\n
1019 \t\tthe destination target
is like
[IP
:]<Pool
>[/Path
]\n
1021 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1022 \t-maxsnap
\tinteger
\n
1023 \t\thow much snapshots will be kept before get erased
, default 1/n
1025 \t\tname of the sync job
, if not set it
is default.
1026 \tIt
is only necessary
if scheduler allready contains this source
.\n
1028 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1030 my $help_create = "$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]/n
1031 \tCreate a sync Job
\n
1033 \t\tthe destination target
is like
[IP
]:<Pool
>[/Path
]\n
1035 \t\tmax sync speed
in kBytes
/s
, default unlimited
\n
1036 \t-maxsnap
\tstring
\n
1037 \t\thow much snapshots will be kept before get erased
, default 1\n
1039 \t\tname of the sync job
, if not set it
is default\n
1041 \t\tif this flag
is set it will skip the first sync
\n
1043 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1045 my $help_destroy = "$PROGNAME destroy
-source
<string
> [OPTIONS
]\n
1046 \tremove a sync Job from the scheduler
\n
1048 \t\tname of the sync job
, if not set it
is default\n
1050 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1052 my $help_help = "$PROGNAME help
<cmd
> [OPTIONS
]\n
1053 \tGet help about specified command
.\n
1056 \t-verbose
\tboolean
\n
1057 \t\tVerbose output format
.\n";
1059 my $help_list = "$PROGNAME list
\n
1060 \tGet a List of all scheduled Sync Jobs
\n";
1062 my $help_status = "$PROGNAME status
\n
1063 \tGet the status of all scheduled Sync Jobs
\n";
1065 my $help_enable = "$PROGNAME enable
-source
<string
> [OPTIONS
]\n
1066 \tenable a syncjob
and reset error
\n
1068 \t\tname of the sync job
, if not set it
is default\n
1070 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1072 my $help_disable = "$PROGNAME disable
-source
<string
> [OPTIONS
]\n
1075 \t\tname of the sync job
, if not set it
is default\n
1077 \t\tthe source can be an
<VMID
> or [IP
:]<ZFSPool
>[/Path
]\n";
1082 if ($command eq 'help') {
1085 } elsif ($command eq 'sync') {
1088 } elsif ($command eq 'destroy') {
1089 die "$help_destroy\n";
1091 } elsif ($command eq 'create') {
1092 die "$help_create\n";
1094 } elsif ($command eq 'list') {
1097 } elsif ($command eq 'status') {
1098 die "$help_status\n";
1100 } elsif ($command eq 'enable') {
1101 die "$help_enable\n";
1103 } elsif ($command eq 'disable') {
1104 die "$help_disable\n";
1111 my $param = parse_argv(@arg);
1113 if ($command eq 'destroy') {
1114 die "$help_destroy\n" if !$param->{source};
1116 check_target($param->{source});
1117 destroy_job($param);
1119 } elsif ($command eq 'sync') {
1120 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1122 check_target($param->{source});
1123 check_target($param->{dest});
1126 } elsif ($command eq 'create') {
1127 die "$help_create\n" if !$param->{source} || !$param->{dest};
1129 check_target($param->{source});
1130 check_target($param->{dest});
1133 } elsif ($command eq 'status') {
1136 } elsif ($command eq 'list') {
1139 } elsif ($command eq 'help') {
1140 my $help_command = $ARGV[1];
1142 if ($help_command && $commands->{$help_command}) {
1143 print help($help_command);
1146 if ($param->{verbose} == 1){
1147 exec("man
$PROGNAME");
1154 } elsif ($command eq 'enable') {
1155 die "$help_enable\n" if !$param->{source};
1157 check_target($param->{source});
1160 } elsif ($command eq 'disable') {
1161 die "$help_disable\n" if !$param->{source};
1163 check_target($param->{source});
1164 disable_job($param);
1171 print("ERROR
:\tno command specified
\n") if !$help;
1172 print("USAGE
:\t$PROGNAME <COMMAND
> [ARGS
] [OPTIONS
]\n");
1173 print("\t$PROGNAME help
[<cmd
>] [OPTIONS
]\n\n");
1174 print("\t$PROGNAME create
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1175 print("\t$PROGNAME destroy
-source
<string
> [OPTIONS
]\n");
1176 print("\t$PROGNAME disable
-source
<string
> [OPTIONS
]\n");
1177 print("\t$PROGNAME enable
-source
<string
> [OPTIONS
]\n");
1178 print("\t$PROGNAME list
\n");
1179 print("\t$PROGNAME status
\n");
1180 print("\t$PROGNAME sync
-dest
<string
> -source
<string
> [OPTIONS
]\n");
1185 parse_target($target);
1192 pve-zsync - PVE ZFS Replication Manager
1196 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1198 pve-zsync help <cmd> [OPTIONS]
1200 Get help about specified command.
1208 Verbose output format.
1210 pve-zsync create -dest <string> -source <string> [OPTIONS]
1216 the destination target is like [IP]:<Pool>[/Path]
1220 max sync speed in kBytes/s, default unlimited
1224 how much snapshots will be kept before get erased, default 1
1228 name of the sync job, if not set it is default
1232 if this flag is set it will skip the first sync
1236 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1238 pve-zsync destroy -source <string> [OPTIONS]
1240 remove a sync Job from the scheduler
1244 name of the sync job, if not set it is default
1248 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1250 pve-zsync disable -source <string> [OPTIONS]
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 enable -source <string> [OPTIONS]
1264 enable a syncjob and reset error
1268 name of the sync job, if not set it is default
1272 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1275 Get a List of all scheduled Sync Jobs
1279 Get the status of all scheduled Sync Jobs
1281 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1287 the destination target is like [IP:]<Pool>[/Path]
1291 max sync speed in kBytes/s, default unlimited
1295 how much snapshots will be kept before get erased, default 1
1299 name of the sync job, if not set it is default.
1300 It is only necessary if scheduler allready contains this source.
1304 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1308 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1309 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1310 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.
1311 To config cron see man crontab.
1313 =head2 PVE ZFS Storage sync Tool
1315 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1319 add sync job from local VM to remote ZFS Server
1320 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1322 =head1 IMPORTANT FILES
1324 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1326 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1328 =head1 COPYRIGHT AND DISCLAIMER
1330 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1332 This program is free software: you can redistribute it and/or modify it
1333 under the terms of the GNU Affero General Public License as published
1334 by the Free Software Foundation, either version 3 of the License, or
1335 (at your option) any later version.
1337 This program is distributed in the hope that it will be useful, but
1338 WITHOUT ANY WARRANTY; without even the implied warranty of
1339 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1340 Affero General Public License for more details.
1342 You should have received a copy of the GNU Affero General Public
1343 License along with this program. If not, see
1344 <http://www.gnu.org/licenses/>.