]>
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
::Path
qw(make_path);
11 use String
::ShellQuote
'shell_quote';
13 my $PROGNAME = "pve-zsync";
14 my $CONFIG_PATH = "/var/lib/${PROGNAME}";
15 my $STATE = "${CONFIG_PATH}/sync_state";
16 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
17 my $PATH = "/usr/sbin";
18 my $PVE_DIR = "/etc/pve/local";
19 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
20 my $LXC_CONF = "${PVE_DIR}/lxc";
21 my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
22 my $PROG_PATH = "$PATH/${PROGNAME}";
26 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
27 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
28 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
29 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
32 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
33 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
34 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
35 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
42 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
43 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
44 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
45 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
46 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
48 my $command = $ARGV[0];
50 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
51 check_bin
('cstream');
57 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} =
59 die "Signal aborting sync\n";
65 foreach my $p (split (/:/, $ENV{PATH
})) {
72 die "unable to find command '$bin'\n";
75 sub cut_target_width
{
76 my ($path, $maxlen) = @_;
79 return $path if length($path) <= $maxlen;
81 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
83 $path =~ s
@/([^/]+/?
)$@@;
86 if (length($tail)+3 == $maxlen) {
88 } elsif (length($tail)+2 >= $maxlen) {
89 return '..'.substr($tail, -$maxlen+2)
92 $path =~ s
@(/[^/]+)(?
:/|$)@@;
94 my $both = length($head) + length($tail);
95 my $remaining = $maxlen-$both-4; # -4 for "/../"
98 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
101 substr($path, ($remaining/2), (length($path)-$remaining), '..');
102 return "$head/" . $path . "/$tail";
107 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
112 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
116 my ($source, $name, $status) = @_;
118 if ($status->{$source->{all
}}->{$name}->{status
}) {
125 sub check_pool_exists
{
126 my ($target, $user) = @_;
131 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
133 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
147 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
150 if ($text !~ $TARGETRE) {
154 $target->{ip
} = $1 if $1;
155 my @parts = split('/', $2);
157 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
159 my $pool = $target->{pool
} = shift(@parts);
160 die "$errstr\n" if !$pool;
162 if ($pool =~ m/^\d+$/) {
163 $target->{vmid
} = $pool;
164 delete $target->{pool
};
167 return $target if (@parts == 0);
168 $target->{last_part
} = pop(@parts);
174 $target->{path
} = join('/', @parts);
182 #This is for the first use to init file;
184 my $new_fh = IO
::File-
>new("> $CRONJOBS");
185 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
190 my $fh = IO
::File-
>new("< $CRONJOBS");
191 die "Could not open file $CRONJOBS: $!\n" if !$fh;
197 return encode_cron
(@text);
212 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
},
229 'properties' => \
$param->{properties
},
232 die "can't parse options\n" if $ret == 0;
234 $param->{name
} //= "default";
235 $param->{maxsnap
} //= 1;
236 $param->{method} //= "ssh";
237 $param->{source_user
} //= "root";
238 $param->{dest_user
} //= "root";
243 sub add_state_to_job
{
246 my $states = read_state
();
247 my $state = $states->{$job->{source
}}->{$job->{name
}};
249 $job->{state} = $state->{state};
250 $job->{lsync
} = $state->{lsync
};
251 $job->{vm_type
} = $state->{vm_type
};
253 for (my $i = 0; $state->{"snap$i"}; $i++) {
254 $job->{"snap$i"} = $state->{"snap$i"};
265 while (my $line = shift(@text)) {
267 my @arg = split('\s', $line);
268 my $param = parse_argv
(@arg);
270 if ($param->{source
} && $param->{dest
}) {
271 my $source = delete $param->{source
};
272 my $name = delete $param->{name
};
274 $cfg->{$source}->{$name} = $param;
286 my $source = parse_target
($param->{source
});
287 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
289 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
290 $job->{dest
} = $param->{dest
} if $param->{dest
};
291 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
292 $job->{method} = "ssh" if !$job->{method};
293 $job->{limit
} = $param->{limit
};
294 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
295 $job->{source
} = $param->{source
};
296 $job->{source_user
} = $param->{source_user
};
297 $job->{dest_user
} = $param->{dest_user
};
298 $job->{properties
} = !!$param->{properties
};
306 make_path
$CONFIG_PATH;
307 my $new_fh = IO
::File-
>new("> $STATE");
308 die "Could not create $STATE: $!\n" if !$new_fh;
314 my $fh = IO
::File-
>new("< $STATE");
315 die "Could not open file $STATE: $!\n" if !$fh;
318 my $states = decode_json
($text);
332 $in_fh = IO
::File-
>new("< $STATE");
333 die "Could not open file $STATE: $!\n" if !$in_fh;
338 my $out_fh = IO
::File-
>new("> $STATE.new");
339 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
344 $states = decode_json
($text);
345 $state = $states->{$job->{source
}}->{$job->{name
}};
348 if ($job->{state} ne "del") {
349 $state->{state} = $job->{state};
350 $state->{lsync
} = $job->{lsync
};
351 $state->{vm_type
} = $job->{vm_type
};
353 for (my $i = 0; $job->{"snap$i"} ; $i++) {
354 $state->{"snap$i"} = $job->{"snap$i"};
356 $states->{$job->{source
}}->{$job->{name
}} = $state;
359 delete $states->{$job->{source
}}->{$job->{name
}};
360 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
363 $text = encode_json
($states);
367 rename "$STATE.new", $STATE;
382 my $header = "SHELL=/bin/sh\n";
383 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
385 my $fh = IO
::File-
>new("< $CRONJOBS");
386 die "Could not open file $CRONJOBS: $!\n" if !$fh;
391 while (my $line = shift(@test)) {
393 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
395 next if $job->{state} eq "del";
396 $text .= format_job
($job, $line);
398 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
407 $text = "$header$text";
411 $text .= format_job
($job);
413 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
414 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
416 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
419 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
424 my ($job, $line) = @_;
427 if ($job->{state} eq "stopped") {
431 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
434 $text .= "*/$INTERVAL * * * *";
437 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
438 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
439 $text .= " --limit $job->{limit}" if $job->{limit
};
440 $text .= " --method $job->{method}";
441 $text .= " --verbose" if $job->{verbose
};
442 $text .= " --source-user $job->{source_user}";
443 $text .= " --dest-user $job->{dest_user}";
444 $text .= " --properties" if $job->{properties
};
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 return undef if !defined($target->{vmid
});
476 my $conf_fn = "$target->{vmid}.conf";
479 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
480 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
481 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
483 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
484 return "lxc" if -f
"$LXC_CONF/$conf_fn";
493 my $cfg = read_cron
();
495 my $job = param_to_job
($param);
497 $job->{state} = "ok";
500 my $source = parse_target
($param->{source
});
501 my $dest = parse_target
($param->{dest
});
503 if (my $ip = $dest->{ip
}) {
504 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
507 if (my $ip = $source->{ip
}) {
508 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
511 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
513 if (!defined($source->{vmid
})) {
514 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
517 my $vm_type = vm_exists
($source, $param->{source_user
});
518 $job->{vm_type
} = $vm_type;
519 $source->{vm_type
} = $vm_type;
521 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
523 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
525 #check if vm has zfs disks if not die;
526 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
532 sync
($param) if !$param->{skip
};
543 my $cfg = read_cron
();
545 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
546 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
548 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
549 $job->{name
} = $param->{name
};
550 $job->{source
} = $param->{source
};
551 $job = add_state_to_job
($job);
559 my $job = get_job
($param);
560 $job->{state} = "del";
569 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
570 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
573 my $date = get_date
();
576 $job = get_job
($param);
579 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
580 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
583 my $dest = parse_target
($param->{dest
});
584 my $source = parse_target
($param->{source
});
586 my $sync_path = sub {
587 my ($source, $dest, $job, $param, $date) = @_;
589 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
591 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
593 send_image
($source, $dest, $param);
595 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
599 my $vm_type = vm_exists
($source, $param->{source_user
});
600 $source->{vm_type
} = $vm_type;
603 $job->{state} = "syncing";
604 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
609 if ($source->{vmid
}) {
610 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
611 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
612 my $disks = get_disks
($source, $param->{source_user
});
614 foreach my $disk (sort keys %{$disks}) {
615 $source->{all
} = $disks->{$disk}->{all
};
616 $source->{pool
} = $disks->{$disk}->{pool
};
617 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
618 $source->{last_part
} = $disks->{$disk}->{last_part
};
619 &$sync_path($source, $dest, $job, $param, $date);
621 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
622 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
});
624 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
});
627 &$sync_path($source, $dest, $job, $param, $date);
632 $job->{state} = "error";
636 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
642 $job->{state} = "ok";
643 $job->{lsync
} = $date;
652 my ($source, $dest, $max_snap, $name, $source_user) = @_;
655 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
656 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
657 push @$cmd, $source->{all
};
659 my $raw = run_cmd
($cmd);
662 my $last_snap = undef;
665 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
667 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
669 $last_snap = $1 if (!$last_snap);
672 if ($index == $max_snap) {
673 $source->{destroy
} = 1;
679 return ($old_snap, $last_snap) if $last_snap;
685 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
687 my $snap_name = "rep_$name\_".$date;
689 $source->{new_snap
} = $snap_name;
691 my $path = "$source->{all}\@$snap_name";
694 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
695 push @$cmd, 'zfs', 'snapshot', $path;
701 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
709 my $text = "SHELL=/bin/sh\n";
710 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
712 my $fh = IO
::File-
>new("> $CRONJOBS");
713 die "Could not open file: $!\n" if !$fh;
715 foreach my $source (sort keys%{$cfg}) {
716 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
717 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
718 $text .= "$PROG_PATH sync";
719 $text .= " -source ";
720 if ($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}->{vmid} ";
724 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
725 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
726 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
729 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
730 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
731 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
732 $text .= " -name $sync_name ";
733 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
734 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
738 die "Can't write to cron\n" if (!print($fh $text));
743 my ($target, $user) = @_;
746 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
748 if ($target->{vm_type
} eq 'qemu') {
749 push @$cmd, 'qm', 'config', $target->{vmid
};
750 } elsif ($target->{vm_type
} eq 'lxc') {
751 push @$cmd, 'pct', 'config', $target->{vmid
};
753 die "VM Type unknown\n";
756 my $res = run_cmd
($cmd);
758 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
765 print "Start CMD\n" if $DEBUG;
766 print Dumper
$cmd if $DEBUG;
767 if (ref($cmd) eq 'ARRAY') {
768 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
770 my $output = `$cmd 2>&1`;
772 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
775 print Dumper
$output if $DEBUG;
776 print "END CMD\n" if $DEBUG;
781 my ($text, $ip, $vm_type, $user) = @_;
786 while ($text && $text =~ s/^(.*?)(\n|$)//) {
789 next if $line =~ /media=cdrom/;
790 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
792 #QEMU if backup is not set include in sync
793 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
795 #LXC if backup is not set do no in sync
796 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
800 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
801 my @parameter = split(/,/,$1);
803 foreach my $opt (@parameter) {
804 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
811 if (!defined($disk) || !defined($stor)) {
812 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
817 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
818 push @$cmd, 'pvesm', 'path', "$stor$disk";
819 my $path = run_cmd($cmd);
821 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
823 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
825 my @array = split('/', $1);
826 $disks->{$num}->{pool} = shift(@array);
827 $disks->{$num}->{all} = $disks->{$num}->{pool};
829 $disks->{$num}->{path} = join('/', @array);
830 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
832 $disks->{$num}->{last_part} = $disk;
833 $disks->{$num}->{all} .= "\
/$disk";
836 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
838 $disks->{$num}->{pool} = $1;
839 $disks->{$num}->{all} = $disks->{$num}->{pool};
842 $disks->{$num}->{path} = $3;
843 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
846 $disks->{$num}->{last_part} = $disk;
847 $disks->{$num}->{all} .= "\
/$disk";
852 die "ERROR
: in path
\n";
856 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
860 sub snapshot_destroy {
861 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
863 my @zfscmd = ('zfs', 'destroy');
864 my $snapshot = "$source->{all
}\
@$snap";
867 if($source->{ip} && $method eq 'ssh'){
868 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
870 run_cmd([@zfscmd, $snapshot]);
877 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
879 my $path = "$dest->{all
}";
880 $path .= "/$source->{last_part
}" if $source->{last_part};
883 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
892 my ($source , $dest, $method, $dest_user) = @_;
895 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
896 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
898 my $path = $dest->{all};
899 $path .= "/$source->{last_part
}" if $source->{last_part};
900 $path .= "\
@$source->{old_snap
}";
906 eval {$text =run_cmd($cmd);};
912 while ($text && $text =~ s/^(.*?)(\n|$)//) {
914 return 1 if $line =~ m/^.*$source->{old_snap}$/;
919 my ($source, $dest, $param) = @_;
923 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
924 push @$cmd, 'zfs', 'send';
925 push @$cmd, '-p', if $param->{properties};
926 push @$cmd, '-v' if $param->{verbose};
928 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_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', "$dest_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
1089 Include the dataset's properties in the stream.
1092 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1098 the destination target is like [IP:]<Pool>[/Path]
1102 name of the user on the destination target, root by default
1106 max sync speed in kBytes/s, default unlimited
1110 how much snapshots will be kept before get erased, default 1
1114 name of the sync job, if not set it is default.
1115 It is only necessary if scheduler allready contains this source.
1119 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1123 name of the user on the source target, root by default
1127 print out the sync progress.
1131 Include the dataset's properties in the stream.
1136 Get a List of all scheduled Sync Jobs
1141 Get the status of all scheduled Sync Jobs
1144 $PROGNAME help <cmd> [OPTIONS]
1146 Get help about specified command.
1154 Verbose output format.
1157 $PROGNAME enable -source <string> [OPTIONS]
1159 enable a syncjob and reset error
1163 name of the sync job, if not set it is default
1167 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1170 $PROGNAME disable -source <string> [OPTIONS]
1176 name of the sync job, if not set it is default
1180 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1182 printpod
=> 'internal command',
1188 } elsif (!$cmd_help->{$command}) {
1189 print "ERROR: unknown command '$command'";
1194 my $param = parse_argv
(@arg);
1198 die "$cmd_help->{$command}\n" if !$param->{$_};
1202 if ($command eq 'destroy') {
1203 check_params
(qw(source));
1205 check_target
($param->{source
});
1206 destroy_job
($param);
1208 } elsif ($command eq 'sync') {
1209 check_params
(qw(source dest));
1211 check_target
($param->{source
});
1212 check_target
($param->{dest
});
1215 } elsif ($command eq 'create') {
1216 check_params
(qw(source dest));
1218 check_target
($param->{source
});
1219 check_target
($param->{dest
});
1222 } elsif ($command eq 'status') {
1225 } elsif ($command eq 'list') {
1228 } elsif ($command eq 'help') {
1229 my $help_command = $ARGV[1];
1231 if ($help_command && $cmd_help->{$help_command}) {
1232 die "$cmd_help->{$help_command}\n";
1235 if ($param->{verbose
}) {
1236 exec("man $PROGNAME");
1243 } elsif ($command eq 'enable') {
1244 check_params
(qw(source));
1246 check_target
($param->{source
});
1249 } elsif ($command eq 'disable') {
1250 check_params
(qw(source));
1252 check_target
($param->{source
});
1253 disable_job
($param);
1255 } elsif ($command eq 'printpod') {
1262 print("ERROR:\tno command specified\n") if !$help;
1263 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1264 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1265 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1266 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1267 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1268 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1269 print("\t$PROGNAME list\n");
1270 print("\t$PROGNAME status\n");
1271 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1276 parse_target
($target);
1281 my $synopsis = join("\n", sort values %$cmd_help);
1286 pve-zsync - PVE ZFS Replication Manager
1290 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1296 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1297 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1298 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.
1299 To config cron see man crontab.
1301 =head2 PVE ZFS Storage sync Tool
1303 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1307 add sync job from local VM to remote ZFS Server
1308 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1310 =head1 IMPORTANT FILES
1312 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1314 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1316 =head1 COPYRIGHT AND DISCLAIMER
1318 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1320 This program is free software: you can redistribute it and/or modify it
1321 under the terms of the GNU Affero General Public License as published
1322 by the Free Software Foundation, either version 3 of the License, or
1323 (at your option) any later version.
1325 This program is distributed in the hope that it will be useful, but
1326 WITHOUT ANY WARRANTY; without even the implied warranty of
1327 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1328 Affero General Public License for more details.
1330 You should have received a copy of the GNU Affero General Public
1331 License along with this program. If not, see
1332 <http://www.gnu.org/licenses/>.