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