]>
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";
588 locked
("$CONFIG_PATH/sync.lock", sub {
590 my $date = get_date
();
597 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
598 eval { $job = get_job
($param); };
600 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
601 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
604 $dest = parse_target
($param->{dest
});
605 $source = parse_target
($param->{source
});
607 $vm_type = vm_exists
($source, $param->{source_user
});
608 $source->{vm_type
} = $vm_type;
611 $job->{state} = "syncing";
612 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
615 }); #cron and state lock
617 my $sync_path = sub {
618 my ($source, $dest, $job, $param, $date) = @_;
620 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
622 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
624 send_image
($source, $dest, $param);
626 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
631 if ($source->{vmid
}) {
632 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
633 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
634 my $disks = get_disks
($source, $param->{source_user
});
636 foreach my $disk (sort keys %{$disks}) {
637 $source->{all
} = $disks->{$disk}->{all
};
638 $source->{pool
} = $disks->{$disk}->{pool
};
639 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
640 $source->{last_part
} = $disks->{$disk}->{last_part
};
641 &$sync_path($source, $dest, $job, $param, $date);
643 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
644 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
646 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
649 &$sync_path($source, $dest, $job, $param, $date);
653 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
654 eval { $job = get_job
($param); };
656 $job->{state} = "error";
660 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
664 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
665 eval { $job = get_job
($param); };
667 $job->{state} = "ok";
668 $job->{lsync
} = $date;
676 my ($source, $dest, $max_snap, $name, $source_user) = @_;
679 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
680 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
681 push @$cmd, $source->{all
};
683 my $raw = run_cmd
($cmd);
686 my $last_snap = undef;
689 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
691 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
693 $last_snap = $1 if (!$last_snap);
696 if ($index == $max_snap) {
697 $source->{destroy
} = 1;
703 return ($old_snap, $last_snap) if $last_snap;
709 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
711 my $snap_name = "rep_$name\_".$date;
713 $source->{new_snap
} = $snap_name;
715 my $path = "$source->{all}\@$snap_name";
718 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
719 push @$cmd, 'zfs', 'snapshot', $path;
725 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
733 my $text = "SHELL=/bin/sh\n";
734 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
736 my $fh = IO
::File-
>new("> $CRONJOBS");
737 die "Could not open file: $!\n" if !$fh;
739 foreach my $source (sort keys%{$cfg}) {
740 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
741 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
742 $text .= "$PROG_PATH sync";
743 $text .= " -source ";
744 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
745 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
746 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
748 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
749 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
750 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
753 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
754 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
755 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
756 $text .= " -name $sync_name ";
757 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
758 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
762 die "Can't write to cron\n" if (!print($fh $text));
767 my ($target, $user) = @_;
770 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
772 if ($target->{vm_type
} eq 'qemu') {
773 push @$cmd, 'qm', 'config', $target->{vmid
};
774 } elsif ($target->{vm_type
} eq 'lxc') {
775 push @$cmd, 'pct', 'config', $target->{vmid
};
777 die "VM Type unknown\n";
780 my $res = run_cmd
($cmd);
782 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
789 print "Start CMD\n" if $DEBUG;
790 print Dumper
$cmd if $DEBUG;
791 if (ref($cmd) eq 'ARRAY') {
792 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
794 my $output = `$cmd 2>&1`;
796 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
799 print Dumper
$output if $DEBUG;
800 print "END CMD\n" if $DEBUG;
805 my ($text, $ip, $vm_type, $user) = @_;
810 while ($text && $text =~ s/^(.*?)(\n|$)//) {
813 next if $line =~ /media=cdrom/;
814 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
816 #QEMU if backup is not set include in sync
817 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
819 #LXC if backup is not set do no in sync
820 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
824 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
825 my @parameter = split(/,/,$1);
827 foreach my $opt (@parameter) {
828 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
835 if (!defined($disk) || !defined($stor)) {
836 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
841 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
842 push @$cmd, 'pvesm', 'path', "$stor$disk";
843 my $path = run_cmd($cmd);
845 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
847 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
849 my @array = split('/', $1);
850 $disks->{$num}->{pool} = shift(@array);
851 $disks->{$num}->{all} = $disks->{$num}->{pool};
853 $disks->{$num}->{path} = join('/', @array);
854 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
856 $disks->{$num}->{last_part} = $disk;
857 $disks->{$num}->{all} .= "\
/$disk";
860 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
862 $disks->{$num}->{pool} = $1;
863 $disks->{$num}->{all} = $disks->{$num}->{pool};
866 $disks->{$num}->{path} = $3;
867 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
870 $disks->{$num}->{last_part} = $disk;
871 $disks->{$num}->{all} .= "\
/$disk";
876 die "ERROR
: in path
\n";
880 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
884 sub snapshot_destroy {
885 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
887 my @zfscmd = ('zfs', 'destroy');
888 my $snapshot = "$source->{all
}\
@$snap";
891 if($source->{ip} && $method eq 'ssh'){
892 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
894 run_cmd([@zfscmd, $snapshot]);
901 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
903 my $path = "$dest->{all
}";
904 $path .= "/$source->{last_part
}" if $source->{last_part};
907 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
916 my ($source , $dest, $method, $dest_user) = @_;
919 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
920 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
922 my $path = $dest->{all};
923 $path .= "/$source->{last_part
}" if $source->{last_part};
924 $path .= "\
@$source->{old_snap
}";
930 eval {$text =run_cmd($cmd);};
936 while ($text && $text =~ s/^(.*?)(\n|$)//) {
938 return 1 if $line =~ m/^.*$source->{old_snap}$/;
943 my ($source, $dest, $param) = @_;
947 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
948 push @$cmd, 'zfs', 'send';
949 push @$cmd, '-p', if $param->{properties};
950 push @$cmd, '-v' if $param->{verbose};
952 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
953 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
955 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
957 if ($param->{limit}){
958 my $bwl = $param->{limit}*1024;
959 push @$cmd, \'|', 'cstream', '-t', $bwl;
961 my $target = "$dest->{all
}";
962 $target .= "/$source->{last_part
}" if $source->{last_part};
966 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
967 push @$cmd, 'zfs', 'recv', '-F', '--';
968 push @$cmd, "$target";
975 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
982 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
984 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
985 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
987 my $config_dir = $dest_config_path // $CONFIG_PATH;
988 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
990 $dest_target_new = $config_dir.'/'.$dest_target_new;
992 if ($method eq 'ssh'){
993 if ($dest->{ip} && $source->{ip}) {
994 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
995 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
996 } elsif ($dest->{ip}) {
997 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
998 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
999 } elsif ($source->{ip}) {
1000 run_cmd(['mkdir', '-p', '--', $config_dir]);
1001 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
1004 if ($source->{destroy}){
1005 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
1007 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
1009 run_cmd(['rm', '-f', '--', $dest_target_old]);
1012 } elsif ($method eq 'local') {
1013 run_cmd(['mkdir', '-p', '--', $config_dir]);
1014 run_cmd(['cp', $source_target, $dest_target_new]);
1019 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1020 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1026 my $cfg = read_cron();
1028 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1030 my $states = read_state();
1032 foreach my $source (sort keys%{$cfg}) {
1033 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1034 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1035 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1036 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1040 return $status_list;
1046 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1047 my $job = get_job($param);
1048 $job->{state} = "ok
";
1057 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1058 my $job = get_job($param);
1059 $job->{state} = "stopped
";
1067 $PROGNAME destroy -source <string> [OPTIONS]
1069 remove a sync Job from the scheduler
1073 name of the sync job, if not set it is default
1077 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1080 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1086 the destination target is like [IP]:<Pool>[/Path]
1090 name of the user on the destination target, root by default
1094 max sync speed in kBytes/s, default unlimited
1098 how much snapshots will be kept before get erased, default 1
1102 name of the sync job, if not set it is default
1106 if this flag is set it will skip the first sync
1110 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1114 name of the user on the source target, root by default
1118 Include the dataset's properties in the stream.
1120 -dest-config-path string
1122 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1125 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1131 the destination target is like [IP:]<Pool>[/Path]
1135 name of the user on the destination target, root by default
1139 max sync speed in kBytes/s, default unlimited
1143 how much snapshots will be kept before get erased, default 1
1147 name of the sync job, if not set it is default.
1148 It is only necessary if scheduler allready contains this source.
1152 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1156 name of the user on the source target, root by default
1160 print out the sync progress.
1164 Include the dataset's properties in the stream.
1166 -dest-config-path string
1168 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1173 Get a List of all scheduled Sync Jobs
1178 Get the status of all scheduled Sync Jobs
1181 $PROGNAME help <cmd> [OPTIONS]
1183 Get help about specified command.
1191 Verbose output format.
1194 $PROGNAME enable -source <string> [OPTIONS]
1196 enable a syncjob and reset error
1200 name of the sync job, if not set it is default
1204 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1207 $PROGNAME disable -source <string> [OPTIONS]
1213 name of the sync job, if not set it is default
1217 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1219 printpod
=> 'internal command',
1225 } elsif (!$cmd_help->{$command}) {
1226 print "ERROR: unknown command '$command'";
1231 my $param = parse_argv
(@arg);
1235 die "$cmd_help->{$command}\n" if !$param->{$_};
1239 if ($command eq 'destroy') {
1240 check_params
(qw(source));
1242 check_target
($param->{source
});
1243 destroy_job
($param);
1245 } elsif ($command eq 'sync') {
1246 check_params
(qw(source dest));
1248 check_target
($param->{source
});
1249 check_target
($param->{dest
});
1252 } elsif ($command eq 'create') {
1253 check_params
(qw(source dest));
1255 check_target
($param->{source
});
1256 check_target
($param->{dest
});
1259 } elsif ($command eq 'status') {
1262 } elsif ($command eq 'list') {
1265 } elsif ($command eq 'help') {
1266 my $help_command = $ARGV[1];
1268 if ($help_command && $cmd_help->{$help_command}) {
1269 die "$cmd_help->{$help_command}\n";
1272 if ($param->{verbose
}) {
1273 exec("man $PROGNAME");
1280 } elsif ($command eq 'enable') {
1281 check_params
(qw(source));
1283 check_target
($param->{source
});
1286 } elsif ($command eq 'disable') {
1287 check_params
(qw(source));
1289 check_target
($param->{source
});
1290 disable_job
($param);
1292 } elsif ($command eq 'printpod') {
1299 print("ERROR:\tno command specified\n") if !$help;
1300 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1301 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1302 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1303 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1304 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1305 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1306 print("\t$PROGNAME list\n");
1307 print("\t$PROGNAME status\n");
1308 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1313 parse_target
($target);
1318 my $synopsis = join("\n", sort values %$cmd_help);
1323 pve-zsync - PVE ZFS Replication Manager
1327 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1333 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1334 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1335 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.
1336 To config cron see man crontab.
1338 =head2 PVE ZFS Storage sync Tool
1340 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1344 add sync job from local VM to remote ZFS Server
1345 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1347 =head1 IMPORTANT FILES
1349 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1351 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1353 =head1 COPYRIGHT AND DISCLAIMER
1355 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1357 This program is free software: you can redistribute it and/or modify it
1358 under the terms of the GNU Affero General Public License as published
1359 by the Free Software Foundation, either version 3 of the License, or
1360 (at your option) any later version.
1362 This program is distributed in the hope that it will be useful, but
1363 WITHOUT ANY WARRANTY; without even the implied warranty of
1364 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1365 Affero General Public License for more details.
1367 You should have received a copy of the GNU Affero General Public
1368 License along with this program. If not, see
1369 <http://www.gnu.org/licenses/>.