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