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