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