]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
caf071274eacb1fc609692df2ae6e96ee49e8f48
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") {
580 send_config
($source, $dest,'ssh');
583 &$sync_path($source, $dest, $job, $param, $date);
588 $job->{state} = "error";
592 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
598 $job->{state} = "ok";
599 $job->{lsync
} = $date;
608 my ($source, $dest, $max_snap, $name) = @_;
611 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
612 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
613 push @$cmd, $source->{all
};
615 my $raw = run_cmd
($cmd);
618 my $last_snap = undef;
621 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
623 if ($line =~ m/(rep_$name.*)$/) {
624 $last_snap = $1 if (!$last_snap);
627 if ($index == $max_snap) {
628 $source->{destroy
} = 1;
634 return ($old_snap, $last_snap) if $last_snap;
640 my ($source, $dest, $name, $date) = @_;
642 my $snap_name = "rep_$name\_".$date;
644 $source->{new_snap
} = $snap_name;
646 my $path = "$source->{all}\@$snap_name";
649 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
650 push @$cmd, 'zfs', 'snapshot', $path;
656 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
664 my $text = "SHELL=/bin/sh\n";
665 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
667 my $fh = IO
::File-
>new("> $CRONJOBS");
668 die "Could not open file: $!\n" if !$fh;
670 foreach my $source (sort keys%{$cfg}) {
671 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
672 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
673 $text .= "$PROG_PATH sync";
674 $text .= " -source ";
675 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
676 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
677 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
679 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
680 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
681 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
684 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
685 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
686 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
687 $text .= " -name $sync_name ";
688 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
689 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
693 die "Can't write to cron\n" if (!print($fh $text));
701 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
703 if ($target->{vm_type
} eq 'qemu') {
704 push @$cmd, 'qm', 'config', $target->{vmid
};
705 } elsif ($target->{vm_type
} eq 'lxc') {
706 push @$cmd, 'pct', 'config', $target->{vmid
};
708 die "VM Type unknown\n";
711 my $res = run_cmd
($cmd);
713 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
});
720 print "Start CMD\n" if $DEBUG;
721 print Dumper
$cmd if $DEBUG;
722 if (ref($cmd) eq 'ARRAY') {
723 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
725 my $output = `$cmd 2>&1`;
727 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
730 print Dumper
$output if $DEBUG;
731 print "END CMD\n" if $DEBUG;
736 my ($text, $ip) = @_;
741 while ($text && $text =~ s/^(.*?)(\n|$)//) {
744 next if $line =~ /cdrom|none/;
745 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
749 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
753 die "disk is not on ZFS Storage\n";
757 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
758 push @$cmd, 'pvesm', 'path', "$stor$disk";
759 my $path = run_cmd
($cmd);
761 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
763 my @array = split('/', $1);
764 $disks->{$num}->{pool
} = shift(@array);
765 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
767 $disks->{$num}->{path
} = join('/', @array);
768 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
770 $disks->{$num}->{last_part
} = $disk;
771 $disks->{$num}->{all
} .= "\/$disk";
774 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w
+.+)\
/(\w+.*)(\/$disk)$/) {
776 $disks->{$num}->{pool
} = $1;
777 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
780 $disks->{$num}->{path
} = $2;
781 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
784 $disks->{$num}->{last_part
} = $disk;
785 $disks->{$num}->{all
} .= "\/$disk";
790 die "ERROR: in path\n";
797 sub snapshot_destroy
{
798 my ($source, $dest, $method, $snap) = @_;
800 my @zfscmd = ('zfs', 'destroy');
801 my $snapshot = "$source->{all}\@$snap";
804 if($source->{ip
} && $method eq 'ssh'){
805 run_cmd
(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
807 run_cmd
([@zfscmd, $snapshot]);
814 my @ssh = $dest->{ip
} ?
('ssh', "root\@$dest->{ip}", '--') : ();
816 my $path = "$dest->{all}\/$source->{last_part}";
819 run_cmd
([@ssh, @zfscmd, "$path\@$snap"]);
828 my ($source ,$dest, $method) = @_;
831 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
832 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
833 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
836 eval {$text =run_cmd
($cmd);};
842 while ($text && $text =~ s/^(.*?)(\n|$)//) {
844 return 1 if $line =~ m/^.*$source->{old_snap}$/;
849 my ($source, $dest, $param) = @_;
853 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip
};
854 push @$cmd, 'zfs', 'send';
855 push @$cmd, '-v' if $param->{verbose
};
857 if($source->{last_snap
} && snapshot_exist
($source ,$dest, $param->{method})) {
858 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
860 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
862 if ($param->{limit
}){
863 my $bwl = $param->{limit
}*1024;
864 push @$cmd, \'|', 'cstream
', '-t
', $bwl;
866 my $target = "$dest->{all}/$source->{last_part}";
870 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
871 push @$cmd, 'zfs', 'recv', '-F', '--';
872 push @$cmd, "$target";
879 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
886 my ($source, $dest, $method) = @_;
888 my $source_target = $source->{vm_type
} eq 'qemu' ?
"$QEMU_CONF$source->{vmid}.conf": "$LXC_CONF$source->{vmid}.conf";
889 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
891 if ($method eq 'ssh'){
892 if ($dest->{ip
} && $source->{ip
}) {
893 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
894 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
895 } elsif ($dest->{ip
}) {
896 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
897 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
898 } elsif ($source->{ip
}) {
899 run_cmd
(['mkdir', '-p', '--', $CONFIG_PATH]);
900 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
903 if ($source->{destroy
}){
904 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
906 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
908 run_cmd
(['rm', '-f', '--', $dest_target_old]);
915 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
916 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
922 my $cfg = read_cron
();
924 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
926 my $states = read_state
();
928 foreach my $source (sort keys%{$cfg}) {
929 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
930 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
931 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
932 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
942 my $job = get_job
($param);
943 $job->{state} = "ok";
951 my $job = get_job
($param);
952 $job->{state} = "stopped";
957 my $command = $ARGV[0];
959 my $commands = {'destroy' => 1,
968 if (!$command || !$commands->{$command}) {
973 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
974 \twill sync one time\n
976 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
978 \t\tmax sync speed in kBytes/s, default unlimited\n
979 \t-maxsnap\tinteger\n
980 \t\thow much snapshots will be kept before get erased, default 1/n
982 \t\tname of the sync job, if not set it is default.
983 \tIt is only necessary if scheduler allready contains this source.\n
985 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
987 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
988 \tCreate a sync Job\n
990 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
992 \t\tmax sync speed in kBytes/s, default unlimited\n
994 \t\thow much snapshots will be kept before get erased, default 1\n
996 \t\tname of the sync job, if not set it is default\n
998 \t\tif this flag is set it will skip the first sync\n
1000 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1002 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
1003 \tremove a sync Job from the scheduler\n
1005 \t\tname of the sync job, if not set it is default\n
1007 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1009 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
1010 \tGet help about specified command.\n
1013 \t-verbose\tboolean\n
1014 \t\tVerbose output format.\n";
1016 my $help_list = "$PROGNAME list\n
1017 \tGet a List of all scheduled Sync Jobs\n";
1019 my $help_status = "$PROGNAME status\n
1020 \tGet the status of all scheduled Sync Jobs\n";
1022 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
1023 \tenable a syncjob and reset error\n
1025 \t\tname of the sync job, if not set it is default\n
1027 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1029 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
1032 \t\tname of the sync job, if not set it is default\n
1034 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1050 die "$help_destroy\n";
1054 die "$help_create\n";
1062 die "$help_status\n";
1066 die "$help_enable\n";
1070 die "$help_enable\n";
1077 my $param = parse_argv
(@arg);
1083 die "$help_destroy\n" if !$param->{source
};
1084 check_target
($param->{source
});
1085 destroy_job
($param);
1089 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1090 check_target
($param->{source
});
1091 check_target
($param->{dest
});
1096 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1097 check_target
($param->{source
});
1098 check_target
($param->{dest
});
1111 my $help_command = $ARGV[1];
1112 if ($help_command && $commands->{$help_command}) {
1113 print help
($help_command);
1115 if ($param->{verbose
} == 1){
1116 exec("man $PROGNAME");
1123 die "$help_enable\n" if !$param->{source
};
1124 check_target
($param->{source
});
1129 die "$help_disable\n" if !$param->{source
};
1130 check_target
($param->{source
});
1131 disable_job
($param);
1138 print("ERROR:\tno command specified\n") if !$help;
1139 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1140 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1141 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1142 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1143 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1144 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1145 print("\t$PROGNAME list\n");
1146 print("\t$PROGNAME status\n");
1147 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1152 parse_target
($target);
1159 pve-zsync - PVE ZFS Replication Manager
1163 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1165 pve-zsync help <cmd> [OPTIONS]
1167 Get help about specified command.
1175 Verbose output format.
1177 pve-zsync create -dest <string> -source <string> [OPTIONS]
1183 the destination target is like [IP]:<Pool>[/Path]
1187 max sync speed in kBytes/s, default unlimited
1191 how much snapshots will be kept before get erased, default 1
1195 name of the sync job, if not set it is default
1199 if this flag is set it will skip the first sync
1203 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1205 pve-zsync destroy -source <string> [OPTIONS]
1207 remove a sync Job from the scheduler
1211 name of the sync job, if not set it is default
1215 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1217 pve-zsync disable -source <string> [OPTIONS]
1223 name of the sync job, if not set it is default
1227 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1229 pve-zsync enable -source <string> [OPTIONS]
1231 enable a syncjob and reset error
1235 name of the sync job, if not set it is default
1239 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1242 Get a List of all scheduled Sync Jobs
1246 Get the status of all scheduled Sync Jobs
1248 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1254 the destination target is like [IP:]<Pool>[/Path]
1258 max sync speed in kBytes/s, default unlimited
1262 how much snapshots will be kept before get erased, default 1
1266 name of the sync job, if not set it is default.
1267 It is only necessary if scheduler allready contains this source.
1271 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1275 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1276 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1277 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.
1278 To config cron see man crontab.
1280 =head2 PVE ZFS Storage sync Tool
1282 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1286 add sync job from local VM to remote ZFS Server
1287 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1289 =head1 IMPORTANT FILES
1291 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1293 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1295 =head1 COPYRIGHT AND DISCLAIMER
1297 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1299 This program is free software: you can redistribute it and/or modify it
1300 under the terms of the GNU Affero General Public License as published
1301 by the Free Software Foundation, either version 3 of the License, or
1302 (at your option) any later version.
1304 This program is distributed in the hope that it will be useful, but
1305 WITHOUT ANY WARRANTY; without even the implied warranty of
1306 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1307 Affero General Public License for more details.
1309 You should have received a copy of the GNU Affero General Public
1310 License along with this program. If not, see
1311 <http://www.gnu.org/licenses/>.