]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
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 my $command = $ARGV[0];
51 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
52 check_bin
('cstream');
58 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} =
60 die "Signal aborting sync\n";
66 foreach my $p (split (/:/, $ENV{PATH
})) {
73 die "unable to find command '$bin'\n";
76 sub cut_target_width
{
77 my ($path, $maxlen) = @_;
80 return $path if length($path) <= $maxlen;
82 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
84 $path =~ s
@/([^/]+/?
)$@@;
87 if (length($tail)+3 == $maxlen) {
89 } elsif (length($tail)+2 >= $maxlen) {
90 return '..'.substr($tail, -$maxlen+2)
93 $path =~ s
@(/[^/]+)(?
:/|$)@@;
95 my $both = length($head) + length($tail);
96 my $remaining = $maxlen-$both-4; # -4 for "/../"
99 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
102 substr($path, ($remaining/2), (length($path)-$remaining), '..');
103 return "$head/" . $path . "/$tail";
108 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
113 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
117 my ($source, $name, $status) = @_;
119 if ($status->{$source->{all
}}->{$name}->{status
}) {
126 sub check_pool_exists
{
132 push @$cmd, 'ssh', "root\@$target->{ip}", '--';
134 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
148 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
151 if ($text !~ $TARGETRE) {
155 $target->{ip
} = $1 if $1;
156 my @parts = split('/', $2);
158 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
160 my $pool = $target->{pool
} = shift(@parts);
161 die "$errstr\n" if !$pool;
163 if ($pool =~ m/^\d+$/) {
164 $target->{vmid
} = $pool;
165 delete $target->{pool
};
168 return $target if (@parts == 0);
169 $target->{last_part
} = pop(@parts);
175 $target->{path
} = join('/', @parts);
183 #This is for the first use to init file;
185 my $new_fh = IO
::File-
>new("> $CRONJOBS");
186 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
191 my $fh = IO
::File-
>new("< $CRONJOBS");
192 die "Could not open file $CRONJOBS: $!\n" if !$fh;
198 return encode_cron
(@text);
205 $param->{dest
} = undef;
206 $param->{source
} = undef;
207 $param->{verbose
} = undef;
208 $param->{limit
} = undef;
209 $param->{maxsnap
} = undef;
210 $param->{name
} = undef;
211 $param->{skip
} = undef;
212 $param->{method} = undef;
214 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
215 'dest=s' => \
$param->{dest
},
216 'source=s' => \
$param->{source
},
217 'verbose' => \
$param->{verbose
},
218 'limit=i' => \
$param->{limit
},
219 'maxsnap=i' => \
$param->{maxsnap
},
220 'name=s' => \
$param->{name
},
221 'skip' => \
$param->{skip
},
222 'method=s' => \
$param->{method});
225 die "can't parse options\n";
228 $param->{name
} = "default" if !$param->{name
};
229 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
230 $param->{method} = "ssh" if !$param->{method};
235 sub add_state_to_job
{
238 my $states = read_state
();
239 my $state = $states->{$job->{source
}}->{$job->{name
}};
241 $job->{state} = $state->{state};
242 $job->{lsync
} = $state->{lsync
};
243 $job->{vm_type
} = $state->{vm_type
};
245 for (my $i = 0; $state->{"snap$i"}; $i++) {
246 $job->{"snap$i"} = $state->{"snap$i"};
257 while (my $line = shift(@text)) {
259 my @arg = split('\s', $line);
260 my $param = parse_argv
(@arg);
262 if ($param->{source
} && $param->{dest
}) {
263 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
264 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
265 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
266 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
267 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
268 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
280 my $source = parse_target
($param->{source
});
281 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
283 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
284 $job->{dest
} = $param->{dest
} if $param->{dest
};
285 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
286 $job->{method} = "ssh" if !$job->{method};
287 $job->{limit
} = $param->{limit
};
288 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
289 $job->{source
} = $param->{source
};
297 make_path
$CONFIG_PATH;
298 my $new_fh = IO
::File-
>new("> $STATE");
299 die "Could not create $STATE: $!\n" if !$new_fh;
305 my $fh = IO
::File-
>new("< $STATE");
306 die "Could not open file $STATE: $!\n" if !$fh;
309 my $states = decode_json
($text);
323 $in_fh = IO
::File-
>new("< $STATE");
324 die "Could not open file $STATE: $!\n" if !$in_fh;
329 my $out_fh = IO
::File-
>new("> $STATE.new");
330 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
335 $states = decode_json
($text);
336 $state = $states->{$job->{source
}}->{$job->{name
}};
339 if ($job->{state} ne "del") {
340 $state->{state} = $job->{state};
341 $state->{lsync
} = $job->{lsync
};
342 $state->{vm_type
} = $job->{vm_type
};
344 for (my $i = 0; $job->{"snap$i"} ; $i++) {
345 $state->{"snap$i"} = $job->{"snap$i"};
347 $states->{$job->{source
}}->{$job->{name
}} = $state;
350 delete $states->{$job->{source
}}->{$job->{name
}};
351 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
354 $text = encode_json
($states);
358 move
("$STATE.new", $STATE);
373 my $header = "SHELL=/bin/sh\n";
374 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
376 my $fh = IO
::File-
>new("< $CRONJOBS");
377 die "Could not open file $CRONJOBS: $!\n" if !$fh;
382 while (my $line = shift(@test)) {
384 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
386 next if $job->{state} eq "del";
387 $text .= format_job
($job, $line);
389 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
398 $text = "$header$text";
402 $text .= format_job
($job);
404 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
405 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
407 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
410 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
415 my ($job, $line) = @_;
418 if ($job->{state} eq "stopped") {
422 $line =~ /^#*(.+) root/;
425 $text .= "*/$INTERVAL * * * *";
428 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
429 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
430 $text .= " --limit $job->{limit}" if $job->{limit
};
431 $text .= " --method $job->{method}";
432 $text .= " --verbose" if $job->{verbose
};
440 my $cfg = read_cron
();
442 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
444 my $states = read_state
();
445 foreach my $source (sort keys%{$cfg}) {
446 foreach my $name (sort keys%{$cfg->{$source}}) {
447 $list .= sprintf("%-25s", cut_target_width
($source, 25));
448 $list .= sprintf("%-25s", cut_target_width
($name, 25));
449 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
450 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
451 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
452 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
462 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
466 return undef if !defined($target->{vmid
});
468 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
470 return "qemu" if $res;
472 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
474 return "lxc" if $res;
482 my $cfg = read_cron
();
484 my $job = param_to_job
($param);
486 $job->{state} = "ok";
489 my $source = parse_target
($param->{source
});
490 my $dest = parse_target
($param->{dest
});
492 if (my $ip = $dest->{ip
}) {
493 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
496 if (my $ip = $source->{ip
}) {
497 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
500 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest);
502 if (!defined($source->{vmid
})) {
503 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source);
506 my $vm_type = vm_exists
($source);
507 $job->{vm_type
} = $vm_type;
508 $source->{vm_type
} = $vm_type;
510 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
512 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
514 #check if vm has zfs disks if not die;
515 get_disks
($source) if $source->{vmid
};
521 sync
($param) if !$param->{skip
};
532 my $cfg = read_cron
();
534 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
535 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
537 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
538 $job->{name
} = $param->{name
};
539 $job->{source
} = $param->{source
};
540 $job = add_state_to_job
($job);
548 my $job = get_job
($param);
549 $job->{state} = "del";
558 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
559 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
562 my $date = get_date
();
565 $job = get_job
($param);
568 if ($job && $job->{state} eq "syncing") {
569 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
572 my $dest = parse_target
($param->{dest
});
573 my $source = parse_target
($param->{source
});
575 my $sync_path = sub {
576 my ($source, $dest, $job, $param, $date) = @_;
578 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
580 snapshot_add
($source, $dest, $param->{name
}, $date);
582 send_image
($source, $dest, $param);
584 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
588 my $vm_type = vm_exists
($source);
589 $source->{vm_type
} = $vm_type;
592 $job->{state} = "syncing";
593 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
598 if ($source->{vmid
}) {
599 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
600 my $disks = get_disks
($source);
602 foreach my $disk (sort keys %{$disks}) {
603 $source->{all
} = $disks->{$disk}->{all
};
604 $source->{pool
} = $disks->{$disk}->{pool
};
605 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
606 $source->{last_part
} = $disks->{$disk}->{last_part
};
607 &$sync_path($source, $dest, $job, $param, $date);
609 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
610 send_config
($source, $dest,'ssh');
612 send_config
($source, $dest,'local');
615 &$sync_path($source, $dest, $job, $param, $date);
620 $job->{state} = "error";
624 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
630 $job->{state} = "ok";
631 $job->{lsync
} = $date;
640 my ($source, $dest, $max_snap, $name) = @_;
643 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
644 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
645 push @$cmd, $source->{all
};
647 my $raw = run_cmd
($cmd);
650 my $last_snap = undef;
653 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
655 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
657 $last_snap = $1 if (!$last_snap);
660 if ($index == $max_snap) {
661 $source->{destroy
} = 1;
667 return ($old_snap, $last_snap) if $last_snap;
673 my ($source, $dest, $name, $date) = @_;
675 my $snap_name = "rep_$name\_".$date;
677 $source->{new_snap
} = $snap_name;
679 my $path = "$source->{all}\@$snap_name";
682 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
683 push @$cmd, 'zfs', 'snapshot', $path;
689 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
697 my $text = "SHELL=/bin/sh\n";
698 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
700 my $fh = IO
::File-
>new("> $CRONJOBS");
701 die "Could not open file: $!\n" if !$fh;
703 foreach my $source (sort keys%{$cfg}) {
704 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
705 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
706 $text .= "$PROG_PATH sync";
707 $text .= " -source ";
708 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
709 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
710 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
712 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
713 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
714 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
717 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
718 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
719 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
720 $text .= " -name $sync_name ";
721 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
722 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
726 die "Can't write to cron\n" if (!print($fh $text));
734 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
736 if ($target->{vm_type
} eq 'qemu') {
737 push @$cmd, 'qm', 'config', $target->{vmid
};
738 } elsif ($target->{vm_type
} eq 'lxc') {
739 push @$cmd, 'pct', 'config', $target->{vmid
};
741 die "VM Type unknown\n";
744 my $res = run_cmd
($cmd);
746 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
});
753 print "Start CMD\n" if $DEBUG;
754 print Dumper
$cmd if $DEBUG;
755 if (ref($cmd) eq 'ARRAY') {
756 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
758 my $output = `$cmd 2>&1`;
760 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
763 print Dumper
$output if $DEBUG;
764 print "END CMD\n" if $DEBUG;
769 my ($text, $ip, $vm_type) = @_;
774 while ($text && $text =~ s/^(.*?)(\n|$)//) {
777 next if $line =~ /media=cdrom/;
778 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
780 #QEMU if backup is not set include in sync
781 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
783 #LXC if backup is not set do no in sync
784 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
788 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
789 my @parameter = split(/,/,$1);
791 foreach my $opt (@parameter) {
792 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
799 if (!defined($disk) || !defined($stor)) {
800 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
805 push @$cmd, 'ssh', "root\
@$ip", '--' if $ip;
806 push @$cmd, 'pvesm', 'path', "$stor$disk";
807 my $path = run_cmd($cmd);
809 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
811 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
813 my @array = split('/', $1);
814 $disks->{$num}->{pool} = shift(@array);
815 $disks->{$num}->{all} = $disks->{$num}->{pool};
817 $disks->{$num}->{path} = join('/', @array);
818 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
820 $disks->{$num}->{last_part} = $disk;
821 $disks->{$num}->{all} .= "\
/$disk";
824 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
826 $disks->{$num}->{pool} = $1;
827 $disks->{$num}->{all} = $disks->{$num}->{pool};
830 $disks->{$num}->{path} = $3;
831 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
834 $disks->{$num}->{last_part} = $disk;
835 $disks->{$num}->{all} .= "\
/$disk";
840 die "ERROR
: in path
\n";
844 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
848 sub snapshot_destroy {
849 my ($source, $dest, $method, $snap) = @_;
851 my @zfscmd = ('zfs', 'destroy');
852 my $snapshot = "$source->{all
}\
@$snap";
855 if($source->{ip} && $method eq 'ssh'){
856 run_cmd(['ssh', "root\
@$source->{ip
}", '--', @zfscmd, $snapshot]);
858 run_cmd([@zfscmd, $snapshot]);
865 my @ssh = $dest->{ip} ? ('ssh', "root\
@$dest->{ip
}", '--') : ();
867 my $path = "$dest->{all
}";
868 $path .= "/$source->{last_part
}" if $source->{last_part};
871 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
880 my ($source , $dest, $method) = @_;
883 push @$cmd, 'ssh', "root\
@$dest->{ip
}", '--' if $dest->{ip};
884 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
886 my $path = $dest->{all};
887 $path .= "/$source->{last_part
}" if $source->{last_part};
888 $path .= "\
@$source->{old_snap
}";
894 eval {$text =run_cmd($cmd);};
900 while ($text && $text =~ s/^(.*?)(\n|$)//) {
902 return 1 if $line =~ m/^.*$source->{old_snap}$/;
907 my ($source, $dest, $param) = @_;
911 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$source->{ip
}", '--' if $source->{ip};
912 push @$cmd, 'zfs', 'send';
913 push @$cmd, '-v' if $param->{verbose};
915 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) {
916 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
918 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
920 if ($param->{limit}){
921 my $bwl = $param->{limit}*1024;
922 push @$cmd, \'|', 'cstream', '-t', $bwl;
924 my $target = "$dest->{all
}";
925 $target .= "/$source->{last_part
}" if $source->{last_part};
929 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\
@$dest->{ip
}", '--' if $dest->{ip};
930 push @$cmd, 'zfs', 'recv', '-F', '--';
931 push @$cmd, "$target";
938 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
945 my ($source, $dest, $method) = @_;
947 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
948 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
950 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
952 $dest_target_new = $config_dir.'/'.$dest_target_new;
954 if ($method eq 'ssh'){
955 if ($dest->{ip} && $source->{ip}) {
956 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
957 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", "root\
@[$dest->{ip
}]:$dest_target_new"]);
958 } elsif ($dest->{ip}) {
959 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
960 run_cmd(['scp', '--', $source_target, "root\
@[$dest->{ip
}]:$dest_target_new"]);
961 } elsif ($source->{ip}) {
962 run_cmd(['mkdir', '-p', '--', $config_dir]);
963 run_cmd(['scp', '--', "root\
@[$source->{ip
}]:$source_target", $dest_target_new]);
966 if ($source->{destroy}){
967 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
969 run_cmd(['ssh', "root\
@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
971 run_cmd(['rm', '-f', '--', $dest_target_old]);
974 } elsif ($method eq 'local') {
975 run_cmd(['mkdir', '-p', '--', $config_dir]);
976 run_cmd(['cp', $source_target, $dest_target_new]);
981 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
982 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
988 my $cfg = read_cron();
990 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
992 my $states = read_state();
994 foreach my $source (sort keys%{$cfg}) {
995 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
996 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
997 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
998 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1002 return $status_list;
1008 my $job = get_job($param);
1009 $job->{state} = "ok
";
1017 my $job = get_job($param);
1018 $job->{state} = "stopped
";
1025 $PROGNAME destroy -source <string> [OPTIONS]
1027 remove a sync Job from the scheduler
1031 name of the sync job, if not set it is default
1035 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1038 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1044 the destination target is like [IP]:<Pool>[/Path]
1048 max sync speed in kBytes/s, default unlimited
1052 how much snapshots will be kept before get erased, default 1
1056 name of the sync job, if not set it is default
1060 if this flag is set it will skip the first sync
1064 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1067 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1073 the destination target is like [IP:]<Pool>[/Path]
1077 max sync speed in kBytes/s, default unlimited
1081 how much snapshots will be kept before get erased, default 1
1085 name of the sync job, if not set it is default.
1086 It is only necessary if scheduler allready contains this source.
1090 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1094 print out the sync progress.
1099 Get a List of all scheduled Sync Jobs
1104 Get the status of all scheduled Sync Jobs
1107 $PROGNAME help <cmd> [OPTIONS]
1109 Get help about specified command.
1117 Verbose output format.
1120 $PROGNAME enable -source <string> [OPTIONS]
1122 enable a syncjob and reset error
1126 name of the sync job, if not set it is default
1130 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1133 $PROGNAME disable -source <string> [OPTIONS]
1139 name of the sync job, if not set it is default
1143 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1145 printpod
=> 'internal command',
1151 } elsif (!$cmd_help->{$command}) {
1152 print "ERROR: unknown command '$command'";
1157 my $param = parse_argv
(@arg);
1161 die "$cmd_help->{$command}\n" if !$param->{$_};
1165 if ($command eq 'destroy') {
1166 check_params
(qw(source));
1168 check_target
($param->{source
});
1169 destroy_job
($param);
1171 } elsif ($command eq 'sync') {
1172 check_params
(qw(source dest));
1174 check_target
($param->{source
});
1175 check_target
($param->{dest
});
1178 } elsif ($command eq 'create') {
1179 check_params
(qw(source dest));
1181 check_target
($param->{source
});
1182 check_target
($param->{dest
});
1185 } elsif ($command eq 'status') {
1188 } elsif ($command eq 'list') {
1191 } elsif ($command eq 'help') {
1192 my $help_command = $ARGV[1];
1194 if ($help_command && $cmd_help->{$help_command}) {
1195 die "$cmd_help->{$command}\n";
1198 if ($param->{verbose
}) {
1199 exec("man $PROGNAME");
1206 } elsif ($command eq 'enable') {
1207 check_params
(qw(source));
1209 check_target
($param->{source
});
1212 } elsif ($command eq 'disable') {
1213 check_params
(qw(source));
1215 check_target
($param->{source
});
1216 disable_job
($param);
1218 } elsif ($command eq 'printpod') {
1225 print("ERROR:\tno command specified\n") if !$help;
1226 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1227 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1228 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1229 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1230 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1231 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1232 print("\t$PROGNAME list\n");
1233 print("\t$PROGNAME status\n");
1234 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1239 parse_target
($target);
1244 my $synopsis = join("\n", sort values %$cmd_help);
1249 pve-zsync - PVE ZFS Replication Manager
1253 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1259 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1260 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1261 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.
1262 To config cron see man crontab.
1264 =head2 PVE ZFS Storage sync Tool
1266 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1270 add sync job from local VM to remote ZFS Server
1271 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1273 =head1 IMPORTANT FILES
1275 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1277 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1279 =head1 COPYRIGHT AND DISCLAIMER
1281 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1283 This program is free software: you can redistribute it and/or modify it
1284 under the terms of the GNU Affero General Public License as published
1285 by the Free Software Foundation, either version 3 of the License, or
1286 (at your option) any later version.
1288 This program is distributed in the hope that it will be useful, but
1289 WITHOUT ANY WARRANTY; without even the implied warranty of
1290 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1291 Affero General Public License for more details.
1293 You should have received a copy of the GNU Affero General Public
1294 License along with this program. If not, see
1295 <http://www.gnu.org/licenses/>.