]>
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);
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;
213 $param->{source_user
} = undef;
214 $param->{dest_user
} = undef;
216 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
217 'dest=s' => \
$param->{dest
},
218 'source=s' => \
$param->{source
},
219 'verbose' => \
$param->{verbose
},
220 'limit=i' => \
$param->{limit
},
221 'maxsnap=i' => \
$param->{maxsnap
},
222 'name=s' => \
$param->{name
},
223 'skip' => \
$param->{skip
},
224 'method=s' => \
$param->{method},
225 'source-user=s' => \
$param->{source_user
},
226 'dest-user=s' => \
$param->{dest_user
});
229 die "can't parse options\n";
232 $param->{name
} = "default" if !$param->{name
};
233 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
234 $param->{method} = "ssh" if !$param->{method};
235 $param->{source_user
} = "root" if(!$param->{source_user
});
236 $param->{dest_user
} = "root" if(!$param->{dest_user
});
241 sub add_state_to_job
{
244 my $states = read_state
();
245 my $state = $states->{$job->{source
}}->{$job->{name
}};
247 $job->{state} = $state->{state};
248 $job->{lsync
} = $state->{lsync
};
249 $job->{vm_type
} = $state->{vm_type
};
251 for (my $i = 0; $state->{"snap$i"}; $i++) {
252 $job->{"snap$i"} = $state->{"snap$i"};
263 while (my $line = shift(@text)) {
265 my @arg = split('\s', $line);
266 my $param = parse_argv
(@arg);
268 if ($param->{source
} && $param->{dest
}) {
269 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
270 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
271 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
272 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
273 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
274 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
275 $cfg->{$param->{source
}}->{$param->{name
}}->{source_user
} = $param->{source_user
};
276 $cfg->{$param->{source
}}->{$param->{name
}}->{dest_user
} = $param->{dest_user
};
288 my $source = parse_target
($param->{source
});
289 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
291 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
292 $job->{dest
} = $param->{dest
} if $param->{dest
};
293 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
294 $job->{method} = "ssh" if !$job->{method};
295 $job->{limit
} = $param->{limit
};
296 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
297 $job->{source
} = $param->{source
};
298 $job->{source_user
} = $param->{source_user
};
299 $job->{dest_user
} = $param->{dest_user
};
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 =~ /^#*(.+) 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}";
452 my $cfg = read_cron
();
454 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
456 my $states = read_state
();
457 foreach my $source (sort keys%{$cfg}) {
458 foreach my $name (sort keys%{$cfg->{$source}}) {
459 $list .= sprintf("%-25s", cut_target_width
($source, 25));
460 $list .= sprintf("%-25s", cut_target_width
($name, 25));
461 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
462 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
463 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
464 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
472 my ($target, $user) = @_;
474 my @cmd = ('ssh', "$user\@$target->{ip}", '--') if $target->{ip
};
478 return undef if !defined($target->{vmid
});
480 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
482 return "qemu" if $res;
484 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
486 return "lxc" if $res;
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 && $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, $source_user) = @_;
896 push @$cmd, 'ssh', "$source_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, '-v' if $param->{verbose};
928 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
929 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
931 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
933 if ($param->{limit}){
934 my $bwl = $param->{limit}*1024;
935 push @$cmd, \'|', 'cstream', '-t', $bwl;
937 my $target = "$dest->{all
}";
938 $target .= "/$source->{last_part
}" if $source->{last_part};
942 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
943 push @$cmd, 'zfs', 'recv', '-F', '--';
944 push @$cmd, "$target";
951 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
958 my ($source, $dest, $method, $source_user, $dest_user) = @_;
960 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
961 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
963 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
965 $dest_target_new = $config_dir.'/'.$dest_target_new;
967 if ($method eq 'ssh'){
968 if ($dest->{ip} && $source->{ip}) {
969 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
970 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
971 } elsif ($dest->{ip}) {
972 run_cmd(['ssh', "$source_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
973 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
974 } elsif ($source->{ip}) {
975 run_cmd(['mkdir', '-p', '--', $config_dir]);
976 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
979 if ($source->{destroy}){
980 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
982 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
984 run_cmd(['rm', '-f', '--', $dest_target_old]);
987 } elsif ($method eq 'local') {
988 run_cmd(['mkdir', '-p', '--', $config_dir]);
989 run_cmd(['cp', $source_target, $dest_target_new]);
994 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
995 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1001 my $cfg = read_cron();
1003 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1005 my $states = read_state();
1007 foreach my $source (sort keys%{$cfg}) {
1008 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1009 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1010 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1011 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1015 return $status_list;
1021 my $job = get_job($param);
1022 $job->{state} = "ok
";
1030 my $job = get_job($param);
1031 $job->{state} = "stopped
";
1038 $PROGNAME destroy -source <string> [OPTIONS]
1040 remove a sync Job from the scheduler
1044 name of the sync job, if not set it is default
1048 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1051 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1057 the destination target is like [IP]:<Pool>[/Path]
1061 name of the user on the destination target, root by default
1065 max sync speed in kBytes/s, default unlimited
1069 how much snapshots will be kept before get erased, default 1
1073 name of the sync job, if not set it is default
1077 if this flag is set it will skip the first sync
1081 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1085 name of the user on the source target, root by default
1088 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1094 the destination target is like [IP:]<Pool>[/Path]
1098 name of the user on the destination target, root by default
1102 max sync speed in kBytes/s, default unlimited
1106 how much snapshots will be kept before get erased, default 1
1110 name of the sync job, if not set it is default.
1111 It is only necessary if scheduler allready contains this source.
1115 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1119 name of the user on the source target, root by default
1123 print out the sync progress.
1128 Get a List of all scheduled Sync Jobs
1133 Get the status of all scheduled Sync Jobs
1136 $PROGNAME help <cmd> [OPTIONS]
1138 Get help about specified command.
1146 Verbose output format.
1149 $PROGNAME enable -source <string> [OPTIONS]
1151 enable a syncjob and reset error
1155 name of the sync job, if not set it is default
1159 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1162 $PROGNAME disable -source <string> [OPTIONS]
1168 name of the sync job, if not set it is default
1172 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1174 printpod
=> 'internal command',
1180 } elsif (!$cmd_help->{$command}) {
1181 print "ERROR: unknown command '$command'";
1186 my $param = parse_argv
(@arg);
1190 die "$cmd_help->{$command}\n" if !$param->{$_};
1194 if ($command eq 'destroy') {
1195 check_params
(qw(source));
1197 check_target
($param->{source
});
1198 destroy_job
($param);
1200 } elsif ($command eq 'sync') {
1201 check_params
(qw(source dest));
1203 check_target
($param->{source
});
1204 check_target
($param->{dest
});
1207 } elsif ($command eq 'create') {
1208 check_params
(qw(source dest));
1210 check_target
($param->{source
});
1211 check_target
($param->{dest
});
1214 } elsif ($command eq 'status') {
1217 } elsif ($command eq 'list') {
1220 } elsif ($command eq 'help') {
1221 my $help_command = $ARGV[1];
1223 if ($help_command && $cmd_help->{$help_command}) {
1224 die "$cmd_help->{$help_command}\n";
1227 if ($param->{verbose
}) {
1228 exec("man $PROGNAME");
1235 } elsif ($command eq 'enable') {
1236 check_params
(qw(source));
1238 check_target
($param->{source
});
1241 } elsif ($command eq 'disable') {
1242 check_params
(qw(source));
1244 check_target
($param->{source
});
1245 disable_job
($param);
1247 } elsif ($command eq 'printpod') {
1254 print("ERROR:\tno command specified\n") if !$help;
1255 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1256 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1257 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1258 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1259 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1260 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1261 print("\t$PROGNAME list\n");
1262 print("\t$PROGNAME status\n");
1263 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1268 parse_target
($target);
1273 my $synopsis = join("\n", sort values %$cmd_help);
1278 pve-zsync - PVE ZFS Replication Manager
1282 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1288 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1289 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1290 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.
1291 To config cron see man crontab.
1293 =head2 PVE ZFS Storage sync Tool
1295 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1299 add sync job from local VM to remote ZFS Server
1300 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1302 =head1 IMPORTANT FILES
1304 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1306 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1308 =head1 COPYRIGHT AND DISCLAIMER
1310 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1312 This program is free software: you can redistribute it and/or modify it
1313 under the terms of the GNU Affero General Public License as published
1314 by the Free Software Foundation, either version 3 of the License, or
1315 (at your option) any later version.
1317 This program is distributed in the hope that it will be useful, but
1318 WITHOUT ANY WARRANTY; without even the implied warranty of
1319 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1320 Affero General Public License for more details.
1322 You should have received a copy of the GNU Affero General Public
1323 License along with this program. If not, see
1324 <http://www.gnu.org/licenses/>.