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