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