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