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