]> git.proxmox.com Git - pve-zsync.git/blame - pve-zsync
fix to many slashes in recv pat
[pve-zsync.git] / pve-zsync
CommitLineData
0bc3e510
WL
1#!/usr/bin/perl
2
0bc3e510
WL
3use strict;
4use warnings;
5use Data::Dumper qw(Dumper);
6use Fcntl qw(:flock SEEK_END);
76b2c677
WL
7use Getopt::Long qw(GetOptionsFromArray);
8use File::Copy qw(move);
21a673e6 9use File::Path qw(make_path);
0bc3e510 10use Switch;
76b2c677
WL
11use JSON;
12use IO::File;
dc54fce8 13use String::ShellQuote 'shell_quote';
0bc3e510 14
c85692fa 15my $PROGNAME = "pve-zsync";
76b2c677
WL
16my $CONFIG_PATH = "/var/lib/${PROGNAME}/";
17my $STATE = "${CONFIG_PATH}sync_state";
c85692fa
WL
18my $CRONJOBS = "/etc/cron.d/$PROGNAME";
19my $PATH = "/usr/sbin/";
20my $QEMU_CONF = "/etc/pve/local/qemu-server/";
76b2c677 21my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock";
c85692fa 22my $PROG_PATH = "$PATH${PROGNAME}";
76b2c677
WL
23my $INTERVAL = 15;
24my $DEBUG = 0;
c85692fa 25
db2ce6d4
WB
26my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
27my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
28my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
29my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
30
31my $IPV6RE = "(?:" .
32 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
33 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
34 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
35 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
36 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
41
42my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
43my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
44my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
45# targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
46my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
47
0bc3e510
WL
48check_bin ('cstream');
49check_bin ('zfs');
50check_bin ('ssh');
51check_bin ('scp');
52
53sub check_bin {
54 my ($bin) = @_;
55
56 foreach my $p (split (/:/, $ENV{PATH})) {
57 my $fn = "$p/$bin";
58 if (-x $fn) {
59 return $fn;
60 }
61 }
62
76b2c677 63 die "unable to find command '$bin'\n";
0bc3e510
WL
64}
65
c85692fa
WL
66sub cut_target_width {
67 my ($target, $max) = @_;
0bc3e510 68
c85692fa
WL
69 return $target if (length($target) <= $max);
70 my @spl = split('/', $target);
0bc3e510
WL
71
72 my $count = length($spl[@spl-1]);
73 return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max;
74
75 $count += length($spl[0]) if @spl > 1;
76 return substr($spl[0], 0, $max-4-length($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
77
c85692fa 78 my $rest = 1;
0bc3e510
WL
79 $rest = $max-$count if ($max-$count > 0);
80
c85692fa 81 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
0bc3e510
WL
82}
83
84sub lock {
85 my ($fh) = @_;
76b2c677 86 flock($fh, LOCK_EX) || die "Can't lock config - $!\n";
0bc3e510
WL
87}
88
89sub unlock {
90 my ($fh) = @_;
76b2c677 91 flock($fh, LOCK_UN) || die "Can't unlock config- $!\n";
0bc3e510
WL
92}
93
76b2c677
WL
94sub get_status {
95 my ($source, $name, $status) = @_;
d3e1a943 96
76b2c677 97 if ($status->{$source->{all}}->{$name}->{status}) {
d3e1a943 98 return $status;
0bc3e510
WL
99 }
100
101 return undef;
102}
103
78d36df7 104sub check_pool_exists {
76b2c677 105 my ($target) = @_;
0bc3e510 106
271c2572
WB
107 my $cmd = [];
108 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
109 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all};
0bc3e510
WL
110 eval {
111 run_cmd($cmd);
112 };
113
76b2c677 114 if ($@) {
0bc3e510
WL
115 return 1;
116 }
117 return undef;
118}
119
76b2c677
WL
120sub parse_target {
121 my ($text) = @_;
0bc3e510 122
76b2c677
WL
123 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
124 my $target = {};
0bc3e510 125
db2ce6d4
WB
126 if ($text !~ $TARGETRE) {
127 die "$errstr\n";
76b2c677 128 }
db2ce6d4
WB
129 $target->{all} = $2;
130 $target->{ip} = $1 if $1;
131 my @parts = split('/', $2);
76b2c677 132
db2ce6d4 133 $target->{ip} =~ s/^\[(.*)\]$/$1/ if $target->{ip};
76b2c677 134
db2ce6d4
WB
135 my $pool = $target->{pool} = shift(@parts);
136 die "$errstr\n" if !$pool;
137
138 if ($pool =~ m/^\d+$/) {
139 $target->{vmid} = $pool;
76b2c677
WL
140 delete $target->{pool};
141 }
142
143 return $target if (@parts == 0);
144 $target->{last_part} = pop(@parts);
145
146 if ($target->{ip}) {
147 pop(@parts);
148 }
149 if (@parts > 0) {
150 $target->{path} = join('/', @parts);
151 }
152
153 return $target;
0bc3e510
WL
154}
155
76b2c677 156sub read_cron {
0bc3e510 157
76b2c677
WL
158 #This is for the first use to init file;
159 if (!-e $CRONJOBS) {
160 my $new_fh = IO::File->new("> $CRONJOBS");
161 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
162 close($new_fh);
0bc3e510
WL
163 return undef;
164 }
165
76b2c677
WL
166 my $fh = IO::File->new("< $CRONJOBS");
167 die "Could not open file $CRONJOBS: $!\n" if !$fh;
0bc3e510 168
76b2c677 169 my @text = <$fh>;
0bc3e510 170
1a7871e7
WL
171 close($fh);
172
76b2c677
WL
173 return encode_cron(@text);
174}
0bc3e510 175
76b2c677
WL
176sub parse_argv {
177 my (@arg) = @_;
178
179 my $param = {};
180 $param->{dest} = undef;
181 $param->{source} = undef;
182 $param->{verbose} = undef;
183 $param->{limit} = undef;
184 $param->{maxsnap} = undef;
185 $param->{name} = undef;
186 $param->{skip} = undef;
187 $param->{method} = undef;
188
189 my ($ret, $ar) = GetOptionsFromArray(\@arg,
190 'dest=s' => \$param->{dest},
191 'source=s' => \$param->{source},
192 'verbose' => \$param->{verbose},
193 'limit=i' => \$param->{limit},
194 'maxsnap=i' => \$param->{maxsnap},
195 'name=s' => \$param->{name},
196 'skip' => \$param->{skip},
197 'method=s' => \$param->{method});
198
199 if ($ret == 0) {
200 die "can't parse options\n";
201 }
202
203 $param->{name} = "default" if !$param->{name};
204 $param->{maxsnap} = 1 if !$param->{maxsnap};
205 $param->{method} = "ssh" if !$param->{method};
206
207 return $param;
0bc3e510
WL
208}
209
76b2c677
WL
210sub add_state_to_job {
211 my ($job) = @_;
28006d67 212
76b2c677
WL
213 my $states = read_state();
214 my $state = $states->{$job->{source}}->{$job->{name}};
215
216 $job->{state} = $state->{state};
217 $job->{lsync} = $state->{lsync};
218
219 for (my $i = 0; $state->{"snap$i"}; $i++) {
220 $job->{"snap$i"} = $state->{"snap$i"};
0bc3e510 221 }
c85692fa 222
76b2c677 223 return $job;
0bc3e510
WL
224}
225
76b2c677
WL
226sub encode_cron {
227 my (@text) = @_;
228
0bc3e510 229 my $cfg = {};
0bc3e510 230
76b2c677
WL
231 while (my $line = shift(@text)) {
232
233 my @arg = split('\s', $line);
234 my $param = parse_argv(@arg);
235
236 if ($param->{source} && $param->{dest}) {
237 $cfg->{$param->{source}}->{$param->{name}}->{dest} = $param->{dest};
238 $cfg->{$param->{source}}->{$param->{name}}->{verbose} = $param->{verbose};
239 $cfg->{$param->{source}}->{$param->{name}}->{limit} = $param->{limit};
240 $cfg->{$param->{source}}->{$param->{name}}->{maxsnap} = $param->{maxsnap};
241 $cfg->{$param->{source}}->{$param->{name}}->{skip} = $param->{skip};
242 $cfg->{$param->{source}}->{$param->{name}}->{method} = $param->{method};
0bc3e510
WL
243 }
244 }
76b2c677 245
0bc3e510
WL
246 return $cfg;
247}
248
76b2c677
WL
249sub param_to_job {
250 my ($param) = @_;
251
252 my $job = {};
253
254 my $source = parse_target($param->{source});
255 my $dest = parse_target($param->{dest}) if $param->{dest};
256
257 $job->{name} = !$param->{name} ? "default" : $param->{name};
258 $job->{dest} = $param->{dest} if $param->{dest};
259 $job->{method} = "local" if !$dest->{ip} && !$source->{ip};
260 $job->{method} = "ssh" if !$job->{method};
261 $job->{limit} = $param->{limit};
262 $job->{maxsnap} = $param->{maxsnap} if $param->{maxsnap};
263 $job->{source} = $param->{source};
264
265 return $job;
266}
267
268sub read_state {
0bc3e510 269
76b2c677 270 if (!-e $STATE) {
21a673e6 271 make_path $CONFIG_PATH;
76b2c677
WL
272 my $new_fh = IO::File->new("> $STATE");
273 die "Could not create $STATE: $!\n" if !$new_fh;
a018f134 274 print $new_fh "{}";
76b2c677
WL
275 close($new_fh);
276 return undef;
277 }
278
279 my $fh = IO::File->new("< $STATE");
280 die "Could not open file $STATE: $!\n" if !$fh;
281
282 my $text = <$fh>;
283 my $states = decode_json($text);
0bc3e510 284
76b2c677
WL
285 close($fh);
286
287 return $states;
288}
289
290sub update_state {
291 my ($job) = @_;
292 my $text;
293 my $in_fh;
294
295 eval {
0bc3e510 296
76b2c677
WL
297 $in_fh = IO::File->new("< $STATE");
298 die "Could not open file $STATE: $!\n" if !$in_fh;
299 lock($in_fh);
300 $text = <$in_fh>;
301 };
302
303 my $out_fh = IO::File->new("> $STATE.new");
304 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
305
306 my $states = {};
307 my $state = {};
308 if ($text){
309 $states = decode_json($text);
310 $state = $states->{$job->{source}}->{$job->{name}};
311 }
312
313 if ($job->{state} ne "del") {
314 $state->{state} = $job->{state};
315 $state->{lsync} = $job->{lsync};
316
317 for (my $i = 0; $job->{"snap$i"} ; $i++) {
318 $state->{"snap$i"} = $job->{"snap$i"};
0bc3e510 319 }
76b2c677
WL
320 $states->{$job->{source}}->{$job->{name}} = $state;
321 } else {
322
323 delete $states->{$job->{source}}->{$job->{name}};
324 delete $states->{$job->{source}} if !keys %{$states->{$job->{source}}};
325 }
326
327 $text = encode_json($states);
328 print $out_fh $text;
329
330 close($out_fh);
331 move("$STATE.new", $STATE);
332 eval {
333 close($in_fh);
334 };
335
336 return $states;
337}
338
339sub update_cron {
340 my ($job) = @_;
341
342 my $updated;
343 my $has_header;
344 my $line_no = 0;
345 my $text = "";
346 my $header = "SHELL=/bin/sh\n";
347 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
348
349 my $fh = IO::File->new("< $CRONJOBS");
350 die "Could not open file $CRONJOBS: $!\n" if !$fh;
351 lock($fh);
0bc3e510 352
76b2c677
WL
353 my @test = <$fh>;
354
355 while (my $line = shift(@test)) {
356 chomp($line);
357 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
358 $updated = 1;
359 next if $job->{state} eq "del";
360 $text .= format_job($job, $line);
0bc3e510 361 } else {
76b2c677
WL
362 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
363 $has_header = 1;
0bc3e510 364 }
76b2c677 365 $text .= "$line\n";
0bc3e510 366 }
76b2c677
WL
367 $line_no++;
368 }
369
370 if (!$has_header) {
371 $text = "$header$text";
372 }
0bc3e510 373
76b2c677
WL
374 if (!$updated) {
375 $text .= format_job($job);
0bc3e510 376 }
76b2c677
WL
377 my $new_fh = IO::File->new("> ${CRONJOBS}.new");
378 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
379
380 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
381 close ($new_fh);
382
383 die "can't move $CRONJOBS.new: $!\n" if !move("${CRONJOBS}.new", "$CRONJOBS");
384 close ($fh);
0bc3e510
WL
385}
386
76b2c677
WL
387sub format_job {
388 my ($job, $line) = @_;
389 my $text = "";
0bc3e510 390
6b4f676d 391 if ($job->{state} eq "stopped") {
76b2c677
WL
392 $text = "#";
393 }
394 if ($line) {
395 $line =~ /^#*(.+) root/;
396 $text .= $1;
397 } else {
398 $text .= "*/$INTERVAL * * * *";
399 }
400 $text .= " root";
401 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
402 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
403 $text .= " --method $job->{method}";
404 $text .= " --verbose" if $job->{verbose};
405 $text .= "\n";
406
407 return $text;
408}
0bc3e510 409
76b2c677 410sub list {
d3e1a943 411
76b2c677 412 my $cfg = read_cron();
28006d67 413
76b2c677 414 my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE");
28006d67 415
76b2c677
WL
416 my $states = read_state();
417 foreach my $source (sort keys%{$cfg}) {
418 foreach my $name (sort keys%{$cfg->{$source}}) {
419 $list .= sprintf("%-25s", cut_target_width($source, 25));
420 $list .= sprintf("%-15s", cut_target_width($name, 15));
421 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
422 $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync});
423 $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method});
0bc3e510
WL
424 }
425 }
426
427 return $list;
428}
429
430sub vm_exists {
431 my ($target) = @_;
432
271c2572
WB
433 my $cmd = [];
434 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
435 push @$cmd, 'qm', 'status', $target->{vmid};
0bc3e510
WL
436
437 my $res = run_cmd($cmd);
438
439 return 1 if ($res =~ m/^status.*$/);
440 return undef;
441}
442
443sub init {
444 my ($param) = @_;
445
76b2c677 446 my $cfg = read_cron();
0bc3e510 447
76b2c677 448 my $job = param_to_job($param);
0bc3e510 449
76b2c677
WL
450 $job->{state} = "ok";
451 $job->{lsync} = 0;
0bc3e510
WL
452
453 my $source = parse_target($param->{source});
454 my $dest = parse_target($param->{dest});
455
76b2c677 456 if (my $ip = $dest->{ip}) {
271c2572 457 run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
0bc3e510
WL
458 }
459
c85692fa 460 if (my $ip = $source->{ip}) {
271c2572 461 run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
0bc3e510
WL
462 }
463
78d36df7 464 die "Pool $dest->{all} does not exists\n" if check_pool_exists($dest);
0bc3e510 465
78d36df7 466 my $check = check_pool_exists($source->{path}, $source->{ip}) if !$source->{vmid} && $source->{path};
0bc3e510 467
76b2c677 468 die "Pool $source->{path} does not exists\n" if undef($check);
8362418a 469
76b2c677 470 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid} && !vm_exists($source);
0bc3e510 471
76b2c677 472 die "Config already exists\n" if $cfg->{$job->{source}}->{$job->{name}};
0bc3e510 473
76b2c677
WL
474 update_cron($job);
475 update_state($job);
c85692fa
WL
476
477 eval {
478 sync($param) if !$param->{skip};
479 };
1a7871e7 480 if(my $err = $@) {
c85692fa 481 destroy_job($param);
1a7871e7
WL
482 print $err;
483 }
0bc3e510
WL
484}
485
76b2c677
WL
486sub get_job {
487 my ($param) = @_;
488
489 my $cfg = read_cron();
490
491 if (!$cfg->{$param->{source}}->{$param->{name}}) {
492 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
493 }
494 my $job = $cfg->{$param->{source}}->{$param->{name}};
495 $job->{name} = $param->{name};
496 $job->{source} = $param->{source};
497 $job = add_state_to_job($job);
498
499 return $job;
500}
501
c85692fa 502sub destroy_job {
0bc3e510
WL
503 my ($param) = @_;
504
76b2c677
WL
505 my $job = get_job($param);
506 $job->{state} = "del";
b0842810 507
76b2c677
WL
508 update_cron($job);
509 update_state($job);
0bc3e510
WL
510}
511
512sub sync {
513 my ($param) = @_;
76b2c677
WL
514
515 my $lock_fh = IO::File->new("> $LOCKFILE");
516 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
b0842810 517 lock($lock_fh);
0bc3e510 518
76b2c677
WL
519 my $date = get_date();
520 my $job;
521 eval {
522 $job = get_job($param);
523 };
0bc3e510 524
a018f134
WL
525 if ($job && $job->{state} eq "syncing") {
526 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
76b2c677 527 }
0bc3e510
WL
528
529 my $dest = parse_target($param->{dest});
530 my $source = parse_target($param->{source});
531
532 my $sync_path = sub {
76b2c677 533 my ($source, $dest, $job, $param, $date) = @_;
0bc3e510 534
76b2c677 535 ($source->{old_snap},$source->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name});
6b4f676d 536
a018f134 537 snapshot_add($source, $dest, $param->{name}, $date);
0bc3e510 538
a018f134
WL
539 send_image($source, $dest, $param);
540
541 snapshot_destroy($source, $dest, $param->{method}, $source->{old_snap}) if ($source->{destroy} && $source->{old_snap});
0bc3e510 542
0bc3e510
WL
543 };
544
76b2c677 545 if ($job) {
76b2c677
WL
546 $job->{state} = "syncing";
547 update_state($job);
548 }
0bc3e510 549
a018f134
WL
550 eval{
551 if ($source->{vmid}) {
552 die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source);
553 my $disks = get_disks($source);
554
555 foreach my $disk (sort keys %{$disks}) {
556 $source->{all} = $disks->{$disk}->{all};
557 $source->{pool} = $disks->{$disk}->{pool};
558 $source->{path} = $disks->{$disk}->{path} if $disks->{$disk}->{path};
559 $source->{last_part} = $disks->{$disk}->{last_part};
560 &$sync_path($source, $dest, $job, $param, $date);
561 }
562 if ($param->{method} eq "ssh") {
563 send_config($source, $dest,'ssh');
564 }
565 } else {
76b2c677 566 &$sync_path($source, $dest, $job, $param, $date);
0bc3e510 567 }
a018f134
WL
568 };
569 if(my $err = $@) {
570 if ($job) {
571 $job->{state} = "error";
572 update_state($job);
573 unlock($lock_fh);
574 close($lock_fh);
575 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
7d93705f 576 }
a018f134 577 die "$err\n";
76b2c677
WL
578 }
579
580 if ($job) {
581 $job->{state} = "ok";
582 $job->{lsync} = $date;
583 update_state($job);
0bc3e510 584 }
76b2c677 585
c85692fa 586 unlock($lock_fh);
b0842810 587 close($lock_fh);
0bc3e510
WL
588}
589
590sub snapshot_get{
591 my ($source, $dest, $max_snap, $name) = @_;
592
271c2572
WB
593 my $cmd = [];
594 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
595 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
596 push @$cmd, $source->{all};
0bc3e510
WL
597
598 my $raw = run_cmd($cmd);
76b2c677 599 my $index = 0;
0bc3e510
WL
600 my $line = "";
601 my $last_snap = undef;
76b2c677 602 my $old_snap;
0bc3e510
WL
603
604 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
605 $line = $1;
76b2c677
WL
606 if ($line =~ m/(rep_$name.*)$/) {
607 $last_snap = $1 if (!$last_snap);
608 $old_snap = $1;
609 $index++;
610 if ($index == $max_snap) {
611 $source->{destroy} = 1;
612 last;
613 };
614 }
0bc3e510
WL
615 }
616
76b2c677 617 return ($old_snap, $last_snap) if $last_snap;
0bc3e510
WL
618
619 return undef;
620}
621
622sub snapshot_add {
76b2c677 623 my ($source, $dest, $name, $date) = @_;
0bc3e510
WL
624
625 my $snap_name = "rep_$name\_".$date;
626
627 $source->{new_snap} = $snap_name;
628
76b2c677 629 my $path = "$source->{all}\@$snap_name";
0bc3e510 630
271c2572
WB
631 my $cmd = [];
632 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
633 push @$cmd, 'zfs', 'snapshot', $path;
0bc3e510
WL
634 eval{
635 run_cmd($cmd);
636 };
637
c85692fa 638 if (my $err = $@) {
0bc3e510
WL
639 snapshot_destroy($source, $dest, 'ssh', $snap_name);
640 die "$err\n";
641 }
0bc3e510
WL
642}
643
c85692fa 644sub write_cron {
28006d67 645 my ($cfg) = @_;
0bc3e510 646
c85692fa
WL
647 my $text = "SHELL=/bin/sh\n";
648 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
2e8fd72a 649
76b2c677
WL
650 my $fh = IO::File->new("> $CRONJOBS");
651 die "Could not open file: $!\n" if !$fh;
0bc3e510 652
c85692fa
WL
653 foreach my $source (sort keys%{$cfg}) {
654 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
28006d67 655 next if $cfg->{$source}->{$sync_name}->{status} ne 'ok';
c85692fa 656 $text .= "$PROG_PATH sync";
0bc3e510 657 $text .= " -source ";
28006d67
WL
658 if ($cfg->{$source}->{$sync_name}->{vmid}) {
659 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
660 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
0bc3e510 661 } else {
28006d67
WL
662 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
663 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
664 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path};
0bc3e510
WL
665 }
666 $text .= " -dest ";
28006d67
WL
667 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip};
668 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
669 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path};
670 $text .= " -name $sync_name ";
671 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit};
672 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap};
0bc3e510 673 $text .= "\n";
0bc3e510
WL
674 }
675 }
c85692fa 676 die "Can't write to cron\n" if (!print($fh $text));
0bc3e510
WL
677 close($fh);
678}
679
680sub get_disks {
681 my ($target) = @_;
682
271c2572
WB
683 my $cmd = [];
684 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
685 push @$cmd, 'qm', 'config', $target->{vmid};
0bc3e510
WL
686
687 my $res = run_cmd($cmd);
688
689 my $disks = parse_disks($res, $target->{ip});
690
691 return $disks;
692}
693
694sub run_cmd {
695 my ($cmd) = @_;
696 print "Start CMD\n" if $DEBUG;
697 print Dumper $cmd if $DEBUG;
dc54fce8
WB
698 if (ref($cmd) eq 'ARRAY') {
699 $cmd = join(' ', map { ref($_) ? $$_ : shell_quote($_) } @$cmd);
700 }
0bc3e510
WL
701 my $output = `$cmd 2>&1`;
702
a018f134 703 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
0bc3e510
WL
704
705 chomp($output);
706 print Dumper $output if $DEBUG;
707 print "END CMD\n" if $DEBUG;
708 return $output;
709}
710
711sub parse_disks {
712 my ($text, $ip) = @_;
713
714 my $disks;
715
716 my $num = 0;
0bc3e510
WL
717 while ($text && $text =~ s/^(.*?)(\n|$)//) {
718 my $line = $1;
78cd57dd
WB
719
720 next if $line =~ /cdrom|none/;
845e36d7 721 next if $line !~ m/^(?:virtio|ide|scsi|sata)\d+: /;
78cd57dd 722
0bc3e510
WL
723 my $disk = undef;
724 my $stor = undef;
4f865f28
WB
725 if($line =~ m/^(?:virtio|ide|scsi|sata)\d+: (.+:)([A-Za-z0-9\-]+),(.*)$/) {
726 $disk = $2;
727 $stor = $1;
845e36d7
WB
728 } else {
729 die "disk is not on ZFS Storage\n";
0bc3e510
WL
730 }
731
271c2572
WB
732 my $cmd = [];
733 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
734 push @$cmd, 'pvesm', 'path', "$stor$disk";
b52d13b3
WB
735 my $path = run_cmd($cmd);
736
ea3ede00 737 if ($path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
b52d13b3
WB
738
739 my @array = split('/', $1);
3df8b97d 740 $disks->{$num}->{pool} = shift(@array);
b52d13b3
WB
741 $disks->{$num}->{all} = $disks->{$num}->{pool};
742 if (0 < @array) {
743 $disks->{$num}->{path} = join('/', @array);
744 $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
0bc3e510 745 }
b52d13b3
WB
746 $disks->{$num}->{last_part} = $disk;
747 $disks->{$num}->{all} .= "\/$disk";
748
749 $num++;
750
751 } else {
752 die "ERROR: in path\n";
0bc3e510
WL
753 }
754 }
76b2c677 755
0bc3e510
WL
756 return $disks;
757}
758
759sub snapshot_destroy {
760 my ($source, $dest, $method, $snap) = @_;
761
271c2572 762 my @zfscmd = ('zfs', 'destroy');
76b2c677 763 my $snapshot = "$source->{all}\@$snap";
0bc3e510
WL
764
765 eval {
766 if($source->{ip} && $method eq 'ssh'){
271c2572 767 run_cmd(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
0bc3e510 768 } else {
271c2572 769 run_cmd([@zfscmd, $snapshot]);
0bc3e510
WL
770 }
771 };
772 if (my $erro = $@) {
773 warn "WARN: $erro";
774 }
c85692fa 775 if ($dest) {
271c2572 776 my @ssh = $dest->{ip} ? ('ssh', "root\@$dest->{ip}", '--') : ();
0bc3e510 777
76b2c677 778 my $path = "$dest->{all}\/$source->{last_part}";
0bc3e510 779
0bc3e510 780 eval {
271c2572 781 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
0bc3e510
WL
782 };
783 if (my $erro = $@) {
784 warn "WARN: $erro";
785 }
786 }
787}
788
789sub snapshot_exist {
790 my ($source ,$dest, $method) = @_;
791
271c2572
WB
792 my $cmd = [];
793 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
794 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
795 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
0bc3e510
WL
796
797 my $text = "";
798 eval {$text =run_cmd($cmd);};
799 if (my $erro = $@) {
800 warn "WARN: $erro";
801 return undef;
802 }
803
804 while ($text && $text =~ s/^(.*?)(\n|$)//) {
805 my $line = $1;
806 return 1 if $line =~ m/^.*$source->{old_snap}$/;
807 }
808}
809
810sub send_image {
76b2c677 811 my ($source, $dest, $param) = @_;
0bc3e510 812
271c2572 813 my $cmd = [];
0bc3e510 814
271c2572
WB
815 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip};
816 push @$cmd, 'zfs', 'send';
817 push @$cmd, '-v' if $param->{verbose};
0bc3e510 818
76b2c677 819 if($source->{last_snap} && snapshot_exist($source ,$dest, $param->{method})) {
271c2572 820 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
0bc3e510 821 }
271c2572 822 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
0bc3e510 823
76b2c677
WL
824 if ($param->{limit}){
825 my $bwl = $param->{limit}*1024;
271c2572 826 push @$cmd, \'|', 'cstream', '-t', $bwl;
0bc3e510 827 }
1193273e
WL
828 my $target = "$dest->{all}/$source->{last_part}";
829 $target =~ s!/+!/!g;
830
271c2572
WB
831 push @$cmd, \'|';
832 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
833 push @$cmd, 'zfs', 'recv', '--';
1193273e 834 push @$cmd, "$target\@$source->{new_snap}";
0bc3e510
WL
835
836 eval {
837 run_cmd($cmd)
838 };
839
840 if (my $erro = $@) {
76b2c677 841 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
0bc3e510
WL
842 die $erro;
843 };
0bc3e510
WL
844}
845
846
847sub send_config{
848 my ($source, $dest, $method) = @_;
849
76b2c677
WL
850 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
851 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
852
0bc3e510
WL
853 if ($method eq 'ssh'){
854 if ($dest->{ip} && $source->{ip}) {
271c2572
WB
855 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
856 run_cmd(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 857 } elsif ($dest->{ip}) {
271c2572
WB
858 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
859 run_cmd(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 860 } elsif ($source->{ip}) {
271c2572
WB
861 run_cmd(['mkdir', '-p', '--', $CONFIG_PATH]);
862 run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
0bc3e510
WL
863 }
864
865 if ($source->{destroy}){
76b2c677 866 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
0bc3e510 867 if($dest->{ip}){
271c2572 868 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
0bc3e510 869 } else {
271c2572 870 run_cmd(['rm', '-f', '--', $dest_target_old]);
0bc3e510
WL
871 }
872 }
873 }
874}
875
876sub get_date {
c85692fa
WL
877 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
878 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
0bc3e510
WL
879
880 return $datestamp;
881}
882
883sub status {
76b2c677 884 my $cfg = read_cron();
0bc3e510 885
c85692fa 886 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
0bc3e510 887
76b2c677
WL
888 my $states = read_state();
889
c85692fa
WL
890 foreach my $source (sort keys%{$cfg}) {
891 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
76b2c677
WL
892 $status_list .= sprintf("%-25s", cut_target_width($source, 25));
893 $status_list .= sprintf("%-15s", cut_target_width($sync_name, 25));
894 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
0bc3e510
WL
895 }
896 }
897
898 return $status_list;
899}
900
c85692fa 901sub enable_job {
28006d67
WL
902 my ($param) = @_;
903
76b2c677
WL
904 my $job = get_job($param);
905 $job->{state} = "ok";
906 update_state($job);
907 update_cron($job);
28006d67
WL
908}
909
c85692fa 910sub disable_job {
28006d67
WL
911 my ($param) = @_;
912
76b2c677 913 my $job = get_job($param);
6b4f676d 914 $job->{state} = "stopped";
76b2c677
WL
915 update_state($job);
916 update_cron($job);
28006d67
WL
917}
918
0bc3e510
WL
919my $command = $ARGV[0];
920
921my $commands = {'destroy' => 1,
922 'create' => 1,
923 'sync' => 1,
924 'list' => 1,
925 'status' => 1,
28006d67
WL
926 'help' => 1,
927 'enable' => 1,
928 'disable' => 1};
0bc3e510 929
28006d67 930if (!$command || !$commands->{$command}) {
0bc3e510
WL
931 usage();
932 die "\n";
933}
934
28006d67 935my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
0bc3e510
WL
936\twill sync one time\n
937\t-dest\tstring\n
938\t\tthe destination target is like [IP:]<Pool>[/Path]\n
939\t-limit\tinteger\n
940\t\tmax sync speed in kBytes/s, default unlimited\n
941\t-maxsnap\tinteger\n
942\t\thow much snapshots will be kept before get erased, default 1/n
943\t-name\tstring\n
944\t\tname of the sync job, if not set it is default.
945\tIt is only necessary if scheduler allready contains this source.\n
946\t-source\tstring\n
947\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
948
28006d67 949my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
0bc3e510
WL
950\tCreate a sync Job\n
951\t-dest\tstringn\n
952\t\tthe destination target is like [IP]:<Pool>[/Path]\n
0bc3e510 953\t-limit\tinteger\n
28006d67 954\t\tmax sync speed in kBytes/s, default unlimited\n
0bc3e510
WL
955\t-maxsnap\tstring\n
956\t\thow much snapshots will be kept before get erased, default 1\n
957\t-name\tstring\n
958\t\tname of the sync job, if not set it is default\n
959\t-skip\tboolean\n
960\t\tif this flag is set it will skip the first sync\n
961\t-source\tstring\n
962\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
963
28006d67 964my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
0bc3e510
WL
965\tremove a sync Job from the scheduler\n
966\t-name\tstring\n
967\t\tname of the sync job, if not set it is default\n
968\t-source\tstring\n
969\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
970
28006d67 971my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
0bc3e510
WL
972\tGet help about specified command.\n
973\t<cmd>\tstring\n
974\t\tCommand name\n
975\t-verbose\tboolean\n
976\t\tVerbose output format.\n";
977
28006d67 978my $help_list = "$PROGNAME list\n
0bc3e510
WL
979\tGet a List of all scheduled Sync Jobs\n";
980
28006d67 981my $help_status = "$PROGNAME status\n
0bc3e510
WL
982\tGet the status of all scheduled Sync Jobs\n";
983
28006d67
WL
984my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
985\tenable a syncjob and reset error\n
986\t-name\tstring\n
987\t\tname of the sync job, if not set it is default\n
988\t-source\tstring\n
989\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
990
991my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
992\tpause a syncjob\n
993\t-name\tstring\n
994\t\tname of the sync job, if not set it is default\n
995\t-source\tstring\n
996\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
997
c85692fa 998sub help {
0bc3e510
WL
999 my ($command) = @_;
1000
1001 switch($command){
1002 case 'help'
1003 {
1004 die "$help_help\n";
1005 }
1006 case 'sync'
1007 {
1008 die "$help_sync\n";
1009 }
1010 case 'destroy'
1011 {
1012 die "$help_destroy\n";
1013 }
1014 case 'create'
1015 {
1016 die "$help_create\n";
1017 }
1018 case 'list'
1019 {
1020 die "$help_list\n";
1021 }
1022 case 'status'
1023 {
1024 die "$help_status\n";
1025 }
28006d67
WL
1026 case 'enable'
1027 {
1028 die "$help_enable\n";
1029 }
1030 case 'disable'
1031 {
1032 die "$help_enable\n";
1033 }
0bc3e510
WL
1034 }
1035
1036}
1037
76b2c677
WL
1038my @arg = @ARGV;
1039my $param = parse_argv(@arg);
0bc3e510 1040
0bc3e510 1041
c85692fa 1042switch($command) {
0bc3e510
WL
1043 case "destroy"
1044 {
76b2c677
WL
1045 die "$help_destroy\n" if !$param->{source};
1046 check_target($param->{source});
c85692fa 1047 destroy_job($param);
0bc3e510
WL
1048 }
1049 case "sync"
1050 {
76b2c677
WL
1051 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1052 check_target($param->{source});
1053 check_target($param->{dest});
0bc3e510
WL
1054 sync($param);
1055 }
1056 case "create"
1057 {
76b2c677
WL
1058 die "$help_create\n" if !$param->{source} || !$param->{dest};
1059 check_target($param->{source});
1060 check_target($param->{dest});
0bc3e510
WL
1061 init($param);
1062 }
1063 case "status"
1064 {
1065 print status();
1066 }
1067 case "list"
1068 {
1069 print list();
1070 }
1071 case "help"
1072 {
1073 my $help_command = $ARGV[1];
1074 if ($help_command && $commands->{$help_command}) {
1075 print help($help_command);
1076 }
76b2c677 1077 if ($param->{verbose} == 1){
0bc3e510
WL
1078 exec("man $PROGNAME");
1079 } else {
1080 usage(1);
1081 }
1082 }
28006d67
WL
1083 case "enable"
1084 {
76b2c677
WL
1085 die "$help_enable\n" if !$param->{source};
1086 check_target($param->{source});
c85692fa 1087 enable_job($param);
28006d67
WL
1088 }
1089 case "disable"
1090 {
76b2c677
WL
1091 die "$help_disable\n" if !$param->{source};
1092 check_target($param->{source});
c85692fa 1093 disable_job($param);
28006d67 1094 }
0bc3e510
WL
1095}
1096
c85692fa 1097sub usage {
0bc3e510
WL
1098 my ($help) = @_;
1099
1100 print("ERROR:\tno command specified\n") if !$help;
1101 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
28006d67
WL
1102 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1103 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1104 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1105 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1106 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1107 print("\t$PROGNAME list\n");
1108 print("\t$PROGNAME status\n");
1109 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
0bc3e510
WL
1110}
1111
c85692fa 1112sub check_target {
0bc3e510 1113 my ($target) = @_;
db2ce6d4 1114 parse_target($target);
0bc3e510
WL
1115}
1116
1117__END__
1118
1119=head1 NAME
1120
1121pve-zsync - PVE ZFS Replication Manager
1122
1123=head1 SYNOPSIS
1124
c85692fa 1125pve-zsync <COMMAND> [ARGS] [OPTIONS]
0bc3e510 1126
c85692fa 1127pve-zsync help <cmd> [OPTIONS]
0bc3e510
WL
1128
1129 Get help about specified command.
1130
1131 <cmd> string
1132
1133 Command name
1134
1135 -verbose boolean
1136
1137 Verbose output format.
1138
c85692fa 1139pve-zsync create -dest <string> -source <string> [OPTIONS]
0bc3e510 1140
6b4f676d 1141 Create a sync Job
0bc3e510 1142
6b4f676d 1143 -dest string
0bc3e510
WL
1144
1145 the destination target is like [IP]:<Pool>[/Path]
1146
6b4f676d 1147 -limit integer
0bc3e510 1148
28006d67 1149 max sync speed in kBytes/s, default unlimited
0bc3e510 1150
6b4f676d 1151 -maxsnap string
0bc3e510
WL
1152
1153 how much snapshots will be kept before get erased, default 1
1154
6b4f676d 1155 -name string
0bc3e510
WL
1156
1157 name of the sync job, if not set it is default
1158
6b4f676d 1159 -skip boolean
0bc3e510
WL
1160
1161 if this flag is set it will skip the first sync
1162
6b4f676d 1163 -source string
0bc3e510
WL
1164
1165 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1166
c85692fa 1167pve-zsync destroy -source <string> [OPTIONS]
0bc3e510 1168
6b4f676d 1169 remove a sync Job from the scheduler
0bc3e510 1170
6b4f676d 1171 -name string
0bc3e510
WL
1172
1173 name of the sync job, if not set it is default
1174
6b4f676d 1175 -source string
0bc3e510
WL
1176
1177 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1178
c85692fa 1179pve-zsync disable -source <string> [OPTIONS]
28006d67 1180
6b4f676d 1181 pause a sync job
28006d67 1182
6b4f676d 1183 -name string
28006d67
WL
1184
1185 name of the sync job, if not set it is default
1186
6b4f676d 1187 -source string
28006d67
WL
1188
1189 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1190
c85692fa 1191pve-zsync enable -source <string> [OPTIONS]
28006d67 1192
6b4f676d 1193 enable a syncjob and reset error
28006d67 1194
6b4f676d 1195 -name string
28006d67
WL
1196
1197 name of the sync job, if not set it is default
1198
6b4f676d 1199 -source string
28006d67
WL
1200
1201 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
c85692fa 1202pve-zsync list
0bc3e510
WL
1203
1204 Get a List of all scheduled Sync Jobs
1205
c85692fa 1206pve-zsync status
0bc3e510
WL
1207
1208 Get the status of all scheduled Sync Jobs
1209
c85692fa 1210pve-zsync sync -dest <string> -source <string> [OPTIONS]
0bc3e510
WL
1211
1212 will sync one time
1213
1214 -dest string
1215
1216 the destination target is like [IP:]<Pool>[/Path]
1217
1218 -limit integer
1219
1220 max sync speed in kBytes/s, default unlimited
1221
6b4f676d 1222 -maxsnap integer
0bc3e510
WL
1223
1224 how much snapshots will be kept before get erased, default 1
1225
6b4f676d 1226 -name string
0bc3e510
WL
1227
1228 name of the sync job, if not set it is default.
1229 It is only necessary if scheduler allready contains this source.
1230
6b4f676d 1231 -source string
0bc3e510
WL
1232
1233 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1234
1235=head1 DESCRIPTION
1236
1237This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1238This tool also has the capability to add jobs to cron so the sync will be automatically done.
76b2c677
WL
1239The 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.
1240To config cron see man crontab.
0bc3e510
WL
1241
1242=head2 PVE ZFS Storage sync Tool
1243
1244This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1245
1246=head1 EXAMPLES
1247
1248add sync job from local VM to remote ZFS Server
c85692fa 1249pve-zsync create -source=100 -dest=192.168.1.2:zfspool
0bc3e510
WL
1250
1251=head1 IMPORTANT FILES
a018f134 1252
7e0605cc 1253Cron jobs and config are stored at /etc/cron.d/pve-zsync
6b4f676d 1254
7e0605cc 1255The VM config get copied on the destination machine to /var/lib/pve-zsync/
0bc3e510 1256
6b4f676d 1257=head1 COPYRIGHT AND DISCLAIMER
0bc3e510
WL
1258
1259Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1260
1261This program is free software: you can redistribute it and/or modify it
1262under the terms of the GNU Affero General Public License as published
1263by the Free Software Foundation, either version 3 of the License, or
1264(at your option) any later version.
1265
1266This program is distributed in the hope that it will be useful, but
1267WITHOUT ANY WARRANTY; without even the implied warranty of
1268MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1269Affero General Public License for more details.
1270
1271You should have received a copy of the GNU Affero General Public
1272License along with this program. If not, see
1273<http://www.gnu.org/licenses/>.