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