]>
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 $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
22 my $PROG_PATH = "$PATH/${PROGNAME}";
27 $DEBUG = 0; # change default here. not above on declaration!
28 $DEBUG ||= $ENV{ZSYNC_DEBUG
};
31 Data
::Dumper-
>import();
35 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
36 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
37 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
38 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
41 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
42 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
43 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
44 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
45 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
46 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
47 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
48 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
49 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
51 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
52 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
53 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
54 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
55 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
57 my $command = $ARGV[0];
59 if (defined($command) && $command ne 'help' && $command ne 'printpod') {
60 check_bin
('cstream');
66 $SIG{TERM
} = $SIG{QUIT
} = $SIG{PIPE
} = $SIG{HUP
} = $SIG{KILL
} = $SIG{INT
} = sub {
67 die "Signaled, aborting sync: $!\n";
73 foreach my $p (split (/:/, $ENV{PATH
})) {
80 die "unable to find command '$bin'\n";
83 sub cut_target_width
{
84 my ($path, $maxlen) = @_;
87 return $path if length($path) <= $maxlen;
89 return '..'.substr($path, -$maxlen+2) if $path !~ m
@/@;
91 $path =~ s
@/([^/]+/?
)$@@;
94 if (length($tail)+3 == $maxlen) {
96 } elsif (length($tail)+2 >= $maxlen) {
97 return '..'.substr($tail, -$maxlen+2)
100 $path =~ s
@(/[^/]+)(?
:/|$)@@;
102 my $both = length($head) + length($tail);
103 my $remaining = $maxlen-$both-4; # -4 for "/../"
105 if ($remaining < 0) {
106 return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../"
109 substr($path, ($remaining/2), (length($path)-$remaining), '..');
110 return "$head/" . $path . "/$tail";
115 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
120 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
124 my ($source, $name, $status) = @_;
126 if ($status->{$source->{all
}}->{$name}->{status
}) {
133 sub check_pool_exists
{
134 my ($target, $user) = @_;
139 push @$cmd, 'ssh', "$user\@$target->{ip}", '--';
141 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
155 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
158 if ($text !~ $TARGETRE) {
162 $target->{ip
} = $1 if $1;
163 my @parts = split('/', $2);
165 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
167 my $pool = $target->{pool
} = shift(@parts);
168 die "$errstr\n" if !$pool;
170 if ($pool =~ m/^\d+$/) {
171 $target->{vmid
} = $pool;
172 delete $target->{pool
};
175 return $target if (@parts == 0);
176 $target->{last_part
} = pop(@parts);
182 $target->{path
} = join('/', @parts);
190 #This is for the first use to init file;
192 my $new_fh = IO
::File-
>new("> $CRONJOBS");
193 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
198 my $fh = IO
::File-
>new("< $CRONJOBS");
199 die "Could not open file $CRONJOBS: $!\n" if !$fh;
205 return encode_cron
(@text);
220 source_user
=> undef,
225 my ($ret) = GetOptionsFromArray
(
227 'dest=s' => \
$param->{dest
},
228 'source=s' => \
$param->{source
},
229 'verbose' => \
$param->{verbose
},
230 'limit=i' => \
$param->{limit
},
231 'maxsnap=i' => \
$param->{maxsnap
},
232 'name=s' => \
$param->{name
},
233 'skip' => \
$param->{skip
},
234 'method=s' => \
$param->{method},
235 'source-user=s' => \
$param->{source_user
},
236 'dest-user=s' => \
$param->{dest_user
},
237 'properties' => \
$param->{properties
},
240 die "can't parse options\n" if $ret == 0;
242 $param->{name
} //= "default";
243 $param->{maxsnap
} //= 1;
244 $param->{method} //= "ssh";
245 $param->{source_user
} //= "root";
246 $param->{dest_user
} //= "root";
251 sub add_state_to_job
{
254 my $states = read_state
();
255 my $state = $states->{$job->{source
}}->{$job->{name
}};
257 $job->{state} = $state->{state};
258 $job->{lsync
} = $state->{lsync
};
259 $job->{vm_type
} = $state->{vm_type
};
261 for (my $i = 0; $state->{"snap$i"}; $i++) {
262 $job->{"snap$i"} = $state->{"snap$i"};
273 while (my $line = shift(@text)) {
275 my @arg = split('\s', $line);
276 my $param = parse_argv
(@arg);
278 if ($param->{source
} && $param->{dest
}) {
279 my $source = delete $param->{source
};
280 my $name = delete $param->{name
};
282 $cfg->{$source}->{$name} = $param;
294 my $source = parse_target
($param->{source
});
295 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
297 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
298 $job->{dest
} = $param->{dest
} if $param->{dest
};
299 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
300 $job->{method} = "ssh" if !$job->{method};
301 $job->{limit
} = $param->{limit
};
302 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
303 $job->{source
} = $param->{source
};
304 $job->{source_user
} = $param->{source_user
};
305 $job->{dest_user
} = $param->{dest_user
};
306 $job->{properties
} = !!$param->{properties
};
314 make_path
$CONFIG_PATH;
315 my $new_fh = IO
::File-
>new("> $STATE");
316 die "Could not create $STATE: $!\n" if !$new_fh;
322 my $fh = IO
::File-
>new("< $STATE");
323 die "Could not open file $STATE: $!\n" if !$fh;
326 my $states = decode_json
($text);
340 $in_fh = IO
::File-
>new("< $STATE");
341 die "Could not open file $STATE: $!\n" if !$in_fh;
346 my $out_fh = IO
::File-
>new("> $STATE.new");
347 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
352 $states = decode_json
($text);
353 $state = $states->{$job->{source
}}->{$job->{name
}};
356 if ($job->{state} ne "del") {
357 $state->{state} = $job->{state};
358 $state->{lsync
} = $job->{lsync
};
359 $state->{vm_type
} = $job->{vm_type
};
361 for (my $i = 0; $job->{"snap$i"} ; $i++) {
362 $state->{"snap$i"} = $job->{"snap$i"};
364 $states->{$job->{source
}}->{$job->{name
}} = $state;
367 delete $states->{$job->{source
}}->{$job->{name
}};
368 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
371 $text = encode_json
($states);
375 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 $fh = IO
::File-
>new("< $CRONJOBS");
394 die "Could not open file $CRONJOBS: $!\n" if !$fh;
399 while (my $line = shift(@test)) {
401 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
403 next if $job->{state} eq "del";
404 $text .= format_job
($job, $line);
406 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
415 $text = "$header$text";
419 $text .= format_job
($job);
421 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
422 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
424 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
427 die "can't move $CRONJOBS.new: $!\n" if !rename "${CRONJOBS}.new", $CRONJOBS;
432 my ($job, $line) = @_;
435 if ($job->{state} eq "stopped") {
439 $line =~ /^#*\s*((?:\S+\s+){4}\S+)\s+root/;
442 $text .= "*/$INTERVAL * * * *";
445 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
446 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
447 $text .= " --limit $job->{limit}" if $job->{limit
};
448 $text .= " --method $job->{method}";
449 $text .= " --verbose" if $job->{verbose
};
450 $text .= " --source-user $job->{source_user}";
451 $text .= " --dest-user $job->{dest_user}";
452 $text .= " --properties" if $job->{properties
};
460 my $cfg = read_cron
();
462 my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
464 my $states = read_state
();
465 foreach my $source (sort keys%{$cfg}) {
466 foreach my $name (sort keys%{$cfg->{$source}}) {
467 $list .= sprintf("%-25s", cut_target_width
($source, 25));
468 $list .= sprintf("%-25s", cut_target_width
($name, 25));
469 $list .= sprintf("%-10s", $states->{$source}->{$name}->{state});
470 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
471 $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type
}) ?
$states->{$source}->{$name}->{vm_type
} : "undef");
472 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
480 my ($target, $user) = @_;
482 return undef if !defined($target->{vmid
});
484 my $conf_fn = "$target->{vmid}.conf";
487 my @cmd = ('ssh', "$user\@$target->{ip}", '--', '/bin/ls');
488 return "qemu" if eval { run_cmd
([@cmd, "$QEMU_CONF/$conf_fn"]) };
489 return "lxc" if eval { run_cmd
([@cmd, "$LXC_CONF/$conf_fn"]) };
491 return "qemu" if -f
"$QEMU_CONF/$conf_fn";
492 return "lxc" if -f
"$LXC_CONF/$conf_fn";
501 my $cfg = read_cron
();
503 my $job = param_to_job
($param);
505 $job->{state} = "ok";
508 my $source = parse_target
($param->{source
});
509 my $dest = parse_target
($param->{dest
});
511 if (my $ip = $dest->{ip
}) {
512 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{dest_user}\@$ip"]);
515 if (my $ip = $source->{ip
}) {
516 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "$param->{source_user}\@$ip"]);
519 die "Pool $dest->{all} does not exists\n" if !check_pool_exists
($dest, $param->{dest_user
});
521 if (!defined($source->{vmid
})) {
522 die "Pool $source->{all} does not exists\n" if !check_pool_exists
($source, $param->{source_user
});
525 my $vm_type = vm_exists
($source, $param->{source_user
});
526 $job->{vm_type
} = $vm_type;
527 $source->{vm_type
} = $vm_type;
529 die "VM $source->{vmid} doesn't exist\n" if $source->{vmid
} && !$vm_type;
531 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
533 #check if vm has zfs disks if not die;
534 get_disks
($source, $param->{source_user
}) if $source->{vmid
};
540 sync
($param) if !$param->{skip
};
551 my $cfg = read_cron
();
553 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
554 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
556 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
557 $job->{name
} = $param->{name
};
558 $job->{source
} = $param->{source
};
559 $job = add_state_to_job
($job);
567 my $job = get_job
($param);
568 $job->{state} = "del";
577 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
578 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
581 my $date = get_date
();
584 $job = get_job
($param);
587 if ($job && defined($job->{state}) && $job->{state} eq "syncing") {
588 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
591 my $dest = parse_target
($param->{dest
});
592 my $source = parse_target
($param->{source
});
594 my $sync_path = sub {
595 my ($source, $dest, $job, $param, $date) = @_;
597 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
}, $param->{source_user
});
599 snapshot_add
($source, $dest, $param->{name
}, $date, $param->{source_user
}, $param->{dest_user
});
601 send_image
($source, $dest, $param);
603 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}, $param->{source_user
}, $param->{dest_user
}) if ($source->{destroy
} && $source->{old_snap
});
607 my $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
};
617 if ($source->{vmid
}) {
618 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
619 die "source-user has to be root for syncing VMs\n" if ($param->{source_user
} ne "root");
620 my $disks = get_disks
($source, $param->{source_user
});
622 foreach my $disk (sort keys %{$disks}) {
623 $source->{all
} = $disks->{$disk}->{all
};
624 $source->{pool
} = $disks->{$disk}->{pool
};
625 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
626 $source->{last_part
} = $disks->{$disk}->{last_part
};
627 &$sync_path($source, $dest, $job, $param, $date);
629 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
630 send_config
($source, $dest,'ssh', $param->{source_user
}, $param->{dest_user
});
632 send_config
($source, $dest,'local', $param->{source_user
}, $param->{dest_user
});
635 &$sync_path($source, $dest, $job, $param, $date);
640 $job->{state} = "error";
644 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
650 $job->{state} = "ok";
651 $job->{lsync
} = $date;
660 my ($source, $dest, $max_snap, $name, $source_user) = @_;
663 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
664 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
665 push @$cmd, $source->{all
};
667 my $raw = run_cmd
($cmd);
670 my $last_snap = undef;
673 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
675 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
677 $last_snap = $1 if (!$last_snap);
680 if ($index == $max_snap) {
681 $source->{destroy
} = 1;
687 return ($old_snap, $last_snap) if $last_snap;
693 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
695 my $snap_name = "rep_$name\_".$date;
697 $source->{new_snap
} = $snap_name;
699 my $path = "$source->{all}\@$snap_name";
702 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip
};
703 push @$cmd, 'zfs', 'snapshot', $path;
709 snapshot_destroy
($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
717 my $text = "SHELL=/bin/sh\n";
718 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
720 my $fh = IO
::File-
>new("> $CRONJOBS");
721 die "Could not open file: $!\n" if !$fh;
723 foreach my $source (sort keys%{$cfg}) {
724 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
725 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
726 $text .= "$PROG_PATH sync";
727 $text .= " -source ";
728 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
729 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
730 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
732 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
733 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
734 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
737 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
738 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
739 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
740 $text .= " -name $sync_name ";
741 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
742 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
746 die "Can't write to cron\n" if (!print($fh $text));
751 my ($target, $user) = @_;
754 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip
};
756 if ($target->{vm_type
} eq 'qemu') {
757 push @$cmd, 'qm', 'config', $target->{vmid
};
758 } elsif ($target->{vm_type
} eq 'lxc') {
759 push @$cmd, 'pct', 'config', $target->{vmid
};
761 die "VM Type unknown\n";
764 my $res = run_cmd
($cmd);
766 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
}, $user);
773 print "Start CMD\n" if $DEBUG;
774 print Dumper
$cmd if $DEBUG;
775 if (ref($cmd) eq 'ARRAY') {
776 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
778 my $output = `$cmd 2>&1`;
780 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
783 print Dumper
$output if $DEBUG;
784 print "END CMD\n" if $DEBUG;
789 my ($text, $ip, $vm_type, $user) = @_;
794 while ($text && $text =~ s/^(.*?)(\n|$)//) {
797 next if $line =~ /media=cdrom/;
798 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
800 #QEMU if backup is not set include in sync
801 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
803 #LXC if backup is not set do no in sync
804 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
808 if($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) {
809 my @parameter = split(/,/,$1);
811 foreach my $opt (@parameter) {
812 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
819 if (!defined($disk) || !defined($stor)) {
820 print "Disk: \"$line\" has no valid zfs dataset format
and will be skipped
\n";
825 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
826 push @$cmd, 'pvesm', 'path', "$stor$disk";
827 my $path = run_cmd($cmd);
829 die "Get
no path from pvesm path
$stor$disk\n" if !$path;
831 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
833 my @array = split('/', $1);
834 $disks->{$num}->{pool} = shift(@array);
835 $disks->{$num}->{all} = $disks->{$num}->{pool};
837 $disks->{$num}->{path} = join('/', @array);
838 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
840 $disks->{$num}->{last_part} = $disk;
841 $disks->{$num}->{all} .= "\
/$disk";
844 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
846 $disks->{$num}->{pool} = $1;
847 $disks->{$num}->{all} = $disks->{$num}->{pool};
850 $disks->{$num}->{path} = $3;
851 $disks->{$num}->{all} .= "\
/$disks->{$num}->{path
}";
854 $disks->{$num}->{last_part} = $disk;
855 $disks->{$num}->{all} .= "\
/$disk";
860 die "ERROR
: in path
\n";
864 die "Vm include
no disk on zfs
.\n" if !$disks->{0};
868 sub snapshot_destroy {
869 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
871 my @zfscmd = ('zfs', 'destroy');
872 my $snapshot = "$source->{all
}\
@$snap";
875 if($source->{ip} && $method eq 'ssh'){
876 run_cmd(['ssh', "$source_user\@$source->{ip
}", '--', @zfscmd, $snapshot]);
878 run_cmd([@zfscmd, $snapshot]);
885 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip
}", '--') : ();
887 my $path = "$dest->{all
}";
888 $path .= "/$source->{last_part
}" if $source->{last_part};
891 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
900 my ($source , $dest, $method, $dest_user) = @_;
903 push @$cmd, 'ssh', "$dest_user\@$dest->{ip
}", '--' if $dest->{ip};
904 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
906 my $path = $dest->{all};
907 $path .= "/$source->{last_part
}" if $source->{last_part};
908 $path .= "\
@$source->{old_snap
}";
914 eval {$text =run_cmd($cmd);};
920 while ($text && $text =~ s/^(.*?)(\n|$)//) {
922 return 1 if $line =~ m/^.*$source->{old_snap}$/;
927 my ($source, $dest, $param) = @_;
931 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user
}\
@$source->{ip
}", '--' if $source->{ip};
932 push @$cmd, 'zfs', 'send';
933 push @$cmd, '-p', if $param->{properties};
934 push @$cmd, '-v' if $param->{verbose};
936 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
937 push @$cmd, '-i', "$source->{all
}\
@$source->{last_snap
}";
939 push @$cmd, '--', "$source->{all
}\
@$source->{new_snap
}";
941 if ($param->{limit}){
942 my $bwl = $param->{limit}*1024;
943 push @$cmd, \'|', 'cstream', '-t', $bwl;
945 my $target = "$dest->{all
}";
946 $target .= "/$source->{last_part
}" if $source->{last_part};
950 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user
}\
@$dest->{ip
}", '--' if $dest->{ip};
951 push @$cmd, 'zfs', 'recv', '-F', '--';
952 push @$cmd, "$target";
959 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
966 my ($source, $dest, $method, $source_user, $dest_user) = @_;
968 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid
}.conf
": "$LXC_CONF/$source->{vmid
}.conf
";
969 my $dest_target_new ="$source->{vmid
}.conf
.$source->{vm_type
}.$source->{new_snap
}";
971 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH
}/$dest->{last_part
}" : $CONFIG_PATH;
973 $dest_target_new = $config_dir.'/'.$dest_target_new;
975 if ($method eq 'ssh'){
976 if ($dest->{ip} && $source->{ip}) {
977 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
978 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
979 } elsif ($dest->{ip}) {
980 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
981 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
982 } elsif ($source->{ip}) {
983 run_cmd(['mkdir', '-p', '--', $config_dir]);
984 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
987 if ($source->{destroy}){
988 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
990 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
992 run_cmd(['rm', '-f', '--', $dest_target_old]);
995 } elsif ($method eq 'local') {
996 run_cmd(['mkdir', '-p', '--', $config_dir]);
997 run_cmd(['cp', $source_target, $dest_target_new]);
1002 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1003 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1009 my $cfg = read_cron();
1011 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1013 my $states = read_state();
1015 foreach my $source (sort keys%{$cfg}) {
1016 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1017 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1018 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1019 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1023 return $status_list;
1029 my $job = get_job($param);
1030 $job->{state} = "ok
";
1038 my $job = get_job($param);
1039 $job->{state} = "stopped
";
1046 $PROGNAME destroy -source <string> [OPTIONS]
1048 remove a sync Job from the scheduler
1052 name of the sync job, if not set it is default
1056 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1059 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1065 the destination target is like [IP]:<Pool>[/Path]
1069 name of the user on the destination target, root by default
1073 max sync speed in kBytes/s, default unlimited
1077 how much snapshots will be kept before get erased, default 1
1081 name of the sync job, if not set it is default
1085 if this flag is set it will skip the first sync
1089 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1093 name of the user on the source target, root by default
1097 Include the dataset's properties in the stream.
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.
1144 Get a List of all scheduled Sync Jobs
1149 Get the status of all scheduled Sync Jobs
1152 $PROGNAME help <cmd> [OPTIONS]
1154 Get help about specified command.
1162 Verbose output format.
1165 $PROGNAME enable -source <string> [OPTIONS]
1167 enable a syncjob and reset error
1171 name of the sync job, if not set it is default
1175 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1178 $PROGNAME disable -source <string> [OPTIONS]
1184 name of the sync job, if not set it is default
1188 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1190 printpod
=> 'internal command',
1196 } elsif (!$cmd_help->{$command}) {
1197 print "ERROR: unknown command '$command'";
1202 my $param = parse_argv
(@arg);
1206 die "$cmd_help->{$command}\n" if !$param->{$_};
1210 if ($command eq 'destroy') {
1211 check_params
(qw(source));
1213 check_target
($param->{source
});
1214 destroy_job
($param);
1216 } elsif ($command eq 'sync') {
1217 check_params
(qw(source dest));
1219 check_target
($param->{source
});
1220 check_target
($param->{dest
});
1223 } elsif ($command eq 'create') {
1224 check_params
(qw(source dest));
1226 check_target
($param->{source
});
1227 check_target
($param->{dest
});
1230 } elsif ($command eq 'status') {
1233 } elsif ($command eq 'list') {
1236 } elsif ($command eq 'help') {
1237 my $help_command = $ARGV[1];
1239 if ($help_command && $cmd_help->{$help_command}) {
1240 die "$cmd_help->{$help_command}\n";
1243 if ($param->{verbose
}) {
1244 exec("man $PROGNAME");
1251 } elsif ($command eq 'enable') {
1252 check_params
(qw(source));
1254 check_target
($param->{source
});
1257 } elsif ($command eq 'disable') {
1258 check_params
(qw(source));
1260 check_target
($param->{source
});
1261 disable_job
($param);
1263 } elsif ($command eq 'printpod') {
1270 print("ERROR:\tno command specified\n") if !$help;
1271 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1272 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1273 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1274 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1275 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1276 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1277 print("\t$PROGNAME list\n");
1278 print("\t$PROGNAME status\n");
1279 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1284 parse_target
($target);
1289 my $synopsis = join("\n", sort values %$cmd_help);
1294 pve-zsync - PVE ZFS Replication Manager
1298 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1304 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1305 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1306 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.
1307 To config cron see man crontab.
1309 =head2 PVE ZFS Storage sync Tool
1311 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1315 add sync job from local VM to remote ZFS Server
1316 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1318 =head1 IMPORTANT FILES
1320 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1322 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1324 =head1 COPYRIGHT AND DISCLAIMER
1326 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1328 This program is free software: you can redistribute it and/or modify it
1329 under the terms of the GNU Affero General Public License as published
1330 by the Free Software Foundation, either version 3 of the License, or
1331 (at your option) any later version.
1333 This program is distributed in the hope that it will be useful, but
1334 WITHOUT ANY WARRANTY; without even the implied warranty of
1335 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1336 Affero General Public License for more details.
1338 You should have received a copy of the GNU Affero General Public
1339 License along with this program. If not, see
1340 <http://www.gnu.org/licenses/>.