]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
bf00c7aaa9446c8d5df400e68cce0e3b33382766
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,
217 my ($ret) = GetOptionsFromArray
(
219 'dest=s' => \
$param->{dest
},
220 'source=s' => \
$param->{source
},
221 'verbose' => \
$param->{verbose
},
222 'limit=i' => \
$param->{limit
},
223 'maxsnap=i' => \
$param->{maxsnap
},
224 'name=s' => \
$param->{name
},
225 'skip' => \
$param->{skip
},
226 'method=s' => \
$param->{method},
227 'source-user=s' => \
$param->{source_user
},
228 'dest-user=s' => \
$param->{dest_user
}
231 die "can't parse options\n" if $ret == 0;
233 $param->{name
} //= "default";
234 $param->{maxsnap
} //= 1;
235 $param->{method} //= "ssh";
236 $param->{source_user
} //= "root";
237 $param->{dest_user
} //= "root";
242 sub add_state_to_job
{
245 my $states = read_state
();
246 my $state = $states->{$job->{source
}}->{$job->{name
}};
248 $job->{state} = $state->{state};
249 $job->{lsync
} = $state->{lsync
};
250 $job->{vm_type
} = $state->{vm_type
};
252 for (my $i = 0; $state->{"snap$i"}; $i++) {
253 $job->{"snap$i"} = $state->{"snap$i"};
264 while (my $line = shift(@text)) {
266 my @arg = split('\s', $line);
267 my $param = parse_argv
(@arg);
269 if ($param->{source
} && $param->{dest
}) {
270 my $source = delete $param->{source
};
271 my $name = delete $param->{name
};
273 $cfg->{$source}->{$name} = $param;
285 my $source = parse_target
($param->{source
});
286 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
288 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
289 $job->{dest
} = $param->{dest
} if $param->{dest
};
290 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
291 $job->{method} = "ssh" if !$job->{method};
292 $job->{limit
} = $param->{limit
};
293 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
294 $job->{source
} = $param->{source
};
295 $job->{source_user
} = $param->{source_user
};
296 $job->{dest_user
} = $param->{dest_user
};
304 make_path
$CONFIG_PATH;
305 my $new_fh = IO
::File-
>new("> $STATE");
306 die "Could not create $STATE: $!\n" if !$new_fh;
312 my $fh = IO
::File-
>new("< $STATE");
313 die "Could not open file $STATE: $!\n" if !$fh;
316 my $states = decode_json
($text);
330 $in_fh = IO
::File-
>new("< $STATE");
331 die "Could not open file $STATE: $!\n" if !$in_fh;
336 my $out_fh = IO
::File-
>new("> $STATE.new");
337 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
342 $states = decode_json
($text);
343 $state = $states->{$job->{source
}}->{$job->{name
}};
346 if ($job->{state} ne "del") {
347 $state->{state} = $job->{state};
348 $state->{lsync
} = $job->{lsync
};
349 $state->{vm_type
} = $job->{vm_type
};
351 for (my $i = 0; $job->{"snap$i"} ; $i++) {
352 $state->{"snap$i"} = $job->{"snap$i"};
354 $states->{$job->{source
}}->{$job->{name
}} = $state;
357 delete $states->{$job->{source
}}->{$job->{name
}};
358 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
361 $text = encode_json
($states);
365 move
("$STATE.new", $STATE);
380 my $header = "SHELL=/bin/sh\n";
381 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
383 my $fh = IO
::File-
>new("< $CRONJOBS");
384 die "Could not open file $CRONJOBS: $!\n" if !$fh;
389 while (my $line = shift(@test)) {
391 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
393 next if $job->{state} eq "del";
394 $text .= format_job
($job, $line);
396 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
405 $text = "$header$text";
409 $text .= format_job
($job);
411 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
412 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
414 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
417 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
422 my ($job, $line) = @_;
425 if ($job->{state} eq "stopped") {
429 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
432 $text .= "*/$INTERVAL * * * *";
435 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
436 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
437 $text .= " --limit $job->{limit}" if $job->{limit
};
438 $text .= " --method $job->{method}";
439 $text .= " --verbose" if $job->{verbose
};
440 $text .= " --source-user $job->{source_user}";
441 $text .= " --dest-user $job->{dest_user}";
449 my $cfg = read_cron
();
451 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
453 my $states = read_state
();
454 foreach my $source (sort keys%{$cfg}) {
455 foreach my $name (sort keys%{$cfg->{$source}}) {
456 $list .= sprintf("%-25s", cut_target_width
($source, 25));
457 $list .= sprintf("%-25s", cut_target_width
($name, 25));
458 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
459 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
460 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
461 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
469 my ($target, $user) = @_;
471 my @cmd = ('ssh', "$user\@$target->{ip}", '--') if $target->{ip
};
475 return undef if !defined($target->{vmid
});
477 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
479 return "qemu" if $res;
481 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
483 return "lxc" if $res;
491 my $cfg = read_cron
();
493 my $job = param_to_job
($param);
495 $job->{state} = "ok";
498 my $source = parse_target
($param->{source
});
499 my $dest = parse_target
($param->{dest
});
501 if (my $ip = $dest->{ip
}) {
502 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
505 if (my $ip = $source->{ip
}) {
506 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
509 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
511 if (!defined($source->{vmid
})) {
512 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
515 my $vm_type = vm_exists
($source, $param->{source_user
});
516 $job->{vm_type
} = $vm_type;
517 $source->{vm_type
} = $vm_type;
519 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
521 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
523 #check if vm has zfs disks if not die;
524 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
530 sync
($param) if !$param->{skip
};
541 my $cfg = read_cron
();
543 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
544 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
546 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
547 $job->{name
} = $param->{name
};
548 $job->{source
} = $param->{source
};
549 $job = add_state_to_job
($job);
557 my $job = get_job
($param);
558 $job->{state} = "del";
567 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
568 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
571 my $date = get_date
();
574 $job = get_job
($param);
577 if ($job && $job->{state} eq "syncing") {
578 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
581 my $dest = parse_target
($param->{dest
});
582 my $source = parse_target
($param->{source
});
584 my $sync_path = sub {
585 my ($source, $dest, $job, $param, $date) = @_;
587 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
589 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
591 send_image
($source, $dest, $param);
593 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
597 my $vm_type = vm_exists
($source, $param->{source_user
});
598 $source->{vm_type
} = $vm_type;
601 $job->{state} = "syncing";
602 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
607 if ($source->{vmid
}) {
608 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
609 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
610 my $disks = get_disks
($source, $param->{source_user
});
612 foreach my $disk (sort keys %{$disks}) {
613 $source->{all
} = $disks->{$disk}->{all
};
614 $source->{pool
} = $disks->{$disk}->{pool
};
615 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
616 $source->{last_part
} = $disks->{$disk}->{last_part
};
617 &$sync_path($source, $dest, $job, $param, $date);
619 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
620 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
});
622 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
});
625 &$sync_path($source, $dest, $job, $param, $date);
630 $job->{state} = "error";
634 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
640 $job->{state} = "ok";
641 $job->{lsync
} = $date;
650 my ($source, $dest, $max_snap, $name, $source_user) = @_;
653 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
654 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
655 push @$cmd, $source->{all
};
657 my $raw = run_cmd
($cmd);
660 my $last_snap = undef;
663 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
665 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
667 $last_snap = $1 if (!$last_snap);
670 if ($index == $max_snap) {
671 $source->{destroy
} = 1;
677 return ($old_snap, $last_snap) if $last_snap;
683 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
685 my $snap_name = "rep_$name\_".$date;
687 $source->{new_snap
} = $snap_name;
689 my $path = "$source->{all}\@$snap_name";
692 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
693 push @$cmd, 'zfs', 'snapshot', $path;
699 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
707 my $text = "SHELL=/bin/sh\n";
708 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
710 my $fh = IO
::File-
>new("> $CRONJOBS");
711 die "Could not open file: $!\n" if !$fh;
713 foreach my $source (sort keys%{$cfg}) {
714 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
715 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
716 $text .= "$PROG_PATH sync";
717 $text .= " -source ";
718 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
719 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
720 $text .= "$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}->{source_pool}";
724 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
727 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
728 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
729 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
730 $text .= " -name $sync_name ";
731 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
732 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
736 die "Can't write to cron\n" if (!print($fh $text));
741 my ($target, $user) = @_;
744 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
746 if ($target->{vm_type
} eq 'qemu') {
747 push @$cmd, 'qm', 'config', $target->{vmid
};
748 } elsif ($target->{vm_type
} eq 'lxc') {
749 push @$cmd, 'pct', 'config', $target->{vmid
};
751 die "VM Type unknown\n";
754 my $res = run_cmd
($cmd);
756 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
763 print "Start CMD\n" if $DEBUG;
764 print Dumper
$cmd if $DEBUG;
765 if (ref($cmd) eq 'ARRAY') {
766 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
768 my $output = `$cmd 2>&1`;
770 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
773 print Dumper
$output if $DEBUG;
774 print "END CMD\n" if $DEBUG;
779 my ($text, $ip, $vm_type, $user) = @_;
784 while ($text && $text =~ s/^(.*?)(\n|$)//) {
787 next if $line =~ /media=cdrom/;
788 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
790 #QEMU if backup is not set include in sync
791 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
793 #LXC if backup is not set do no in sync
794 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
798 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
799 my @parameter = split(/,/,$1);
801 foreach my $opt (@parameter) {
802 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
809 if (!defined($disk) || !defined($stor)) {
810 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
815 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
816 push @$cmd, 'pvesm', 'path', "$stor$disk";
817 my $path = run_cmd($cmd);
819 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
821 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
823 my @array = split('/', $1);
824 $disks->{$num}->{pool} = shift(@array);
825 $disks->{$num}->{all} = $disks->{$num}->{pool};
827 $disks->{$num}->{path} = join('/', @array);
828 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
830 $disks->{$num}->{last_part} = $disk;
831 $disks->{$num}->{all} .= "\
/$disk";
834 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
836 $disks->{$num}->{pool} = $1;
837 $disks->{$num}->{all} = $disks->{$num}->{pool};
840 $disks->{$num}->{path} = $3;
841 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
844 $disks->{$num}->{last_part} = $disk;
845 $disks->{$num}->{all} .= "\
/$disk";
850 die "ERROR
: in path
\n";
854 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
858 sub snapshot_destroy {
859 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
861 my @zfscmd = ('zfs', 'destroy');
862 my $snapshot = "$source->{all
}\
@$snap";
865 if($source->{ip} && $method eq 'ssh'){
866 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
868 run_cmd([@zfscmd, $snapshot]);
875 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
877 my $path = "$dest->{all
}";
878 $path .= "/$source->{last_part
}" if $source->{last_part};
881 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
890 my ($source , $dest, $method, $source_user) = @_;
893 push @$cmd, 'ssh', "$source_user\@$dest->{ip
}", '--' if $dest->{ip};
894 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
896 my $path = $dest->{all};
897 $path .= "/$source->{last_part
}" if $source->{last_part};
898 $path .= "\
@$source->{old_snap
}";
904 eval {$text =run_cmd($cmd);};
910 while ($text && $text =~ s/^(.*?)(\n|$)//) {
912 return 1 if $line =~ m/^.*$source->{old_snap}$/;
917 my ($source, $dest, $param) = @_;
921 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
922 push @$cmd, 'zfs', 'send';
923 push @$cmd, '-v' if $param->{verbose};
925 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
926 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
928 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
930 if ($param->{limit}){
931 my $bwl = $param->{limit}*1024;
932 push @$cmd, \'|', 'cstream', '-t', $bwl;
934 my $target = "$dest->{all
}";
935 $target .= "/$source->{last_part
}" if $source->{last_part};
939 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
940 push @$cmd, 'zfs', 'recv', '-F', '--';
941 push @$cmd, "$target";
948 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
955 my ($source, $dest, $method, $source_user, $dest_user) = @_;
957 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
958 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
960 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
962 $dest_target_new = $config_dir.'/'.$dest_target_new;
964 if ($method eq 'ssh'){
965 if ($dest->{ip} && $source->{ip}) {
966 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
967 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
968 } elsif ($dest->{ip}) {
969 run_cmd(['ssh', "$source_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
970 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
971 } elsif ($source->{ip}) {
972 run_cmd(['mkdir', '-p', '--', $config_dir]);
973 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
976 if ($source->{destroy}){
977 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
979 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
981 run_cmd(['rm', '-f', '--', $dest_target_old]);
984 } elsif ($method eq 'local') {
985 run_cmd(['mkdir', '-p', '--', $config_dir]);
986 run_cmd(['cp', $source_target, $dest_target_new]);
991 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
992 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
998 my $cfg = read_cron();
1000 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1002 my $states = read_state();
1004 foreach my $source (sort keys%{$cfg}) {
1005 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1006 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1007 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1008 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1012 return $status_list;
1018 my $job = get_job($param);
1019 $job->{state} = "ok
";
1027 my $job = get_job($param);
1028 $job->{state} = "stopped
";
1035 $PROGNAME destroy -source <string> [OPTIONS]
1037 remove a sync Job from the scheduler
1041 name of the sync job, if not set it is default
1045 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1048 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1054 the destination target is like [IP]:<Pool>[/Path]
1058 name of the user on the destination target, root by default
1062 max sync speed in kBytes/s, default unlimited
1066 how much snapshots will be kept before get erased, default 1
1070 name of the sync job, if not set it is default
1074 if this flag is set it will skip the first sync
1078 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1082 name of the user on the source target, root by default
1085 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1091 the destination target is like [IP:]<Pool>[/Path]
1095 name of the user on the destination target, root by default
1099 max sync speed in kBytes/s, default unlimited
1103 how much snapshots will be kept before get erased, default 1
1107 name of the sync job, if not set it is default.
1108 It is only necessary if scheduler allready contains this source.
1112 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1116 name of the user on the source target, root by default
1120 print out the sync progress.
1125 Get a List of all scheduled Sync Jobs
1130 Get the status of all scheduled Sync Jobs
1133 $PROGNAME help <cmd> [OPTIONS]
1135 Get help about specified command.
1143 Verbose output format.
1146 $PROGNAME enable -source <string> [OPTIONS]
1148 enable a syncjob and reset error
1152 name of the sync job, if not set it is default
1156 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1159 $PROGNAME disable -source <string> [OPTIONS]
1165 name of the sync job, if not set it is default
1169 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1171 printpod
=> 'internal command',
1177 } elsif (!$cmd_help->{$command}) {
1178 print "ERROR: unknown command '$command'";
1183 my $param = parse_argv
(@arg);
1187 die "$cmd_help->{$command}\n" if !$param->{$_};
1191 if ($command eq 'destroy') {
1192 check_params
(qw(source));
1194 check_target
($param->{source
});
1195 destroy_job
($param);
1197 } elsif ($command eq 'sync') {
1198 check_params
(qw(source dest));
1200 check_target
($param->{source
});
1201 check_target
($param->{dest
});
1204 } elsif ($command eq 'create') {
1205 check_params
(qw(source dest));
1207 check_target
($param->{source
});
1208 check_target
($param->{dest
});
1211 } elsif ($command eq 'status') {
1214 } elsif ($command eq 'list') {
1217 } elsif ($command eq 'help') {
1218 my $help_command = $ARGV[1];
1220 if ($help_command && $cmd_help->{$help_command}) {
1221 die "$cmd_help->{$help_command}\n";
1224 if ($param->{verbose
}) {
1225 exec("man $PROGNAME");
1232 } elsif ($command eq 'enable') {
1233 check_params
(qw(source));
1235 check_target
($param->{source
});
1238 } elsif ($command eq 'disable') {
1239 check_params
(qw(source));
1241 check_target
($param->{source
});
1242 disable_job
($param);
1244 } elsif ($command eq 'printpod') {
1251 print("ERROR:\tno command specified\n") if !$help;
1252 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1253 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1254 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1255 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1256 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1257 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1258 print("\t$PROGNAME list\n");
1259 print("\t$PROGNAME status\n");
1260 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1265 parse_target
($target);
1270 my $synopsis = join("\n", sort values %$cmd_help);
1275 pve-zsync - PVE ZFS Replication Manager
1279 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1285 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1286 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1287 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.
1288 To config cron see man crontab.
1290 =head2 PVE ZFS Storage sync Tool
1292 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1296 add sync job from local VM to remote ZFS Server
1297 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1299 =head1 IMPORTANT FILES
1301 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1303 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1305 =head1 COPYRIGHT AND DISCLAIMER
1307 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1309 This program is free software: you can redistribute it and/or modify it
1310 under the terms of the GNU Affero General Public License as published
1311 by the Free Software Foundation, either version 3 of the License, or
1312 (at your option) any later version.
1314 This program is distributed in the hope that it will be useful, but
1315 WITHOUT ANY WARRANTY; without even the implied warranty of
1316 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1317 Affero General Public License for more details.
1319 You should have received a copy of the GNU Affero General Public
1320 License along with this program. If not, see
1321 <http://www.gnu.org/licenses/>.