]>
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
();
593 $job = get_job
($param);
596 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
597 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
600 my $dest = parse_target
($param->{dest
});
601 my $source = parse_target
($param->{source
});
603 my $sync_path = sub {
604 my ($source, $dest, $job, $param, $date) = @_;
606 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
608 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
610 send_image
($source, $dest, $param);
612 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
616 my $vm_type = vm_exists
($source, $param->{source_user
});
617 $source->{vm_type
} = $vm_type;
620 $job->{state} = "syncing";
621 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
622 locked
("$CONFIG_PATH/cron_and_state.lock", sub { update_state
($job); });
626 if ($source->{vmid
}) {
627 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
628 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
629 my $disks = get_disks
($source, $param->{source_user
});
631 foreach my $disk (sort keys %{$disks}) {
632 $source->{all
} = $disks->{$disk}->{all
};
633 $source->{pool
} = $disks->{$disk}->{pool
};
634 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
635 $source->{last_part
} = $disks->{$disk}->{last_part
};
636 &$sync_path($source, $dest, $job, $param, $date);
638 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
639 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
641 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
644 &$sync_path($source, $dest, $job, $param, $date);
649 $job->{state} = "error";
650 locked
("$CONFIG_PATH/cron_and_state.lock", sub { update_state
($job); });
651 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
657 $job->{state} = "ok";
658 $job->{lsync
} = $date;
659 locked
("$CONFIG_PATH/cron_and_state.lock", sub { update_state
($job); });
665 my ($source, $dest, $max_snap, $name, $source_user) = @_;
668 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
669 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
670 push @$cmd, $source->{all
};
672 my $raw = run_cmd
($cmd);
675 my $last_snap = undef;
678 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
680 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
682 $last_snap = $1 if (!$last_snap);
685 if ($index == $max_snap) {
686 $source->{destroy
} = 1;
692 return ($old_snap, $last_snap) if $last_snap;
698 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
700 my $snap_name = "rep_$name\_".$date;
702 $source->{new_snap
} = $snap_name;
704 my $path = "$source->{all}\@$snap_name";
707 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
708 push @$cmd, 'zfs', 'snapshot', $path;
714 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
722 my $text = "SHELL=/bin/sh\n";
723 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
725 my $fh = IO
::File-
>new("> $CRONJOBS");
726 die "Could not open file: $!\n" if !$fh;
728 foreach my $source (sort keys%{$cfg}) {
729 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
730 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
731 $text .= "$PROG_PATH sync";
732 $text .= " -source ";
733 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
734 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
735 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
737 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
738 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
739 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
742 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
743 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
744 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
745 $text .= " -name $sync_name ";
746 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
747 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
751 die "Can't write to cron\n" if (!print($fh $text));
756 my ($target, $user) = @_;
759 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
761 if ($target->{vm_type
} eq 'qemu') {
762 push @$cmd, 'qm', 'config', $target->{vmid
};
763 } elsif ($target->{vm_type
} eq 'lxc') {
764 push @$cmd, 'pct', 'config', $target->{vmid
};
766 die "VM Type unknown\n";
769 my $res = run_cmd
($cmd);
771 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
778 print "Start CMD\n" if $DEBUG;
779 print Dumper
$cmd if $DEBUG;
780 if (ref($cmd) eq 'ARRAY') {
781 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
783 my $output = `$cmd 2>&1`;
785 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
788 print Dumper
$output if $DEBUG;
789 print "END CMD\n" if $DEBUG;
794 my ($text, $ip, $vm_type, $user) = @_;
799 while ($text && $text =~ s/^(.*?)(\n|$)//) {
802 next if $line =~ /media=cdrom/;
803 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
805 #QEMU if backup is not set include in sync
806 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
808 #LXC if backup is not set do no in sync
809 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
813 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
814 my @parameter = split(/,/,$1);
816 foreach my $opt (@parameter) {
817 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
824 if (!defined($disk) || !defined($stor)) {
825 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
830 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
831 push @$cmd, 'pvesm', 'path', "$stor$disk";
832 my $path = run_cmd($cmd);
834 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
836 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
838 my @array = split('/', $1);
839 $disks->{$num}->{pool} = shift(@array);
840 $disks->{$num}->{all} = $disks->{$num}->{pool};
842 $disks->{$num}->{path} = join('/', @array);
843 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
845 $disks->{$num}->{last_part} = $disk;
846 $disks->{$num}->{all} .= "\
/$disk";
849 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
851 $disks->{$num}->{pool} = $1;
852 $disks->{$num}->{all} = $disks->{$num}->{pool};
855 $disks->{$num}->{path} = $3;
856 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
859 $disks->{$num}->{last_part} = $disk;
860 $disks->{$num}->{all} .= "\
/$disk";
865 die "ERROR
: in path
\n";
869 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
873 sub snapshot_destroy {
874 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
876 my @zfscmd = ('zfs', 'destroy');
877 my $snapshot = "$source->{all
}\
@$snap";
880 if($source->{ip} && $method eq 'ssh'){
881 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
883 run_cmd([@zfscmd, $snapshot]);
890 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
892 my $path = "$dest->{all
}";
893 $path .= "/$source->{last_part
}" if $source->{last_part};
896 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
905 my ($source , $dest, $method, $dest_user) = @_;
908 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
909 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
911 my $path = $dest->{all};
912 $path .= "/$source->{last_part
}" if $source->{last_part};
913 $path .= "\
@$source->{old_snap
}";
919 eval {$text =run_cmd($cmd);};
925 while ($text && $text =~ s/^(.*?)(\n|$)//) {
927 return 1 if $line =~ m/^.*$source->{old_snap}$/;
932 my ($source, $dest, $param) = @_;
936 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
937 push @$cmd, 'zfs', 'send';
938 push @$cmd, '-p', if $param->{properties};
939 push @$cmd, '-v' if $param->{verbose};
941 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
942 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
944 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
946 if ($param->{limit}){
947 my $bwl = $param->{limit}*1024;
948 push @$cmd, \'|', 'cstream', '-t', $bwl;
950 my $target = "$dest->{all
}";
951 $target .= "/$source->{last_part
}" if $source->{last_part};
955 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
956 push @$cmd, 'zfs', 'recv', '-F', '--';
957 push @$cmd, "$target";
964 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
971 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
973 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
974 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
976 my $config_dir = $dest_config_path // $CONFIG_PATH;
977 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
979 $dest_target_new = $config_dir.'/'.$dest_target_new;
981 if ($method eq 'ssh'){
982 if ($dest->{ip} && $source->{ip}) {
983 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
984 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
985 } elsif ($dest->{ip}) {
986 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
987 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
988 } elsif ($source->{ip}) {
989 run_cmd(['mkdir', '-p', '--', $config_dir]);
990 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
993 if ($source->{destroy}){
994 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
996 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
998 run_cmd(['rm', '-f', '--', $dest_target_old]);
1001 } elsif ($method eq 'local') {
1002 run_cmd(['mkdir', '-p', '--', $config_dir]);
1003 run_cmd(['cp', $source_target, $dest_target_new]);
1008 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1009 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1015 my $cfg = read_cron();
1017 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1019 my $states = read_state();
1021 foreach my $source (sort keys%{$cfg}) {
1022 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1023 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1024 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1025 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1029 return $status_list;
1035 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1036 my $job = get_job($param);
1037 $job->{state} = "ok
";
1046 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1047 my $job = get_job($param);
1048 $job->{state} = "stopped
";
1056 $PROGNAME destroy -source <string> [OPTIONS]
1058 remove a sync Job from the scheduler
1062 name of the sync job, if not set it is default
1066 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1069 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1075 the destination target is like [IP]:<Pool>[/Path]
1079 name of the user on the destination target, root by default
1083 max sync speed in kBytes/s, default unlimited
1087 how much snapshots will be kept before get erased, default 1
1091 name of the sync job, if not set it is default
1095 if this flag is set it will skip the first sync
1099 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1103 name of the user on the source target, root by default
1107 Include the dataset's properties in the stream.
1109 -dest-config-path string
1111 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1114 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1120 the destination target is like [IP:]<Pool>[/Path]
1124 name of the user on the destination target, root by default
1128 max sync speed in kBytes/s, default unlimited
1132 how much snapshots will be kept before get erased, default 1
1136 name of the sync job, if not set it is default.
1137 It is only necessary if scheduler allready contains this source.
1141 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1145 name of the user on the source target, root by default
1149 print out the sync progress.
1153 Include the dataset's properties in the stream.
1155 -dest-config-path string
1157 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1162 Get a List of all scheduled Sync Jobs
1167 Get the status of all scheduled Sync Jobs
1170 $PROGNAME help <cmd> [OPTIONS]
1172 Get help about specified command.
1180 Verbose output format.
1183 $PROGNAME enable -source <string> [OPTIONS]
1185 enable a syncjob and reset error
1189 name of the sync job, if not set it is default
1193 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1196 $PROGNAME disable -source <string> [OPTIONS]
1202 name of the sync job, if not set it is default
1206 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1208 printpod
=> 'internal command',
1214 } elsif (!$cmd_help->{$command}) {
1215 print "ERROR: unknown command '$command'";
1220 my $param = parse_argv
(@arg);
1224 die "$cmd_help->{$command}\n" if !$param->{$_};
1228 if ($command eq 'destroy') {
1229 check_params
(qw(source));
1231 check_target
($param->{source
});
1232 destroy_job
($param);
1234 } elsif ($command eq 'sync') {
1235 check_params
(qw(source dest));
1237 check_target
($param->{source
});
1238 check_target
($param->{dest
});
1241 } elsif ($command eq 'create') {
1242 check_params
(qw(source dest));
1244 check_target
($param->{source
});
1245 check_target
($param->{dest
});
1248 } elsif ($command eq 'status') {
1251 } elsif ($command eq 'list') {
1254 } elsif ($command eq 'help') {
1255 my $help_command = $ARGV[1];
1257 if ($help_command && $cmd_help->{$help_command}) {
1258 die "$cmd_help->{$help_command}\n";
1261 if ($param->{verbose
}) {
1262 exec("man $PROGNAME");
1269 } elsif ($command eq 'enable') {
1270 check_params
(qw(source));
1272 check_target
($param->{source
});
1275 } elsif ($command eq 'disable') {
1276 check_params
(qw(source));
1278 check_target
($param->{source
});
1279 disable_job
($param);
1281 } elsif ($command eq 'printpod') {
1288 print("ERROR:\tno command specified\n") if !$help;
1289 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1290 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1291 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1292 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1293 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1294 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1295 print("\t$PROGNAME list\n");
1296 print("\t$PROGNAME status\n");
1297 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1302 parse_target
($target);
1307 my $synopsis = join("\n", sort values %$cmd_help);
1312 pve-zsync - PVE ZFS Replication Manager
1316 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1322 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1323 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1324 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.
1325 To config cron see man crontab.
1327 =head2 PVE ZFS Storage sync Tool
1329 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1333 add sync job from local VM to remote ZFS Server
1334 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1336 =head1 IMPORTANT FILES
1338 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1340 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1342 =head1 COPYRIGHT AND DISCLAIMER
1344 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1346 This program is free software: you can redistribute it and/or modify it
1347 under the terms of the GNU Affero General Public License as published
1348 by the Free Software Foundation, either version 3 of the License, or
1349 (at your option) any later version.
1351 This program is distributed in the hope that it will be useful, but
1352 WITHOUT ANY WARRANTY; without even the implied warranty of
1353 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1354 Affero General Public License for more details.
1356 You should have received a copy of the GNU Affero General Public
1357 License along with this program. If not, see
1358 <http://www.gnu.org/licenses/>.