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