]>
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 $DISK_KEY_RE = qr/^(?:(?:(?:virtio|ide|scsi|sata|efidisk|mp)\d+)|rootfs): /;
58 my $command = $ARGV[0];
60 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
61 check_bin
('cstream');
67 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} = sub {
68 die "Signaled, aborting sync: $!\n";
74 foreach my $p (split (/:/, $ENV{PATH
})) {
81 die "unable to find command '$bin'\n";
84 sub cut_target_width
{
85 my ($path, $maxlen) = @_;
88 return $path if length($path) <= $maxlen;
90 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
92 $path =~ s
@/([^/]+/?
)$@@;
95 if (length($tail)+3 == $maxlen) {
97 } elsif (length($tail)+2 >= $maxlen) {
98 return '..'.substr($tail, -$maxlen+2)
101 $path =~ s
@(/[^/]+)(?
:/|$)@@;
103 my $both = length($head) + length($tail);
104 my $remaining = $maxlen-$both-4; # -4 for "/../"
106 if ($remaining < 0) {
107 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
110 substr($path, ($remaining/2), (length($path)-$remaining), '..');
111 return "$head/" . $path . "/$tail";
115 my ($lock_fn, $code) = @_;
117 my $lock_fh = IO
::File-
>new("> $lock_fn");
119 flock($lock_fh, LOCK_EX
) || die "Couldn't acquire lock - $!\n";
120 my $res = eval { $code->() };
123 flock($lock_fh, LOCK_UN
) || warn "Error unlocking - $!\n";
131 my ($source, $name, $status) = @_;
133 if ($status->{$source->{all
}}->{$name}->{status
}) {
140 sub check_pool_exists
{
141 my ($target, $user) = @_;
146 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
148 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
162 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
165 if ($text !~ $TARGETRE) {
169 $target->{ip
} = $1 if $1;
170 my @parts = split('/', $2);
172 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
174 my $pool = $target->{pool
} = shift(@parts);
175 die "$errstr\n" if !$pool;
177 if ($pool =~ m/^\d+$/) {
178 $target->{vmid
} = $pool;
179 delete $target->{pool
};
182 return $target if (@parts == 0);
183 $target->{last_part
} = pop(@parts);
189 $target->{path
} = join('/', @parts);
197 #This is for the first use to init file;
199 my $new_fh = IO
::File-
>new("> $CRONJOBS");
200 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
205 my $fh = IO
::File-
>new("< $CRONJOBS");
206 die "Could not open file $CRONJOBS: $!\n" if !$fh;
212 return encode_cron
(@text);
227 source_user
=> undef,
230 dest_config_path
=> undef,
233 my ($ret) = GetOptionsFromArray
(
235 'dest=s' => \
$param->{dest
},
236 'source=s' => \
$param->{source
},
237 'verbose' => \
$param->{verbose
},
238 'limit=i' => \
$param->{limit
},
239 'maxsnap=i' => \
$param->{maxsnap
},
240 'name=s' => \
$param->{name
},
241 'skip' => \
$param->{skip
},
242 'method=s' => \
$param->{method},
243 'source-user=s' => \
$param->{source_user
},
244 'dest-user=s' => \
$param->{dest_user
},
245 'properties' => \
$param->{properties
},
246 'dest-config-path=s' => \
$param->{dest_config_path
},
249 die "can't parse options\n" if $ret == 0;
251 $param->{name
} //= "default";
252 $param->{maxsnap
} //= 1;
253 $param->{method} //= "ssh";
254 $param->{source_user
} //= "root";
255 $param->{dest_user
} //= "root";
260 sub add_state_to_job
{
263 my $states = read_state
();
264 my $state = $states->{$job->{source
}}->{$job->{name
}};
266 $job->{state} = $state->{state};
267 $job->{lsync
} = $state->{lsync
};
268 $job->{vm_type
} = $state->{vm_type
};
270 for (my $i = 0; $state->{"snap$i"}; $i++) {
271 $job->{"snap$i"} = $state->{"snap$i"};
282 while (my $line = shift(@text)) {
284 my @arg = split('\s', $line);
285 my $param = parse_argv
(@arg);
287 if ($param->{source
} && $param->{dest
}) {
288 my $source = delete $param->{source
};
289 my $name = delete $param->{name
};
291 $cfg->{$source}->{$name} = $param;
303 my $source = parse_target
($param->{source
});
304 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
306 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
307 $job->{dest
} = $param->{dest
} if $param->{dest
};
308 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
309 $job->{method} = "ssh" if !$job->{method};
310 $job->{limit
} = $param->{limit
};
311 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
312 $job->{source
} = $param->{source
};
313 $job->{source_user
} = $param->{source_user
};
314 $job->{dest_user
} = $param->{dest_user
};
315 $job->{properties
} = !!$param->{properties
};
316 $job->{dest_config_path
} = $param->{dest_config_path
} if $param->{dest_config_path
};
324 make_path
$CONFIG_PATH;
325 my $new_fh = IO
::File-
>new("> $STATE");
326 die "Could not create $STATE: $!\n" if !$new_fh;
332 my $fh = IO
::File-
>new("< $STATE");
333 die "Could not open file $STATE: $!\n" if !$fh;
336 my $states = decode_json
($text);
350 $in_fh = IO
::File-
>new("< $STATE");
351 die "Could not open file $STATE: $!\n" if !$in_fh;
355 my $out_fh = IO
::File-
>new("> $STATE.new");
356 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
361 $states = decode_json
($text);
362 $state = $states->{$job->{source
}}->{$job->{name
}};
365 if ($job->{state} ne "del") {
366 $state->{state} = $job->{state};
367 $state->{lsync
} = $job->{lsync
};
368 $state->{vm_type
} = $job->{vm_type
};
370 for (my $i = 0; $job->{"snap$i"} ; $i++) {
371 $state->{"snap$i"} = $job->{"snap$i"};
373 $states->{$job->{source
}}->{$job->{name
}} = $state;
376 delete $states->{$job->{source
}}->{$job->{name
}};
377 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
380 $text = encode_json
($states);
384 rename "$STATE.new", $STATE;
399 my $header = "SHELL=/bin/sh\n";
400 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
402 my $fh = IO
::File-
>new("< $CRONJOBS");
403 die "Could not open file $CRONJOBS: $!\n" if !$fh;
407 while (my $line = shift(@test)) {
409 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
411 next if $job->{state} eq "del";
412 $text .= format_job
($job, $line);
414 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
423 $text = "$header$text";
427 $text .= format_job
($job);
429 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
430 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
432 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
435 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
440 my ($job, $line) = @_;
443 if ($job->{state} eq "stopped") {
447 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
450 $text .= "*/$INTERVAL * * * *";
453 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
454 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
455 $text .= " --limit $job->{limit}" if $job->{limit
};
456 $text .= " --method $job->{method}";
457 $text .= " --verbose" if $job->{verbose
};
458 $text .= " --source-user $job->{source_user}";
459 $text .= " --dest-user $job->{dest_user}";
460 $text .= " --properties" if $job->{properties
};
461 $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path
};
469 my $cfg = read_cron
();
471 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
473 my $states = read_state
();
474 foreach my $source (sort keys%{$cfg}) {
475 foreach my $name (sort keys%{$cfg->{$source}}) {
476 $list .= sprintf("%-25s", cut_target_width
($source, 25));
477 $list .= sprintf("%-25s", cut_target_width
($name, 25));
478 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
479 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
480 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
481 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
489 my ($target, $user) = @_;
491 return undef if !defined($target->{vmid
});
493 my $conf_fn = "$target->{vmid}.conf";
496 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
497 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
498 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
500 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
501 return "lxc" if -f
"$LXC_CONF/$conf_fn";
510 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
511 my $cfg = read_cron
();
513 my $job = param_to_job
($param);
515 $job->{state} = "ok";
518 my $source = parse_target
($param->{source
});
519 my $dest = parse_target
($param->{dest
});
521 if (my $ip = $dest->{ip
}) {
522 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
525 if (my $ip = $source->{ip
}) {
526 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
529 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
531 if (!defined($source->{vmid
})) {
532 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
535 my $vm_type = vm_exists
($source, $param->{source_user
});
536 $job->{vm_type
} = $vm_type;
537 $source->{vm_type
} = $vm_type;
539 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
541 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
543 #check if vm has zfs disks if not die;
544 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
548 }); #cron and state lock
550 return if $param->{skip
};
552 eval { sync
($param) };
562 my $cfg = read_cron
();
564 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
565 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
567 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
568 $job->{name
} = $param->{name
};
569 $job->{source
} = $param->{source
};
570 $job = add_state_to_job
($job);
578 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
579 my $job = get_job
($param);
580 $job->{state} = "del";
592 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
593 eval { $job = get_job
($param) };
596 if (defined($job->{state}) && ($job->{state} eq "syncing" || $job->{state} eq "waiting")) {
597 die "Job --source $param->{source} --name $param->{name} is already scheduled to sync\n";
600 $job->{state} = "waiting";
605 locked
("$CONFIG_PATH/sync.lock", sub {
607 my $date = get_date
();
613 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
614 #job might've changed while we waited for the sync lock, but we can be sure it's not syncing
615 eval { $job = get_job
($param); };
617 if ($job && defined($job->{state}) && $job->{state} eq "stopped") {
618 die "Job --source $param->{source} --name $param->{name} has been disabled\n";
621 $dest = parse_target
($param->{dest
});
622 $source = parse_target
($param->{source
});
624 $vm_type = vm_exists
($source, $param->{source_user
});
625 $source->{vm_type
} = $vm_type;
628 $job->{state} = "syncing";
629 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
632 }); #cron and state lock
634 my $sync_path = sub {
635 my ($source, $dest, $job, $param, $date) = @_;
637 ($dest->{old_snap
}, $dest->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{dest_user
});
639 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
641 send_image
($source, $dest, $param);
643 snapshot_destroy
($source, $dest, $param->{method}, $dest->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $dest->{old_snap
});
648 if ($source->{vmid
}) {
649 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
650 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
651 my $disks = get_disks
($source, $param->{source_user
});
653 foreach my $disk (sort keys %{$disks}) {
654 $source->{all
} = $disks->{$disk}->{all
};
655 $source->{pool
} = $disks->{$disk}->{pool
};
656 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
657 $source->{last_part
} = $disks->{$disk}->{last_part
};
658 &$sync_path($source, $dest, $job, $param, $date);
660 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
661 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
663 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
666 &$sync_path($source, $dest, $job, $param, $date);
670 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
671 eval { $job = get_job
($param); };
673 $job->{state} = "error";
677 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
681 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
682 eval { $job = get_job
($param); };
684 if (defined($job->{state}) && $job->{state} eq "stopped") {
685 $job->{state} = "stopped";
687 $job->{state} = "ok";
689 $job->{lsync
} = $date;
697 my ($source, $dest, $max_snap, $name, $dest_user) = @_;
700 push @$cmd, 'ssh', "$dest_user\@$dest->{ip}", '--', if $dest->{ip
};
701 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
703 my $path = $dest->{all
};
704 $path .= "/$source->{last_part}" if $source->{last_part
};
708 eval {$raw = run_cmd
($cmd)};
709 if (my $erro =$@) { #this means the volume doesn't exist on dest yet
715 my $last_snap = undef;
718 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
720 if ($line =~ m/@(.*)$/) {
721 $last_snap = $1 if (!$last_snap);
723 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
726 if ($index == $max_snap) {
727 $source->{destroy
} = 1;
733 return ($old_snap, $last_snap) if $last_snap;
739 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
741 my $snap_name = "rep_$name\_".$date;
743 $source->{new_snap
} = $snap_name;
745 my $path = "$source->{all}\@$snap_name";
748 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
749 push @$cmd, 'zfs', 'snapshot', $path;
755 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
763 my $text = "SHELL=/bin/sh\n";
764 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
766 my $fh = IO
::File-
>new("> $CRONJOBS");
767 die "Could not open file: $!\n" if !$fh;
769 foreach my $source (sort keys%{$cfg}) {
770 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
771 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
772 $text .= "$PROG_PATH sync";
773 $text .= " -source ";
774 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
775 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
776 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
778 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
779 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
780 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
783 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
784 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
785 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
786 $text .= " -name $sync_name ";
787 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
788 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
792 die "Can't write to cron\n" if (!print($fh $text));
797 my ($target, $user) = @_;
800 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
802 if ($target->{vm_type
} eq 'qemu') {
803 push @$cmd, 'qm', 'config', $target->{vmid
};
804 } elsif ($target->{vm_type
} eq 'lxc') {
805 push @$cmd, 'pct', 'config', $target->{vmid
};
807 die "VM Type unknown\n";
810 my $res = run_cmd
($cmd);
812 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
819 print "Start CMD\n" if $DEBUG;
820 print Dumper
$cmd if $DEBUG;
821 if (ref($cmd) eq 'ARRAY') {
822 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
824 my $output = `$cmd 2>&1`;
826 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
829 print Dumper
$output if $DEBUG;
830 print "END CMD\n" if $DEBUG;
835 my ($text, $ip, $vm_type, $user) = @_;
840 while ($text && $text =~ s/^(.*?)(\n|$)//) {
843 next if $line =~ /media=cdrom/;
844 next if $line !~ m/$DISK_KEY_RE/;
846 #QEMU if backup is not set include in sync
847 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
849 #LXC if backup is not set do no in sync
850 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
854 if($line =~ m/$DISK_KEY_RE(.*)$/) {
855 my @parameter = split(/,/,$1);
857 foreach my $opt (@parameter) {
858 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
865 if (!defined($disk) || !defined($stor)) {
866 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
871 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
872 push @$cmd, 'pvesm', 'path', "$stor$disk";
873 my $path = run_cmd($cmd);
875 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
877 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
879 my @array = split('/', $1);
880 $disks->{$num}->{pool} = shift(@array);
881 $disks->{$num}->{all} = $disks->{$num}->{pool};
883 $disks->{$num}->{path} = join('/', @array);
884 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
886 $disks->{$num}->{last_part} = $disk;
887 $disks->{$num}->{all} .= "\
/$disk";
890 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
892 $disks->{$num}->{pool} = $1;
893 $disks->{$num}->{all} = $disks->{$num}->{pool};
896 $disks->{$num}->{path} = $3;
897 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
900 $disks->{$num}->{last_part} = $disk;
901 $disks->{$num}->{all} .= "\
/$disk";
906 die "ERROR
: in path
\n";
910 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
914 sub snapshot_destroy {
915 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
917 my @zfscmd = ('zfs', 'destroy');
918 my $snapshot = "$source->{all
}\
@$snap";
921 if($source->{ip} && $method eq 'ssh'){
922 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
924 run_cmd([@zfscmd, $snapshot]);
931 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
933 my $path = "$dest->{all
}";
934 $path .= "/$source->{last_part
}" if $source->{last_part};
937 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
945 # check if snapshot for incremental sync exist on source side
947 my ($source , $dest, $method, $source_user) = @_;
950 push @$cmd, 'ssh', "$source_user\@$source->{ip
}", '--' if $source->{ip};
951 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
953 my $path = $source->{all};
954 $path .= "\
@$dest->{last_snap
}";
958 eval {run_cmd($cmd)};
967 my ($source, $dest, $param) = @_;
971 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
972 push @$cmd, 'zfs', 'send';
973 push @$cmd, '-p', if $param->{properties};
974 push @$cmd, '-v' if $param->{verbose};
976 if($dest->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
977 push @$cmd, '-i', "$source->{all
}\
@$dest->{last_snap
}";
979 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
981 if ($param->{limit}){
982 my $bwl = $param->{limit}*1024;
983 push @$cmd, \'|', 'cstream', '-t', $bwl;
985 my $target = "$dest->{all
}";
986 $target .= "/$source->{last_part
}" if $source->{last_part};
990 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
991 push @$cmd, 'zfs', 'recv', '-F', '--';
992 push @$cmd, "$target";
999 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
1006 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
1008 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
1009 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
1011 my $config_dir = $dest_config_path // $CONFIG_PATH;
1012 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
1014 $dest_target_new = $config_dir.'/'.$dest_target_new;
1016 if ($method eq 'ssh'){
1017 if ($dest->{ip} && $source->{ip}) {
1018 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1019 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1020 } elsif ($dest->{ip}) {
1021 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1022 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1023 } elsif ($source->{ip}) {
1024 run_cmd(['mkdir', '-p', '--', $config_dir]);
1025 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
1028 if ($source->{destroy}){
1029 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$dest->{old_snap
}";
1031 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
1033 run_cmd(['rm', '-f', '--', $dest_target_old]);
1036 } elsif ($method eq 'local') {
1037 run_cmd(['mkdir', '-p', '--', $config_dir]);
1038 run_cmd(['cp', $source_target, $dest_target_new]);
1043 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1044 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1050 my $cfg = read_cron();
1052 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1054 my $states = read_state();
1056 foreach my $source (sort keys%{$cfg}) {
1057 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1058 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1059 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1060 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1064 return $status_list;
1070 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1071 my $job = get_job($param);
1072 $job->{state} = "ok
";
1081 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1082 my $job = get_job($param);
1083 $job->{state} = "stopped
";
1091 $PROGNAME destroy -source <string> [OPTIONS]
1093 remove a sync Job from the scheduler
1097 name of the sync job, if not set it is default
1101 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1104 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1110 the destination target is like [IP]:<Pool>[/Path]
1114 name of the user on the destination target, root by default
1118 max sync speed in kBytes/s, default unlimited
1122 how much snapshots will be kept before get erased, default 1
1126 name of the sync job, if not set it is default
1130 if this flag is set it will skip the first sync
1134 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1138 name of the user on the source target, root by default
1142 Include the dataset's properties in the stream.
1144 -dest-config-path string
1146 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1149 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1155 the destination target is like [IP:]<Pool>[/Path]
1159 name of the user on the destination target, root by default
1163 max sync speed in kBytes/s, default unlimited
1167 how much snapshots will be kept before get erased, default 1
1171 name of the sync job, if not set it is default.
1172 It is only necessary if scheduler allready contains this source.
1176 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1180 name of the user on the source target, root by default
1184 print out the sync progress.
1188 Include the dataset's properties in the stream.
1190 -dest-config-path string
1192 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1197 Get a List of all scheduled Sync Jobs
1202 Get the status of all scheduled Sync Jobs
1205 $PROGNAME help <cmd> [OPTIONS]
1207 Get help about specified command.
1215 Verbose output format.
1218 $PROGNAME enable -source <string> [OPTIONS]
1220 enable a syncjob and reset error
1224 name of the sync job, if not set it is default
1228 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1231 $PROGNAME disable -source <string> [OPTIONS]
1237 name of the sync job, if not set it is default
1241 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1243 printpod
=> 'internal command',
1249 } elsif (!$cmd_help->{$command}) {
1250 print "ERROR: unknown command '$command'";
1255 my $param = parse_argv
(@arg);
1259 die "$cmd_help->{$command}\n" if !$param->{$_};
1263 if ($command eq 'destroy') {
1264 check_params
(qw(source));
1266 check_target
($param->{source
});
1267 destroy_job
($param);
1269 } elsif ($command eq 'sync') {
1270 check_params
(qw(source dest));
1272 check_target
($param->{source
});
1273 check_target
($param->{dest
});
1276 } elsif ($command eq 'create') {
1277 check_params
(qw(source dest));
1279 check_target
($param->{source
});
1280 check_target
($param->{dest
});
1283 } elsif ($command eq 'status') {
1286 } elsif ($command eq 'list') {
1289 } elsif ($command eq 'help') {
1290 my $help_command = $ARGV[1];
1292 if ($help_command && $cmd_help->{$help_command}) {
1293 die "$cmd_help->{$help_command}\n";
1296 if ($param->{verbose
}) {
1297 exec("man $PROGNAME");
1304 } elsif ($command eq 'enable') {
1305 check_params
(qw(source));
1307 check_target
($param->{source
});
1310 } elsif ($command eq 'disable') {
1311 check_params
(qw(source));
1313 check_target
($param->{source
});
1314 disable_job
($param);
1316 } elsif ($command eq 'printpod') {
1323 print("ERROR:\tno command specified\n") if !$help;
1324 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1325 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1326 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1327 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1328 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1329 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1330 print("\t$PROGNAME list\n");
1331 print("\t$PROGNAME status\n");
1332 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1337 parse_target
($target);
1342 my $synopsis = join("\n", sort values %$cmd_help);
1347 pve-zsync - PVE ZFS Replication Manager
1351 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1357 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1358 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1359 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.
1360 To config cron see man crontab.
1362 =head2 PVE ZFS Storage sync Tool
1364 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1368 add sync job from local VM to remote ZFS Server
1369 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1371 =head1 IMPORTANT FILES
1373 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1375 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1377 =head1 COPYRIGHT AND DISCLAIMER
1379 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1381 This program is free software: you can redistribute it and/or modify it
1382 under the terms of the GNU Affero General Public License as published
1383 by the Free Software Foundation, either version 3 of the License, or
1384 (at your option) any later version.
1386 This program is distributed in the hope that it will be useful, but
1387 WITHOUT ANY WARRANTY; without even the implied warranty of
1388 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1389 Affero General Public License for more details.
1391 You should have received a copy of the GNU Affero General Public
1392 License along with this program. If not, see
1393 <http://www.gnu.org/licenses/>.