]>
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 = $CONFIG_PATH;
972 $config_dir .= "/$dest->{last_part
}" if $dest->{last_part};
974 $dest_target_new = $config_dir.'/'.$dest_target_new;
976 if ($method eq 'ssh'){
977 if ($dest->{ip} && $source->{ip}) {
978 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
979 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
980 } elsif ($dest->{ip}) {
981 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'mkdir', '-p', '--', $config_dir]);
982 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip
}]:$dest_target_new"]);
983 } elsif ($source->{ip}) {
984 run_cmd(['mkdir', '-p', '--', $config_dir]);
985 run_cmd(['scp', '--', "$source_user\@[$source->{ip
}]:$source_target", $dest_target_new]);
988 if ($source->{destroy}){
989 my $dest_target_old ="${config_dir
}/$source->{vmid
}.conf
.$source->{vm_type
}.$source->{old_snap
}";
991 run_cmd(['ssh', "$dest_user\@$dest->{ip
}", '--', 'rm', '-f', '--', $dest_target_old]);
993 run_cmd(['rm', '-f', '--', $dest_target_old]);
996 } elsif ($method eq 'local') {
997 run_cmd(['mkdir', '-p', '--', $config_dir]);
998 run_cmd(['cp', $source_target, $dest_target_new]);
1003 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1004 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
1010 my $cfg = read_cron();
1012 my $status_list = sprintf("%-25s
%-25s
%-10s
\n", "SOURCE
", "NAME
", "STATUS
");
1014 my $states = read_state();
1016 foreach my $source (sort keys%{$cfg}) {
1017 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1018 $status_list .= sprintf("%-25s
", cut_target_width($source, 25));
1019 $status_list .= sprintf("%-25s
", cut_target_width($sync_name, 25));
1020 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
1024 return $status_list;
1030 my $job = get_job($param);
1031 $job->{state} = "ok
";
1039 my $job = get_job($param);
1040 $job->{state} = "stopped
";
1047 $PROGNAME destroy -source <string> [OPTIONS]
1049 remove a sync Job from the scheduler
1053 name of the sync job, if not set it is default
1057 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1060 $PROGNAME create -dest <string> -source <string> [OPTIONS]
1066 the destination target is like [IP]:<Pool>[/Path]
1070 name of the user on the destination target, root by default
1074 max sync speed in kBytes/s, default unlimited
1078 how much snapshots will be kept before get erased, default 1
1082 name of the sync job, if not set it is default
1086 if this flag is set it will skip the first sync
1090 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1094 name of the user on the source target, root by default
1098 Include the dataset's properties in the stream.
1101 $PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
1107 the destination target is like [IP:]<Pool>[/Path]
1111 name of the user on the destination target, root by default
1115 max sync speed in kBytes/s, default unlimited
1119 how much snapshots will be kept before get erased, default 1
1123 name of the sync job, if not set it is default.
1124 It is only necessary if scheduler allready contains this source.
1128 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1132 name of the user on the source target, root by default
1136 print out the sync progress.
1140 Include the dataset's properties in the stream.
1145 Get a List of all scheduled Sync Jobs
1150 Get the status of all scheduled Sync Jobs
1153 $PROGNAME help <cmd> [OPTIONS]
1155 Get help about specified command.
1163 Verbose output format.
1166 $PROGNAME enable -source <string> [OPTIONS]
1168 enable a syncjob and reset error
1172 name of the sync job, if not set it is default
1176 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1179 $PROGNAME disable -source <string> [OPTIONS]
1185 name of the sync job, if not set it is default
1189 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1191 printpod
=> 'internal command',
1197 } elsif (!$cmd_help->{$command}) {
1198 print "ERROR: unknown command '$command'";
1203 my $param = parse_argv
(@arg);
1207 die "$cmd_help->{$command}\n" if !$param->{$_};
1211 if ($command eq 'destroy') {
1212 check_params
(qw(source));
1214 check_target
($param->{source
});
1215 destroy_job
($param);
1217 } elsif ($command eq 'sync') {
1218 check_params
(qw(source dest));
1220 check_target
($param->{source
});
1221 check_target
($param->{dest
});
1224 } elsif ($command eq 'create') {
1225 check_params
(qw(source dest));
1227 check_target
($param->{source
});
1228 check_target
($param->{dest
});
1231 } elsif ($command eq 'status') {
1234 } elsif ($command eq 'list') {
1237 } elsif ($command eq 'help') {
1238 my $help_command = $ARGV[1];
1240 if ($help_command && $cmd_help->{$help_command}) {
1241 die "$cmd_help->{$help_command}\n";
1244 if ($param->{verbose
}) {
1245 exec("man $PROGNAME");
1252 } elsif ($command eq 'enable') {
1253 check_params
(qw(source));
1255 check_target
($param->{source
});
1258 } elsif ($command eq 'disable') {
1259 check_params
(qw(source));
1261 check_target
($param->{source
});
1262 disable_job
($param);
1264 } elsif ($command eq 'printpod') {
1271 print("ERROR:\tno command specified\n") if !$help;
1272 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1273 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1274 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1275 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1276 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1277 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1278 print("\t$PROGNAME list\n");
1279 print("\t$PROGNAME status\n");
1280 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1285 parse_target
($target);
1290 my $synopsis = join("\n", sort values %$cmd_help);
1295 pve-zsync - PVE ZFS Replication Manager
1299 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1305 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1306 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1307 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.
1308 To config cron see man crontab.
1310 =head2 PVE ZFS Storage sync Tool
1312 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1316 add sync job from local VM to remote ZFS Server
1317 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1319 =head1 IMPORTANT FILES
1321 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1323 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1325 =head1 COPYRIGHT AND DISCLAIMER
1327 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1329 This program is free software: you can redistribute it and/or modify it
1330 under the terms of the GNU Affero General Public License as published
1331 by the Free Software Foundation, either version 3 of the License, or
1332 (at your option) any later version.
1334 This program is distributed in the hope that it will be useful, but
1335 WITHOUT ANY WARRANTY; without even the implied warranty of
1336 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1337 Affero General Public License for more details.
1339 You should have received a copy of the GNU Affero General Public
1340 License along with this program. If not, see
1341 <http://www.gnu.org/licenses/>.