]> git.proxmox.com Git - pve-zsync.git/blame - pve-zsync
bump version to 1.6-2
[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 }
271c2572
WB
828 push @$cmd, \'|';
829 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
830 push @$cmd, 'zfs', 'recv', '--';
831 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{new_snap}";
0bc3e510
WL
832
833 eval {
834 run_cmd($cmd)
835 };
836
837 if (my $erro = $@) {
76b2c677 838 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
0bc3e510
WL
839 die $erro;
840 };
0bc3e510
WL
841}
842
843
844sub send_config{
845 my ($source, $dest, $method) = @_;
846
76b2c677
WL
847 my $source_target ="$QEMU_CONF$source->{vmid}.conf";
848 my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}";
849
0bc3e510
WL
850 if ($method eq 'ssh'){
851 if ($dest->{ip} && $source->{ip}) {
271c2572
WB
852 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
853 run_cmd(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 854 } elsif ($dest->{ip}) {
271c2572
WB
855 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]);
856 run_cmd(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 857 } elsif ($source->{ip}) {
271c2572
WB
858 run_cmd(['mkdir', '-p', '--', $CONFIG_PATH]);
859 run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
0bc3e510
WL
860 }
861
862 if ($source->{destroy}){
76b2c677 863 my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}";
0bc3e510 864 if($dest->{ip}){
271c2572 865 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
0bc3e510 866 } else {
271c2572 867 run_cmd(['rm', '-f', '--', $dest_target_old]);
0bc3e510
WL
868 }
869 }
870 }
871}
872
873sub get_date {
c85692fa
WL
874 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
875 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
0bc3e510
WL
876
877 return $datestamp;
878}
879
880sub status {
76b2c677 881 my $cfg = read_cron();
0bc3e510 882
c85692fa 883 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
0bc3e510 884
76b2c677
WL
885 my $states = read_state();
886
c85692fa
WL
887 foreach my $source (sort keys%{$cfg}) {
888 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
76b2c677
WL
889 $status_list .= sprintf("%-25s", cut_target_width($source, 25));
890 $status_list .= sprintf("%-15s", cut_target_width($sync_name, 25));
891 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
0bc3e510
WL
892 }
893 }
894
895 return $status_list;
896}
897
c85692fa 898sub enable_job {
28006d67
WL
899 my ($param) = @_;
900
76b2c677
WL
901 my $job = get_job($param);
902 $job->{state} = "ok";
903 update_state($job);
904 update_cron($job);
28006d67
WL
905}
906
c85692fa 907sub disable_job {
28006d67
WL
908 my ($param) = @_;
909
76b2c677 910 my $job = get_job($param);
6b4f676d 911 $job->{state} = "stopped";
76b2c677
WL
912 update_state($job);
913 update_cron($job);
28006d67
WL
914}
915
0bc3e510
WL
916my $command = $ARGV[0];
917
918my $commands = {'destroy' => 1,
919 'create' => 1,
920 'sync' => 1,
921 'list' => 1,
922 'status' => 1,
28006d67
WL
923 'help' => 1,
924 'enable' => 1,
925 'disable' => 1};
0bc3e510 926
28006d67 927if (!$command || !$commands->{$command}) {
0bc3e510
WL
928 usage();
929 die "\n";
930}
931
28006d67 932my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
0bc3e510
WL
933\twill sync one time\n
934\t-dest\tstring\n
935\t\tthe destination target is like [IP:]<Pool>[/Path]\n
936\t-limit\tinteger\n
937\t\tmax sync speed in kBytes/s, default unlimited\n
938\t-maxsnap\tinteger\n
939\t\thow much snapshots will be kept before get erased, default 1/n
940\t-name\tstring\n
941\t\tname of the sync job, if not set it is default.
942\tIt is only necessary if scheduler allready contains this source.\n
943\t-source\tstring\n
944\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
945
28006d67 946my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
0bc3e510
WL
947\tCreate a sync Job\n
948\t-dest\tstringn\n
949\t\tthe destination target is like [IP]:<Pool>[/Path]\n
0bc3e510 950\t-limit\tinteger\n
28006d67 951\t\tmax sync speed in kBytes/s, default unlimited\n
0bc3e510
WL
952\t-maxsnap\tstring\n
953\t\thow much snapshots will be kept before get erased, default 1\n
954\t-name\tstring\n
955\t\tname of the sync job, if not set it is default\n
956\t-skip\tboolean\n
957\t\tif this flag is set it will skip the first sync\n
958\t-source\tstring\n
959\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
960
28006d67 961my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
0bc3e510
WL
962\tremove a sync Job from the scheduler\n
963\t-name\tstring\n
964\t\tname of the sync job, if not set it is default\n
965\t-source\tstring\n
966\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
967
28006d67 968my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
0bc3e510
WL
969\tGet help about specified command.\n
970\t<cmd>\tstring\n
971\t\tCommand name\n
972\t-verbose\tboolean\n
973\t\tVerbose output format.\n";
974
28006d67 975my $help_list = "$PROGNAME list\n
0bc3e510
WL
976\tGet a List of all scheduled Sync Jobs\n";
977
28006d67 978my $help_status = "$PROGNAME status\n
0bc3e510
WL
979\tGet the status of all scheduled Sync Jobs\n";
980
28006d67
WL
981my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
982\tenable a syncjob and reset error\n
983\t-name\tstring\n
984\t\tname of the sync job, if not set it is default\n
985\t-source\tstring\n
986\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
987
988my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
989\tpause a syncjob\n
990\t-name\tstring\n
991\t\tname of the sync job, if not set it is default\n
992\t-source\tstring\n
993\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
994
c85692fa 995sub help {
0bc3e510
WL
996 my ($command) = @_;
997
998 switch($command){
999 case 'help'
1000 {
1001 die "$help_help\n";
1002 }
1003 case 'sync'
1004 {
1005 die "$help_sync\n";
1006 }
1007 case 'destroy'
1008 {
1009 die "$help_destroy\n";
1010 }
1011 case 'create'
1012 {
1013 die "$help_create\n";
1014 }
1015 case 'list'
1016 {
1017 die "$help_list\n";
1018 }
1019 case 'status'
1020 {
1021 die "$help_status\n";
1022 }
28006d67
WL
1023 case 'enable'
1024 {
1025 die "$help_enable\n";
1026 }
1027 case 'disable'
1028 {
1029 die "$help_enable\n";
1030 }
0bc3e510
WL
1031 }
1032
1033}
1034
76b2c677
WL
1035my @arg = @ARGV;
1036my $param = parse_argv(@arg);
0bc3e510 1037
0bc3e510 1038
c85692fa 1039switch($command) {
0bc3e510
WL
1040 case "destroy"
1041 {
76b2c677
WL
1042 die "$help_destroy\n" if !$param->{source};
1043 check_target($param->{source});
c85692fa 1044 destroy_job($param);
0bc3e510
WL
1045 }
1046 case "sync"
1047 {
76b2c677
WL
1048 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1049 check_target($param->{source});
1050 check_target($param->{dest});
0bc3e510
WL
1051 sync($param);
1052 }
1053 case "create"
1054 {
76b2c677
WL
1055 die "$help_create\n" if !$param->{source} || !$param->{dest};
1056 check_target($param->{source});
1057 check_target($param->{dest});
0bc3e510
WL
1058 init($param);
1059 }
1060 case "status"
1061 {
1062 print status();
1063 }
1064 case "list"
1065 {
1066 print list();
1067 }
1068 case "help"
1069 {
1070 my $help_command = $ARGV[1];
1071 if ($help_command && $commands->{$help_command}) {
1072 print help($help_command);
1073 }
76b2c677 1074 if ($param->{verbose} == 1){
0bc3e510
WL
1075 exec("man $PROGNAME");
1076 } else {
1077 usage(1);
1078 }
1079 }
28006d67
WL
1080 case "enable"
1081 {
76b2c677
WL
1082 die "$help_enable\n" if !$param->{source};
1083 check_target($param->{source});
c85692fa 1084 enable_job($param);
28006d67
WL
1085 }
1086 case "disable"
1087 {
76b2c677
WL
1088 die "$help_disable\n" if !$param->{source};
1089 check_target($param->{source});
c85692fa 1090 disable_job($param);
28006d67 1091 }
0bc3e510
WL
1092}
1093
c85692fa 1094sub usage {
0bc3e510
WL
1095 my ($help) = @_;
1096
1097 print("ERROR:\tno command specified\n") if !$help;
1098 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
28006d67
WL
1099 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1100 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1101 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1102 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1103 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1104 print("\t$PROGNAME list\n");
1105 print("\t$PROGNAME status\n");
1106 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
0bc3e510
WL
1107}
1108
c85692fa 1109sub check_target {
0bc3e510 1110 my ($target) = @_;
db2ce6d4 1111 parse_target($target);
0bc3e510
WL
1112}
1113
1114__END__
1115
1116=head1 NAME
1117
1118pve-zsync - PVE ZFS Replication Manager
1119
1120=head1 SYNOPSIS
1121
c85692fa 1122pve-zsync <COMMAND> [ARGS] [OPTIONS]
0bc3e510 1123
c85692fa 1124pve-zsync help <cmd> [OPTIONS]
0bc3e510
WL
1125
1126 Get help about specified command.
1127
1128 <cmd> string
1129
1130 Command name
1131
1132 -verbose boolean
1133
1134 Verbose output format.
1135
c85692fa 1136pve-zsync create -dest <string> -source <string> [OPTIONS]
0bc3e510 1137
6b4f676d 1138 Create a sync Job
0bc3e510 1139
6b4f676d 1140 -dest string
0bc3e510
WL
1141
1142 the destination target is like [IP]:<Pool>[/Path]
1143
6b4f676d 1144 -limit integer
0bc3e510 1145
28006d67 1146 max sync speed in kBytes/s, default unlimited
0bc3e510 1147
6b4f676d 1148 -maxsnap string
0bc3e510
WL
1149
1150 how much snapshots will be kept before get erased, default 1
1151
6b4f676d 1152 -name string
0bc3e510
WL
1153
1154 name of the sync job, if not set it is default
1155
6b4f676d 1156 -skip boolean
0bc3e510
WL
1157
1158 if this flag is set it will skip the first sync
1159
6b4f676d 1160 -source string
0bc3e510
WL
1161
1162 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1163
c85692fa 1164pve-zsync destroy -source <string> [OPTIONS]
0bc3e510 1165
6b4f676d 1166 remove a sync Job from the scheduler
0bc3e510 1167
6b4f676d 1168 -name string
0bc3e510
WL
1169
1170 name of the sync job, if not set it is default
1171
6b4f676d 1172 -source string
0bc3e510
WL
1173
1174 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1175
c85692fa 1176pve-zsync disable -source <string> [OPTIONS]
28006d67 1177
6b4f676d 1178 pause a sync job
28006d67 1179
6b4f676d 1180 -name string
28006d67
WL
1181
1182 name of the sync job, if not set it is default
1183
6b4f676d 1184 -source string
28006d67
WL
1185
1186 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1187
c85692fa 1188pve-zsync enable -source <string> [OPTIONS]
28006d67 1189
6b4f676d 1190 enable a syncjob and reset error
28006d67 1191
6b4f676d 1192 -name string
28006d67
WL
1193
1194 name of the sync job, if not set it is default
1195
6b4f676d 1196 -source string
28006d67
WL
1197
1198 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
c85692fa 1199pve-zsync list
0bc3e510
WL
1200
1201 Get a List of all scheduled Sync Jobs
1202
c85692fa 1203pve-zsync status
0bc3e510
WL
1204
1205 Get the status of all scheduled Sync Jobs
1206
c85692fa 1207pve-zsync sync -dest <string> -source <string> [OPTIONS]
0bc3e510
WL
1208
1209 will sync one time
1210
1211 -dest string
1212
1213 the destination target is like [IP:]<Pool>[/Path]
1214
1215 -limit integer
1216
1217 max sync speed in kBytes/s, default unlimited
1218
6b4f676d 1219 -maxsnap integer
0bc3e510
WL
1220
1221 how much snapshots will be kept before get erased, default 1
1222
6b4f676d 1223 -name string
0bc3e510
WL
1224
1225 name of the sync job, if not set it is default.
1226 It is only necessary if scheduler allready contains this source.
1227
6b4f676d 1228 -source string
0bc3e510
WL
1229
1230 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1231
1232=head1 DESCRIPTION
1233
1234This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1235This tool also has the capability to add jobs to cron so the sync will be automatically done.
76b2c677
WL
1236The 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.
1237To config cron see man crontab.
0bc3e510
WL
1238
1239=head2 PVE ZFS Storage sync Tool
1240
1241This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1242
1243=head1 EXAMPLES
1244
1245add sync job from local VM to remote ZFS Server
c85692fa 1246pve-zsync create -source=100 -dest=192.168.1.2:zfspool
0bc3e510
WL
1247
1248=head1 IMPORTANT FILES
a018f134 1249
7e0605cc 1250Cron jobs and config are stored at /etc/cron.d/pve-zsync
6b4f676d 1251
7e0605cc 1252The VM config get copied on the destination machine to /var/lib/pve-zsync/
0bc3e510 1253
6b4f676d 1254=head1 COPYRIGHT AND DISCLAIMER
0bc3e510
WL
1255
1256Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1257
1258This program is free software: you can redistribute it and/or modify it
1259under the terms of the GNU Affero General Public License as published
1260by the Free Software Foundation, either version 3 of the License, or
1261(at your option) any later version.
1262
1263This program is distributed in the hope that it will be useful, but
1264WITHOUT ANY WARRANTY; without even the implied warranty of
1265MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1266Affero General Public License for more details.
1267
1268You should have received a copy of the GNU Affero General Public
1269License along with this program. If not, see
1270<http://www.gnu.org/licenses/>.