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