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