]>
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
{
127 my ($target, $user) = @_;
132 push @$cmd, 'ssh', "$user\@$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);
213 source_user
=> undef,
218 my ($ret) = GetOptionsFromArray
(
220 'dest=s' => \
$param->{dest
},
221 'source=s' => \
$param->{source
},
222 'verbose' => \
$param->{verbose
},
223 'limit=i' => \
$param->{limit
},
224 'maxsnap=i' => \
$param->{maxsnap
},
225 'name=s' => \
$param->{name
},
226 'skip' => \
$param->{skip
},
227 'method=s' => \
$param->{method},
228 'source-user=s' => \
$param->{source_user
},
229 'dest-user=s' => \
$param->{dest_user
},
230 'properties' => \
$param->{properties
},
233 die "can't parse options\n" if $ret == 0;
235 $param->{name
} //= "default";
236 $param->{maxsnap
} //= 1;
237 $param->{method} //= "ssh";
238 $param->{source_user
} //= "root";
239 $param->{dest_user
} //= "root";
244 sub add_state_to_job
{
247 my $states = read_state
();
248 my $state = $states->{$job->{source
}}->{$job->{name
}};
250 $job->{state} = $state->{state};
251 $job->{lsync
} = $state->{lsync
};
252 $job->{vm_type
} = $state->{vm_type
};
254 for (my $i = 0; $state->{"snap$i"}; $i++) {
255 $job->{"snap$i"} = $state->{"snap$i"};
266 while (my $line = shift(@text)) {
268 my @arg = split('\s', $line);
269 my $param = parse_argv
(@arg);
271 if ($param->{source
} && $param->{dest
}) {
272 my $source = delete $param->{source
};
273 my $name = delete $param->{name
};
275 $cfg->{$source}->{$name} = $param;
287 my $source = parse_target
($param->{source
});
288 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
290 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
291 $job->{dest
} = $param->{dest
} if $param->{dest
};
292 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
293 $job->{method} = "ssh" if !$job->{method};
294 $job->{limit
} = $param->{limit
};
295 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
296 $job->{source
} = $param->{source
};
297 $job->{source_user
} = $param->{source_user
};
298 $job->{dest_user
} = $param->{dest_user
};
299 $job->{properties
} = !!$param->{properties
};
307 make_path
$CONFIG_PATH;
308 my $new_fh = IO
::File-
>new("> $STATE");
309 die "Could not create $STATE: $!\n" if !$new_fh;
315 my $fh = IO
::File-
>new("< $STATE");
316 die "Could not open file $STATE: $!\n" if !$fh;
319 my $states = decode_json
($text);
333 $in_fh = IO
::File-
>new("< $STATE");
334 die "Could not open file $STATE: $!\n" if !$in_fh;
339 my $out_fh = IO
::File-
>new("> $STATE.new");
340 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
345 $states = decode_json
($text);
346 $state = $states->{$job->{source
}}->{$job->{name
}};
349 if ($job->{state} ne "del") {
350 $state->{state} = $job->{state};
351 $state->{lsync
} = $job->{lsync
};
352 $state->{vm_type
} = $job->{vm_type
};
354 for (my $i = 0; $job->{"snap$i"} ; $i++) {
355 $state->{"snap$i"} = $job->{"snap$i"};
357 $states->{$job->{source
}}->{$job->{name
}} = $state;
360 delete $states->{$job->{source
}}->{$job->{name
}};
361 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
364 $text = encode_json
($states);
368 move
("$STATE.new", $STATE);
383 my $header = "SHELL=/bin/sh\n";
384 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
386 my $fh = IO
::File-
>new("< $CRONJOBS");
387 die "Could not open file $CRONJOBS: $!\n" if !$fh;
392 while (my $line = shift(@test)) {
394 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
396 next if $job->{state} eq "del";
397 $text .= format_job
($job, $line);
399 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
408 $text = "$header$text";
412 $text .= format_job
($job);
414 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
415 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
417 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
420 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
425 my ($job, $line) = @_;
428 if ($job->{state} eq "stopped") {
432 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
435 $text .= "*/$INTERVAL * * * *";
438 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
439 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
440 $text .= " --limit $job->{limit}" if $job->{limit
};
441 $text .= " --method $job->{method}";
442 $text .= " --verbose" if $job->{verbose
};
443 $text .= " --source-user $job->{source_user}";
444 $text .= " --dest-user $job->{dest_user}";
445 $text .= " --properties" if $job->{properties
};
453 my $cfg = read_cron
();
455 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
457 my $states = read_state
();
458 foreach my $source (sort keys%{$cfg}) {
459 foreach my $name (sort keys%{$cfg->{$source}}) {
460 $list .= sprintf("%-25s", cut_target_width
($source, 25));
461 $list .= sprintf("%-25s", cut_target_width
($name, 25));
462 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
463 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
464 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
465 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
473 my ($target, $user) = @_;
475 return undef if !defined($target->{vmid
});
477 my $conf_fn = "$target->{vmid}.conf";
480 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
481 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
482 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
484 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
485 return "lxc" if -f
"$LXC_CONF/$conf_fn";
494 my $cfg = read_cron
();
496 my $job = param_to_job
($param);
498 $job->{state} = "ok";
501 my $source = parse_target
($param->{source
});
502 my $dest = parse_target
($param->{dest
});
504 if (my $ip = $dest->{ip
}) {
505 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
508 if (my $ip = $source->{ip
}) {
509 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
512 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
514 if (!defined($source->{vmid
})) {
515 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
518 my $vm_type = vm_exists
($source, $param->{source_user
});
519 $job->{vm_type
} = $vm_type;
520 $source->{vm_type
} = $vm_type;
522 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
524 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
526 #check if vm has zfs disks if not die;
527 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
533 sync
($param) if !$param->{skip
};
544 my $cfg = read_cron
();
546 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
547 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
549 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
550 $job->{name
} = $param->{name
};
551 $job->{source
} = $param->{source
};
552 $job = add_state_to_job
($job);
560 my $job = get_job
($param);
561 $job->{state} = "del";
570 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
571 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
574 my $date = get_date
();
577 $job = get_job
($param);
580 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
581 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
584 my $dest = parse_target
($param->{dest
});
585 my $source = parse_target
($param->{source
});
587 my $sync_path = sub {
588 my ($source, $dest, $job, $param, $date) = @_;
590 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
592 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
594 send_image
($source, $dest, $param);
596 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
600 my $vm_type = vm_exists
($source, $param->{source_user
});
601 $source->{vm_type
} = $vm_type;
604 $job->{state} = "syncing";
605 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
610 if ($source->{vmid
}) {
611 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
612 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
613 my $disks = get_disks
($source, $param->{source_user
});
615 foreach my $disk (sort keys %{$disks}) {
616 $source->{all
} = $disks->{$disk}->{all
};
617 $source->{pool
} = $disks->{$disk}->{pool
};
618 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
619 $source->{last_part
} = $disks->{$disk}->{last_part
};
620 &$sync_path($source, $dest, $job, $param, $date);
622 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
623 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
});
625 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
});
628 &$sync_path($source, $dest, $job, $param, $date);
633 $job->{state} = "error";
637 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
643 $job->{state} = "ok";
644 $job->{lsync
} = $date;
653 my ($source, $dest, $max_snap, $name, $source_user) = @_;
656 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
657 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
658 push @$cmd, $source->{all
};
660 my $raw = run_cmd
($cmd);
663 my $last_snap = undef;
666 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
668 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
670 $last_snap = $1 if (!$last_snap);
673 if ($index == $max_snap) {
674 $source->{destroy
} = 1;
680 return ($old_snap, $last_snap) if $last_snap;
686 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
688 my $snap_name = "rep_$name\_".$date;
690 $source->{new_snap
} = $snap_name;
692 my $path = "$source->{all}\@$snap_name";
695 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
696 push @$cmd, 'zfs', 'snapshot', $path;
702 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
710 my $text = "SHELL=/bin/sh\n";
711 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
713 my $fh = IO
::File-
>new("> $CRONJOBS");
714 die "Could not open file: $!\n" if !$fh;
716 foreach my $source (sort keys%{$cfg}) {
717 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
718 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
719 $text .= "$PROG_PATH sync";
720 $text .= " -source ";
721 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
722 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
723 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
725 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
726 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
727 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
730 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
731 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
732 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
733 $text .= " -name $sync_name ";
734 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
735 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
739 die "Can't write to cron\n" if (!print($fh $text));
744 my ($target, $user) = @_;
747 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
749 if ($target->{vm_type
} eq 'qemu') {
750 push @$cmd, 'qm', 'config', $target->{vmid
};
751 } elsif ($target->{vm_type
} eq 'lxc') {
752 push @$cmd, 'pct', 'config', $target->{vmid
};
754 die "VM Type unknown\n";
757 my $res = run_cmd
($cmd);
759 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
766 print "Start CMD\n" if $DEBUG;
767 print Dumper
$cmd if $DEBUG;
768 if (ref($cmd) eq 'ARRAY') {
769 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
771 my $output = `$cmd 2>&1`;
773 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
776 print Dumper
$output if $DEBUG;
777 print "END CMD\n" if $DEBUG;
782 my ($text, $ip, $vm_type, $user) = @_;
787 while ($text && $text =~ s/^(.*?)(\n|$)//) {
790 next if $line =~ /media=cdrom/;
791 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
793 #QEMU if backup is not set include in sync
794 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
796 #LXC if backup is not set do no in sync
797 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
801 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
802 my @parameter = split(/,/,$1);
804 foreach my $opt (@parameter) {
805 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
812 if (!defined($disk) || !defined($stor)) {
813 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
818 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
819 push @$cmd, 'pvesm', 'path', "$stor$disk";
820 my $path = run_cmd($cmd);
822 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
824 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
826 my @array = split('/', $1);
827 $disks->{$num}->{pool} = shift(@array);
828 $disks->{$num}->{all} = $disks->{$num}->{pool};
830 $disks->{$num}->{path} = join('/', @array);
831 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
833 $disks->{$num}->{last_part} = $disk;
834 $disks->{$num}->{all} .= "\
/$disk";
837 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
839 $disks->{$num}->{pool} = $1;
840 $disks->{$num}->{all} = $disks->{$num}->{pool};
843 $disks->{$num}->{path} = $3;
844 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
847 $disks->{$num}->{last_part} = $disk;
848 $disks->{$num}->{all} .= "\
/$disk";
853 die "ERROR
: in path
\n";
857 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
861 sub snapshot_destroy {
862 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
864 my @zfscmd = ('zfs', 'destroy');
865 my $snapshot = "$source->{all
}\
@$snap";
868 if($source->{ip} && $method eq 'ssh'){
869 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
871 run_cmd([@zfscmd, $snapshot]);
878 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
880 my $path = "$dest->{all
}";
881 $path .= "/$source->{last_part
}" if $source->{last_part};
884 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
893 my ($source , $dest, $method, $dest_user) = @_;
896 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
897 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
899 my $path = $dest->{all};
900 $path .= "/$source->{last_part
}" if $source->{last_part};
901 $path .= "\
@$source->{old_snap
}";
907 eval {$text =run_cmd($cmd);};
913 while ($text && $text =~ s/^(.*?)(\n|$)//) {
915 return 1 if $line =~ m/^.*$source->{old_snap}$/;
920 my ($source, $dest, $param) = @_;
924 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
925 push @$cmd, 'zfs', 'send';
926 push @$cmd, '-p', if $param->{properties};
927 push @$cmd, '-v' if $param->{verbose};
929 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
930 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
932 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
934 if ($param->{limit}){
935 my $bwl = $param->{limit}*1024;
936 push @$cmd, \'|', 'cstream', '-t', $bwl;
938 my $target = "$dest->{all
}";
939 $target .= "/$source->{last_part
}" if $source->{last_part};
943 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
944 push @$cmd, 'zfs', 'recv', '-F', '--';
945 push @$cmd, "$target";
952 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
959 my ($source, $dest, $method, $source_user, $dest_user) = @_;
961 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
962 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
964 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
966 $dest_target_new = $config_dir.'/'.$dest_target_new;
968 if ($method eq 'ssh'){
969 if ($dest->{ip} && $source->{ip}) {
970 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
971 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
972 } elsif ($dest->{ip}) {
973 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
974 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
975 } elsif ($source->{ip}) {
976 run_cmd(['mkdir', '-p', '--', $config_dir]);
977 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
980 if ($source->{destroy}){
981 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
983 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
985 run_cmd(['rm', '-f', '--', $dest_target_old]);
988 } elsif ($method eq 'local') {
989 run_cmd(['mkdir', '-p', '--', $config_dir]);
990 run_cmd(['cp', $source_target, $dest_target_new]);
995 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
996 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1002 my $cfg = read_cron();
1004 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1006 my $states = read_state();
1008 foreach my $source (sort keys%{$cfg}) {
1009 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1010 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1011 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1012 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1016 return $status_list;
1022 my $job = get_job($param);
1023 $job->{state} = "ok
";
1031 my $job = get_job($param);
1032 $job->{state} = "stopped
";
1039 $PROGNAME destroy -source <string> [OPTIONS]
1041 remove a sync Job from the scheduler
1045 name of the sync job, if not set it is default
1049 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1052 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1058 the destination target is like [IP]:<Pool>[/Path]
1062 name of the user on the destination target, root by default
1066 max sync speed in kBytes/s, default unlimited
1070 how much snapshots will be kept before get erased, default 1
1074 name of the sync job, if not set it is default
1078 if this flag is set it will skip the first sync
1082 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1086 name of the user on the source target, root by default
1090 Include the dataset's properties in the stream.
1093 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1099 the destination target is like [IP:]<Pool>[/Path]
1103 name of the user on the destination target, root by default
1107 max sync speed in kBytes/s, default unlimited
1111 how much snapshots will be kept before get erased, default 1
1115 name of the sync job, if not set it is default.
1116 It is only necessary if scheduler allready contains this source.
1120 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1124 name of the user on the source target, root by default
1128 print out the sync progress.
1132 Include the dataset's properties in the stream.
1137 Get a List of all scheduled Sync Jobs
1142 Get the status of all scheduled Sync Jobs
1145 $PROGNAME help <cmd> [OPTIONS]
1147 Get help about specified command.
1155 Verbose output format.
1158 $PROGNAME enable -source <string> [OPTIONS]
1160 enable a syncjob and reset error
1164 name of the sync job, if not set it is default
1168 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1171 $PROGNAME disable -source <string> [OPTIONS]
1177 name of the sync job, if not set it is default
1181 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1183 printpod
=> 'internal command',
1189 } elsif (!$cmd_help->{$command}) {
1190 print "ERROR: unknown command '$command'";
1195 my $param = parse_argv
(@arg);
1199 die "$cmd_help->{$command}\n" if !$param->{$_};
1203 if ($command eq 'destroy') {
1204 check_params
(qw(source));
1206 check_target
($param->{source
});
1207 destroy_job
($param);
1209 } elsif ($command eq 'sync') {
1210 check_params
(qw(source dest));
1212 check_target
($param->{source
});
1213 check_target
($param->{dest
});
1216 } elsif ($command eq 'create') {
1217 check_params
(qw(source dest));
1219 check_target
($param->{source
});
1220 check_target
($param->{dest
});
1223 } elsif ($command eq 'status') {
1226 } elsif ($command eq 'list') {
1229 } elsif ($command eq 'help') {
1230 my $help_command = $ARGV[1];
1232 if ($help_command && $cmd_help->{$help_command}) {
1233 die "$cmd_help->{$help_command}\n";
1236 if ($param->{verbose
}) {
1237 exec("man $PROGNAME");
1244 } elsif ($command eq 'enable') {
1245 check_params
(qw(source));
1247 check_target
($param->{source
});
1250 } elsif ($command eq 'disable') {
1251 check_params
(qw(source));
1253 check_target
($param->{source
});
1254 disable_job
($param);
1256 } elsif ($command eq 'printpod') {
1263 print("ERROR:\tno command specified\n") if !$help;
1264 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1265 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1266 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1267 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1268 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1269 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1270 print("\t$PROGNAME list\n");
1271 print("\t$PROGNAME status\n");
1272 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1277 parse_target
($target);
1282 my $synopsis = join("\n", sort values %$cmd_help);
1287 pve-zsync - PVE ZFS Replication Manager
1291 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1297 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1298 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1299 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.
1300 To config cron see man crontab.
1302 =head2 PVE ZFS Storage sync Tool
1304 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1308 add sync job from local VM to remote ZFS Server
1309 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1311 =head1 IMPORTANT FILES
1313 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1315 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1317 =head1 COPYRIGHT AND DISCLAIMER
1319 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1321 This program is free software: you can redistribute it and/or modify it
1322 under the terms of the GNU Affero General Public License as published
1323 by the Free Software Foundation, either version 3 of the License, or
1324 (at your option) any later version.
1326 This program is distributed in the hope that it will be useful, but
1327 WITHOUT ANY WARRANTY; without even the implied warranty of
1328 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1329 Affero General Public License for more details.
1331 You should have received a copy of the GNU Affero General Public
1332 License along with this program. If not, see
1333 <http://www.gnu.org/licenses/>.