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