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