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