]>
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";
85 my ($filename, $one_line_only) = @_;
87 my $fh = IO
::File-
>new($filename, "r")
88 or die "Could not open file ${filename}: $!\n";
90 my $text = $one_line_only ?
<$fh> : [ <$fh> ];
97 sub cut_target_width
{
98 my ($path, $maxlen) = @_;
101 return $path if length($path) <= $maxlen;
103 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
105 $path =~ s
@/([^/]+/?
)$@@;
108 if (length($tail)+3 == $maxlen) {
110 } elsif (length($tail)+2 >= $maxlen) {
111 return '..'.substr($tail, -$maxlen+2)
114 $path =~ s
@(/[^/]+)(?
:/|$)@@;
116 my $both = length($head) + length($tail);
117 my $remaining = $maxlen-$both-4; # -4 for "/../"
119 if ($remaining < 0) {
120 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
123 substr($path, ($remaining/2), (length($path)-$remaining), '..');
124 return "$head/" . $path . "/$tail";
128 my ($lock_fn, $code) = @_;
130 my $lock_fh = IO
::File-
>new("> $lock_fn");
132 flock($lock_fh, LOCK_EX
) || die "Couldn't acquire lock - $!\n";
133 my $res = eval { $code->() };
136 flock($lock_fh, LOCK_UN
) || warn "Error unlocking - $!\n";
144 my ($source, $name, $status) = @_;
146 if ($status->{$source->{all
}}->{$name}->{status
}) {
153 sub check_pool_exists
{
154 my ($target, $user) = @_;
159 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
161 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
175 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
178 if ($text !~ $TARGETRE) {
182 $target->{ip
} = $1 if $1;
183 my @parts = split('/', $2);
185 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
187 my $pool = $target->{pool
} = shift(@parts);
188 die "$errstr\n" if !$pool;
190 if ($pool =~ m/^\d+$/) {
191 $target->{vmid
} = $pool;
192 delete $target->{pool
};
195 return $target if (@parts == 0);
196 $target->{last_part
} = pop(@parts);
202 $target->{path
} = join('/', @parts);
210 #This is for the first use to init file;
212 my $new_fh = IO
::File-
>new("> $CRONJOBS");
213 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
218 my $text = read_file
($CRONJOBS, 0);
220 return encode_cron
(@{$text});
235 source_user
=> undef,
238 dest_config_path
=> undef,
241 my ($ret) = GetOptionsFromArray
(
243 'dest=s' => \
$param->{dest
},
244 'source=s' => \
$param->{source
},
245 'verbose' => \
$param->{verbose
},
246 'limit=i' => \
$param->{limit
},
247 'maxsnap=i' => \
$param->{maxsnap
},
248 'name=s' => \
$param->{name
},
249 'skip' => \
$param->{skip
},
250 'method=s' => \
$param->{method},
251 'source-user=s' => \
$param->{source_user
},
252 'dest-user=s' => \
$param->{dest_user
},
253 'properties' => \
$param->{properties
},
254 'dest-config-path=s' => \
$param->{dest_config_path
},
257 die "can't parse options\n" if $ret == 0;
259 $param->{name
} //= "default";
260 $param->{maxsnap
} //= 1;
261 $param->{method} //= "ssh";
262 $param->{source_user
} //= "root";
263 $param->{dest_user
} //= "root";
268 sub add_state_to_job
{
271 my $states = read_state
();
272 my $state = $states->{$job->{source
}}->{$job->{name
}};
274 $job->{state} = $state->{state};
275 $job->{lsync
} = $state->{lsync
};
276 $job->{vm_type
} = $state->{vm_type
};
278 for (my $i = 0; $state->{"snap$i"}; $i++) {
279 $job->{"snap$i"} = $state->{"snap$i"};
290 while (my $line = shift(@text)) {
292 my @arg = split('\s', $line);
293 my $param = parse_argv
(@arg);
295 if ($param->{source
} && $param->{dest
}) {
296 my $source = delete $param->{source
};
297 my $name = delete $param->{name
};
299 $cfg->{$source}->{$name} = $param;
311 my $source = parse_target
($param->{source
});
312 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
314 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
315 $job->{dest
} = $param->{dest
} if $param->{dest
};
316 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
317 $job->{method} = "ssh" if !$job->{method};
318 $job->{limit
} = $param->{limit
};
319 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
320 $job->{source
} = $param->{source
};
321 $job->{source_user
} = $param->{source_user
};
322 $job->{dest_user
} = $param->{dest_user
};
323 $job->{properties
} = !!$param->{properties
};
324 $job->{dest_config_path
} = $param->{dest_config_path
} if $param->{dest_config_path
};
332 make_path
$CONFIG_PATH;
333 my $new_fh = IO
::File-
>new("> $STATE");
334 die "Could not create $STATE: $!\n" if !$new_fh;
340 my $text = read_file
($STATE, 1);
341 return decode_json
($text);
347 my $text = eval { read_file
($STATE, 1); };
349 my $out_fh = IO
::File-
>new("> $STATE.new");
350 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
355 $states = decode_json
($text);
356 $state = $states->{$job->{source
}}->{$job->{name
}};
359 if ($job->{state} ne "del") {
360 $state->{state} = $job->{state};
361 $state->{lsync
} = $job->{lsync
};
362 $state->{vm_type
} = $job->{vm_type
};
364 for (my $i = 0; $job->{"snap$i"} ; $i++) {
365 $state->{"snap$i"} = $job->{"snap$i"};
367 $states->{$job->{source
}}->{$job->{name
}} = $state;
370 delete $states->{$job->{source
}}->{$job->{name
}};
371 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
374 $text = encode_json
($states);
378 rename "$STATE.new", $STATE;
390 my $header = "SHELL=/bin/sh\n";
391 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
393 my $current = read_file
($CRONJOBS, 0);
395 foreach my $line (@{$current}) {
397 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
399 next if $job->{state} eq "del";
400 $text .= format_job
($job, $line);
402 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
411 $text = "$header$text";
415 $text .= format_job
($job);
417 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
418 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
420 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
423 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
427 my ($job, $line) = @_;
430 if ($job->{state} eq "stopped") {
434 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
437 $text .= "*/$INTERVAL * * * *";
440 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
441 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
442 $text .= " --limit $job->{limit}" if $job->{limit
};
443 $text .= " --method $job->{method}";
444 $text .= " --verbose" if $job->{verbose
};
445 $text .= " --source-user $job->{source_user}";
446 $text .= " --dest-user $job->{dest_user}";
447 $text .= " --properties" if $job->{properties
};
448 $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path
};
456 my $cfg = read_cron
();
458 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
460 my $states = read_state
();
461 foreach my $source (sort keys%{$cfg}) {
462 foreach my $name (sort keys%{$cfg->{$source}}) {
463 $list .= sprintf("%-25s", cut_target_width
($source, 25));
464 $list .= sprintf("%-25s", cut_target_width
($name, 25));
465 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
466 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
467 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
468 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
476 my ($target, $user) = @_;
478 return undef if !defined($target->{vmid
});
480 my $conf_fn = "$target->{vmid}.conf";
483 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
484 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
485 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
487 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
488 return "lxc" if -f
"$LXC_CONF/$conf_fn";
497 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
498 my $cfg = read_cron
();
500 my $job = param_to_job
($param);
502 $job->{state} = "ok";
505 my $source = parse_target
($param->{source
});
506 my $dest = parse_target
($param->{dest
});
508 if (my $ip = $dest->{ip
}) {
509 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
512 if (my $ip = $source->{ip
}) {
513 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
516 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
518 if (!defined($source->{vmid
})) {
519 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
522 my $vm_type = vm_exists
($source, $param->{source_user
});
523 $job->{vm_type
} = $vm_type;
524 $source->{vm_type
} = $vm_type;
526 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
528 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
530 #check if vm has zfs disks if not die;
531 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
535 }); #cron and state lock
537 return if $param->{skip
};
539 eval { sync
($param) };
549 my $cfg = read_cron
();
551 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
552 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
554 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
555 $job->{name
} = $param->{name
};
556 $job->{source
} = $param->{source
};
557 $job = add_state_to_job
($job);
565 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
566 my $job = get_job
($param);
567 $job->{state} = "del";
579 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
580 eval { $job = get_job
($param) };
583 if (defined($job->{state}) && ($job->{state} eq "syncing" || $job->{state} eq "waiting")) {
584 die "Job --source $param->{source} --name $param->{name} is already scheduled to sync\n";
587 $job->{state} = "waiting";
592 locked
("$CONFIG_PATH/sync.lock", sub {
594 my $date = get_date
();
600 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
601 #job might've changed while we waited for the sync lock, but we can be sure it's not syncing
602 eval { $job = get_job
($param); };
604 if ($job && defined($job->{state}) && $job->{state} eq "stopped") {
605 die "Job --source $param->{source} --name $param->{name} has been disabled\n";
608 $dest = parse_target
($param->{dest
});
609 $source = parse_target
($param->{source
});
611 $vm_type = vm_exists
($source, $param->{source_user
});
612 $source->{vm_type
} = $vm_type;
615 $job->{state} = "syncing";
616 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
619 }); #cron and state lock
621 my $sync_path = sub {
622 my ($source, $dest, $job, $param, $date) = @_;
624 ($dest->{old_snap
}, $dest->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{dest_user
});
626 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
628 send_image
($source, $dest, $param);
630 snapshot_destroy
($source, $dest, $param->{method}, $dest->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $dest->{old_snap
});
635 if ($source->{vmid
}) {
636 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
637 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
638 my $disks = get_disks
($source, $param->{source_user
});
640 foreach my $disk (sort keys %{$disks}) {
641 $source->{all
} = $disks->{$disk}->{all
};
642 $source->{pool
} = $disks->{$disk}->{pool
};
643 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
644 $source->{last_part
} = $disks->{$disk}->{last_part
};
645 &$sync_path($source, $dest, $job, $param, $date);
647 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
648 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
650 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
653 &$sync_path($source, $dest, $job, $param, $date);
657 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
658 eval { $job = get_job
($param); };
660 $job->{state} = "error";
664 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
668 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
669 eval { $job = get_job
($param); };
671 if (defined($job->{state}) && $job->{state} eq "stopped") {
672 $job->{state} = "stopped";
674 $job->{state} = "ok";
676 $job->{lsync
} = $date;
684 my ($source, $dest, $max_snap, $name, $dest_user) = @_;
687 push @$cmd, 'ssh', "$dest_user\@$dest->{ip}", '--', if $dest->{ip
};
688 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
690 my $path = $dest->{all
};
691 $path .= "/$source->{last_part}" if $source->{last_part
};
695 eval {$raw = run_cmd
($cmd)};
696 if (my $erro =$@) { #this means the volume doesn't exist on dest yet
702 my $last_snap = undef;
705 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
707 if ($line =~ m/@(.*)$/) {
708 $last_snap = $1 if (!$last_snap);
710 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
713 if ($index == $max_snap) {
714 $source->{destroy
} = 1;
720 return ($old_snap, $last_snap) if $last_snap;
726 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
728 my $snap_name = "rep_$name\_".$date;
730 $source->{new_snap
} = $snap_name;
732 my $path = "$source->{all}\@$snap_name";
735 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
736 push @$cmd, 'zfs', 'snapshot', $path;
742 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
748 my ($target, $user) = @_;
751 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
753 if ($target->{vm_type
} eq 'qemu') {
754 push @$cmd, 'qm', 'config', $target->{vmid
};
755 } elsif ($target->{vm_type
} eq 'lxc') {
756 push @$cmd, 'pct', 'config', $target->{vmid
};
758 die "VM Type unknown\n";
761 my $res = run_cmd
($cmd);
763 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
770 print "Start CMD\n" if $DEBUG;
771 print Dumper
$cmd if $DEBUG;
772 if (ref($cmd) eq 'ARRAY') {
773 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
775 my $output = `$cmd 2>&1`;
777 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
780 print Dumper
$output if $DEBUG;
781 print "END CMD\n" if $DEBUG;
786 my ($text, $ip, $vm_type, $user) = @_;
791 while ($text && $text =~ s/^(.*?)(\n|$)//) {
794 next if $line =~ /media=cdrom/;
795 next if $line !~ m/$DISK_KEY_RE/;
797 #QEMU if backup is not set include in sync
798 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
800 #LXC if backup is not set do no in sync
801 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
805 if($line =~ m/$DISK_KEY_RE(.*)$/) {
806 my @parameter = split(/,/,$1);
808 foreach my $opt (@parameter) {
809 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
816 if (!defined($disk) || !defined($stor)) {
817 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
822 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
823 push @$cmd, 'pvesm', 'path', "$stor$disk";
824 my $path = run_cmd($cmd);
826 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
828 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
830 my @array = split('/', $1);
831 $disks->{$num}->{pool} = shift(@array);
832 $disks->{$num}->{all} = $disks->{$num}->{pool};
834 $disks->{$num}->{path} = join('/', @array);
835 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
837 $disks->{$num}->{last_part} = $disk;
838 $disks->{$num}->{all} .= "\
/$disk";
841 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
843 $disks->{$num}->{pool} = $1;
844 $disks->{$num}->{all} = $disks->{$num}->{pool};
847 $disks->{$num}->{path} = $3;
848 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
851 $disks->{$num}->{last_part} = $disk;
852 $disks->{$num}->{all} .= "\
/$disk";
857 die "ERROR
: in path
\n";
861 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
865 sub snapshot_destroy {
866 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
868 my @zfscmd = ('zfs', 'destroy');
869 my $snapshot = "$source->{all
}\
@$snap";
872 if($source->{ip} && $method eq 'ssh'){
873 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
875 run_cmd([@zfscmd, $snapshot]);
882 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
884 my $path = "$dest->{all
}";
885 $path .= "/$source->{last_part
}" if $source->{last_part};
888 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
896 # check if snapshot for incremental sync exist on source side
898 my ($source , $dest, $method, $source_user) = @_;
901 push @$cmd, 'ssh', "$source_user\@$source->{ip
}", '--' if $source->{ip};
902 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
904 my $path = $source->{all};
905 $path .= "\
@$dest->{last_snap
}";
909 eval {run_cmd($cmd)};
918 my ($source, $dest, $param) = @_;
922 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
923 push @$cmd, 'zfs', 'send';
924 push @$cmd, '-p', if $param->{properties};
925 push @$cmd, '-v' if $param->{verbose};
927 if($dest->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
928 push @$cmd, '-i', "$source->{all
}\
@$dest->{last_snap
}";
930 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
932 if ($param->{limit}){
933 my $bwl = $param->{limit}*1024;
934 push @$cmd, \'|', 'cstream', '-t', $bwl;
936 my $target = "$dest->{all
}";
937 $target .= "/$source->{last_part
}" if $source->{last_part};
941 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
942 push @$cmd, 'zfs', 'recv', '-F', '--';
943 push @$cmd, "$target";
950 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
957 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
959 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
960 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
962 my $config_dir = $dest_config_path // $CONFIG_PATH;
963 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
965 $dest_target_new = $config_dir.'/'.$dest_target_new;
967 if ($method eq 'ssh'){
968 if ($dest->{ip} && $source->{ip}) {
969 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
970 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
971 } elsif ($dest->{ip}) {
972 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
973 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
974 } elsif ($source->{ip}) {
975 run_cmd(['mkdir', '-p', '--', $config_dir]);
976 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
979 if ($source->{destroy}){
980 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$dest->{old_snap
}";
982 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
984 run_cmd(['rm', '-f', '--', $dest_target_old]);
987 } elsif ($method eq 'local') {
988 run_cmd(['mkdir', '-p', '--', $config_dir]);
989 run_cmd(['cp', $source_target, $dest_target_new]);
994 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
995 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1001 my $cfg = read_cron();
1003 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1005 my $states = read_state();
1007 foreach my $source (sort keys%{$cfg}) {
1008 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1009 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1010 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1011 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1015 return $status_list;
1021 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1022 my $job = get_job($param);
1023 $job->{state} = "ok
";
1032 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1033 my $job = get_job($param);
1034 $job->{state} = "stopped
";
1042 $PROGNAME destroy -source <string> [OPTIONS]
1044 remove a sync Job from the scheduler
1048 name of the sync job, if not set it is default
1052 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1055 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1061 the destination target is like [IP]:<Pool>[/Path]
1065 name of the user on the destination target, root by default
1069 max sync speed in kBytes/s, default unlimited
1073 how much snapshots will be kept before get erased, default 1
1077 name of the sync job, if not set it is default
1081 if this flag is set it will skip the first sync
1085 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1089 name of the user on the source target, root by default
1093 Include the dataset's properties in the stream.
1095 -dest-config-path string
1097 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1100 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1106 the destination target is like [IP:]<Pool>[/Path]
1110 name of the user on the destination target, root by default
1114 max sync speed in kBytes/s, default unlimited
1118 how much snapshots will be kept before get erased, default 1
1122 name of the sync job, if not set it is default.
1123 It is only necessary if scheduler allready contains this source.
1127 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1131 name of the user on the source target, root by default
1135 print out the sync progress.
1139 Include the dataset's properties in the stream.
1141 -dest-config-path string
1143 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1148 Get a List of all scheduled Sync Jobs
1153 Get the status of all scheduled Sync Jobs
1156 $PROGNAME help <cmd> [OPTIONS]
1158 Get help about specified command.
1166 Verbose output format.
1169 $PROGNAME enable -source <string> [OPTIONS]
1171 enable a syncjob and reset error
1175 name of the sync job, if not set it is default
1179 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1182 $PROGNAME disable -source <string> [OPTIONS]
1188 name of the sync job, if not set it is default
1192 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1194 printpod
=> 'internal command',
1200 } elsif (!$cmd_help->{$command}) {
1201 print "ERROR: unknown command '$command'";
1206 my $param = parse_argv
(@arg);
1210 die "$cmd_help->{$command}\n" if !$param->{$_};
1214 if ($command eq 'destroy') {
1215 check_params
(qw(source));
1217 check_target
($param->{source
});
1218 destroy_job
($param);
1220 } elsif ($command eq 'sync') {
1221 check_params
(qw(source dest));
1223 check_target
($param->{source
});
1224 check_target
($param->{dest
});
1227 } elsif ($command eq 'create') {
1228 check_params
(qw(source dest));
1230 check_target
($param->{source
});
1231 check_target
($param->{dest
});
1234 } elsif ($command eq 'status') {
1237 } elsif ($command eq 'list') {
1240 } elsif ($command eq 'help') {
1241 my $help_command = $ARGV[1];
1243 if ($help_command && $cmd_help->{$help_command}) {
1244 die "$cmd_help->{$help_command}\n";
1247 if ($param->{verbose
}) {
1248 exec("man $PROGNAME");
1255 } elsif ($command eq 'enable') {
1256 check_params
(qw(source));
1258 check_target
($param->{source
});
1261 } elsif ($command eq 'disable') {
1262 check_params
(qw(source));
1264 check_target
($param->{source
});
1265 disable_job
($param);
1267 } elsif ($command eq 'printpod') {
1274 print("ERROR:\tno command specified\n") if !$help;
1275 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1276 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1277 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1278 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1279 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1280 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1281 print("\t$PROGNAME list\n");
1282 print("\t$PROGNAME status\n");
1283 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1288 parse_target
($target);
1293 my $synopsis = join("\n", sort values %$cmd_help);
1298 pve-zsync - PVE ZFS Replication Manager
1302 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1308 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1309 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1310 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.
1311 To config cron see man crontab.
1313 =head2 PVE ZFS Storage sync Tool
1315 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1319 add sync job from local VM to remote ZFS Server
1320 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1322 =head1 IMPORTANT FILES
1324 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1326 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1328 =head1 COPYRIGHT AND DISCLAIMER
1330 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1332 This program is free software: you can redistribute it and/or modify it
1333 under the terms of the GNU Affero General Public License as published
1334 by the Free Software Foundation, either version 3 of the License, or
1335 (at your option) any later version.
1337 This program is distributed in the hope that it will be useful, but
1338 WITHOUT ANY WARRANTY; without even the implied warranty of
1339 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1340 Affero General Public License for more details.
1342 You should have received a copy of the GNU Affero General Public
1343 License along with this program. If not, see
1344 <http://www.gnu.org/licenses/>.