]> git.proxmox.com Git - pve-zsync.git/blame - pve-zsync
decode in config name the type of config.
[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 }
c9cbba3d 579 if ($param->{method} eq "ssh" && ($source->{ip} || $dest->{ip})) {
a018f134 580 send_config($source, $dest,'ssh');
c9cbba3d
WL
581 } else {
582 send_config($source, $dest,'local');
a018f134
WL
583 }
584 } else {
76b2c677 585 &$sync_path($source, $dest, $job, $param, $date);
0bc3e510 586 }
a018f134
WL
587 };
588 if(my $err = $@) {
589 if ($job) {
590 $job->{state} = "error";
591 update_state($job);
592 unlock($lock_fh);
593 close($lock_fh);
594 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
7d93705f 595 }
a018f134 596 die "$err\n";
76b2c677
WL
597 }
598
599 if ($job) {
600 $job->{state} = "ok";
601 $job->{lsync} = $date;
602 update_state($job);
0bc3e510 603 }
76b2c677 604
c85692fa 605 unlock($lock_fh);
b0842810 606 close($lock_fh);
0bc3e510
WL
607}
608
609sub snapshot_get{
610 my ($source, $dest, $max_snap, $name) = @_;
611
271c2572
WB
612 my $cmd = [];
613 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
614 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
615 push @$cmd, $source->{all};
0bc3e510
WL
616
617 my $raw = run_cmd($cmd);
76b2c677 618 my $index = 0;
0bc3e510
WL
619 my $line = "";
620 my $last_snap = undef;
76b2c677 621 my $old_snap;
0bc3e510
WL
622
623 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
624 $line = $1;
76b2c677
WL
625 if ($line =~ m/(rep_$name.*)$/) {
626 $last_snap = $1 if (!$last_snap);
627 $old_snap = $1;
628 $index++;
629 if ($index == $max_snap) {
630 $source->{destroy} = 1;
631 last;
632 };
633 }
0bc3e510
WL
634 }
635
76b2c677 636 return ($old_snap, $last_snap) if $last_snap;
0bc3e510
WL
637
638 return undef;
639}
640
641sub snapshot_add {
76b2c677 642 my ($source, $dest, $name, $date) = @_;
0bc3e510
WL
643
644 my $snap_name = "rep_$name\_".$date;
645
646 $source->{new_snap} = $snap_name;
647
76b2c677 648 my $path = "$source->{all}\@$snap_name";
0bc3e510 649
271c2572
WB
650 my $cmd = [];
651 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip};
652 push @$cmd, 'zfs', 'snapshot', $path;
0bc3e510
WL
653 eval{
654 run_cmd($cmd);
655 };
656
c85692fa 657 if (my $err = $@) {
0bc3e510
WL
658 snapshot_destroy($source, $dest, 'ssh', $snap_name);
659 die "$err\n";
660 }
0bc3e510
WL
661}
662
c85692fa 663sub write_cron {
28006d67 664 my ($cfg) = @_;
0bc3e510 665
c85692fa
WL
666 my $text = "SHELL=/bin/sh\n";
667 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
2e8fd72a 668
76b2c677
WL
669 my $fh = IO::File->new("> $CRONJOBS");
670 die "Could not open file: $!\n" if !$fh;
0bc3e510 671
c85692fa
WL
672 foreach my $source (sort keys%{$cfg}) {
673 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
28006d67 674 next if $cfg->{$source}->{$sync_name}->{status} ne 'ok';
c85692fa 675 $text .= "$PROG_PATH sync";
0bc3e510 676 $text .= " -source ";
28006d67
WL
677 if ($cfg->{$source}->{$sync_name}->{vmid}) {
678 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
679 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
0bc3e510 680 } else {
28006d67
WL
681 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
682 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
683 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path};
0bc3e510
WL
684 }
685 $text .= " -dest ";
28006d67
WL
686 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip};
687 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
688 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path};
689 $text .= " -name $sync_name ";
690 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit};
691 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap};
0bc3e510 692 $text .= "\n";
0bc3e510
WL
693 }
694 }
c85692fa 695 die "Can't write to cron\n" if (!print($fh $text));
0bc3e510
WL
696 close($fh);
697}
698
699sub get_disks {
700 my ($target) = @_;
701
271c2572
WB
702 my $cmd = [];
703 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip};
9e7685c2
WL
704
705 if ($target->{vm_type} eq 'qemu') {
706 push @$cmd, 'qm', 'config', $target->{vmid};
707 } elsif ($target->{vm_type} eq 'lxc') {
708 push @$cmd, 'pct', 'config', $target->{vmid};
709 } else {
710 die "VM Type unknown\n";
711 }
0bc3e510
WL
712
713 my $res = run_cmd($cmd);
714
9e7685c2 715 my $disks = parse_disks($res, $target->{ip}, $target->{vm_type});
0bc3e510
WL
716
717 return $disks;
718}
719
720sub run_cmd {
721 my ($cmd) = @_;
722 print "Start CMD\n" if $DEBUG;
723 print Dumper $cmd if $DEBUG;
dc54fce8
WB
724 if (ref($cmd) eq 'ARRAY') {
725 $cmd = join(' ', map { ref($_) ? $$_ : shell_quote($_) } @$cmd);
726 }
0bc3e510
WL
727 my $output = `$cmd 2>&1`;
728
a018f134 729 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
0bc3e510
WL
730
731 chomp($output);
732 print Dumper $output if $DEBUG;
733 print "END CMD\n" if $DEBUG;
734 return $output;
735}
736
737sub parse_disks {
c9cbba3d 738 my ($text, $ip, $vm_type) = @_;
0bc3e510
WL
739
740 my $disks;
741
742 my $num = 0;
0bc3e510
WL
743 while ($text && $text =~ s/^(.*?)(\n|$)//) {
744 my $line = $1;
78cd57dd
WB
745
746 next if $line =~ /cdrom|none/;
9e7685c2 747 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
78cd57dd 748
0bc3e510
WL
749 my $disk = undef;
750 my $stor = undef;
9e7685c2
WL
751 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
752 $disk = $3;
753 $stor = $2;
845e36d7
WB
754 } else {
755 die "disk is not on ZFS Storage\n";
0bc3e510
WL
756 }
757
271c2572
WB
758 my $cmd = [];
759 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
760 push @$cmd, 'pvesm', 'path', "$stor$disk";
b52d13b3
WB
761 my $path = run_cmd($cmd);
762
9e7685c2 763 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
b52d13b3
WB
764
765 my @array = split('/', $1);
3df8b97d 766 $disks->{$num}->{pool} = shift(@array);
b52d13b3
WB
767 $disks->{$num}->{all} = $disks->{$num}->{pool};
768 if (0 < @array) {
769 $disks->{$num}->{path} = join('/', @array);
770 $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
0bc3e510 771 }
b52d13b3
WB
772 $disks->{$num}->{last_part} = $disk;
773 $disks->{$num}->{all} .= "\/$disk";
774
9e7685c2
WL
775 $num++;
776 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)\/(\w+.*)(\/$disk)$/) {
777
778 $disks->{$num}->{pool} = $1;
779 $disks->{$num}->{all} = $disks->{$num}->{pool};
780
781 if ($2) {
782 $disks->{$num}->{path} = $2;
783 $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
784 }
785
786 $disks->{$num}->{last_part} = $disk;
787 $disks->{$num}->{all} .= "\/$disk";
788
b52d13b3
WB
789 $num++;
790
791 } else {
792 die "ERROR: in path\n";
0bc3e510
WL
793 }
794 }
76b2c677 795
0bc3e510
WL
796 return $disks;
797}
798
799sub snapshot_destroy {
800 my ($source, $dest, $method, $snap) = @_;
801
271c2572 802 my @zfscmd = ('zfs', 'destroy');
76b2c677 803 my $snapshot = "$source->{all}\@$snap";
0bc3e510
WL
804
805 eval {
806 if($source->{ip} && $method eq 'ssh'){
271c2572 807 run_cmd(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
0bc3e510 808 } else {
271c2572 809 run_cmd([@zfscmd, $snapshot]);
0bc3e510
WL
810 }
811 };
812 if (my $erro = $@) {
813 warn "WARN: $erro";
814 }
c85692fa 815 if ($dest) {
271c2572 816 my @ssh = $dest->{ip} ? ('ssh', "root\@$dest->{ip}", '--') : ();
0bc3e510 817
76b2c677 818 my $path = "$dest->{all}\/$source->{last_part}";
0bc3e510 819
0bc3e510 820 eval {
271c2572 821 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
0bc3e510
WL
822 };
823 if (my $erro = $@) {
824 warn "WARN: $erro";
825 }
826 }
827}
828
829sub snapshot_exist {
830 my ($source ,$dest, $method) = @_;
831
271c2572
WB
832 my $cmd = [];
833 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
834 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
835 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
0bc3e510
WL
836
837 my $text = "";
838 eval {$text =run_cmd($cmd);};
839 if (my $erro = $@) {
840 warn "WARN: $erro";
841 return undef;
842 }
843
844 while ($text && $text =~ s/^(.*?)(\n|$)//) {
845 my $line = $1;
846 return 1 if $line =~ m/^.*$source->{old_snap}$/;
847 }
848}
849
850sub send_image {
76b2c677 851 my ($source, $dest, $param) = @_;
0bc3e510 852
271c2572 853 my $cmd = [];
0bc3e510 854
271c2572
WB
855 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip};
856 push @$cmd, 'zfs', 'send';
857 push @$cmd, '-v' if $param->{verbose};
0bc3e510 858
76b2c677 859 if($source->{last_snap} && snapshot_exist($source ,$dest, $param->{method})) {
271c2572 860 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
0bc3e510 861 }
271c2572 862 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
0bc3e510 863
76b2c677
WL
864 if ($param->{limit}){
865 my $bwl = $param->{limit}*1024;
271c2572 866 push @$cmd, \'|', 'cstream', '-t', $bwl;
0bc3e510 867 }
1193273e
WL
868 my $target = "$dest->{all}/$source->{last_part}";
869 $target =~ s!/+!/!g;
870
271c2572
WB
871 push @$cmd, \'|';
872 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip};
cffc38e0
WL
873 push @$cmd, 'zfs', 'recv', '-F', '--';
874 push @$cmd, "$target";
0bc3e510
WL
875
876 eval {
877 run_cmd($cmd)
878 };
879
880 if (my $erro = $@) {
76b2c677 881 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap});
0bc3e510
WL
882 die $erro;
883 };
0bc3e510
WL
884}
885
886
887sub send_config{
888 my ($source, $dest, $method) = @_;
889
ce6b3240 890 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF$source->{vmid}.conf": "$LXC_CONF$source->{vmid}.conf";
fac83537 891 my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}";
76b2c677 892
739c195a
WL
893 my $config_dir = $dest->{last_part} ? "${CONFIG_PATH}$dest->{last_part}/" : $CONFIG_PATH;
894
895 $dest_target_new = $config_dir.$dest_target_new;
896
897 print Dumper $dest_target_new, $dest;
0bc3e510
WL
898 if ($method eq 'ssh'){
899 if ($dest->{ip} && $source->{ip}) {
739c195a 900 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
271c2572 901 run_cmd(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 902 } elsif ($dest->{ip}) {
739c195a 903 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
271c2572 904 run_cmd(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
0bc3e510 905 } elsif ($source->{ip}) {
739c195a 906 run_cmd(['mkdir', '-p', '--', $config_dir]);
271c2572 907 run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
0bc3e510
WL
908 }
909
910 if ($source->{destroy}){
739c195a 911 my $dest_target_old ="${config_dir}$source->{vmid}.conf.$source->{old_snap}";
0bc3e510 912 if($dest->{ip}){
271c2572 913 run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
0bc3e510 914 } else {
271c2572 915 run_cmd(['rm', '-f', '--', $dest_target_old]);
0bc3e510
WL
916 }
917 }
c9cbba3d 918 } elsif ($method eq 'local') {
739c195a 919 run_cmd(['mkdir', '-p', '--', $config_dir]);
c9cbba3d 920 run_cmd(['cp', $source_target, $dest_target_new]);
0bc3e510
WL
921 }
922}
923
924sub get_date {
c85692fa
WL
925 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
926 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
0bc3e510
WL
927
928 return $datestamp;
929}
930
931sub status {
76b2c677 932 my $cfg = read_cron();
0bc3e510 933
c85692fa 934 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
0bc3e510 935
76b2c677
WL
936 my $states = read_state();
937
c85692fa
WL
938 foreach my $source (sort keys%{$cfg}) {
939 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
76b2c677
WL
940 $status_list .= sprintf("%-25s", cut_target_width($source, 25));
941 $status_list .= sprintf("%-15s", cut_target_width($sync_name, 25));
942 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
0bc3e510
WL
943 }
944 }
945
946 return $status_list;
947}
948
c85692fa 949sub enable_job {
28006d67
WL
950 my ($param) = @_;
951
76b2c677
WL
952 my $job = get_job($param);
953 $job->{state} = "ok";
954 update_state($job);
955 update_cron($job);
28006d67
WL
956}
957
c85692fa 958sub disable_job {
28006d67
WL
959 my ($param) = @_;
960
76b2c677 961 my $job = get_job($param);
6b4f676d 962 $job->{state} = "stopped";
76b2c677
WL
963 update_state($job);
964 update_cron($job);
28006d67
WL
965}
966
0bc3e510
WL
967my $command = $ARGV[0];
968
969my $commands = {'destroy' => 1,
970 'create' => 1,
971 'sync' => 1,
972 'list' => 1,
973 'status' => 1,
28006d67
WL
974 'help' => 1,
975 'enable' => 1,
976 'disable' => 1};
0bc3e510 977
28006d67 978if (!$command || !$commands->{$command}) {
0bc3e510
WL
979 usage();
980 die "\n";
981}
982
28006d67 983my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
0bc3e510
WL
984\twill sync one time\n
985\t-dest\tstring\n
986\t\tthe destination target is like [IP:]<Pool>[/Path]\n
987\t-limit\tinteger\n
988\t\tmax sync speed in kBytes/s, default unlimited\n
989\t-maxsnap\tinteger\n
990\t\thow much snapshots will be kept before get erased, default 1/n
991\t-name\tstring\n
992\t\tname of the sync job, if not set it is default.
993\tIt is only necessary if scheduler allready contains this source.\n
994\t-source\tstring\n
995\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
996
28006d67 997my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
0bc3e510
WL
998\tCreate a sync Job\n
999\t-dest\tstringn\n
1000\t\tthe destination target is like [IP]:<Pool>[/Path]\n
0bc3e510 1001\t-limit\tinteger\n
28006d67 1002\t\tmax sync speed in kBytes/s, default unlimited\n
0bc3e510
WL
1003\t-maxsnap\tstring\n
1004\t\thow much snapshots will be kept before get erased, default 1\n
1005\t-name\tstring\n
1006\t\tname of the sync job, if not set it is default\n
1007\t-skip\tboolean\n
1008\t\tif this flag is set it will skip the first sync\n
1009\t-source\tstring\n
1010\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1011
28006d67 1012my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
0bc3e510
WL
1013\tremove a sync Job from the scheduler\n
1014\t-name\tstring\n
1015\t\tname of the sync job, if not set it is default\n
1016\t-source\tstring\n
1017\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1018
28006d67 1019my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
0bc3e510
WL
1020\tGet help about specified command.\n
1021\t<cmd>\tstring\n
1022\t\tCommand name\n
1023\t-verbose\tboolean\n
1024\t\tVerbose output format.\n";
1025
28006d67 1026my $help_list = "$PROGNAME list\n
0bc3e510
WL
1027\tGet a List of all scheduled Sync Jobs\n";
1028
28006d67 1029my $help_status = "$PROGNAME status\n
0bc3e510
WL
1030\tGet the status of all scheduled Sync Jobs\n";
1031
28006d67
WL
1032my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
1033\tenable a syncjob and reset error\n
1034\t-name\tstring\n
1035\t\tname of the sync job, if not set it is default\n
1036\t-source\tstring\n
1037\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1038
1039my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
1040\tpause a syncjob\n
1041\t-name\tstring\n
1042\t\tname of the sync job, if not set it is default\n
1043\t-source\tstring\n
1044\t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1045
c85692fa 1046sub help {
0bc3e510
WL
1047 my ($command) = @_;
1048
1049 switch($command){
1050 case 'help'
1051 {
1052 die "$help_help\n";
1053 }
1054 case 'sync'
1055 {
1056 die "$help_sync\n";
1057 }
1058 case 'destroy'
1059 {
1060 die "$help_destroy\n";
1061 }
1062 case 'create'
1063 {
1064 die "$help_create\n";
1065 }
1066 case 'list'
1067 {
1068 die "$help_list\n";
1069 }
1070 case 'status'
1071 {
1072 die "$help_status\n";
1073 }
28006d67
WL
1074 case 'enable'
1075 {
1076 die "$help_enable\n";
1077 }
1078 case 'disable'
1079 {
1080 die "$help_enable\n";
1081 }
0bc3e510
WL
1082 }
1083
1084}
1085
76b2c677
WL
1086my @arg = @ARGV;
1087my $param = parse_argv(@arg);
0bc3e510 1088
0bc3e510 1089
c85692fa 1090switch($command) {
0bc3e510
WL
1091 case "destroy"
1092 {
76b2c677
WL
1093 die "$help_destroy\n" if !$param->{source};
1094 check_target($param->{source});
c85692fa 1095 destroy_job($param);
0bc3e510
WL
1096 }
1097 case "sync"
1098 {
76b2c677
WL
1099 die "$help_sync\n" if !$param->{source} || !$param->{dest};
1100 check_target($param->{source});
1101 check_target($param->{dest});
0bc3e510
WL
1102 sync($param);
1103 }
1104 case "create"
1105 {
76b2c677
WL
1106 die "$help_create\n" if !$param->{source} || !$param->{dest};
1107 check_target($param->{source});
1108 check_target($param->{dest});
0bc3e510
WL
1109 init($param);
1110 }
1111 case "status"
1112 {
1113 print status();
1114 }
1115 case "list"
1116 {
1117 print list();
1118 }
1119 case "help"
1120 {
1121 my $help_command = $ARGV[1];
1122 if ($help_command && $commands->{$help_command}) {
1123 print help($help_command);
1124 }
76b2c677 1125 if ($param->{verbose} == 1){
0bc3e510
WL
1126 exec("man $PROGNAME");
1127 } else {
1128 usage(1);
1129 }
1130 }
28006d67
WL
1131 case "enable"
1132 {
76b2c677
WL
1133 die "$help_enable\n" if !$param->{source};
1134 check_target($param->{source});
c85692fa 1135 enable_job($param);
28006d67
WL
1136 }
1137 case "disable"
1138 {
76b2c677
WL
1139 die "$help_disable\n" if !$param->{source};
1140 check_target($param->{source});
c85692fa 1141 disable_job($param);
28006d67 1142 }
0bc3e510
WL
1143}
1144
c85692fa 1145sub usage {
0bc3e510
WL
1146 my ($help) = @_;
1147
1148 print("ERROR:\tno command specified\n") if !$help;
1149 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
28006d67
WL
1150 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1151 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1152 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1153 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1154 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1155 print("\t$PROGNAME list\n");
1156 print("\t$PROGNAME status\n");
1157 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
0bc3e510
WL
1158}
1159
c85692fa 1160sub check_target {
0bc3e510 1161 my ($target) = @_;
db2ce6d4 1162 parse_target($target);
0bc3e510
WL
1163}
1164
1165__END__
1166
1167=head1 NAME
1168
1169pve-zsync - PVE ZFS Replication Manager
1170
1171=head1 SYNOPSIS
1172
c85692fa 1173pve-zsync <COMMAND> [ARGS] [OPTIONS]
0bc3e510 1174
c85692fa 1175pve-zsync help <cmd> [OPTIONS]
0bc3e510
WL
1176
1177 Get help about specified command.
1178
1179 <cmd> string
1180
1181 Command name
1182
1183 -verbose boolean
1184
1185 Verbose output format.
1186
c85692fa 1187pve-zsync create -dest <string> -source <string> [OPTIONS]
0bc3e510 1188
6b4f676d 1189 Create a sync Job
0bc3e510 1190
6b4f676d 1191 -dest string
0bc3e510
WL
1192
1193 the destination target is like [IP]:<Pool>[/Path]
1194
6b4f676d 1195 -limit integer
0bc3e510 1196
28006d67 1197 max sync speed in kBytes/s, default unlimited
0bc3e510 1198
6b4f676d 1199 -maxsnap string
0bc3e510
WL
1200
1201 how much snapshots will be kept before get erased, default 1
1202
6b4f676d 1203 -name string
0bc3e510
WL
1204
1205 name of the sync job, if not set it is default
1206
6b4f676d 1207 -skip boolean
0bc3e510
WL
1208
1209 if this flag is set it will skip the first sync
1210
6b4f676d 1211 -source string
0bc3e510
WL
1212
1213 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1214
c85692fa 1215pve-zsync destroy -source <string> [OPTIONS]
0bc3e510 1216
6b4f676d 1217 remove a sync Job from the scheduler
0bc3e510 1218
6b4f676d 1219 -name string
0bc3e510
WL
1220
1221 name of the sync job, if not set it is default
1222
6b4f676d 1223 -source string
0bc3e510
WL
1224
1225 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1226
c85692fa 1227pve-zsync disable -source <string> [OPTIONS]
28006d67 1228
6b4f676d 1229 pause a sync job
28006d67 1230
6b4f676d 1231 -name string
28006d67
WL
1232
1233 name of the sync job, if not set it is default
1234
6b4f676d 1235 -source string
28006d67
WL
1236
1237 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1238
c85692fa 1239pve-zsync enable -source <string> [OPTIONS]
28006d67 1240
6b4f676d 1241 enable a syncjob and reset error
28006d67 1242
6b4f676d 1243 -name string
28006d67
WL
1244
1245 name of the sync job, if not set it is default
1246
6b4f676d 1247 -source string
28006d67
WL
1248
1249 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
c85692fa 1250pve-zsync list
0bc3e510
WL
1251
1252 Get a List of all scheduled Sync Jobs
1253
c85692fa 1254pve-zsync status
0bc3e510
WL
1255
1256 Get the status of all scheduled Sync Jobs
1257
c85692fa 1258pve-zsync sync -dest <string> -source <string> [OPTIONS]
0bc3e510
WL
1259
1260 will sync one time
1261
1262 -dest string
1263
1264 the destination target is like [IP:]<Pool>[/Path]
1265
1266 -limit integer
1267
1268 max sync speed in kBytes/s, default unlimited
1269
6b4f676d 1270 -maxsnap integer
0bc3e510
WL
1271
1272 how much snapshots will be kept before get erased, default 1
1273
6b4f676d 1274 -name string
0bc3e510
WL
1275
1276 name of the sync job, if not set it is default.
1277 It is only necessary if scheduler allready contains this source.
1278
6b4f676d 1279 -source string
0bc3e510
WL
1280
1281 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1282
1283=head1 DESCRIPTION
1284
1285This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1286This tool also has the capability to add jobs to cron so the sync will be automatically done.
76b2c677
WL
1287The 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.
1288To config cron see man crontab.
0bc3e510
WL
1289
1290=head2 PVE ZFS Storage sync Tool
1291
1292This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1293
1294=head1 EXAMPLES
1295
1296add sync job from local VM to remote ZFS Server
c85692fa 1297pve-zsync create -source=100 -dest=192.168.1.2:zfspool
0bc3e510
WL
1298
1299=head1 IMPORTANT FILES
a018f134 1300
7e0605cc 1301Cron jobs and config are stored at /etc/cron.d/pve-zsync
6b4f676d 1302
7e0605cc 1303The VM config get copied on the destination machine to /var/lib/pve-zsync/
0bc3e510 1304
6b4f676d 1305=head1 COPYRIGHT AND DISCLAIMER
0bc3e510
WL
1306
1307Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1308
1309This program is free software: you can redistribute it and/or modify it
1310under the terms of the GNU Affero General Public License as published
1311by the Free Software Foundation, either version 3 of the License, or
1312(at your option) any later version.
1313
1314This program is distributed in the hope that it will be useful, but
1315WITHOUT ANY WARRANTY; without even the implied warranty of
1316MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1317Affero General Public License for more details.
1318
1319You should have received a copy of the GNU Affero General Public
1320License along with this program. If not, see
1321<http://www.gnu.org/licenses/>.