]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
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}";
27 $DEBUG = 0; # change default here. not above on declaration!
28 $DEBUG ||= $ENV{ZSYNC_DEBUG
};
31 Data
::Dumper-
>import();
35 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
36 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
37 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
38 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
41 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
42 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
43 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
44 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
45 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
46 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
47 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
48 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
49 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
51 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
52 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
53 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
54 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
55 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
57 my $command = $ARGV[0];
59 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
60 check_bin
('cstream');
66 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} = sub {
67 die "Signaled, aborting sync: $!\n";
73 foreach my $p (split (/:/, $ENV{PATH
})) {
80 die "unable to find command '$bin'\n";
83 sub cut_target_width
{
84 my ($path, $maxlen) = @_;
87 return $path if length($path) <= $maxlen;
89 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
91 $path =~ s
@/([^/]+/?
)$@@;
94 if (length($tail)+3 == $maxlen) {
96 } elsif (length($tail)+2 >= $maxlen) {
97 return '..'.substr($tail, -$maxlen+2)
100 $path =~ s
@(/[^/]+)(?
:/|$)@@;
102 my $both = length($head) + length($tail);
103 my $remaining = $maxlen-$both-4; # -4 for "/../"
105 if ($remaining < 0) {
106 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
109 substr($path, ($remaining/2), (length($path)-$remaining), '..');
110 return "$head/" . $path . "/$tail";
115 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
120 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
124 my ($source, $name, $status) = @_;
126 if ($status->{$source->{all
}}->{$name}->{status
}) {
133 sub check_pool_exists
{
134 my ($target, $user) = @_;
139 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
141 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
155 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
158 if ($text !~ $TARGETRE) {
162 $target->{ip
} = $1 if $1;
163 my @parts = split('/', $2);
165 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
167 my $pool = $target->{pool
} = shift(@parts);
168 die "$errstr\n" if !$pool;
170 if ($pool =~ m/^\d+$/) {
171 $target->{vmid
} = $pool;
172 delete $target->{pool
};
175 return $target if (@parts == 0);
176 $target->{last_part
} = pop(@parts);
182 $target->{path
} = join('/', @parts);
190 #This is for the first use to init file;
192 my $new_fh = IO
::File-
>new("> $CRONJOBS");
193 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
198 my $fh = IO
::File-
>new("< $CRONJOBS");
199 die "Could not open file $CRONJOBS: $!\n" if !$fh;
205 return encode_cron
(@text);
220 source_user
=> undef,
223 dest_config_path
=> undef,
226 my ($ret) = GetOptionsFromArray
(
228 'dest=s' => \
$param->{dest
},
229 'source=s' => \
$param->{source
},
230 'verbose' => \
$param->{verbose
},
231 'limit=i' => \
$param->{limit
},
232 'maxsnap=i' => \
$param->{maxsnap
},
233 'name=s' => \
$param->{name
},
234 'skip' => \
$param->{skip
},
235 'method=s' => \
$param->{method},
236 'source-user=s' => \
$param->{source_user
},
237 'dest-user=s' => \
$param->{dest_user
},
238 'properties' => \
$param->{properties
},
239 'dest-config-path=s' => \
$param->{dest_config_path
},
242 die "can't parse options\n" if $ret == 0;
244 $param->{name
} //= "default";
245 $param->{maxsnap
} //= 1;
246 $param->{method} //= "ssh";
247 $param->{source_user
} //= "root";
248 $param->{dest_user
} //= "root";
253 sub add_state_to_job
{
256 my $states = read_state
();
257 my $state = $states->{$job->{source
}}->{$job->{name
}};
259 $job->{state} = $state->{state};
260 $job->{lsync
} = $state->{lsync
};
261 $job->{vm_type
} = $state->{vm_type
};
263 for (my $i = 0; $state->{"snap$i"}; $i++) {
264 $job->{"snap$i"} = $state->{"snap$i"};
275 while (my $line = shift(@text)) {
277 my @arg = split('\s', $line);
278 my $param = parse_argv
(@arg);
280 if ($param->{source
} && $param->{dest
}) {
281 my $source = delete $param->{source
};
282 my $name = delete $param->{name
};
284 $cfg->{$source}->{$name} = $param;
296 my $source = parse_target
($param->{source
});
297 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
299 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
300 $job->{dest
} = $param->{dest
} if $param->{dest
};
301 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
302 $job->{method} = "ssh" if !$job->{method};
303 $job->{limit
} = $param->{limit
};
304 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
305 $job->{source
} = $param->{source
};
306 $job->{source_user
} = $param->{source_user
};
307 $job->{dest_user
} = $param->{dest_user
};
308 $job->{properties
} = !!$param->{properties
};
309 $job->{dest_config_path
} = $param->{dest_config_path
} if $param->{dest_config_path
};
317 make_path
$CONFIG_PATH;
318 my $new_fh = IO
::File-
>new("> $STATE");
319 die "Could not create $STATE: $!\n" if !$new_fh;
325 my $fh = IO
::File-
>new("< $STATE");
326 die "Could not open file $STATE: $!\n" if !$fh;
329 my $states = decode_json
($text);
343 $in_fh = IO
::File-
>new("< $STATE");
344 die "Could not open file $STATE: $!\n" if !$in_fh;
349 my $out_fh = IO
::File-
>new("> $STATE.new");
350 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
355 $states = decode_json
($text);
356 $state = $states->{$job->{source
}}->{$job->{name
}};
359 if ($job->{state} ne "del") {
360 $state->{state} = $job->{state};
361 $state->{lsync
} = $job->{lsync
};
362 $state->{vm_type
} = $job->{vm_type
};
364 for (my $i = 0; $job->{"snap$i"} ; $i++) {
365 $state->{"snap$i"} = $job->{"snap$i"};
367 $states->{$job->{source
}}->{$job->{name
}} = $state;
370 delete $states->{$job->{source
}}->{$job->{name
}};
371 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
374 $text = encode_json
($states);
378 rename "$STATE.new", $STATE;
393 my $header = "SHELL=/bin/sh\n";
394 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
396 my $fh = IO
::File-
>new("< $CRONJOBS");
397 die "Could not open file $CRONJOBS: $!\n" if !$fh;
402 while (my $line = shift(@test)) {
404 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
406 next if $job->{state} eq "del";
407 $text .= format_job
($job, $line);
409 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
418 $text = "$header$text";
422 $text .= format_job
($job);
424 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
425 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
427 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
430 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
435 my ($job, $line) = @_;
438 if ($job->{state} eq "stopped") {
442 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
445 $text .= "*/$INTERVAL * * * *";
448 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
449 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
450 $text .= " --limit $job->{limit}" if $job->{limit
};
451 $text .= " --method $job->{method}";
452 $text .= " --verbose" if $job->{verbose
};
453 $text .= " --source-user $job->{source_user}";
454 $text .= " --dest-user $job->{dest_user}";
455 $text .= " --properties" if $job->{properties
};
456 $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path
};
464 my $cfg = read_cron
();
466 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
468 my $states = read_state
();
469 foreach my $source (sort keys%{$cfg}) {
470 foreach my $name (sort keys%{$cfg->{$source}}) {
471 $list .= sprintf("%-25s", cut_target_width
($source, 25));
472 $list .= sprintf("%-25s", cut_target_width
($name, 25));
473 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
474 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
475 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
476 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
484 my ($target, $user) = @_;
486 return undef if !defined($target->{vmid
});
488 my $conf_fn = "$target->{vmid}.conf";
491 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
492 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
493 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
495 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
496 return "lxc" if -f
"$LXC_CONF/$conf_fn";
505 my $cfg = read_cron
();
507 my $job = param_to_job
($param);
509 $job->{state} = "ok";
512 my $source = parse_target
($param->{source
});
513 my $dest = parse_target
($param->{dest
});
515 if (my $ip = $dest->{ip
}) {
516 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
519 if (my $ip = $source->{ip
}) {
520 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
523 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
525 if (!defined($source->{vmid
})) {
526 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
529 my $vm_type = vm_exists
($source, $param->{source_user
});
530 $job->{vm_type
} = $vm_type;
531 $source->{vm_type
} = $vm_type;
533 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
535 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
537 #check if vm has zfs disks if not die;
538 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
544 sync
($param) if !$param->{skip
};
555 my $cfg = read_cron
();
557 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
558 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
560 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
561 $job->{name
} = $param->{name
};
562 $job->{source
} = $param->{source
};
563 $job = add_state_to_job
($job);
571 my $job = get_job
($param);
572 $job->{state} = "del";
581 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
582 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
585 my $date = get_date
();
588 $job = get_job
($param);
591 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
592 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
595 my $dest = parse_target
($param->{dest
});
596 my $source = parse_target
($param->{source
});
598 my $sync_path = sub {
599 my ($source, $dest, $job, $param, $date) = @_;
601 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
603 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
605 send_image
($source, $dest, $param);
607 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
611 my $vm_type = vm_exists
($source, $param->{source_user
});
612 $source->{vm_type
} = $vm_type;
615 $job->{state} = "syncing";
616 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
621 if ($source->{vmid
}) {
622 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
623 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
624 my $disks = get_disks
($source, $param->{source_user
});
626 foreach my $disk (sort keys %{$disks}) {
627 $source->{all
} = $disks->{$disk}->{all
};
628 $source->{pool
} = $disks->{$disk}->{pool
};
629 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
630 $source->{last_part
} = $disks->{$disk}->{last_part
};
631 &$sync_path($source, $dest, $job, $param, $date);
633 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
634 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
636 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
639 &$sync_path($source, $dest, $job, $param, $date);
644 $job->{state} = "error";
648 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
654 $job->{state} = "ok";
655 $job->{lsync
} = $date;
664 my ($source, $dest, $max_snap, $name, $source_user) = @_;
667 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
668 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
669 push @$cmd, $source->{all
};
671 my $raw = run_cmd
($cmd);
674 my $last_snap = undef;
677 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
679 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
681 $last_snap = $1 if (!$last_snap);
684 if ($index == $max_snap) {
685 $source->{destroy
} = 1;
691 return ($old_snap, $last_snap) if $last_snap;
697 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
699 my $snap_name = "rep_$name\_".$date;
701 $source->{new_snap
} = $snap_name;
703 my $path = "$source->{all}\@$snap_name";
706 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
707 push @$cmd, 'zfs', 'snapshot', $path;
713 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
721 my $text = "SHELL=/bin/sh\n";
722 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
724 my $fh = IO
::File-
>new("> $CRONJOBS");
725 die "Could not open file: $!\n" if !$fh;
727 foreach my $source (sort keys%{$cfg}) {
728 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
729 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
730 $text .= "$PROG_PATH sync";
731 $text .= " -source ";
732 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
733 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
734 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
736 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
737 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
738 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
741 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
742 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
743 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
744 $text .= " -name $sync_name ";
745 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
746 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
750 die "Can't write to cron\n" if (!print($fh $text));
755 my ($target, $user) = @_;
758 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
760 if ($target->{vm_type
} eq 'qemu') {
761 push @$cmd, 'qm', 'config', $target->{vmid
};
762 } elsif ($target->{vm_type
} eq 'lxc') {
763 push @$cmd, 'pct', 'config', $target->{vmid
};
765 die "VM Type unknown\n";
768 my $res = run_cmd
($cmd);
770 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
777 print "Start CMD\n" if $DEBUG;
778 print Dumper
$cmd if $DEBUG;
779 if (ref($cmd) eq 'ARRAY') {
780 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
782 my $output = `$cmd 2>&1`;
784 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
787 print Dumper
$output if $DEBUG;
788 print "END CMD\n" if $DEBUG;
793 my ($text, $ip, $vm_type, $user) = @_;
798 while ($text && $text =~ s/^(.*?)(\n|$)//) {
801 next if $line =~ /media=cdrom/;
802 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
804 #QEMU if backup is not set include in sync
805 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
807 #LXC if backup is not set do no in sync
808 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
812 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
813 my @parameter = split(/,/,$1);
815 foreach my $opt (@parameter) {
816 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
823 if (!defined($disk) || !defined($stor)) {
824 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
829 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
830 push @$cmd, 'pvesm', 'path', "$stor$disk";
831 my $path = run_cmd($cmd);
833 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
835 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
837 my @array = split('/', $1);
838 $disks->{$num}->{pool} = shift(@array);
839 $disks->{$num}->{all} = $disks->{$num}->{pool};
841 $disks->{$num}->{path} = join('/', @array);
842 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
844 $disks->{$num}->{last_part} = $disk;
845 $disks->{$num}->{all} .= "\
/$disk";
848 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
850 $disks->{$num}->{pool} = $1;
851 $disks->{$num}->{all} = $disks->{$num}->{pool};
854 $disks->{$num}->{path} = $3;
855 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
858 $disks->{$num}->{last_part} = $disk;
859 $disks->{$num}->{all} .= "\
/$disk";
864 die "ERROR
: in path
\n";
868 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
872 sub snapshot_destroy {
873 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
875 my @zfscmd = ('zfs', 'destroy');
876 my $snapshot = "$source->{all
}\
@$snap";
879 if($source->{ip} && $method eq 'ssh'){
880 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
882 run_cmd([@zfscmd, $snapshot]);
889 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
891 my $path = "$dest->{all
}";
892 $path .= "/$source->{last_part
}" if $source->{last_part};
895 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
904 my ($source , $dest, $method, $dest_user) = @_;
907 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
908 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
910 my $path = $dest->{all};
911 $path .= "/$source->{last_part
}" if $source->{last_part};
912 $path .= "\
@$source->{old_snap
}";
918 eval {$text =run_cmd($cmd);};
924 while ($text && $text =~ s/^(.*?)(\n|$)//) {
926 return 1 if $line =~ m/^.*$source->{old_snap}$/;
931 my ($source, $dest, $param) = @_;
935 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
936 push @$cmd, 'zfs', 'send';
937 push @$cmd, '-p', if $param->{properties};
938 push @$cmd, '-v' if $param->{verbose};
940 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
941 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
943 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
945 if ($param->{limit}){
946 my $bwl = $param->{limit}*1024;
947 push @$cmd, \'|', 'cstream', '-t', $bwl;
949 my $target = "$dest->{all
}";
950 $target .= "/$source->{last_part
}" if $source->{last_part};
954 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
955 push @$cmd, 'zfs', 'recv', '-F', '--';
956 push @$cmd, "$target";
963 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
970 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
972 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
973 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
975 my $config_dir = $dest_config_path // $CONFIG_PATH;
976 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
978 $dest_target_new = $config_dir.'/'.$dest_target_new;
980 if ($method eq 'ssh'){
981 if ($dest->{ip} && $source->{ip}) {
982 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
983 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
984 } elsif ($dest->{ip}) {
985 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
986 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
987 } elsif ($source->{ip}) {
988 run_cmd(['mkdir', '-p', '--', $config_dir]);
989 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
992 if ($source->{destroy}){
993 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
995 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
997 run_cmd(['rm', '-f', '--', $dest_target_old]);
1000 } elsif ($method eq 'local') {
1001 run_cmd(['mkdir', '-p', '--', $config_dir]);
1002 run_cmd(['cp', $source_target, $dest_target_new]);
1007 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1008 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1014 my $cfg = read_cron();
1016 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1018 my $states = read_state();
1020 foreach my $source (sort keys%{$cfg}) {
1021 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1022 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1023 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1024 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1028 return $status_list;
1034 my $job = get_job($param);
1035 $job->{state} = "ok
";
1043 my $job = get_job($param);
1044 $job->{state} = "stopped
";
1051 $PROGNAME destroy -source <string> [OPTIONS]
1053 remove a sync Job from the scheduler
1057 name of the sync job, if not set it is default
1061 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1064 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1070 the destination target is like [IP]:<Pool>[/Path]
1074 name of the user on the destination target, root by default
1078 max sync speed in kBytes/s, default unlimited
1082 how much snapshots will be kept before get erased, default 1
1086 name of the sync job, if not set it is default
1090 if this flag is set it will skip the first sync
1094 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1098 name of the user on the source target, root by default
1102 Include the dataset's properties in the stream.
1104 -dest-config-path string
1106 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1109 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1115 the destination target is like [IP:]<Pool>[/Path]
1119 name of the user on the destination target, root by default
1123 max sync speed in kBytes/s, default unlimited
1127 how much snapshots will be kept before get erased, default 1
1131 name of the sync job, if not set it is default.
1132 It is only necessary if scheduler allready contains this source.
1136 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1140 name of the user on the source target, root by default
1144 print out the sync progress.
1148 Include the dataset's properties in the stream.
1150 -dest-config-path string
1152 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1157 Get a List of all scheduled Sync Jobs
1162 Get the status of all scheduled Sync Jobs
1165 $PROGNAME help <cmd> [OPTIONS]
1167 Get help about specified command.
1175 Verbose output format.
1178 $PROGNAME enable -source <string> [OPTIONS]
1180 enable a syncjob and reset error
1184 name of the sync job, if not set it is default
1188 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1191 $PROGNAME disable -source <string> [OPTIONS]
1197 name of the sync job, if not set it is default
1201 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1203 printpod
=> 'internal command',
1209 } elsif (!$cmd_help->{$command}) {
1210 print "ERROR: unknown command '$command'";
1215 my $param = parse_argv
(@arg);
1219 die "$cmd_help->{$command}\n" if !$param->{$_};
1223 if ($command eq 'destroy') {
1224 check_params
(qw(source));
1226 check_target
($param->{source
});
1227 destroy_job
($param);
1229 } elsif ($command eq 'sync') {
1230 check_params
(qw(source dest));
1232 check_target
($param->{source
});
1233 check_target
($param->{dest
});
1236 } elsif ($command eq 'create') {
1237 check_params
(qw(source dest));
1239 check_target
($param->{source
});
1240 check_target
($param->{dest
});
1243 } elsif ($command eq 'status') {
1246 } elsif ($command eq 'list') {
1249 } elsif ($command eq 'help') {
1250 my $help_command = $ARGV[1];
1252 if ($help_command && $cmd_help->{$help_command}) {
1253 die "$cmd_help->{$help_command}\n";
1256 if ($param->{verbose
}) {
1257 exec("man $PROGNAME");
1264 } elsif ($command eq 'enable') {
1265 check_params
(qw(source));
1267 check_target
($param->{source
});
1270 } elsif ($command eq 'disable') {
1271 check_params
(qw(source));
1273 check_target
($param->{source
});
1274 disable_job
($param);
1276 } elsif ($command eq 'printpod') {
1283 print("ERROR:\tno command specified\n") if !$help;
1284 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1285 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1286 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1287 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1288 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1289 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1290 print("\t$PROGNAME list\n");
1291 print("\t$PROGNAME status\n");
1292 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1297 parse_target
($target);
1302 my $synopsis = join("\n", sort values %$cmd_help);
1307 pve-zsync - PVE ZFS Replication Manager
1311 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1317 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1318 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1319 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.
1320 To config cron see man crontab.
1322 =head2 PVE ZFS Storage sync Tool
1324 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1328 add sync job from local VM to remote ZFS Server
1329 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1331 =head1 IMPORTANT FILES
1333 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1335 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1337 =head1 COPYRIGHT AND DISCLAIMER
1339 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1341 This program is free software: you can redistribute it and/or modify it
1342 under the terms of the GNU Affero General Public License as published
1343 by the Free Software Foundation, either version 3 of the License, or
1344 (at your option) any later version.
1346 This program is distributed in the hope that it will be useful, but
1347 WITHOUT ANY WARRANTY; without even the implied warranty of
1348 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1349 Affero General Public License for more details.
1351 You should have received a copy of the GNU Affero General Public
1352 License along with this program. If not, see
1353 <http://www.gnu.org/licenses/>.