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