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