]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
6 use Fcntl
qw(:flock SEEK_END);
7 use Getopt
::Long
qw(GetOptionsFromArray);
8 use File
::Path
qw(make_path);
11 use String
::ShellQuote
'shell_quote';
13 my $PROGNAME = "pve-zsync";
14 my $CONFIG_PATH = "/var/lib/${PROGNAME}";
15 my $STATE = "${CONFIG_PATH}/sync_state";
16 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
17 my $PATH = "/usr/sbin";
18 my $PVE_DIR = "/etc/pve/local";
19 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
20 my $LXC_CONF = "${PVE_DIR}/lxc";
21 my $PROG_PATH = "$PATH/${PROGNAME}";
26 $DEBUG = 0; # change default here. not above on declaration!
27 $DEBUG ||= $ENV{ZSYNC_DEBUG
};
30 Data
::Dumper-
>import();
34 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
35 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
36 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
37 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
40 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
41 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
42 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
43 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
44 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
45 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
46 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
47 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
48 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
50 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
51 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
52 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
53 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
54 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
56 my $DISK_KEY_RE = qr/^(?:(?:(?:virtio|ide|scsi|sata|efidisk|mp)\d+)|rootfs): /;
58 my $command = $ARGV[0];
60 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
61 check_bin
('cstream');
67 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} = sub {
68 die "Signaled, aborting sync: $!\n";
74 foreach my $p (split (/:/, $ENV{PATH
})) {
81 die "unable to find command '$bin'\n";
84 sub cut_target_width
{
85 my ($path, $maxlen) = @_;
88 return $path if length($path) <= $maxlen;
90 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
92 $path =~ s
@/([^/]+/?
)$@@;
95 if (length($tail)+3 == $maxlen) {
97 } elsif (length($tail)+2 >= $maxlen) {
98 return '..'.substr($tail, -$maxlen+2)
101 $path =~ s
@(/[^/]+)(?
:/|$)@@;
103 my $both = length($head) + length($tail);
104 my $remaining = $maxlen-$both-4; # -4 for "/../"
106 if ($remaining < 0) {
107 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
110 substr($path, ($remaining/2), (length($path)-$remaining), '..');
111 return "$head/" . $path . "/$tail";
115 my ($lock_fn, $code) = @_;
117 my $lock_fh = IO
::File-
>new("> $lock_fn");
119 flock($lock_fh, LOCK_EX
) || die "Couldn't acquire lock - $!\n";
120 my $res = eval { $code->() };
123 flock($lock_fh, LOCK_UN
) || warn "Error unlocking - $!\n";
131 my ($source, $name, $status) = @_;
133 if ($status->{$source->{all
}}->{$name}->{status
}) {
140 sub check_pool_exists
{
141 my ($target, $user) = @_;
146 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
148 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
162 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
165 if ($text !~ $TARGETRE) {
169 $target->{ip
} = $1 if $1;
170 my @parts = split('/', $2);
172 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
174 my $pool = $target->{pool
} = shift(@parts);
175 die "$errstr\n" if !$pool;
177 if ($pool =~ m/^\d+$/) {
178 $target->{vmid
} = $pool;
179 delete $target->{pool
};
182 return $target if (@parts == 0);
183 $target->{last_part
} = pop(@parts);
189 $target->{path
} = join('/', @parts);
197 #This is for the first use to init file;
199 my $new_fh = IO
::File-
>new("> $CRONJOBS");
200 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
205 my $fh = IO
::File-
>new("< $CRONJOBS");
206 die "Could not open file $CRONJOBS: $!\n" if !$fh;
212 return encode_cron
(@text);
227 source_user
=> undef,
230 dest_config_path
=> undef,
233 my ($ret) = GetOptionsFromArray
(
235 'dest=s' => \
$param->{dest
},
236 'source=s' => \
$param->{source
},
237 'verbose' => \
$param->{verbose
},
238 'limit=i' => \
$param->{limit
},
239 'maxsnap=i' => \
$param->{maxsnap
},
240 'name=s' => \
$param->{name
},
241 'skip' => \
$param->{skip
},
242 'method=s' => \
$param->{method},
243 'source-user=s' => \
$param->{source_user
},
244 'dest-user=s' => \
$param->{dest_user
},
245 'properties' => \
$param->{properties
},
246 'dest-config-path=s' => \
$param->{dest_config_path
},
249 die "can't parse options\n" if $ret == 0;
251 $param->{name
} //= "default";
252 $param->{maxsnap
} //= 1;
253 $param->{method} //= "ssh";
254 $param->{source_user
} //= "root";
255 $param->{dest_user
} //= "root";
260 sub add_state_to_job
{
263 my $states = read_state
();
264 my $state = $states->{$job->{source
}}->{$job->{name
}};
266 $job->{state} = $state->{state};
267 $job->{lsync
} = $state->{lsync
};
268 $job->{vm_type
} = $state->{vm_type
};
270 for (my $i = 0; $state->{"snap$i"}; $i++) {
271 $job->{"snap$i"} = $state->{"snap$i"};
282 while (my $line = shift(@text)) {
284 my @arg = split('\s', $line);
285 my $param = parse_argv
(@arg);
287 if ($param->{source
} && $param->{dest
}) {
288 my $source = delete $param->{source
};
289 my $name = delete $param->{name
};
291 $cfg->{$source}->{$name} = $param;
303 my $source = parse_target
($param->{source
});
304 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
306 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
307 $job->{dest
} = $param->{dest
} if $param->{dest
};
308 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
309 $job->{method} = "ssh" if !$job->{method};
310 $job->{limit
} = $param->{limit
};
311 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
312 $job->{source
} = $param->{source
};
313 $job->{source_user
} = $param->{source_user
};
314 $job->{dest_user
} = $param->{dest_user
};
315 $job->{properties
} = !!$param->{properties
};
316 $job->{dest_config_path
} = $param->{dest_config_path
} if $param->{dest_config_path
};
324 make_path
$CONFIG_PATH;
325 my $new_fh = IO
::File-
>new("> $STATE");
326 die "Could not create $STATE: $!\n" if !$new_fh;
332 my $fh = IO
::File-
>new("< $STATE");
333 die "Could not open file $STATE: $!\n" if !$fh;
336 my $states = decode_json
($text);
350 $in_fh = IO
::File-
>new("< $STATE");
351 die "Could not open file $STATE: $!\n" if !$in_fh;
355 my $out_fh = IO
::File-
>new("> $STATE.new");
356 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
361 $states = decode_json
($text);
362 $state = $states->{$job->{source
}}->{$job->{name
}};
365 if ($job->{state} ne "del") {
366 $state->{state} = $job->{state};
367 $state->{lsync
} = $job->{lsync
};
368 $state->{vm_type
} = $job->{vm_type
};
370 for (my $i = 0; $job->{"snap$i"} ; $i++) {
371 $state->{"snap$i"} = $job->{"snap$i"};
373 $states->{$job->{source
}}->{$job->{name
}} = $state;
376 delete $states->{$job->{source
}}->{$job->{name
}};
377 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
380 $text = encode_json
($states);
384 rename "$STATE.new", $STATE;
399 my $header = "SHELL=/bin/sh\n";
400 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
402 my $fh = IO
::File-
>new("< $CRONJOBS");
403 die "Could not open file $CRONJOBS: $!\n" if !$fh;
407 while (my $line = shift(@test)) {
409 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
411 next if $job->{state} eq "del";
412 $text .= format_job
($job, $line);
414 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
423 $text = "$header$text";
427 $text .= format_job
($job);
429 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
430 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
432 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
435 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
440 my ($job, $line) = @_;
443 if ($job->{state} eq "stopped") {
447 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
450 $text .= "*/$INTERVAL * * * *";
453 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
454 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
455 $text .= " --limit $job->{limit}" if $job->{limit
};
456 $text .= " --method $job->{method}";
457 $text .= " --verbose" if $job->{verbose
};
458 $text .= " --source-user $job->{source_user}";
459 $text .= " --dest-user $job->{dest_user}";
460 $text .= " --properties" if $job->{properties
};
461 $text .= " --dest-config-path $job->{dest_config_path}" if $job->{dest_config_path
};
469 my $cfg = read_cron
();
471 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
473 my $states = read_state
();
474 foreach my $source (sort keys%{$cfg}) {
475 foreach my $name (sort keys%{$cfg->{$source}}) {
476 $list .= sprintf("%-25s", cut_target_width
($source, 25));
477 $list .= sprintf("%-25s", cut_target_width
($name, 25));
478 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
479 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
480 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
481 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
489 my ($target, $user) = @_;
491 return undef if !defined($target->{vmid
});
493 my $conf_fn = "$target->{vmid}.conf";
496 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
497 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
498 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
500 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
501 return "lxc" if -f
"$LXC_CONF/$conf_fn";
510 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
511 my $cfg = read_cron
();
513 my $job = param_to_job
($param);
515 $job->{state} = "ok";
518 my $source = parse_target
($param->{source
});
519 my $dest = parse_target
($param->{dest
});
521 if (my $ip = $dest->{ip
}) {
522 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
525 if (my $ip = $source->{ip
}) {
526 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
529 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
531 if (!defined($source->{vmid
})) {
532 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
535 my $vm_type = vm_exists
($source, $param->{source_user
});
536 $job->{vm_type
} = $vm_type;
537 $source->{vm_type
} = $vm_type;
539 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
541 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
543 #check if vm has zfs disks if not die;
544 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
548 }); #cron and state lock
550 return if $param->{skip
};
552 eval { sync
($param) };
562 my $cfg = read_cron
();
564 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
565 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
567 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
568 $job->{name
} = $param->{name
};
569 $job->{source
} = $param->{source
};
570 $job = add_state_to_job
($job);
578 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
579 my $job = get_job
($param);
580 $job->{state} = "del";
592 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
593 eval { $job = get_job
($param) };
596 if (defined($job->{state}) && ($job->{state} eq "syncing" || $job->{state} eq "waiting")) {
597 die "Job --source $param->{source} --name $param->{name} is already scheduled to sync\n";
600 $job->{state} = "waiting";
605 locked
("$CONFIG_PATH/sync.lock", sub {
607 my $date = get_date
();
613 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
614 #job might've changed while we waited for the sync lock, but we can be sure it's not syncing
615 eval { $job = get_job
($param); };
617 if ($job && defined($job->{state}) && $job->{state} eq "stopped") {
618 die "Job --source $param->{source} --name $param->{name} has been disabled\n";
621 $dest = parse_target
($param->{dest
});
622 $source = parse_target
($param->{source
});
624 $vm_type = vm_exists
($source, $param->{source_user
});
625 $source->{vm_type
} = $vm_type;
628 $job->{state} = "syncing";
629 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
632 }); #cron and state lock
634 my $sync_path = sub {
635 my ($source, $dest, $job, $param, $date) = @_;
637 ($dest->{old_snap
}, $dest->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{dest_user
});
639 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
641 send_image
($source, $dest, $param);
643 snapshot_destroy
($source, $dest, $param->{method}, $dest->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $dest->{old_snap
});
648 if ($source->{vmid
}) {
649 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
650 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
651 my $disks = get_disks
($source, $param->{source_user
});
653 foreach my $disk (sort keys %{$disks}) {
654 $source->{all
} = $disks->{$disk}->{all
};
655 $source->{pool
} = $disks->{$disk}->{pool
};
656 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
657 $source->{last_part
} = $disks->{$disk}->{last_part
};
658 &$sync_path($source, $dest, $job, $param, $date);
660 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
661 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
663 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
}, $param->{dest_config_path
});
666 &$sync_path($source, $dest, $job, $param, $date);
670 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
671 eval { $job = get_job
($param); };
673 $job->{state} = "error";
677 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
681 locked
("$CONFIG_PATH/cron_and_state.lock", sub {
682 eval { $job = get_job
($param); };
684 if (defined($job->{state}) && $job->{state} eq "stopped") {
685 $job->{state} = "stopped";
687 $job->{state} = "ok";
689 $job->{lsync
} = $date;
697 my ($source, $dest, $max_snap, $name, $dest_user) = @_;
700 push @$cmd, 'ssh', "$dest_user\@$dest->{ip}", '--', if $dest->{ip
};
701 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
703 my $path = $dest->{all
};
704 $path .= "/$source->{last_part}" if $source->{last_part
};
708 eval {$raw = run_cmd
($cmd)};
709 if (my $erro =$@) { #this means the volume doesn't exist on dest yet
715 my $last_snap = undef;
718 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
720 if ($line =~ m/@(.*)$/) {
721 $last_snap = $1 if (!$last_snap);
723 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
726 if ($index == $max_snap) {
727 $source->{destroy
} = 1;
733 return ($old_snap, $last_snap) if $last_snap;
739 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
741 my $snap_name = "rep_$name\_".$date;
743 $source->{new_snap
} = $snap_name;
745 my $path = "$source->{all}\@$snap_name";
748 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
749 push @$cmd, 'zfs', 'snapshot', $path;
755 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
761 my ($target, $user) = @_;
764 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
766 if ($target->{vm_type
} eq 'qemu') {
767 push @$cmd, 'qm', 'config', $target->{vmid
};
768 } elsif ($target->{vm_type
} eq 'lxc') {
769 push @$cmd, 'pct', 'config', $target->{vmid
};
771 die "VM Type unknown\n";
774 my $res = run_cmd
($cmd);
776 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
783 print "Start CMD\n" if $DEBUG;
784 print Dumper
$cmd if $DEBUG;
785 if (ref($cmd) eq 'ARRAY') {
786 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
788 my $output = `$cmd 2>&1`;
790 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
793 print Dumper
$output if $DEBUG;
794 print "END CMD\n" if $DEBUG;
799 my ($text, $ip, $vm_type, $user) = @_;
804 while ($text && $text =~ s/^(.*?)(\n|$)//) {
807 next if $line =~ /media=cdrom/;
808 next if $line !~ m/$DISK_KEY_RE/;
810 #QEMU if backup is not set include in sync
811 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
813 #LXC if backup is not set do no in sync
814 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
818 if($line =~ m/$DISK_KEY_RE(.*)$/) {
819 my @parameter = split(/,/,$1);
821 foreach my $opt (@parameter) {
822 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
829 if (!defined($disk) || !defined($stor)) {
830 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
835 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
836 push @$cmd, 'pvesm', 'path', "$stor$disk";
837 my $path = run_cmd($cmd);
839 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
841 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
843 my @array = split('/', $1);
844 $disks->{$num}->{pool} = shift(@array);
845 $disks->{$num}->{all} = $disks->{$num}->{pool};
847 $disks->{$num}->{path} = join('/', @array);
848 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
850 $disks->{$num}->{last_part} = $disk;
851 $disks->{$num}->{all} .= "\
/$disk";
854 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
856 $disks->{$num}->{pool} = $1;
857 $disks->{$num}->{all} = $disks->{$num}->{pool};
860 $disks->{$num}->{path} = $3;
861 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
864 $disks->{$num}->{last_part} = $disk;
865 $disks->{$num}->{all} .= "\
/$disk";
870 die "ERROR
: in path
\n";
874 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
878 sub snapshot_destroy {
879 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
881 my @zfscmd = ('zfs', 'destroy');
882 my $snapshot = "$source->{all
}\
@$snap";
885 if($source->{ip} && $method eq 'ssh'){
886 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
888 run_cmd([@zfscmd, $snapshot]);
895 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
897 my $path = "$dest->{all
}";
898 $path .= "/$source->{last_part
}" if $source->{last_part};
901 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
909 # check if snapshot for incremental sync exist on source side
911 my ($source , $dest, $method, $source_user) = @_;
914 push @$cmd, 'ssh', "$source_user\@$source->{ip
}", '--' if $source->{ip};
915 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
917 my $path = $source->{all};
918 $path .= "\
@$dest->{last_snap
}";
922 eval {run_cmd($cmd)};
931 my ($source, $dest, $param) = @_;
935 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
936 push @$cmd, 'zfs', 'send';
937 push @$cmd, '-p', if $param->{properties};
938 push @$cmd, '-v' if $param->{verbose};
940 if($dest->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{source_user})) {
941 push @$cmd, '-i', "$source->{all
}\
@$dest->{last_snap
}";
943 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
945 if ($param->{limit}){
946 my $bwl = $param->{limit}*1024;
947 push @$cmd, \'|', 'cstream', '-t', $bwl;
949 my $target = "$dest->{all
}";
950 $target .= "/$source->{last_part
}" if $source->{last_part};
954 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
955 push @$cmd, 'zfs', 'recv', '-F', '--';
956 push @$cmd, "$target";
963 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
970 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
972 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
973 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
975 my $config_dir = $dest_config_path // $CONFIG_PATH;
976 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
978 $dest_target_new = $config_dir.'/'.$dest_target_new;
980 if ($method eq 'ssh'){
981 if ($dest->{ip} && $source->{ip}) {
982 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
983 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
984 } elsif ($dest->{ip}) {
985 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
986 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
987 } elsif ($source->{ip}) {
988 run_cmd(['mkdir', '-p', '--', $config_dir]);
989 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
992 if ($source->{destroy}){
993 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$dest->{old_snap
}";
995 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
997 run_cmd(['rm', '-f', '--', $dest_target_old]);
1000 } elsif ($method eq 'local') {
1001 run_cmd(['mkdir', '-p', '--', $config_dir]);
1002 run_cmd(['cp', $source_target, $dest_target_new]);
1007 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1008 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1014 my $cfg = read_cron();
1016 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1018 my $states = read_state();
1020 foreach my $source (sort keys%{$cfg}) {
1021 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1022 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1023 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1024 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1028 return $status_list;
1034 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1035 my $job = get_job($param);
1036 $job->{state} = "ok
";
1045 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1046 my $job = get_job($param);
1047 $job->{state} = "stopped
";
1055 $PROGNAME destroy -source <string> [OPTIONS]
1057 remove a sync Job from the scheduler
1061 name of the sync job, if not set it is default
1065 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1068 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1074 the destination target is like [IP]:<Pool>[/Path]
1078 name of the user on the destination target, root by default
1082 max sync speed in kBytes/s, default unlimited
1086 how much snapshots will be kept before get erased, default 1
1090 name of the sync job, if not set it is default
1094 if this flag is set it will skip the first sync
1098 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1102 name of the user on the source target, root by default
1106 Include the dataset's properties in the stream.
1108 -dest-config-path string
1110 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1113 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1119 the destination target is like [IP:]<Pool>[/Path]
1123 name of the user on the destination target, root by default
1127 max sync speed in kBytes/s, default unlimited
1131 how much snapshots will be kept before get erased, default 1
1135 name of the sync job, if not set it is default.
1136 It is only necessary if scheduler allready contains this source.
1140 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1144 name of the user on the source target, root by default
1148 print out the sync progress.
1152 Include the dataset's properties in the stream.
1154 -dest-config-path string
1156 specify a custom config path on the destination target. default is /var/lib/pve-zsync
1161 Get a List of all scheduled Sync Jobs
1166 Get the status of all scheduled Sync Jobs
1169 $PROGNAME help <cmd> [OPTIONS]
1171 Get help about specified command.
1179 Verbose output format.
1182 $PROGNAME enable -source <string> [OPTIONS]
1184 enable a syncjob and reset error
1188 name of the sync job, if not set it is default
1192 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1195 $PROGNAME disable -source <string> [OPTIONS]
1201 name of the sync job, if not set it is default
1205 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1207 printpod
=> 'internal command',
1213 } elsif (!$cmd_help->{$command}) {
1214 print "ERROR: unknown command '$command'";
1219 my $param = parse_argv
(@arg);
1223 die "$cmd_help->{$command}\n" if !$param->{$_};
1227 if ($command eq 'destroy') {
1228 check_params
(qw(source));
1230 check_target
($param->{source
});
1231 destroy_job
($param);
1233 } elsif ($command eq 'sync') {
1234 check_params
(qw(source dest));
1236 check_target
($param->{source
});
1237 check_target
($param->{dest
});
1240 } elsif ($command eq 'create') {
1241 check_params
(qw(source dest));
1243 check_target
($param->{source
});
1244 check_target
($param->{dest
});
1247 } elsif ($command eq 'status') {
1250 } elsif ($command eq 'list') {
1253 } elsif ($command eq 'help') {
1254 my $help_command = $ARGV[1];
1256 if ($help_command && $cmd_help->{$help_command}) {
1257 die "$cmd_help->{$help_command}\n";
1260 if ($param->{verbose
}) {
1261 exec("man $PROGNAME");
1268 } elsif ($command eq 'enable') {
1269 check_params
(qw(source));
1271 check_target
($param->{source
});
1274 } elsif ($command eq 'disable') {
1275 check_params
(qw(source));
1277 check_target
($param->{source
});
1278 disable_job
($param);
1280 } elsif ($command eq 'printpod') {
1287 print("ERROR:\tno command specified\n") if !$help;
1288 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1289 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1290 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1291 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1292 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1293 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1294 print("\t$PROGNAME list\n");
1295 print("\t$PROGNAME status\n");
1296 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1301 parse_target
($target);
1306 my $synopsis = join("\n", sort values %$cmd_help);
1311 pve-zsync - PVE ZFS Replication Manager
1315 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1321 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1322 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1323 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.
1324 To config cron see man crontab.
1326 =head2 PVE ZFS Storage sync Tool
1328 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1332 add sync job from local VM to remote ZFS Server
1333 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1335 =head1 IMPORTANT FILES
1337 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1339 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1341 =head1 COPYRIGHT AND DISCLAIMER
1343 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1345 This program is free software: you can redistribute it and/or modify it
1346 under the terms of the GNU Affero General Public License as published
1347 by the Free Software Foundation, either version 3 of the License, or
1348 (at your option) any later version.
1350 This program is distributed in the hope that it will be useful, but
1351 WITHOUT ANY WARRANTY; without even the implied warranty of
1352 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1353 Affero General Public License for more details.
1355 You should have received a copy of the GNU Affero General Public
1356 License along with this program. If not, see
1357 <http://www.gnu.org/licenses/>.