]>
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 $PROG_PATH = "$PATH/${PROGNAME}";
26 $DEBUG = 0; # change default here. not above on declaration!
27 $DEBUG ||= $ENV{ZSYNC_DEBUG
};
30 Data
::Dumper-
>import();
34 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
35 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
36 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
37 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
40 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
41 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
42 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
43 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
44 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
45 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
46 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
47 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
48 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
50 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
51 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
52 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
53 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
54 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
56 my $command = $ARGV[0];
58 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
59 check_bin
('cstream');
65 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} = sub {
66 die "Signaled, aborting sync: $!\n";
72 foreach my $p (split (/:/, $ENV{PATH
})) {
79 die "unable to find command '$bin'\n";
82 sub cut_target_width
{
83 my ($path, $maxlen) = @_;
86 return $path if length($path) <= $maxlen;
88 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
90 $path =~ s
@/([^/]+/?
)$@@;
93 if (length($tail)+3 == $maxlen) {
95 } elsif (length($tail)+2 >= $maxlen) {
96 return '..'.substr($tail, -$maxlen+2)
99 $path =~ s
@(/[^/]+)(?
:/|$)@@;
101 my $both = length($head) + length($tail);
102 my $remaining = $maxlen-$both-4; # -4 for "/../"
104 if ($remaining < 0) {
105 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
108 substr($path, ($remaining/2), (length($path)-$remaining), '..');
109 return "$head/" . $path . "/$tail";
113 my ($lock_fn, $code) = @_;
115 my $lock_fh = IO
::File-
>new("> $lock_fn");
117 flock($lock_fh, LOCK_EX
) || die "Couldn't acquire lock - $!\n";
118 my $res = eval { $code->() };
121 flock($lock_fh, LOCK_UN
) || warn "Error unlocking - $!\n";
129 my ($source, $name, $status) = @_;
131 if ($status->{$source->{all
}}->{$name}->{status
}) {
138 sub check_pool_exists
{
139 my ($target, $user) = @_;
144 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
146 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
160 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
163 if ($text !~ $TARGETRE) {
167 $target->{ip
} = $1 if $1;
168 my @parts = split('/', $2);
170 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
172 my $pool = $target->{pool
} = shift(@parts);
173 die "$errstr\n" if !$pool;
175 if ($pool =~ m/^\d+$/) {
176 $target->{vmid
} = $pool;
177 delete $target->{pool
};
180 return $target if (@parts == 0);
181 $target->{last_part
} = pop(@parts);
187 $target->{path
} = join('/', @parts);
195 #This is for the first use to init file;
197 my $new_fh = IO
::File-
>new("> $CRONJOBS");
198 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
203 my $fh = IO
::File-
>new("< $CRONJOBS");
204 die "Could not open file $CRONJOBS: $!\n" if !$fh;
210 return encode_cron
(@text);
225 source_user
=> undef,
228 dest_config_path
=> undef,
231 my ($ret) = GetOptionsFromArray
(
233 'dest=s' => \
$param->{dest
},
234 'source=s' => \
$param->{source
},
235 'verbose' => \
$param->{verbose
},
236 'limit=i' => \
$param->{limit
},
237 'maxsnap=i' => \
$param->{maxsnap
},
238 'name=s' => \
$param->{name
},
239 'skip' => \
$param->{skip
},
240 'method=s' => \
$param->{method},
241 'source-user=s' => \
$param->{source_user
},
242 'dest-user=s' => \
$param->{dest_user
},
243 'properties' => \
$param->{properties
},
244 'dest-config-path=s' => \
$param->{dest_config_path
},
247 die "can't parse options\n" if $ret == 0;
249 $param->{name
} //= "default";
250 $param->{maxsnap
} //= 1;
251 $param->{method} //= "ssh";
252 $param->{source_user
} //= "root";
253 $param->{dest_user
} //= "root";
258 sub add_state_to_job
{
261 my $states = read_state
();
262 my $state = $states->{$job->{source
}}->{$job->{name
}};
264 $job->{state} = $state->{state};
265 $job->{lsync
} = $state->{lsync
};
266 $job->{vm_type
} = $state->{vm_type
};
268 for (my $i = 0; $state->{"snap$i"}; $i++) {
269 $job->{"snap$i"} = $state->{"snap$i"};
280 while (my $line = shift(@text)) {
282 my @arg = split('\s', $line);
283 my $param = parse_argv
(@arg);
285 if ($param->{source
} && $param->{dest
}) {
286 my $source = delete $param->{source
};
287 my $name = delete $param->{name
};
289 $cfg->{$source}->{$name} = $param;
301 my $source = parse_target
($param->{source
});
302 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
304 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
305 $job->{dest
} = $param->{dest
} if $param->{dest
};
306 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
307 $job->{method} = "ssh" if !$job->{method};
308 $job->{limit
} = $param->{limit
};
309 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
310 $job->{source
} = $param->{source
};
311 $job->{source_user
} = $param->{source_user
};
312 $job->{dest_user
} = $param->{dest_user
};
313 $job->{properties
} = !!$param->{properties
};
314 $job->{dest_config_path
} = $param->{dest_config_path
} if $param->{dest_config_path
};
322 make_path
$CONFIG_PATH;
323 my $new_fh = IO
::File-
>new("> $STATE");
324 die "Could not create $STATE: $!\n" if !$new_fh;
330 my $fh = IO
::File-
>new("< $STATE");
331 die "Could not open file $STATE: $!\n" if !$fh;
334 my $states = decode_json
($text);
348 $in_fh = IO
::File-
>new("< $STATE");
349 die "Could not open file $STATE: $!\n" if !$in_fh;
353 my $out_fh = IO
::File-
>new("> $STATE.new");
354 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
359 $states = decode_json
($text);
360 $state = $states->{$job->{source
}}->{$job->{name
}};
363 if ($job->{state} ne "del") {
364 $state->{state} = $job->{state};
365 $state->{lsync
} = $job->{lsync
};
366 $state->{vm_type
} = $job->{vm_type
};
368 for (my $i = 0; $job->{"snap$i"} ; $i++) {
369 $state->{"snap$i"} = $job->{"snap$i"};
371 $states->{$job->{source
}}->{$job->{name
}} = $state;
374 delete $states->{$job->{source
}}->{$job->{name
}};
375 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
378 $text = encode_json
($states);
382 rename "$STATE.new", $STATE;
397 my $header = "SHELL=/bin/sh\n";
398 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
400 my $fh = IO
::File-
>new("< $CRONJOBS");
401 die "Could not open file $CRONJOBS: $!\n" if !$fh;
405 while (my $line = shift(@test)) {
407 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
409 next if $job->{state} eq "del";
410 $text .= format_job
($job, $line);
412 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
421 $text = "$header$text";
425 $text .= format_job
($job);
427 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
428 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
430 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
433 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
438 my ($job, $line) = @_;
441 if ($job->{state} eq "stopped") {
445 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
448 $text .= "*/$INTERVAL * * * *";
451 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
452 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
453 $text .= " --limit $job->{limit}" if $job->{limit
};
454 $text .= " --method $job->{method}";
455 $text .= " --verbose" if $job->{verbose
};
456 $text .= " --source-user $job->{source_user}";
457 $text .= " --dest-user $job->{dest_user}";
458 $text .= " --properties" if $job->{properties
};
459 $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path
};
467 my $cfg = read_cron
();
469 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
471 my $states = read_state
();
472 foreach my $source (sort keys%{$cfg}) {
473 foreach my $name (sort keys%{$cfg->{$source}}) {
474 $list .= sprintf("%-25s", cut_target_width
($source, 25));
475 $list .= sprintf("%-25s", cut_target_width
($name, 25));
476 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
477 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
478 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
479 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
487 my ($target, $user) = @_;
489 return undef if !defined($target->{vmid
});
491 my $conf_fn = "$target->{vmid}.conf";
494 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
495 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
496 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
498 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
499 return "lxc" if -f
"$LXC_CONF/$conf_fn";
508 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
509 my $cfg = read_cron
();
511 my $job = param_to_job
($param);
513 $job->{state} = "ok";
516 my $source = parse_target
($param->{source
});
517 my $dest = parse_target
($param->{dest
});
519 if (my $ip = $dest->{ip
}) {
520 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
523 if (my $ip = $source->{ip
}) {
524 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
527 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
529 if (!defined($source->{vmid
})) {
530 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
533 my $vm_type = vm_exists
($source, $param->{source_user
});
534 $job->{vm_type
} = $vm_type;
535 $source->{vm_type
} = $vm_type;
537 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
539 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
541 #check if vm has zfs disks if not die;
542 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
546 }); #cron and state lock
549 sync
($param) if !$param->{skip
};
560 my $cfg = read_cron
();
562 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
563 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
565 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
566 $job->{name
} = $param->{name
};
567 $job->{source
} = $param->{source
};
568 $job = add_state_to_job
($job);
576 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
577 my $job = get_job
($param);
578 $job->{state} = "del";
590 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
591 eval { $job = get_job
($param) };
594 if (defined($job->{state}) && ($job->{state} eq "syncing" || $job->{state} eq "waiting")) {
595 die "Job --source $param->{source} --name $param->{name} is already scheduled to sync\n";
598 $job->{state} = "waiting";
603 locked
("$CONFIG_PATH/sync.lock", sub {
605 my $date = get_date
();
611 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
612 #job might've changed while we waited for the sync lock, but we can be sure it's not syncing
613 eval { $job = get_job
($param); };
615 if ($job && defined($job->{state}) && $job->{state} eq "stopped") {
616 die "Job --source $param->{source} --name $param->{name} has been disabled\n";
619 $dest = parse_target
($param->{dest
});
620 $source = parse_target
($param->{source
});
622 $vm_type = vm_exists
($source, $param->{source_user
});
623 $source->{vm_type
} = $vm_type;
626 $job->{state} = "syncing";
627 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
630 }); #cron and state lock
632 my $sync_path = sub {
633 my ($source, $dest, $job, $param, $date) = @_;
635 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
637 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
639 send_image
($source, $dest, $param);
641 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
646 if ($source->{vmid
}) {
647 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
648 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
649 my $disks = get_disks
($source, $param->{source_user
});
651 foreach my $disk (sort keys %{$disks}) {
652 $source->{all
} = $disks->{$disk}->{all
};
653 $source->{pool
} = $disks->{$disk}->{pool
};
654 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
655 $source->{last_part
} = $disks->{$disk}->{last_part
};
656 &$sync_path($source, $dest, $job, $param, $date);
658 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
659 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
661 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
664 &$sync_path($source, $dest, $job, $param, $date);
668 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
669 eval { $job = get_job
($param); };
671 $job->{state} = "error";
675 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
679 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
680 eval { $job = get_job
($param); };
682 if (defined($job->{state}) && $job->{state} eq "stopped") {
683 $job->{state} = "stopped";
685 $job->{state} = "ok";
687 $job->{lsync
} = $date;
695 my ($source, $dest, $max_snap, $name, $source_user) = @_;
698 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
699 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
700 push @$cmd, $source->{all
};
702 my $raw = run_cmd
($cmd);
705 my $last_snap = undef;
708 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
710 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
712 $last_snap = $1 if (!$last_snap);
715 if ($index == $max_snap) {
716 $source->{destroy
} = 1;
722 return ($old_snap, $last_snap) if $last_snap;
728 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
730 my $snap_name = "rep_$name\_".$date;
732 $source->{new_snap
} = $snap_name;
734 my $path = "$source->{all}\@$snap_name";
737 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
738 push @$cmd, 'zfs', 'snapshot', $path;
744 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
752 my $text = "SHELL=/bin/sh\n";
753 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
755 my $fh = IO
::File-
>new("> $CRONJOBS");
756 die "Could not open file: $!\n" if !$fh;
758 foreach my $source (sort keys%{$cfg}) {
759 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
760 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
761 $text .= "$PROG_PATH sync";
762 $text .= " -source ";
763 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
764 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
765 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
767 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
768 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
769 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
772 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
773 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
774 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
775 $text .= " -name $sync_name ";
776 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
777 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
781 die "Can't write to cron\n" if (!print($fh $text));
786 my ($target, $user) = @_;
789 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
791 if ($target->{vm_type
} eq 'qemu') {
792 push @$cmd, 'qm', 'config', $target->{vmid
};
793 } elsif ($target->{vm_type
} eq 'lxc') {
794 push @$cmd, 'pct', 'config', $target->{vmid
};
796 die "VM Type unknown\n";
799 my $res = run_cmd
($cmd);
801 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
808 print "Start CMD\n" if $DEBUG;
809 print Dumper
$cmd if $DEBUG;
810 if (ref($cmd) eq 'ARRAY') {
811 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
813 my $output = `$cmd 2>&1`;
815 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
818 print Dumper
$output if $DEBUG;
819 print "END CMD\n" if $DEBUG;
824 my ($text, $ip, $vm_type, $user) = @_;
829 while ($text && $text =~ s/^(.*?)(\n|$)//) {
832 next if $line =~ /media=cdrom/;
833 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
835 #QEMU if backup is not set include in sync
836 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
838 #LXC if backup is not set do no in sync
839 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
843 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
844 my @parameter = split(/,/,$1);
846 foreach my $opt (@parameter) {
847 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
854 if (!defined($disk) || !defined($stor)) {
855 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
860 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
861 push @$cmd, 'pvesm', 'path', "$stor$disk";
862 my $path = run_cmd($cmd);
864 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
866 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
868 my @array = split('/', $1);
869 $disks->{$num}->{pool} = shift(@array);
870 $disks->{$num}->{all} = $disks->{$num}->{pool};
872 $disks->{$num}->{path} = join('/', @array);
873 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
875 $disks->{$num}->{last_part} = $disk;
876 $disks->{$num}->{all} .= "\
/$disk";
879 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
881 $disks->{$num}->{pool} = $1;
882 $disks->{$num}->{all} = $disks->{$num}->{pool};
885 $disks->{$num}->{path} = $3;
886 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
889 $disks->{$num}->{last_part} = $disk;
890 $disks->{$num}->{all} .= "\
/$disk";
895 die "ERROR
: in path
\n";
899 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
903 sub snapshot_destroy {
904 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
906 my @zfscmd = ('zfs', 'destroy');
907 my $snapshot = "$source->{all
}\
@$snap";
910 if($source->{ip} && $method eq 'ssh'){
911 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
913 run_cmd([@zfscmd, $snapshot]);
920 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
922 my $path = "$dest->{all
}";
923 $path .= "/$source->{last_part
}" if $source->{last_part};
926 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
935 my ($source , $dest, $method, $dest_user) = @_;
938 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
939 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
941 my $path = $dest->{all};
942 $path .= "/$source->{last_part
}" if $source->{last_part};
943 $path .= "\
@$source->{old_snap
}";
949 eval {$text =run_cmd($cmd);};
955 while ($text && $text =~ s/^(.*?)(\n|$)//) {
957 return 1 if $line =~ m/^.*$source->{old_snap}$/;
962 my ($source, $dest, $param) = @_;
966 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
967 push @$cmd, 'zfs', 'send';
968 push @$cmd, '-p', if $param->{properties};
969 push @$cmd, '-v' if $param->{verbose};
971 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
972 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
974 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
976 if ($param->{limit}){
977 my $bwl = $param->{limit}*1024;
978 push @$cmd, \'|', 'cstream', '-t', $bwl;
980 my $target = "$dest->{all
}";
981 $target .= "/$source->{last_part
}" if $source->{last_part};
985 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
986 push @$cmd, 'zfs', 'recv', '-F', '--';
987 push @$cmd, "$target";
994 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
1001 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
1003 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
1004 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
1006 my $config_dir = $dest_config_path // $CONFIG_PATH;
1007 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
1009 $dest_target_new = $config_dir.'/'.$dest_target_new;
1011 if ($method eq 'ssh'){
1012 if ($dest->{ip} && $source->{ip}) {
1013 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1014 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1015 } elsif ($dest->{ip}) {
1016 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1017 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1018 } elsif ($source->{ip}) {
1019 run_cmd(['mkdir', '-p', '--', $config_dir]);
1020 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
1023 if ($source->{destroy}){
1024 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
1026 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
1028 run_cmd(['rm', '-f', '--', $dest_target_old]);
1031 } elsif ($method eq 'local') {
1032 run_cmd(['mkdir', '-p', '--', $config_dir]);
1033 run_cmd(['cp', $source_target, $dest_target_new]);
1038 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1039 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1045 my $cfg = read_cron();
1047 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1049 my $states = read_state();
1051 foreach my $source (sort keys%{$cfg}) {
1052 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1053 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1054 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1055 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1059 return $status_list;
1065 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1066 my $job = get_job($param);
1067 $job->{state} = "ok
";
1076 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1077 my $job = get_job($param);
1078 $job->{state} = "stopped
";
1086 $PROGNAME destroy -source <string> [OPTIONS]
1088 remove a sync Job from the scheduler
1092 name of the sync job, if not set it is default
1096 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1099 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1105 the destination target is like [IP]:<Pool>[/Path]
1109 name of the user on the destination target, root by default
1113 max sync speed in kBytes/s, default unlimited
1117 how much snapshots will be kept before get erased, default 1
1121 name of the sync job, if not set it is default
1125 if this flag is set it will skip the first sync
1129 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1133 name of the user on the source target, root by default
1137 Include the dataset's properties in the stream.
1139 -dest-config-path string
1141 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1144 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1150 the destination target is like [IP:]<Pool>[/Path]
1154 name of the user on the destination target, root by default
1158 max sync speed in kBytes/s, default unlimited
1162 how much snapshots will be kept before get erased, default 1
1166 name of the sync job, if not set it is default.
1167 It is only necessary if scheduler allready contains this source.
1171 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1175 name of the user on the source target, root by default
1179 print out the sync progress.
1183 Include the dataset's properties in the stream.
1185 -dest-config-path string
1187 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1192 Get a List of all scheduled Sync Jobs
1197 Get the status of all scheduled Sync Jobs
1200 $PROGNAME help <cmd> [OPTIONS]
1202 Get help about specified command.
1210 Verbose output format.
1213 $PROGNAME enable -source <string> [OPTIONS]
1215 enable a syncjob and reset error
1219 name of the sync job, if not set it is default
1223 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1226 $PROGNAME disable -source <string> [OPTIONS]
1232 name of the sync job, if not set it is default
1236 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1238 printpod
=> 'internal command',
1244 } elsif (!$cmd_help->{$command}) {
1245 print "ERROR: unknown command '$command'";
1250 my $param = parse_argv
(@arg);
1254 die "$cmd_help->{$command}\n" if !$param->{$_};
1258 if ($command eq 'destroy') {
1259 check_params
(qw(source));
1261 check_target
($param->{source
});
1262 destroy_job
($param);
1264 } elsif ($command eq 'sync') {
1265 check_params
(qw(source dest));
1267 check_target
($param->{source
});
1268 check_target
($param->{dest
});
1271 } elsif ($command eq 'create') {
1272 check_params
(qw(source dest));
1274 check_target
($param->{source
});
1275 check_target
($param->{dest
});
1278 } elsif ($command eq 'status') {
1281 } elsif ($command eq 'list') {
1284 } elsif ($command eq 'help') {
1285 my $help_command = $ARGV[1];
1287 if ($help_command && $cmd_help->{$help_command}) {
1288 die "$cmd_help->{$help_command}\n";
1291 if ($param->{verbose
}) {
1292 exec("man $PROGNAME");
1299 } elsif ($command eq 'enable') {
1300 check_params
(qw(source));
1302 check_target
($param->{source
});
1305 } elsif ($command eq 'disable') {
1306 check_params
(qw(source));
1308 check_target
($param->{source
});
1309 disable_job
($param);
1311 } elsif ($command eq 'printpod') {
1318 print("ERROR:\tno command specified\n") if !$help;
1319 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1320 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1321 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1322 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1323 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1324 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1325 print("\t$PROGNAME list\n");
1326 print("\t$PROGNAME status\n");
1327 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1332 parse_target
($target);
1337 my $synopsis = join("\n", sort values %$cmd_help);
1342 pve-zsync - PVE ZFS Replication Manager
1346 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1352 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1353 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1354 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.
1355 To config cron see man crontab.
1357 =head2 PVE ZFS Storage sync Tool
1359 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1363 add sync job from local VM to remote ZFS Server
1364 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1366 =head1 IMPORTANT FILES
1368 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1370 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1372 =head1 COPYRIGHT AND DISCLAIMER
1374 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1376 This program is free software: you can redistribute it and/or modify it
1377 under the terms of the GNU Affero General Public License as published
1378 by the Free Software Foundation, either version 3 of the License, or
1379 (at your option) any later version.
1381 This program is distributed in the hope that it will be useful, but
1382 WITHOUT ANY WARRANTY; without even the implied warranty of
1383 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1384 Affero General Public License for more details.
1386 You should have received a copy of the GNU Affero General Public
1387 License along with this program. If not, see
1388 <http://www.gnu.org/licenses/>.