]> git.proxmox.com Git - pve-zsync.git/blame - pve-zsync
bump version to 2.0-2
[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
96ed8176 637 ($source->{old_snap}, $source->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name}, $param->{source_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
96ed8176 643 snapshot_destroy($source, $dest, $param->{method}, $source->{old_snap}, $param->{source_user}, $param->{dest_user}) if ($source->{destroy} && $source->{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{
eac174d7 697 my ($source, $dest, $max_snap, $name, $source_user) = @_;
0bc3e510 698
271c2572 699 my $cmd = [];
eac174d7 700 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip};
271c2572
WB
701 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
702 push @$cmd, $source->{all};
0bc3e510
WL
703
704 my $raw = run_cmd($cmd);
76b2c677 705 my $index = 0;
0bc3e510
WL
706 my $line = "";
707 my $last_snap = undef;
76b2c677 708 my $old_snap;
0bc3e510
WL
709
710 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
711 $line = $1;
2152b2b8 712 if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) {
d9e8f4ec 713
76b2c677
WL
714 $last_snap = $1 if (!$last_snap);
715 $old_snap = $1;
716 $index++;
717 if ($index == $max_snap) {
718 $source->{destroy} = 1;
719 last;
720 };
721 }
0bc3e510
WL
722 }
723
76b2c677 724 return ($old_snap, $last_snap) if $last_snap;
0bc3e510
WL
725
726 return undef;
727}
728
729sub snapshot_add {
eac174d7 730 my ($source, $dest, $name, $date, $source_user, $dest_user) = @_;
0bc3e510
WL
731
732 my $snap_name = "rep_$name\_".$date;
733
734 $source->{new_snap} = $snap_name;
735
76b2c677 736 my $path = "$source->{all}\@$snap_name";
0bc3e510 737
271c2572 738 my $cmd = [];
eac174d7 739 push @$cmd, 'ssh', "$source_user\@$source->{ip}", '--', if $source->{ip};
271c2572 740 push @$cmd, 'zfs', 'snapshot', $path;
0bc3e510
WL
741 eval{
742 run_cmd($cmd);
743 };
744
c85692fa 745 if (my $err = $@) {
eac174d7 746 snapshot_destroy($source, $dest, 'ssh', $snap_name, $source_user, $dest_user);
0bc3e510
WL
747 die "$err\n";
748 }
0bc3e510
WL
749}
750
c85692fa 751sub write_cron {
28006d67 752 my ($cfg) = @_;
0bc3e510 753
c85692fa
WL
754 my $text = "SHELL=/bin/sh\n";
755 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
2e8fd72a 756
76b2c677
WL
757 my $fh = IO::File->new("> $CRONJOBS");
758 die "Could not open file: $!\n" if !$fh;
0bc3e510 759
c85692fa
WL
760 foreach my $source (sort keys%{$cfg}) {
761 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
28006d67 762 next if $cfg->{$source}->{$sync_name}->{status} ne 'ok';
eee21241
WL
763 $text .= "$PROG_PATH sync";
764 $text .= " -source ";
765 if ($cfg->{$source}->{$sync_name}->{vmid}) {
766 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
767 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
768 } else {
769 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip};
770 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
771 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path};
772 }
773 $text .= " -dest ";
774 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip};
775 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
776 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path};
777 $text .= " -name $sync_name ";
778 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit};
779 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap};
780 $text .= "\n";
0bc3e510
WL
781 }
782 }
c85692fa 783 die "Can't write to cron\n" if (!print($fh $text));
0bc3e510
WL
784 close($fh);
785}
786
787sub get_disks {
eac174d7 788 my ($target, $user) = @_;
0bc3e510 789
271c2572 790 my $cmd = [];
eac174d7 791 push @$cmd, 'ssh', "$user\@$target->{ip}", '--', if $target->{ip};
9e7685c2
WL
792
793 if ($target->{vm_type} eq 'qemu') {
794 push @$cmd, 'qm', 'config', $target->{vmid};
795 } elsif ($target->{vm_type} eq 'lxc') {
796 push @$cmd, 'pct', 'config', $target->{vmid};
797 } else {
798 die "VM Type unknown\n";
799 }
0bc3e510
WL
800
801 my $res = run_cmd($cmd);
802
eac174d7 803 my $disks = parse_disks($res, $target->{ip}, $target->{vm_type}, $user);
0bc3e510
WL
804
805 return $disks;
806}
807
808sub run_cmd {
809 my ($cmd) = @_;
810 print "Start CMD\n" if $DEBUG;
811 print Dumper $cmd if $DEBUG;
dc54fce8
WB
812 if (ref($cmd) eq 'ARRAY') {
813 $cmd = join(' ', map { ref($_) ? $$_ : shell_quote($_) } @$cmd);
814 }
0bc3e510
WL
815 my $output = `$cmd 2>&1`;
816
a018f134 817 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
0bc3e510
WL
818
819 chomp($output);
820 print Dumper $output if $DEBUG;
821 print "END CMD\n" if $DEBUG;
822 return $output;
823}
824
825sub parse_disks {
eac174d7 826 my ($text, $ip, $vm_type, $user) = @_;
0bc3e510
WL
827
828 my $disks;
829
830 my $num = 0;
0bc3e510
WL
831 while ($text && $text =~ s/^(.*?)(\n|$)//) {
832 my $line = $1;
78cd57dd 833
354f3634 834 next if $line =~ /media=cdrom/;
e45536e5 835 next if $line !~ m/$DISK_KEY_RE/;
78cd57dd 836
3db33e1f 837 #QEMU if backup is not set include in sync
968cdfe8 838 next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/);
3db33e1f
WL
839
840 #LXC if backup is not set do no in sync
1fe362eb 841 next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/);
3db33e1f 842
0bc3e510
WL
843 my $disk = undef;
844 my $stor = undef;
e45536e5 845 if($line =~ m/$DISK_KEY_RE(.*)$/) {
e5f52a63
WL
846 my @parameter = split(/,/,$1);
847
848 foreach my $opt (@parameter) {
849 if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){
850 $disk = $2;
851 $stor = $1;
852 last;
853 }
854 }
44408b4a
WB
855 }
856 if (!defined($disk) || !defined($stor)) {
857 print "Disk: \"$line\" has no valid zfs dataset format and will be skipped\n";
3db33e1f 858 next;
0bc3e510
WL
859 }
860
271c2572 861 my $cmd = [];
eac174d7 862 push @$cmd, 'ssh', "$user\@$ip", '--' if $ip;
271c2572 863 push @$cmd, 'pvesm', 'path', "$stor$disk";
b52d13b3
WB
864 my $path = run_cmd($cmd);
865
9303e6fc
WL
866 die "Get no path from pvesm path $stor$disk\n" if !$path;
867
9e7685c2 868 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) {
b52d13b3
WB
869
870 my @array = split('/', $1);
3df8b97d 871 $disks->{$num}->{pool} = shift(@array);
b52d13b3
WB
872 $disks->{$num}->{all} = $disks->{$num}->{pool};
873 if (0 < @array) {
874 $disks->{$num}->{path} = join('/', @array);
875 $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
0bc3e510 876 }
b52d13b3
WB
877 $disks->{$num}->{last_part} = $disk;
878 $disks->{$num}->{all} .= "\/$disk";
879
9e7685c2 880 $num++;
aec521ca 881 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) {
9e7685c2
WL
882
883 $disks->{$num}->{pool} = $1;
884 $disks->{$num}->{all} = $disks->{$num}->{pool};
885
886 if ($2) {
aec521ca 887 $disks->{$num}->{path} = $3;
9e7685c2
WL
888 $disks->{$num}->{all} .= "\/$disks->{$num}->{path}";
889 }
890
891 $disks->{$num}->{last_part} = $disk;
892 $disks->{$num}->{all} .= "\/$disk";
893
b52d13b3
WB
894 $num++;
895
896 } else {
897 die "ERROR: in path\n";
0bc3e510
WL
898 }
899 }
76b2c677 900
3db33e1f 901 die "Vm include no disk on zfs.\n" if !$disks->{0};
0bc3e510
WL
902 return $disks;
903}
904
905sub snapshot_destroy {
eac174d7 906 my ($source, $dest, $method, $snap, $source_user, $dest_user) = @_;
0bc3e510 907
271c2572 908 my @zfscmd = ('zfs', 'destroy');
76b2c677 909 my $snapshot = "$source->{all}\@$snap";
0bc3e510
WL
910
911 eval {
912 if($source->{ip} && $method eq 'ssh'){
eac174d7 913 run_cmd(['ssh', "$source_user\@$source->{ip}", '--', @zfscmd, $snapshot]);
0bc3e510 914 } else {
271c2572 915 run_cmd([@zfscmd, $snapshot]);
0bc3e510
WL
916 }
917 };
918 if (my $erro = $@) {
919 warn "WARN: $erro";
920 }
c85692fa 921 if ($dest) {
eac174d7 922 my @ssh = $dest->{ip} ? ('ssh', "$dest_user\@$dest->{ip}", '--') : ();
0bc3e510 923
c613b5f1
WL
924 my $path = "$dest->{all}";
925 $path .= "/$source->{last_part}" if $source->{last_part};
0bc3e510 926
0bc3e510 927 eval {
271c2572 928 run_cmd([@ssh, @zfscmd, "$path\@$snap"]);
0bc3e510
WL
929 };
930 if (my $erro = $@) {
931 warn "WARN: $erro";
932 }
933 }
934}
935
936sub snapshot_exist {
c9e2de94 937 my ($source , $dest, $method, $dest_user) = @_;
0bc3e510 938
271c2572 939 my $cmd = [];
c9e2de94 940 push @$cmd, 'ssh', "$dest_user\@$dest->{ip}", '--' if $dest->{ip};
271c2572 941 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
c613b5f1
WL
942
943 my $path = $dest->{all};
944 $path .= "/$source->{last_part}" if $source->{last_part};
945 $path .= "\@$source->{old_snap}";
946
947 push @$cmd, $path;
948
0bc3e510
WL
949
950 my $text = "";
951 eval {$text =run_cmd($cmd);};
eee21241 952 if (my $erro =$@) {
0bc3e510
WL
953 warn "WARN: $erro";
954 return undef;
955 }
956
957 while ($text && $text =~ s/^(.*?)(\n|$)//) {
eee21241 958 my $line =$1;
0bc3e510
WL
959 return 1 if $line =~ m/^.*$source->{old_snap}$/;
960 }
961}
962
963sub send_image {
76b2c677 964 my ($source, $dest, $param) = @_;
0bc3e510 965
271c2572 966 my $cmd = [];
0bc3e510 967
eac174d7 968 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{source_user}\@$source->{ip}", '--' if $source->{ip};
271c2572 969 push @$cmd, 'zfs', 'send';
70253912 970 push @$cmd, '-p', if $param->{properties};
271c2572 971 push @$cmd, '-v' if $param->{verbose};
0bc3e510 972
c9e2de94 973 if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method}, $param->{dest_user})) {
271c2572 974 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
0bc3e510 975 }
271c2572 976 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
0bc3e510 977
76b2c677
WL
978 if ($param->{limit}){
979 my $bwl = $param->{limit}*1024;
271c2572 980 push @$cmd, \'|', 'cstream', '-t', $bwl;
0bc3e510 981 }
c613b5f1
WL
982 my $target = "$dest->{all}";
983 $target .= "/$source->{last_part}" if $source->{last_part};
1193273e
WL
984 $target =~ s!/+!/!g;
985
271c2572 986 push @$cmd, \'|';
eac174d7 987 push @$cmd, 'ssh', '-o', 'BatchMode=yes', "$param->{dest_user}\@$dest->{ip}", '--' if $dest->{ip};
ce6bc53e
TL
988 push @$cmd, 'zfs', 'recv', '-F', '--';
989 push @$cmd, "$target";
0bc3e510 990
ce6bc53e
TL
991 eval {
992 run_cmd($cmd)
993 };
0bc3e510 994
ce6bc53e 995 if (my $erro = $@) {
eac174d7 996 snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}, $param->{source_user}, $param->{dest_user});
ce6bc53e
TL
997 die $erro;
998 };
999}
0bc3e510
WL
1000
1001
ce6bc53e 1002sub send_config{
7ee3e8e0 1003 my ($source, $dest, $method, $source_user, $dest_user, $dest_config_path) = @_;
0bc3e510 1004
ce6bc53e
TL
1005 my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid}.conf": "$LXC_CONF/$source->{vmid}.conf";
1006 my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}";
76b2c677 1007
7ee3e8e0 1008 my $config_dir = $dest_config_path // $CONFIG_PATH;
37bdf053 1009 $config_dir .= "/$dest->{last_part}" if $dest->{last_part};
739c195a 1010
ce6bc53e 1011 $dest_target_new = $config_dir.'/'.$dest_target_new;
739c195a 1012
ce6bc53e
TL
1013 if ($method eq 'ssh'){
1014 if ($dest->{ip} && $source->{ip}) {
eac174d7
DL
1015 run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
1016 run_cmd(['scp', '--', "$source_user\@[$source->{ip}]:$source_target", "$dest_user\@[$dest->{ip}]:$dest_target_new"]);
ce6bc53e 1017 } elsif ($dest->{ip}) {
d034d438 1018 run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
eac174d7 1019 run_cmd(['scp', '--', $source_target, "$dest_user\@[$dest->{ip}]:$dest_target_new"]);
ce6bc53e
TL
1020 } elsif ($source->{ip}) {
1021 run_cmd(['mkdir', '-p', '--', $config_dir]);
eac174d7 1022 run_cmd(['scp', '--', "$source_user\@[$source->{ip}]:$source_target", $dest_target_new]);
ce6bc53e 1023 }
0bc3e510 1024
ce6bc53e
TL
1025 if ($source->{destroy}){
1026 my $dest_target_old ="${config_dir}/$source->{vmid}.conf.$source->{vm_type}.$source->{old_snap}";
1027 if($dest->{ip}){
eac174d7 1028 run_cmd(['ssh', "$dest_user\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
ce6bc53e
TL
1029 } else {
1030 run_cmd(['rm', '-f', '--', $dest_target_old]);
0bc3e510
WL
1031 }
1032 }
ce6bc53e
TL
1033 } elsif ($method eq 'local') {
1034 run_cmd(['mkdir', '-p', '--', $config_dir]);
1035 run_cmd(['cp', $source_target, $dest_target_new]);
0bc3e510 1036 }
ce6bc53e 1037}
0bc3e510 1038
ce6bc53e
TL
1039sub get_date {
1040 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
1041 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
0bc3e510 1042
ce6bc53e
TL
1043 return $datestamp;
1044}
0bc3e510 1045
ce6bc53e
TL
1046sub status {
1047 my $cfg = read_cron();
0bc3e510 1048
ce6bc53e 1049 my $status_list = sprintf("%-25s%-25s%-10s\n", "SOURCE", "NAME", "STATUS");
0bc3e510 1050
ce6bc53e 1051 my $states = read_state();
76b2c677 1052
ce6bc53e
TL
1053 foreach my $source (sort keys%{$cfg}) {
1054 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
1055 $status_list .= sprintf("%-25s", cut_target_width($source, 25));
1056 $status_list .= sprintf("%-25s", cut_target_width($sync_name, 25));
1057 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
0bc3e510 1058 }
eee21241 1059 }
0bc3e510 1060
ce6bc53e
TL
1061 return $status_list;
1062}
28006d67 1063
ce6bc53e
TL
1064sub enable_job {
1065 my ($param) = @_;
28006d67 1066
96ed8176
FE
1067 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1068 my $job = get_job($param);
1069 $job->{state} = "ok";
1070 update_state($job);
1071 update_cron($job);
1072 });
ce6bc53e 1073}
28006d67 1074
ce6bc53e
TL
1075sub disable_job {
1076 my ($param) = @_;
28006d67 1077
96ed8176
FE
1078 locked("$CONFIG_PATH/cron_and_state.lock", sub {
1079 my $job = get_job($param);
1080 $job->{state} = "stopped";
1081 update_state($job);
1082 update_cron($job);
1083 });
ce6bc53e 1084}
0bc3e510 1085
fdb4de53
TL
1086my $cmd_help = {
1087 destroy => qq{
1088$PROGNAME destroy -source <string> [OPTIONS]
6bae4d8e 1089
fdb4de53 1090 remove a sync Job from the scheduler
6bae4d8e
WL
1091
1092 -name string
1093
fdb4de53 1094 name of the sync job, if not set it is default
6bae4d8e
WL
1095
1096 -source string
1097
fdb4de53
TL
1098 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1099 },
1100 create => qq{
7d54b8ba 1101$PROGNAME create -dest <string> -source <string> [OPTIONS]
6bae4d8e
WL
1102
1103 Create a sync Job
1104
1105 -dest string
1106
1107 the destination target is like [IP]:<Pool>[/Path]
1108
eac174d7
DL
1109 -dest-user string
1110
1111 name of the user on the destination target, root by default
1112
6bae4d8e
WL
1113 -limit integer
1114
1115 max sync speed in kBytes/s, default unlimited
1116
1117 -maxsnap string
1118
1119 how much snapshots will be kept before get erased, default 1
1120
1121 -name string
1122
1123 name of the sync job, if not set it is default
1124
1125 -skip boolean
1126
1127 if this flag is set it will skip the first sync
1128
1129 -source string
1130
1131 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
eac174d7
DL
1132
1133 -source-user string
1134
1135 name of the user on the source target, root by default
70253912
WL
1136
1137 -properties boolean
1138
1139 Include the dataset's properties in the stream.
7ee3e8e0
ML
1140
1141 -dest-config-path string
1142
1143 specify a custom config path on the destination target. default is /var/lib/pve-zsync
fdb4de53
TL
1144 },
1145 sync => qq{
1146$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
6bae4d8e 1147
fdb4de53 1148 will sync one time
6bae4d8e 1149
fdb4de53 1150 -dest string
6bae4d8e 1151
fdb4de53 1152 the destination target is like [IP:]<Pool>[/Path]
6bae4d8e 1153
eac174d7
DL
1154 -dest-user string
1155
1156 name of the user on the destination target, root by default
1157
fdb4de53 1158 -limit integer
6bae4d8e 1159
fdb4de53 1160 max sync speed in kBytes/s, default unlimited
6bae4d8e 1161
fdb4de53 1162 -maxsnap integer
6bae4d8e 1163
fdb4de53 1164 how much snapshots will be kept before get erased, default 1
6bae4d8e 1165
fdb4de53 1166 -name string
6bae4d8e 1167
fdb4de53
TL
1168 name of the sync job, if not set it is default.
1169 It is only necessary if scheduler allready contains this source.
6bae4d8e 1170
fdb4de53 1171 -source string
6bae4d8e 1172
fdb4de53 1173 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
6bae4d8e 1174
eac174d7
DL
1175 -source-user string
1176
1177 name of the user on the source target, root by default
1178
fdb4de53 1179 -verbose boolean
6bae4d8e 1180
fdb4de53 1181 print out the sync progress.
70253912
WL
1182
1183 -properties boolean
1184
1185 Include the dataset's properties in the stream.
7ee3e8e0
ML
1186
1187 -dest-config-path string
1188
1189 specify a custom config path on the destination target. default is /var/lib/pve-zsync
fdb4de53
TL
1190 },
1191 list => qq{
7d54b8ba 1192$PROGNAME list
6bae4d8e
WL
1193
1194 Get a List of all scheduled Sync Jobs
fdb4de53
TL
1195 },
1196 status => qq{
7d54b8ba 1197$PROGNAME status
6bae4d8e
WL
1198
1199 Get the status of all scheduled Sync Jobs
fdb4de53
TL
1200 },
1201 help => qq{
1202$PROGNAME help <cmd> [OPTIONS]
1203
1204 Get help about specified command.
6bae4d8e 1205
fdb4de53
TL
1206 <cmd> string
1207
1208 Command name
1209
1210 -verbose boolean
1211
1212 Verbose output format.
1213 },
1214 enable => qq{
7d54b8ba 1215$PROGNAME enable -source <string> [OPTIONS]
6bae4d8e
WL
1216
1217 enable a syncjob and reset error
1218
1219 -name string
1220
1221 name of the sync job, if not set it is default
1222
1223 -source string
1224
1225 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
fdb4de53
TL
1226 },
1227 disable => qq{
7d54b8ba 1228$PROGNAME disable -source <string> [OPTIONS]
6bae4d8e
WL
1229
1230 pause a sync job
1231
1232 -name string
1233
1234 name of the sync job, if not set it is default
1235
1236 -source string
1237
1238 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
fdb4de53
TL
1239 },
1240 printpod => 'internal command',
28006d67 1241
fdb4de53 1242};
eee21241 1243
fdb4de53
TL
1244if (!$command) {
1245 usage(); die "\n";
1246} elsif (!$cmd_help->{$command}) {
1247 print "ERROR: unknown command '$command'";
1248 usage(1); die "\n";
ce6bc53e 1249}
eee21241 1250
ce6bc53e
TL
1251my @arg = @ARGV;
1252my $param = parse_argv(@arg);
c8c745ec 1253
fdb4de53
TL
1254sub check_params {
1255 for (@_) {
1256 die "$cmd_help->{$command}\n" if !$param->{$_};
1257 }
1258}
1259
ce6bc53e 1260if ($command eq 'destroy') {
fdb4de53 1261 check_params(qw(source));
c8c745ec 1262
ce6bc53e
TL
1263 check_target($param->{source});
1264 destroy_job($param);
c8c745ec 1265
ce6bc53e 1266} elsif ($command eq 'sync') {
fdb4de53 1267 check_params(qw(source dest));
c8c745ec 1268
ce6bc53e
TL
1269 check_target($param->{source});
1270 check_target($param->{dest});
1271 sync($param);
c8c745ec 1272
ce6bc53e 1273} elsif ($command eq 'create') {
fdb4de53 1274 check_params(qw(source dest));
c8c745ec 1275
ce6bc53e
TL
1276 check_target($param->{source});
1277 check_target($param->{dest});
1278 init($param);
c8c745ec 1279
ce6bc53e
TL
1280} elsif ($command eq 'status') {
1281 print status();
c8c745ec 1282
ce6bc53e
TL
1283} elsif ($command eq 'list') {
1284 print list();
c8c745ec 1285
ce6bc53e
TL
1286} elsif ($command eq 'help') {
1287 my $help_command = $ARGV[1];
eee21241 1288
fdb4de53 1289 if ($help_command && $cmd_help->{$help_command}) {
e9301b73 1290 die "$cmd_help->{$help_command}\n";
c8c745ec 1291
ce6bc53e 1292 }
dfd3d834 1293 if ($param->{verbose}) {
ce6bc53e 1294 exec("man $PROGNAME");
c8c745ec 1295
ce6bc53e
TL
1296 } else {
1297 usage(1);
c8c745ec 1298
ce6bc53e 1299 }
c8c745ec 1300
ce6bc53e 1301} elsif ($command eq 'enable') {
fdb4de53 1302 check_params(qw(source));
c8c745ec 1303
ce6bc53e
TL
1304 check_target($param->{source});
1305 enable_job($param);
c8c745ec 1306
ce6bc53e 1307} elsif ($command eq 'disable') {
fdb4de53 1308 check_params(qw(source));
c8c745ec 1309
ce6bc53e
TL
1310 check_target($param->{source});
1311 disable_job($param);
0bc3e510 1312
956c7885
TL
1313} elsif ($command eq 'printpod') {
1314 print_pod();
ce6bc53e 1315}
0bc3e510 1316
ce6bc53e
TL
1317sub usage {
1318 my ($help) = @_;
1319
1320 print("ERROR:\tno command specified\n") if !$help;
1321 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1322 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1323 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1324 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1325 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1326 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1327 print("\t$PROGNAME list\n");
1328 print("\t$PROGNAME status\n");
1329 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1330}
1331
1332sub check_target {
1333 my ($target) = @_;
1334 parse_target($target);
1335}
0bc3e510 1336
956c7885 1337sub print_pod {
fdb4de53
TL
1338
1339 my $synopsis = join("\n", sort values %$cmd_help);
1340
956c7885 1341 print <<EOF;
d9e8f4ec 1342=head1 NAME
0bc3e510 1343
d9e8f4ec 1344pve-zsync - PVE ZFS Replication Manager
0bc3e510 1345
d9e8f4ec 1346=head1 SYNOPSIS
0bc3e510 1347
d9e8f4ec 1348pve-zsync <COMMAND> [ARGS] [OPTIONS]
0bc3e510 1349
fdb4de53 1350$synopsis
db24d113 1351
0bc3e510
WL
1352=head1 DESCRIPTION
1353
1354This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1355This tool also has the capability to add jobs to cron so the sync will be automatically done.
76b2c677 1356The 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 1357To config cron see man crontab.
0bc3e510 1358
d9e8f4ec 1359=head2 PVE ZFS Storage sync Tool
0bc3e510 1360
d9e8f4ec 1361This Tool can get remote pool on other PVE or send Pool to others ZFS machines
0bc3e510 1362
d9e8f4ec 1363=head1 EXAMPLES
0bc3e510 1364
d9e8f4ec
WL
1365add sync job from local VM to remote ZFS Server
1366pve-zsync create -source=100 -dest=192.168.1.2:zfspool
0bc3e510 1367
d9e8f4ec 1368=head1 IMPORTANT FILES
a018f134 1369
d9e8f4ec 1370Cron jobs and config are stored at /etc/cron.d/pve-zsync
6b4f676d 1371
d9e8f4ec 1372The VM config get copied on the destination machine to /var/lib/pve-zsync/
0bc3e510 1373
d9e8f4ec 1374=head1 COPYRIGHT AND DISCLAIMER
0bc3e510 1375
d9e8f4ec 1376Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
0bc3e510 1377
d9e8f4ec 1378This program is free software: you can redistribute it and/or modify it
0bc3e510
WL
1379under the terms of the GNU Affero General Public License as published
1380by the Free Software Foundation, either version 3 of the License, or
1381(at your option) any later version.
1382
d9e8f4ec
WL
1383This program is distributed in the hope that it will be useful, but
1384WITHOUT ANY WARRANTY; without even the implied warranty of
1385MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1386Affero General Public License for more details.
0bc3e510 1387
d9e8f4ec
WL
1388You should have received a copy of the GNU Affero General Public
1389License along with this program. If not, see
1390<http://www.gnu.org/licenses/>.
956c7885
TL
1391
1392EOF
1393}