]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
5 use Data
::Dumper
qw(Dumper);
6 use Fcntl
qw(:flock SEEK_END);
7 use Getopt
::Long
qw(GetOptionsFromArray);
8 use File
::Copy
qw(move);
9 use File
::Path
qw(make_path);
13 use String
::ShellQuote
'shell_quote';
15 my $PROGNAME = "pve-zsync";
16 my $CONFIG_PATH = "/var/lib/${PROGNAME}/";
17 my $STATE = "${CONFIG_PATH}sync_state";
18 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
19 my $PATH = "/usr/sbin/";
20 my $PVE_DIR = "/etc/pve/local/";
21 my $QEMU_CONF = "${PVE_DIR}qemu-server/";
22 my $LXC_CONF = "${PVE_DIR}lxc/";
23 my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
24 my $PROG_PATH = "$PATH${PROGNAME}";
28 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
29 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
30 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
31 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
34 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
35 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
36 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
42 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
44 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
45 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
46 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
47 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
48 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
50 check_bin
('cstream');
58 foreach my $p (split (/:/, $ENV{PATH
})) {
65 die "unable to find command '$bin'\n";
68 sub cut_target_width
{
69 my ($target, $max) = @_;
71 return $target if (length($target) <= $max);
72 my @spl = split('/', $target);
74 my $count = length($spl[@spl-1]);
75 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
77 $count += length($spl[0]) if @spl > 1;
78 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
81 $rest = $max-$count if ($max-$count > 0);
83 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
88 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
93 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
97 my ($source, $name, $status) = @_;
99 if ($status->{$source->{all
}}->{$name}->{status
}) {
106 sub check_pool_exists
{
110 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
111 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
125 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
128 if ($text !~ $TARGETRE) {
132 $target->{ip
} = $1 if $1;
133 my @parts = split('/', $2);
135 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
137 my $pool = $target->{pool
} = shift(@parts);
138 die "$errstr\n" if !$pool;
140 if ($pool =~ m/^\d+$/) {
141 $target->{vmid
} = $pool;
142 delete $target->{pool
};
145 return $target if (@parts == 0);
146 $target->{last_part
} = pop(@parts);
152 $target->{path
} = join('/', @parts);
160 #This is for the first use to init file;
162 my $new_fh = IO
::File-
>new("> $CRONJOBS");
163 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
168 my $fh = IO
::File-
>new("< $CRONJOBS");
169 die "Could not open file $CRONJOBS: $!\n" if !$fh;
175 return encode_cron
(@text);
182 $param->{dest
} = undef;
183 $param->{source
} = undef;
184 $param->{verbose
} = undef;
185 $param->{limit
} = undef;
186 $param->{maxsnap
} = undef;
187 $param->{name
} = undef;
188 $param->{skip
} = undef;
189 $param->{method} = undef;
191 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
192 'dest=s' => \
$param->{dest
},
193 'source=s' => \
$param->{source
},
194 'verbose' => \
$param->{verbose
},
195 'limit=i' => \
$param->{limit
},
196 'maxsnap=i' => \
$param->{maxsnap
},
197 'name=s' => \
$param->{name
},
198 'skip' => \
$param->{skip
},
199 'method=s' => \
$param->{method});
202 die "can't parse options\n";
205 $param->{name
} = "default" if !$param->{name
};
206 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
207 $param->{method} = "ssh" if !$param->{method};
212 sub add_state_to_job
{
215 my $states = read_state
();
216 my $state = $states->{$job->{source
}}->{$job->{name
}};
218 $job->{state} = $state->{state};
219 $job->{lsync
} = $state->{lsync
};
220 $job->{vm_type
} = $state->{vm_type
};
222 for (my $i = 0; $state->{"snap$i"}; $i++) {
223 $job->{"snap$i"} = $state->{"snap$i"};
234 while (my $line = shift(@text)) {
236 my @arg = split('\s', $line);
237 my $param = parse_argv
(@arg);
239 if ($param->{source
} && $param->{dest
}) {
240 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
257 my $source = parse_target
($param->{source
});
258 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
260 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
261 $job->{dest
} = $param->{dest
} if $param->{dest
};
262 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
263 $job->{method} = "ssh" if !$job->{method};
264 $job->{limit
} = $param->{limit
};
265 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
266 $job->{source
} = $param->{source
};
274 make_path
$CONFIG_PATH;
275 my $new_fh = IO
::File-
>new("> $STATE");
276 die "Could not create $STATE: $!\n" if !$new_fh;
282 my $fh = IO
::File-
>new("< $STATE");
283 die "Could not open file $STATE: $!\n" if !$fh;
286 my $states = decode_json
($text);
300 $in_fh = IO
::File-
>new("< $STATE");
301 die "Could not open file $STATE: $!\n" if !$in_fh;
306 my $out_fh = IO
::File-
>new("> $STATE.new");
307 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
312 $states = decode_json
($text);
313 $state = $states->{$job->{source
}}->{$job->{name
}};
316 if ($job->{state} ne "del") {
317 $state->{state} = $job->{state};
318 $state->{lsync
} = $job->{lsync
};
319 $state->{vm_type
} = $job->{vm_type
};
321 for (my $i = 0; $job->{"snap$i"} ; $i++) {
322 $state->{"snap$i"} = $job->{"snap$i"};
324 $states->{$job->{source
}}->{$job->{name
}} = $state;
327 delete $states->{$job->{source
}}->{$job->{name
}};
328 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
331 $text = encode_json
($states);
335 move
("$STATE.new", $STATE);
350 my $header = "SHELL=/bin/sh\n";
351 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
353 my $fh = IO
::File-
>new("< $CRONJOBS");
354 die "Could not open file $CRONJOBS: $!\n" if !$fh;
359 while (my $line = shift(@test)) {
361 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
363 next if $job->{state} eq "del";
364 $text .= format_job
($job, $line);
366 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
375 $text = "$header$text";
379 $text .= format_job
($job);
381 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
382 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
384 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
387 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
392 my ($job, $line) = @_;
395 if ($job->{state} eq "stopped") {
399 $line =~ /^#*(.+) root/;
402 $text .= "*/$INTERVAL * * * *";
405 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
406 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
407 $text .= " --method $job->{method}";
408 $text .= " --verbose" if $job->{verbose
};
416 my $cfg = read_cron
();
418 my $list = sprintf("%-25s%-10s%-7s%-20s%-5s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
420 my $states = read_state
();
421 foreach my $source (sort keys%{$cfg}) {
422 foreach my $name (sort keys%{$cfg->{$source}}) {
423 $list .= sprintf("%-25s", cut_target_width
($source, 25));
424 $list .= sprintf("%-10s", cut_target_width
($name, 10));
425 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
426 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync
});
427 $list .= sprintf("%-5s",$states->{$source}->{$name}->{vm_type
});
428 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
438 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
442 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF$target->{vmid}.conf"]) };
444 return "qemu" if $res;
446 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF$target->{vmid}.conf"]) };
448 return "lxc" if $res;
456 my $cfg = read_cron
();
458 my $job = param_to_job
($param);
460 $job->{state} = "ok";
463 my $source = parse_target
($param->{source
});
464 my $dest = parse_target
($param->{dest
});
466 if (my $ip = $dest->{ip
}) {
467 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
470 if (my $ip = $source->{ip
}) {
471 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
474 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
476 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
478 die "Pool $source->{path} does not exists\n" if undef($check);
480 my $vm_type = vm_exists
($source);
481 $job->{vm_type
} = $vm_type;
483 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !$vm_type;
485 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
491 sync
($param) if !$param->{skip
};
502 my $cfg = read_cron
();
504 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
505 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
507 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
508 $job->{name
} = $param->{name
};
509 $job->{source
} = $param->{source
};
510 $job = add_state_to_job
($job);
518 my $job = get_job
($param);
519 $job->{state} = "del";
528 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
529 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
532 my $date = get_date
();
535 $job = get_job
($param);
538 if ($job && $job->{state} eq "syncing") {
539 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
542 my $dest = parse_target
($param->{dest
});
543 my $source = parse_target
($param->{source
});
545 my $sync_path = sub {
546 my ($source, $dest, $job, $param, $date) = @_;
548 ($source->{old_snap
},$source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
550 snapshot_add
($source, $dest, $param->{name
}, $date);
552 send_image
($source, $dest, $param);
554 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
558 my $vm_type = vm_exists
($source);
559 $source->{vm_type
} = $vm_type;
562 $job->{state} = "syncing";
563 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
568 if ($source->{vmid
}) {
569 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
570 my $disks = get_disks
($source);
572 foreach my $disk (sort keys %{$disks}) {
573 $source->{all
} = $disks->{$disk}->{all
};
574 $source->{pool
} = $disks->{$disk}->{pool
};
575 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
576 $source->{last_part
} = $disks->{$disk}->{last_part
};
577 &$sync_path($source, $dest, $job, $param, $date);
579 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
580 send_config
($source, $dest,'ssh');
582 send_config
($source, $dest,'local');
585 &$sync_path($source, $dest, $job, $param, $date);
590 $job->{state} = "error";
594 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
600 $job->{state} = "ok";
601 $job->{lsync
} = $date;
610 my ($source, $dest, $max_snap, $name) = @_;
613 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
614 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
615 push @$cmd, $source->{all
};
617 my $raw = run_cmd
($cmd);
620 my $last_snap = undef;
623 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
625 if ($line =~ m/(rep_$name.*)$/) {
626 $last_snap = $1 if (!$last_snap);
629 if ($index == $max_snap) {
630 $source->{destroy
} = 1;
636 return ($old_snap, $last_snap) if $last_snap;
642 my ($source, $dest, $name, $date) = @_;
644 my $snap_name = "rep_$name\_".$date;
646 $source->{new_snap
} = $snap_name;
648 my $path = "$source->{all}\@$snap_name";
651 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
652 push @$cmd, 'zfs', 'snapshot', $path;
658 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
666 my $text = "SHELL=/bin/sh\n";
667 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
669 my $fh = IO
::File-
>new("> $CRONJOBS");
670 die "Could not open file: $!\n" if !$fh;
672 foreach my $source (sort keys%{$cfg}) {
673 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
674 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
675 $text .= "$PROG_PATH sync";
676 $text .= " -source ";
677 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
678 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
679 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
681 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
682 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
683 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
686 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
687 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
688 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
689 $text .= " -name $sync_name ";
690 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
691 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
695 die "Can't write to cron\n" if (!print($fh $text));
703 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
705 if ($target->{vm_type
} eq 'qemu') {
706 push @$cmd, 'qm', 'config', $target->{vmid
};
707 } elsif ($target->{vm_type
} eq 'lxc') {
708 push @$cmd, 'pct', 'config', $target->{vmid
};
710 die "VM Type unknown\n";
713 my $res = run_cmd
($cmd);
715 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
});
722 print "Start CMD\n" if $DEBUG;
723 print Dumper
$cmd if $DEBUG;
724 if (ref($cmd) eq 'ARRAY') {
725 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
727 my $output = `$cmd 2>&1`;
729 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
732 print Dumper
$output if $DEBUG;
733 print "END CMD\n" if $DEBUG;
738 my ($text, $ip, $vm_type) = @_;
743 while ($text && $text =~ s/^(.*?)(\n|$)//) {
746 next if $line =~ /cdrom|none/;
747 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
751 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
755 die "disk is not on ZFS Storage\n";
759 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
760 push @$cmd, 'pvesm', 'path', "$stor$disk";
761 my $path = run_cmd
($cmd);
763 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
765 my @array = split('/', $1);
766 $disks->{$num}->{pool
} = shift(@array);
767 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
769 $disks->{$num}->{path
} = join('/', @array);
770 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
772 $disks->{$num}->{last_part
} = $disk;
773 $disks->{$num}->{all
} .= "\/$disk";
776 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w
+.+)\
/(\w+.*)(\/$disk)$/) {
778 $disks->{$num}->{pool
} = $1;
779 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
782 $disks->{$num}->{path
} = $2;
783 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
786 $disks->{$num}->{last_part
} = $disk;
787 $disks->{$num}->{all
} .= "\/$disk";
792 die "ERROR: in path\n";
799 sub snapshot_destroy
{
800 my ($source, $dest, $method, $snap) = @_;
802 my @zfscmd = ('zfs', 'destroy');
803 my $snapshot = "$source->{all}\@$snap";
806 if($source->{ip
} && $method eq 'ssh'){
807 run_cmd
(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
809 run_cmd
([@zfscmd, $snapshot]);
816 my @ssh = $dest->{ip
} ?
('ssh', "root\@$dest->{ip}", '--') : ();
818 my $path = "$dest->{all}\/$source->{last_part}";
821 run_cmd
([@ssh, @zfscmd, "$path\@$snap"]);
830 my ($source ,$dest, $method) = @_;
833 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
834 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
835 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
838 eval {$text =run_cmd
($cmd);};
844 while ($text && $text =~ s/^(.*?)(\n|$)//) {
846 return 1 if $line =~ m/^.*$source->{old_snap}$/;
851 my ($source, $dest, $param) = @_;
855 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip
};
856 push @$cmd, 'zfs', 'send';
857 push @$cmd, '-v' if $param->{verbose
};
859 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
860 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
862 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
864 if ($param->{limit
}){
865 my $bwl = $param->{limit
}*1024;
866 push @$cmd, \'|', 'cstream
', '-t
', $bwl;
868 my $target = "$dest->{all}/$source->{last_part}";
872 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
873 push @$cmd, 'zfs', 'recv', '-F', '--';
874 push @$cmd, "$target";
881 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
888 my ($source, $dest, $method) = @_;
890 my $source_target = $source->{vm_type
} eq 'qemu' ?
"$QEMU_CONF$source->{vmid}.conf": "$LXC_CONF$source->{vmid}.conf";
891 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
893 if ($method eq 'ssh'){
894 if ($dest->{ip
} && $source->{ip
}) {
895 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
896 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
897 } elsif ($dest->{ip
}) {
898 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
899 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
900 } elsif ($source->{ip
}) {
901 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
902 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
905 if ($source->{destroy
}){
906 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
908 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
910 run_cmd
(['rm', '-f', '--', $dest_target_old]);
913 } elsif ($method eq 'local') {
914 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
915 run_cmd
(['cp', $source_target, $dest_target_new]);
920 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
921 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
927 my $cfg = read_cron
();
929 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
931 my $states = read_state
();
933 foreach my $source (sort keys%{$cfg}) {
934 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
935 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
936 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
937 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
947 my $job = get_job
($param);
948 $job->{state} = "ok";
956 my $job = get_job
($param);
957 $job->{state} = "stopped";
962 my $command = $ARGV[0];
964 my $commands = {'destroy' => 1,
973 if (!$command || !$commands->{$command}) {
978 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
979 \twill sync one time\n
981 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
983 \t\tmax sync speed in kBytes/s, default unlimited\n
984 \t-maxsnap\tinteger\n
985 \t\thow much snapshots will be kept before get erased, default 1/n
987 \t\tname of the sync job, if not set it is default.
988 \tIt is only necessary if scheduler allready contains this source.\n
990 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
992 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
993 \tCreate a sync Job\n
995 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
997 \t\tmax sync speed in kBytes/s, default unlimited\n
999 \t\thow much snapshots will be kept before get erased, default 1\n
1001 \t\tname of the sync job, if not set it is default\n
1003 \t\tif this flag is set it will skip the first sync\n
1005 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1007 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
1008 \tremove a sync Job from the scheduler\n
1010 \t\tname of the sync job, if not set it is default\n
1012 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1014 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
1015 \tGet help about specified command.\n
1018 \t-verbose\tboolean\n
1019 \t\tVerbose output format.\n";
1021 my $help_list = "$PROGNAME list\n
1022 \tGet a List of all scheduled Sync Jobs\n";
1024 my $help_status = "$PROGNAME status\n
1025 \tGet the status of all scheduled Sync Jobs\n";
1027 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
1028 \tenable a syncjob and reset error\n
1030 \t\tname of the sync job, if not set it is default\n
1032 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1034 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
1037 \t\tname of the sync job, if not set it is default\n
1039 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1055 die "$help_destroy\n";
1059 die "$help_create\n";
1067 die "$help_status\n";
1071 die "$help_enable\n";
1075 die "$help_enable\n";
1082 my $param = parse_argv
(@arg);
1088 die "$help_destroy\n" if !$param->{source
};
1089 check_target
($param->{source
});
1090 destroy_job
($param);
1094 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1095 check_target
($param->{source
});
1096 check_target
($param->{dest
});
1101 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1102 check_target
($param->{source
});
1103 check_target
($param->{dest
});
1116 my $help_command = $ARGV[1];
1117 if ($help_command && $commands->{$help_command}) {
1118 print help
($help_command);
1120 if ($param->{verbose
} == 1){
1121 exec("man $PROGNAME");
1128 die "$help_enable\n" if !$param->{source
};
1129 check_target
($param->{source
});
1134 die "$help_disable\n" if !$param->{source
};
1135 check_target
($param->{source
});
1136 disable_job
($param);
1143 print("ERROR:\tno command specified\n") if !$help;
1144 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1145 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1146 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1147 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1148 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1149 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1150 print("\t$PROGNAME list\n");
1151 print("\t$PROGNAME status\n");
1152 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1157 parse_target
($target);
1164 pve-zsync - PVE ZFS Replication Manager
1168 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1170 pve-zsync help <cmd> [OPTIONS]
1172 Get help about specified command.
1180 Verbose output format.
1182 pve-zsync create -dest <string> -source <string> [OPTIONS]
1188 the destination target is like [IP]:<Pool>[/Path]
1192 max sync speed in kBytes/s, default unlimited
1196 how much snapshots will be kept before get erased, default 1
1200 name of the sync job, if not set it is default
1204 if this flag is set it will skip the first sync
1208 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1210 pve-zsync destroy -source <string> [OPTIONS]
1212 remove a sync Job from the scheduler
1216 name of the sync job, if not set it is default
1220 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1222 pve-zsync disable -source <string> [OPTIONS]
1228 name of the sync job, if not set it is default
1232 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1234 pve-zsync enable -source <string> [OPTIONS]
1236 enable a syncjob and reset error
1240 name of the sync job, if not set it is default
1244 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1247 Get a List of all scheduled Sync Jobs
1251 Get the status of all scheduled Sync Jobs
1253 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1259 the destination target is like [IP:]<Pool>[/Path]
1263 max sync speed in kBytes/s, default unlimited
1267 how much snapshots will be kept before get erased, default 1
1271 name of the sync job, if not set it is default.
1272 It is only necessary if scheduler allready contains this source.
1276 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1280 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1281 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1282 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.
1283 To config cron see man crontab.
1285 =head2 PVE ZFS Storage sync Tool
1287 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1291 add sync job from local VM to remote ZFS Server
1292 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1294 =head1 IMPORTANT FILES
1296 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1298 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1300 =head1 COPYRIGHT AND DISCLAIMER
1302 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1304 This program is free software: you can redistribute it and/or modify it
1305 under the terms of the GNU Affero General Public License as published
1306 by the Free Software Foundation, either version 3 of the License, or
1307 (at your option) any later version.
1309 This program is distributed in the hope that it will be useful, but
1310 WITHOUT ANY WARRANTY; without even the implied warranty of
1311 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1312 Affero General Public License for more details.
1314 You should have received a copy of the GNU Affero General Public
1315 License along with this program. If not, see
1316 <http://www.gnu.org/licenses/>.