]>
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,
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 return undef if !defined($target->{vmid
});
473 my $conf_fn = "$target->{vmid}.conf";
476 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
477 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
478 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
480 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
481 return "lxc" if -f
"$LXC_CONF/$conf_fn";
490 my $cfg = read_cron
();
492 my $job = param_to_job
($param);
494 $job->{state} = "ok";
497 my $source = parse_target
($param->{source
});
498 my $dest = parse_target
($param->{dest
});
500 if (my $ip = $dest->{ip
}) {
501 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
504 if (my $ip = $source->{ip
}) {
505 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
508 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
510 if (!defined($source->{vmid
})) {
511 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
514 my $vm_type = vm_exists
($source, $param->{source_user
});
515 $job->{vm_type
} = $vm_type;
516 $source->{vm_type
} = $vm_type;
518 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
520 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
522 #check if vm has zfs disks if not die;
523 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
529 sync
($param) if !$param->{skip
};
540 my $cfg = read_cron
();
542 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
543 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
545 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
546 $job->{name
} = $param->{name
};
547 $job->{source
} = $param->{source
};
548 $job = add_state_to_job
($job);
556 my $job = get_job
($param);
557 $job->{state} = "del";
566 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
567 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
570 my $date = get_date
();
573 $job = get_job
($param);
576 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
577 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
580 my $dest = parse_target
($param->{dest
});
581 my $source = parse_target
($param->{source
});
583 my $sync_path = sub {
584 my ($source, $dest, $job, $param, $date) = @_;
586 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
588 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
590 send_image
($source, $dest, $param);
592 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
596 my $vm_type = vm_exists
($source, $param->{source_user
});
597 $source->{vm_type
} = $vm_type;
600 $job->{state} = "syncing";
601 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
606 if ($source->{vmid
}) {
607 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
608 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
609 my $disks = get_disks
($source, $param->{source_user
});
611 foreach my $disk (sort keys %{$disks}) {
612 $source->{all
} = $disks->{$disk}->{all
};
613 $source->{pool
} = $disks->{$disk}->{pool
};
614 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
615 $source->{last_part
} = $disks->{$disk}->{last_part
};
616 &$sync_path($source, $dest, $job, $param, $date);
618 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
619 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
});
621 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
});
624 &$sync_path($source, $dest, $job, $param, $date);
629 $job->{state} = "error";
633 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
639 $job->{state} = "ok";
640 $job->{lsync
} = $date;
649 my ($source, $dest, $max_snap, $name, $source_user) = @_;
652 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
653 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
654 push @$cmd, $source->{all
};
656 my $raw = run_cmd
($cmd);
659 my $last_snap = undef;
662 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
664 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
666 $last_snap = $1 if (!$last_snap);
669 if ($index == $max_snap) {
670 $source->{destroy
} = 1;
676 return ($old_snap, $last_snap) if $last_snap;
682 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
684 my $snap_name = "rep_$name\_".$date;
686 $source->{new_snap
} = $snap_name;
688 my $path = "$source->{all}\@$snap_name";
691 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
692 push @$cmd, 'zfs', 'snapshot', $path;
698 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
706 my $text = "SHELL=/bin/sh\n";
707 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
709 my $fh = IO
::File-
>new("> $CRONJOBS");
710 die "Could not open file: $!\n" if !$fh;
712 foreach my $source (sort keys%{$cfg}) {
713 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
714 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
715 $text .= "$PROG_PATH sync";
716 $text .= " -source ";
717 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
718 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
719 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
721 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
722 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
723 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
726 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
727 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
728 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
729 $text .= " -name $sync_name ";
730 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
731 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
735 die "Can't write to cron\n" if (!print($fh $text));
740 my ($target, $user) = @_;
743 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
745 if ($target->{vm_type
} eq 'qemu') {
746 push @$cmd, 'qm', 'config', $target->{vmid
};
747 } elsif ($target->{vm_type
} eq 'lxc') {
748 push @$cmd, 'pct', 'config', $target->{vmid
};
750 die "VM Type unknown\n";
753 my $res = run_cmd
($cmd);
755 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
762 print "Start CMD\n" if $DEBUG;
763 print Dumper
$cmd if $DEBUG;
764 if (ref($cmd) eq 'ARRAY') {
765 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
767 my $output = `$cmd 2>&1`;
769 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
772 print Dumper
$output if $DEBUG;
773 print "END CMD\n" if $DEBUG;
778 my ($text, $ip, $vm_type, $user) = @_;
783 while ($text && $text =~ s/^(.*?)(\n|$)//) {
786 next if $line =~ /media=cdrom/;
787 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
789 #QEMU if backup is not set include in sync
790 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
792 #LXC if backup is not set do no in sync
793 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
797 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
798 my @parameter = split(/,/,$1);
800 foreach my $opt (@parameter) {
801 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
808 if (!defined($disk) || !defined($stor)) {
809 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
814 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
815 push @$cmd, 'pvesm', 'path', "$stor$disk";
816 my $path = run_cmd($cmd);
818 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
820 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
822 my @array = split('/', $1);
823 $disks->{$num}->{pool} = shift(@array);
824 $disks->{$num}->{all} = $disks->{$num}->{pool};
826 $disks->{$num}->{path} = join('/', @array);
827 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
829 $disks->{$num}->{last_part} = $disk;
830 $disks->{$num}->{all} .= "\
/$disk";
833 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
835 $disks->{$num}->{pool} = $1;
836 $disks->{$num}->{all} = $disks->{$num}->{pool};
839 $disks->{$num}->{path} = $3;
840 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
843 $disks->{$num}->{last_part} = $disk;
844 $disks->{$num}->{all} .= "\
/$disk";
849 die "ERROR
: in path
\n";
853 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
857 sub snapshot_destroy {
858 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
860 my @zfscmd = ('zfs', 'destroy');
861 my $snapshot = "$source->{all
}\
@$snap";
864 if($source->{ip} && $method eq 'ssh'){
865 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
867 run_cmd([@zfscmd, $snapshot]);
874 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
876 my $path = "$dest->{all
}";
877 $path .= "/$source->{last_part
}" if $source->{last_part};
880 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
889 my ($source , $dest, $method, $dest_user) = @_;
892 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
893 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
895 my $path = $dest->{all};
896 $path .= "/$source->{last_part
}" if $source->{last_part};
897 $path .= "\
@$source->{old_snap
}";
903 eval {$text =run_cmd($cmd);};
909 while ($text && $text =~ s/^(.*?)(\n|$)//) {
911 return 1 if $line =~ m/^.*$source->{old_snap}$/;
916 my ($source, $dest, $param) = @_;
920 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
921 push @$cmd, 'zfs', 'send';
922 push @$cmd, '-v' if $param->{verbose};
924 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
925 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
927 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
929 if ($param->{limit}){
930 my $bwl = $param->{limit}*1024;
931 push @$cmd, \'|', 'cstream', '-t', $bwl;
933 my $target = "$dest->{all
}";
934 $target .= "/$source->{last_part
}" if $source->{last_part};
938 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
939 push @$cmd, 'zfs', 'recv', '-F', '--';
940 push @$cmd, "$target";
947 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
954 my ($source, $dest, $method, $source_user, $dest_user) = @_;
956 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
957 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
959 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
961 $dest_target_new = $config_dir.'/'.$dest_target_new;
963 if ($method eq 'ssh'){
964 if ($dest->{ip} && $source->{ip}) {
965 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
966 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
967 } elsif ($dest->{ip}) {
968 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
969 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
970 } elsif ($source->{ip}) {
971 run_cmd(['mkdir', '-p', '--', $config_dir]);
972 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
975 if ($source->{destroy}){
976 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
978 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
980 run_cmd(['rm', '-f', '--', $dest_target_old]);
983 } elsif ($method eq 'local') {
984 run_cmd(['mkdir', '-p', '--', $config_dir]);
985 run_cmd(['cp', $source_target, $dest_target_new]);
990 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
991 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
997 my $cfg = read_cron();
999 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1001 my $states = read_state();
1003 foreach my $source (sort keys%{$cfg}) {
1004 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1005 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1006 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1007 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1011 return $status_list;
1017 my $job = get_job($param);
1018 $job->{state} = "ok
";
1026 my $job = get_job($param);
1027 $job->{state} = "stopped
";
1034 $PROGNAME destroy -source <string> [OPTIONS]
1036 remove a sync Job from the scheduler
1040 name of the sync job, if not set it is default
1044 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1047 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1053 the destination target is like [IP]:<Pool>[/Path]
1057 name of the user on the destination target, root by default
1061 max sync speed in kBytes/s, default unlimited
1065 how much snapshots will be kept before get erased, default 1
1069 name of the sync job, if not set it is default
1073 if this flag is set it will skip the first sync
1077 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1081 name of the user on the source target, root by default
1084 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1090 the destination target is like [IP:]<Pool>[/Path]
1094 name of the user on the destination target, root by default
1098 max sync speed in kBytes/s, default unlimited
1102 how much snapshots will be kept before get erased, default 1
1106 name of the sync job, if not set it is default.
1107 It is only necessary if scheduler allready contains this source.
1111 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1115 name of the user on the source target, root by default
1119 print out the sync progress.
1124 Get a List of all scheduled Sync Jobs
1129 Get the status of all scheduled Sync Jobs
1132 $PROGNAME help <cmd> [OPTIONS]
1134 Get help about specified command.
1142 Verbose output format.
1145 $PROGNAME enable -source <string> [OPTIONS]
1147 enable a syncjob and reset error
1151 name of the sync job, if not set it is default
1155 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1158 $PROGNAME disable -source <string> [OPTIONS]
1164 name of the sync job, if not set it is default
1168 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1170 printpod
=> 'internal command',
1176 } elsif (!$cmd_help->{$command}) {
1177 print "ERROR: unknown command '$command'";
1182 my $param = parse_argv
(@arg);
1186 die "$cmd_help->{$command}\n" if !$param->{$_};
1190 if ($command eq 'destroy') {
1191 check_params
(qw(source));
1193 check_target
($param->{source
});
1194 destroy_job
($param);
1196 } elsif ($command eq 'sync') {
1197 check_params
(qw(source dest));
1199 check_target
($param->{source
});
1200 check_target
($param->{dest
});
1203 } elsif ($command eq 'create') {
1204 check_params
(qw(source dest));
1206 check_target
($param->{source
});
1207 check_target
($param->{dest
});
1210 } elsif ($command eq 'status') {
1213 } elsif ($command eq 'list') {
1216 } elsif ($command eq 'help') {
1217 my $help_command = $ARGV[1];
1219 if ($help_command && $cmd_help->{$help_command}) {
1220 die "$cmd_help->{$help_command}\n";
1223 if ($param->{verbose
}) {
1224 exec("man $PROGNAME");
1231 } elsif ($command eq 'enable') {
1232 check_params
(qw(source));
1234 check_target
($param->{source
});
1237 } elsif ($command eq 'disable') {
1238 check_params
(qw(source));
1240 check_target
($param->{source
});
1241 disable_job
($param);
1243 } elsif ($command eq 'printpod') {
1250 print("ERROR:\tno command specified\n") if !$help;
1251 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1252 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1253 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1254 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1255 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1256 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1257 print("\t$PROGNAME list\n");
1258 print("\t$PROGNAME status\n");
1259 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1264 parse_target
($target);
1269 my $synopsis = join("\n", sort values %$cmd_help);
1274 pve-zsync - PVE ZFS Replication Manager
1278 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1284 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1285 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1286 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.
1287 To config cron see man crontab.
1289 =head2 PVE ZFS Storage sync Tool
1291 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1295 add sync job from local VM to remote ZFS Server
1296 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1298 =head1 IMPORTANT FILES
1300 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1302 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1304 =head1 COPYRIGHT AND DISCLAIMER
1306 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1308 This program is free software: you can redistribute it and/or modify it
1309 under the terms of the GNU Affero General Public License as published
1310 by the Free Software Foundation, either version 3 of the License, or
1311 (at your option) any later version.
1313 This program is distributed in the hope that it will be useful, but
1314 WITHOUT ANY WARRANTY; without even the implied warranty of
1315 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1316 Affero General Public License for more details.
1318 You should have received a copy of the GNU Affero General Public
1319 License along with this program. If not, see
1320 <http://www.gnu.org/licenses/>.