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