]>
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 $dest = parse_target
($param->{dest
});
616 $source = parse_target
($param->{source
});
618 $vm_type = vm_exists
($source, $param->{source_user
});
619 $source->{vm_type
} = $vm_type;
622 $job->{state} = "syncing";
623 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
626 }); #cron and state lock
628 my $sync_path = sub {
629 my ($source, $dest, $job, $param, $date) = @_;
631 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
633 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
635 send_image
($source, $dest, $param);
637 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
642 if ($source->{vmid
}) {
643 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
644 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
645 my $disks = get_disks
($source, $param->{source_user
});
647 foreach my $disk (sort keys %{$disks}) {
648 $source->{all
} = $disks->{$disk}->{all
};
649 $source->{pool
} = $disks->{$disk}->{pool
};
650 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
651 $source->{last_part
} = $disks->{$disk}->{last_part
};
652 &$sync_path($source, $dest, $job, $param, $date);
654 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
655 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
657 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
660 &$sync_path($source, $dest, $job, $param, $date);
664 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
665 eval { $job = get_job
($param); };
667 $job->{state} = "error";
671 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
675 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
676 eval { $job = get_job
($param); };
678 $job->{state} = "ok";
679 $job->{lsync
} = $date;
687 my ($source, $dest, $max_snap, $name, $source_user) = @_;
690 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
691 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
692 push @$cmd, $source->{all
};
694 my $raw = run_cmd
($cmd);
697 my $last_snap = undef;
700 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
702 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
704 $last_snap = $1 if (!$last_snap);
707 if ($index == $max_snap) {
708 $source->{destroy
} = 1;
714 return ($old_snap, $last_snap) if $last_snap;
720 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
722 my $snap_name = "rep_$name\_".$date;
724 $source->{new_snap
} = $snap_name;
726 my $path = "$source->{all}\@$snap_name";
729 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
730 push @$cmd, 'zfs', 'snapshot', $path;
736 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
744 my $text = "SHELL=/bin/sh\n";
745 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
747 my $fh = IO
::File-
>new("> $CRONJOBS");
748 die "Could not open file: $!\n" if !$fh;
750 foreach my $source (sort keys%{$cfg}) {
751 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
752 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
753 $text .= "$PROG_PATH sync";
754 $text .= " -source ";
755 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
756 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
757 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
759 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
760 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
761 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
764 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
765 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
766 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
767 $text .= " -name $sync_name ";
768 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
769 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
773 die "Can't write to cron\n" if (!print($fh $text));
778 my ($target, $user) = @_;
781 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
783 if ($target->{vm_type
} eq 'qemu') {
784 push @$cmd, 'qm', 'config', $target->{vmid
};
785 } elsif ($target->{vm_type
} eq 'lxc') {
786 push @$cmd, 'pct', 'config', $target->{vmid
};
788 die "VM Type unknown\n";
791 my $res = run_cmd
($cmd);
793 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
800 print "Start CMD\n" if $DEBUG;
801 print Dumper
$cmd if $DEBUG;
802 if (ref($cmd) eq 'ARRAY') {
803 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
805 my $output = `$cmd 2>&1`;
807 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
810 print Dumper
$output if $DEBUG;
811 print "END CMD\n" if $DEBUG;
816 my ($text, $ip, $vm_type, $user) = @_;
821 while ($text && $text =~ s/^(.*?)(\n|$)//) {
824 next if $line =~ /media=cdrom/;
825 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
827 #QEMU if backup is not set include in sync
828 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
830 #LXC if backup is not set do no in sync
831 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
835 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
836 my @parameter = split(/,/,$1);
838 foreach my $opt (@parameter) {
839 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
846 if (!defined($disk) || !defined($stor)) {
847 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
852 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
853 push @$cmd, 'pvesm', 'path', "$stor$disk";
854 my $path = run_cmd($cmd);
856 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
858 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
860 my @array = split('/', $1);
861 $disks->{$num}->{pool} = shift(@array);
862 $disks->{$num}->{all} = $disks->{$num}->{pool};
864 $disks->{$num}->{path} = join('/', @array);
865 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
867 $disks->{$num}->{last_part} = $disk;
868 $disks->{$num}->{all} .= "\
/$disk";
871 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
873 $disks->{$num}->{pool} = $1;
874 $disks->{$num}->{all} = $disks->{$num}->{pool};
877 $disks->{$num}->{path} = $3;
878 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
881 $disks->{$num}->{last_part} = $disk;
882 $disks->{$num}->{all} .= "\
/$disk";
887 die "ERROR
: in path
\n";
891 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
895 sub snapshot_destroy {
896 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
898 my @zfscmd = ('zfs', 'destroy');
899 my $snapshot = "$source->{all
}\
@$snap";
902 if($source->{ip} && $method eq 'ssh'){
903 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
905 run_cmd([@zfscmd, $snapshot]);
912 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
914 my $path = "$dest->{all
}";
915 $path .= "/$source->{last_part
}" if $source->{last_part};
918 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
927 my ($source , $dest, $method, $dest_user) = @_;
930 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
931 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
933 my $path = $dest->{all};
934 $path .= "/$source->{last_part
}" if $source->{last_part};
935 $path .= "\
@$source->{old_snap
}";
941 eval {$text =run_cmd($cmd);};
947 while ($text && $text =~ s/^(.*?)(\n|$)//) {
949 return 1 if $line =~ m/^.*$source->{old_snap}$/;
954 my ($source, $dest, $param) = @_;
958 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
959 push @$cmd, 'zfs', 'send';
960 push @$cmd, '-p', if $param->{properties};
961 push @$cmd, '-v' if $param->{verbose};
963 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
964 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
966 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
968 if ($param->{limit}){
969 my $bwl = $param->{limit}*1024;
970 push @$cmd, \'|', 'cstream', '-t', $bwl;
972 my $target = "$dest->{all
}";
973 $target .= "/$source->{last_part
}" if $source->{last_part};
977 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
978 push @$cmd, 'zfs', 'recv', '-F', '--';
979 push @$cmd, "$target";
986 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
993 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
995 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
996 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
998 my $config_dir = $dest_config_path // $CONFIG_PATH;
999 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
1001 $dest_target_new = $config_dir.'/'.$dest_target_new;
1003 if ($method eq 'ssh'){
1004 if ($dest->{ip} && $source->{ip}) {
1005 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1006 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1007 } elsif ($dest->{ip}) {
1008 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
1009 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
1010 } elsif ($source->{ip}) {
1011 run_cmd(['mkdir', '-p', '--', $config_dir]);
1012 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
1015 if ($source->{destroy}){
1016 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
1018 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
1020 run_cmd(['rm', '-f', '--', $dest_target_old]);
1023 } elsif ($method eq 'local') {
1024 run_cmd(['mkdir', '-p', '--', $config_dir]);
1025 run_cmd(['cp', $source_target, $dest_target_new]);
1030 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1031 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1037 my $cfg = read_cron();
1039 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1041 my $states = read_state();
1043 foreach my $source (sort keys%{$cfg}) {
1044 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1045 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1046 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1047 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1051 return $status_list;
1057 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1058 my $job = get_job($param);
1059 $job->{state} = "ok
";
1068 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1069 my $job = get_job($param);
1070 $job->{state} = "stopped
";
1078 $PROGNAME destroy -source <string> [OPTIONS]
1080 remove a sync Job from the scheduler
1084 name of the sync job, if not set it is default
1088 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1091 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1097 the destination target is like [IP]:<Pool>[/Path]
1101 name of the user on the destination target, root by default
1105 max sync speed in kBytes/s, default unlimited
1109 how much snapshots will be kept before get erased, default 1
1113 name of the sync job, if not set it is default
1117 if this flag is set it will skip the first sync
1121 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1125 name of the user on the source target, root by default
1129 Include the dataset's properties in the stream.
1131 -dest-config-path string
1133 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1136 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1142 the destination target is like [IP:]<Pool>[/Path]
1146 name of the user on the destination target, root by default
1150 max sync speed in kBytes/s, default unlimited
1154 how much snapshots will be kept before get erased, default 1
1158 name of the sync job, if not set it is default.
1159 It is only necessary if scheduler allready contains this source.
1163 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1167 name of the user on the source target, root by default
1171 print out the sync progress.
1175 Include the dataset's properties in the stream.
1177 -dest-config-path string
1179 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1184 Get a List of all scheduled Sync Jobs
1189 Get the status of all scheduled Sync Jobs
1192 $PROGNAME help <cmd> [OPTIONS]
1194 Get help about specified command.
1202 Verbose output format.
1205 $PROGNAME enable -source <string> [OPTIONS]
1207 enable a syncjob and reset error
1211 name of the sync job, if not set it is default
1215 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1218 $PROGNAME disable -source <string> [OPTIONS]
1224 name of the sync job, if not set it is default
1228 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1230 printpod
=> 'internal command',
1236 } elsif (!$cmd_help->{$command}) {
1237 print "ERROR: unknown command '$command'";
1242 my $param = parse_argv
(@arg);
1246 die "$cmd_help->{$command}\n" if !$param->{$_};
1250 if ($command eq 'destroy') {
1251 check_params
(qw(source));
1253 check_target
($param->{source
});
1254 destroy_job
($param);
1256 } elsif ($command eq 'sync') {
1257 check_params
(qw(source dest));
1259 check_target
($param->{source
});
1260 check_target
($param->{dest
});
1263 } elsif ($command eq 'create') {
1264 check_params
(qw(source dest));
1266 check_target
($param->{source
});
1267 check_target
($param->{dest
});
1270 } elsif ($command eq 'status') {
1273 } elsif ($command eq 'list') {
1276 } elsif ($command eq 'help') {
1277 my $help_command = $ARGV[1];
1279 if ($help_command && $cmd_help->{$help_command}) {
1280 die "$cmd_help->{$help_command}\n";
1283 if ($param->{verbose
}) {
1284 exec("man $PROGNAME");
1291 } elsif ($command eq 'enable') {
1292 check_params
(qw(source));
1294 check_target
($param->{source
});
1297 } elsif ($command eq 'disable') {
1298 check_params
(qw(source));
1300 check_target
($param->{source
});
1301 disable_job
($param);
1303 } elsif ($command eq 'printpod') {
1310 print("ERROR:\tno command specified\n") if !$help;
1311 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1312 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1313 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1314 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1315 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1316 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1317 print("\t$PROGNAME list\n");
1318 print("\t$PROGNAME status\n");
1319 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1324 parse_target
($target);
1329 my $synopsis = join("\n", sort values %$cmd_help);
1334 pve-zsync - PVE ZFS Replication Manager
1338 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1344 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1345 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1346 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.
1347 To config cron see man crontab.
1349 =head2 PVE ZFS Storage sync Tool
1351 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1355 add sync job from local VM to remote ZFS Server
1356 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1358 =head1 IMPORTANT FILES
1360 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1362 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1364 =head1 COPYRIGHT AND DISCLAIMER
1366 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1368 This program is free software: you can redistribute it and/or modify it
1369 under the terms of the GNU Affero General Public License as published
1370 by the Free Software Foundation, either version 3 of the License, or
1371 (at your option) any later version.
1373 This program is distributed in the hope that it will be useful, but
1374 WITHOUT ANY WARRANTY; without even the implied warranty of
1375 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1376 Affero General Public License for more details.
1378 You should have received a copy of the GNU Affero General Public
1379 License along with this program. If not, see
1380 <http://www.gnu.org/licenses/>.