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