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