]> git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
9d49730f2465041bec679dbc92bd71c09e461838
[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_cron = 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};
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};
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_cron($path, $name, $cfg)
422
423 } else {
424
425 my $path = $source->{pool};
426 $path .= $source->{path} if $source->{path};
427
428 &$delete_cron($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 }
475 die "$err\n";
476 }
477
478 if ($job_status) {
479 my $conf_name = $source->{abs_path};
480 $conf_name = $source->{vmid} if $source->{vmid};
481 $cfg->{$conf_name}->{$name}->{status} = "ok";
482 $cfg->{$conf_name}->{$name}->{lsync} = $date;
483 write_to_config($cfg);
484 }
485 };
486
487 $param->{method} = "ssh" if !$param->{method};
488
489 if ($source->{vmid}) {
490 die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source);
491 my $disks = get_disks($source);
492
493 foreach my $disk (sort keys %{$disks}) {
494 $source->{abs_path} = $disks->{$disk}->{pool};
495 $source->{abs_path} .= "\/$disks->{$disk}->{path}" if $disks->{$disk}->{path};
496
497 $source->{pool} = $disks->{$disk}->{pool};
498 $source->{path} = "\/$disks->{$disk}->{path}";
499
500 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
501 }
502 if ($method eq "ssh") {
503 send_config($source, $dest,'ssh');
504 }
505 } else {
506 &$sync_path($source, $name, $cfg, $max_snap, $dest, $method);
507 }
508 }
509
510 sub snapshot_get{
511 my ($source, $dest, $max_snap, $name) = @_;
512
513 my $cmd = "zfs list -r -t snapshot -Ho name, -S creation ";
514
515 $cmd .= $source->{abs_path};
516 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip};
517
518 my $raw = run_cmd($cmd);
519 my $index = 1;
520 my $line = "";
521 my $last_snap = undef;
522
523 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
524 $line = $1;
525 $last_snap = $line if $index == 1;
526 if ($index == $max_snap) {
527 $source->{destroy} = 1;
528 last;
529 };
530 $index++;
531 }
532
533 $line =~ m/^(.+)\@(rep_$name\_.+)(\n|$)/;
534 return ($2, $last_snap) if $2;
535
536 return undef;
537 }
538
539 sub snapshot_add {
540 my ($source, $dest, $name) = @_;
541
542 my $date = get_date();
543
544 my $snap_name = "rep_$name\_".$date;
545
546 $source->{new_snap} = $snap_name;
547
548 my $path = $source->{abs_path}."\@".$snap_name;
549
550 my $cmd = "zfs snapshot $path";
551 $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip};
552
553 eval{
554 run_cmd($cmd);
555 };
556
557 if (my $err = $@){
558 snapshot_destroy($source, $dest, 'ssh', $snap_name);
559 die "$err\n";
560 }
561 return $date;
562 }
563
564 sub cron_add {
565 my ($vm) = @_;
566
567 open(my $fh, '>>', "$CRONJOBS")
568 or die "Could not open file: $!\n";
569
570 foreach my $name (keys%{$vm}){
571 my $text = "*/$vm->{$name}->{interval} * * * * root ";
572 $text .= "$PATH$PROGNAME sync";
573 $text .= " -source ";
574 if ($vm->{$name}->{vmid}) {
575 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip};
576 $text .= "$vm->{$name}->{vmid} ";
577 } else {
578 $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip};
579 $text .= "$vm->{$name}->{source_pool}";
580 $text .= "$vm->{$name}->{source_path}" if $vm->{$name}->{source_path};
581 }
582 $text .= " -dest ";
583 $text .= "$vm->{$name}->{dest_ip}:" if $vm->{$name}->{dest_ip};
584 $text .= "$vm->{$name}->{dest_pool}";
585 $text .= "$vm->{$name}->{dest_path}" if $vm->{$name}->{dest_path};
586 $text .= " -name $name ";
587 $text .= " -limit $vm->{$name}->{limit}" if $vm->{$name}->{limit};
588 $text .= " -maxsnap $vm->{$name}->{maxsnap}" if $vm->{$name}->{maxsnap};
589 $text .= "\n";
590 print($fh $text);
591 }
592 close($fh);
593 }
594
595 sub cron_del {
596 my ($source, $dest, $name) = @_;
597
598 open(my $fh, '<', "$CRONJOBS")
599 or die "Could not open file: $!\n";
600
601 $/ = undef;
602
603 my $text = <$fh>;
604 my $buffer = "";
605 close($fh);
606 while ($text && $text =~ s/^(.*?)(\n|$)//) {
607 my $line = $1.$2;
608 if ($line !~ m/^.*root $PATH$PROGNAME sync -source $source.*-dest $dest.*-name $name.*$/){
609 $buffer .= $line;
610 }
611 }
612 open($fh, '>', "$CRONJOBS")
613 or die "Could not open file: $!\n";
614 print($fh $buffer);
615 close($fh);
616 }
617
618 sub get_disks {
619 my ($target) = @_;
620
621 my $cmd = "";
622 $cmd = "ssh root\@$target->{ip} " if $target->{ip};
623 $cmd .= "qm config $target->{vmid}";
624
625 my $res = run_cmd($cmd);
626
627 my $disks = parse_disks($res, $target->{ip});
628
629 return $disks;
630 }
631
632 sub run_cmd {
633 my ($cmd) = @_;
634 print "Start CMD\n" if $DEBUG;
635 print Dumper $cmd if $DEBUG;
636 my $output = `$cmd 2>&1`;
637
638 die $output if 0 != $?;
639
640 chomp($output);
641 print Dumper $output if $DEBUG;
642 print "END CMD\n" if $DEBUG;
643 return $output;
644 }
645
646 sub parse_disks {
647 my ($text, $ip) = @_;
648
649 my $disks;
650
651 my $num = 0;
652 my $cmd = "";
653 $cmd .= "ssh root\@$ip " if $ip;
654 $cmd .= "pvesm zfsscan";
655 my $zfs_pools = run_cmd($cmd);
656 while ($text && $text =~ s/^(.*?)(\n|$)//) {
657 my $line = $1;
658 my $disk = undef;
659 my $stor = undef;
660 my $is_disk = $line =~ m/^(virtio|ide|scsi|sata){1}\d+: /;
661 if($line =~ m/^(virtio\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
662 $disk = $3;
663 $stor = $2;
664 } elsif($line =~ m/^(ide\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
665 $disk = $3;
666 $stor = $2;
667 } elsif($line =~ m/^(scsi\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
668 $disk = $3;
669 $stor = $2;
670 } elsif($line =~ m/^(sata\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) {
671 $disk = $3;
672 $stor = $2;
673 }
674
675 die "disk is not on ZFS Storage\n" if $is_disk && !$disk && $line !~ m/cdrom/;
676
677 if($disk && $line !~ m/none/ && $line !~ m/cdrom/ ) {
678 my $cmd = "";
679 $cmd .= "ssh root\@$ip " if $ip;
680 $cmd .= "pvesm path $stor$disk";
681 my $path = run_cmd($cmd);
682
683 if ($path =~ m/^\/dev\/zvol\/(\w+).*(\/$disk)$/){
684
685 $disks->{$num}->{pool} = $1;
686 $disks->{$num}->{path} = $disk;
687 $num++;
688
689 } else {
690 die "ERROR: in path\n";
691 }
692 }
693 }
694 return $disks;
695 }
696
697 sub snapshot_destroy {
698 my ($source, $dest, $method, $snap) = @_;
699
700 my $zfscmd = "zfs destroy ";
701 my $name = "$source->{path}\@$snap";
702
703 eval {
704 if($source->{ip} && $method eq 'ssh'){
705 run_cmd("ssh root\@$source->{ip} $zfscmd $source->{pool}$name");
706 } else {
707 run_cmd("$zfscmd $source->{pool}$name");
708 }
709 };
710 if (my $erro = $@) {
711 warn "WARN: $erro";
712 }
713 if ($dest){
714 my $ssh = $dest->{ip} ? "ssh root\@$dest->{ip}" : "";
715
716 my $path = "";
717 $path ="$dest->{path}" if $dest->{path};
718
719 my @dir = split(/\//, $source->{path});
720 eval {
721 run_cmd("$ssh $zfscmd $dest->{pool}$path\/$dir[@dir-1]\@$snap ");
722 };
723 if (my $erro = $@) {
724 warn "WARN: $erro";
725 }
726 }
727 }
728
729 sub snapshot_exist {
730 my ($source ,$dest, $method) = @_;
731
732 my $cmd = "";
733 $cmd = "ssh root\@$dest->{ip} " if $dest->{ip};
734 $cmd .= "zfs list -rt snapshot -Ho name $dest->{pool}";
735 $cmd .= "$dest->{path}" if $dest->{path};
736 my @dir = split(/\//, $source->{path});
737 $cmd .= "\/$dir[@dir-1]\@$source->{old_snap}";
738
739 my $text = "";
740 eval {$text =run_cmd($cmd);};
741 if (my $erro = $@) {
742 warn "WARN: $erro";
743 return undef;
744 }
745
746 while ($text && $text =~ s/^(.*?)(\n|$)//) {
747 my $line = $1;
748 return 1 if $line =~ m/^.*$source->{old_snap}$/;
749 }
750 }
751
752 sub send_image {
753 my ($source, $dest, $method, $verbose, $limit) = @_;
754
755 my $cmd = "";
756
757 $cmd .= "ssh root\@$source->{ip} " if $source->{ip};
758 $cmd .= "zfs send ";
759 $cmd .= "-v " if $verbose;
760
761 if($source->{last_snap} && snapshot_exist($source ,$dest, $method)) {
762 $cmd .= "-i $source->{abs_path}\@$source->{old_snap} $source->{abs_path}\@$source->{new_snap} ";
763 } else {
764 $cmd .= "$source->{abs_path}\@$source->{new_snap} ";
765 }
766
767 if ($limit){
768 my $bwl = $limit*1024;
769 $cmd .= "| cstream -t $bwl";
770 }
771 $cmd .= "| ";
772 $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip};
773 $cmd .= "zfs recv $dest->{pool}";
774 $cmd .= "$dest->{path}" if $dest->{path};
775
776 my @dir = split(/\//,$source->{path});
777 $cmd .= "\/$dir[@dir-1]\@$source->{new_snap}";
778
779 eval {
780 run_cmd($cmd)
781 };
782
783 if (my $erro = $@) {
784 snapshot_destroy($source, undef, $method, $source->{new_snap});
785 die $erro;
786 };
787 }
788
789
790 sub send_config{
791 my ($source, $dest, $method) = @_;
792
793 if ($method eq 'ssh'){
794 if ($dest->{ip} && $source->{ip}) {
795 run_cmd("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
796 run_cmd("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
797 } elsif ($dest->{ip}) {
798 run_cmd("ssh root\@$dest->{ip} mkdir $VMCONFIG -p");
799 run_cmd("scp $QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
800 } elsif ($source->{ip}) {
801 run_cmd("mkdir $VMCONFIG -p");
802 run_cmd("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf $VMCONFIG$source->{vmid}.conf.$source->{new_snap}");
803 }
804
805 if ($source->{destroy}){
806 if($dest->{ip}){
807 run_cmd("ssh root\@$dest->{ip} rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
808 } else {
809 run_cmd("rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}");
810 }
811 }
812 }
813 }
814
815 sub get_date {
816 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
817 my $datestamp = sprintf ( "%04d-%02d-%02d_%02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec);
818
819 return $datestamp;
820 }
821
822 sub status {
823 my $cfg = read_from_config("$CONFIG_PATH$CONFIG");
824
825 my $status_list = sprintf("%-25s%-15s%-10s\n","SOURCE","NAME","STATUS");
826
827 foreach my $source (sort keys%{$cfg}){
828 foreach my $sync_name (sort keys%{$cfg->{$source}}){
829 my $status;
830
831 my $source_name = $source;
832
833 $source_name = $cfg->{$source}->{$sync_name}->{source_ip}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip};
834
835 $status = sprintf("%-10s",$cfg->{$source}->{$sync_name}->{status});
836
837 $status_list .= sprintf("%-25s%-15s", cut_to_width($source_name,25), cut_to_width($sync_name,15));
838 $status_list .= "$status\n";
839 }
840 }
841
842 return $status_list;
843 }
844
845
846 my $command = $ARGV[0];
847
848 my $commands = {'destroy' => 1,
849 'create' => 1,
850 'sync' => 1,
851 'list' => 1,
852 'status' => 1,
853 'help' => 1};
854
855 if (!$command || !$commands->{$command}) {
856 usage();
857 die "\n";
858 }
859
860 my $dest = '';
861 my $source = '';
862 my $verbose = '';
863 my $interval = '';
864 my $limit = '';
865 my $maxsnap = '';
866 my $name = '';
867 my $skip = '';
868
869 my $help_sync = "zfs-zsync sync -dest <string> -source <string> [OPTIONS]\n
870 \twill sync one time\n
871 \t-dest\tstring\n
872 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
873 \t-limit\tinteger\n
874 \t\tmax sync speed in kBytes/s, default unlimited\n
875 \t-maxsnap\tinteger\n
876 \t\thow much snapshots will be kept before get erased, default 1/n
877 \t-name\tstring\n
878 \t\tname of the sync job, if not set it is default.
879 \tIt is only necessary if scheduler allready contains this source.\n
880 \t-source\tstring\n
881 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
882
883 my $help_create = "zfs-zsync create -dest <string> -source <string> [OPTIONS]/n
884 \tCreate a sync Job\n
885 \t-dest\tstringn\n
886 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
887 \t-interval\tinteger\n
888 \t\tthe interval in min in witch the zfs will sync,
889 \t\tdefault is 15 min\n
890 \t-limit\tinteger\n
891 \t\tmax sync speed, default unlimited\n
892 \t-maxsnap\tstring\n
893 \t\thow much snapshots will be kept before get erased, default 1\n
894 \t-name\tstring\n
895 \t\tname of the sync job, if not set it is default\n
896 \t-skip\tboolean\n
897 \t\tif this flag is set it will skip the first sync\n
898 \t-source\tstring\n
899 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
900
901 my $help_destroy = "zfs-zsync destroy -source <string> [OPTIONS]\n
902 \tremove a sync Job from the scheduler\n
903 \t-name\tstring\n
904 \t\tname of the sync job, if not set it is default\n
905 \t-source\tstring\n
906 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
907
908 my $help_help = "zfs-zsync help <cmd> [OPTIONS]\n
909 \tGet help about specified command.\n
910 \t<cmd>\tstring\n
911 \t\tCommand name\n
912 \t-verbose\tboolean\n
913 \t\tVerbose output format.\n";
914
915 my $help_list = "zfs-zsync list\n
916 \tGet a List of all scheduled Sync Jobs\n";
917
918 my $help_status = "zfs-zsync status\n
919 \tGet the status of all scheduled Sync Jobs\n";
920
921 sub help{
922 my ($command) = @_;
923
924 switch($command){
925 case 'help'
926 {
927 die "$help_help\n";
928 }
929 case 'sync'
930 {
931 die "$help_sync\n";
932 }
933 case 'destroy'
934 {
935 die "$help_destroy\n";
936 }
937 case 'create'
938 {
939 die "$help_create\n";
940 }
941 case 'list'
942 {
943 die "$help_list\n";
944 }
945 case 'status'
946 {
947 die "$help_status\n";
948 }
949 }
950
951 }
952
953 my $err = GetOptions ('dest=s' => \$dest,
954 'source=s' => \$source,
955 'verbose' => \$verbose,
956 'interval=i' => \$interval,
957 'limit=i' => \$limit,
958 'maxsnap=i' => \$maxsnap,
959 'name=s' => \$name,
960 'skip' => \$skip);
961
962 if ($err == 0) {
963 die "can't parse options\n";
964 }
965
966 my $param;
967 $param->{dest} = $dest;
968 $param->{source} = $source;
969 $param->{verbose} = $verbose;
970 $param->{interval} = $interval;
971 $param->{limit} = $limit;
972 $param->{maxsnap} = $maxsnap;
973 $param->{name} = $name;
974 $param->{skip} = $skip;
975
976 switch($command){
977 case "destroy"
978 {
979 die "$help_destroy\n" if !$source;
980 check_target($source);
981 destroy($param);
982 }
983 case "sync"
984 {
985 die "$help_sync\n" if !$source || !$dest;
986 check_target($source);
987 check_target($dest);
988 sync($param);
989 }
990 case "create"
991 {
992 die "$help_create\n" if !$source || !$dest;
993 check_target($source);
994 check_target($dest);
995 init($param);
996 }
997 case "status"
998 {
999 print status();
1000 }
1001 case "list"
1002 {
1003 print list();
1004 }
1005 case "help"
1006 {
1007 my $help_command = $ARGV[1];
1008 if ($help_command && $commands->{$help_command}) {
1009 print help($help_command);
1010 }
1011 if ($verbose){
1012 exec("man $PROGNAME");
1013 } else {
1014 usage(1);
1015 }
1016 }
1017 }
1018
1019 sub usage{
1020 my ($help) = @_;
1021
1022 print("ERROR:\tno command specified\n") if !$help;
1023 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1024 print("\tpve-zsync help [<cmd>] [OPTIONS]\n\n");
1025 print("\tpve-zsync create -dest <string> -source <string> [OPTIONS]\n");
1026 print("\tpve-zsync destroy -source <string> [OPTIONS]\n");
1027 print("\tpve-zsync list\n");
1028 print("\tpve-zsync status\n");
1029 print("\tpve-zsync sync -dest <string> -source <string> [OPTIONS]\n");
1030 }
1031
1032 sub check_target{
1033 my ($target) = @_;
1034
1035 chomp($target);
1036
1037 if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\/.+)?/){
1038 print("ERROR:\t$target is not valid.\n\tUse [IP:]<ZFSPool>[/Path]!\n");
1039 return 1;
1040 }
1041 return undef;
1042 }
1043
1044 __END__
1045
1046 =head1 NAME
1047
1048 pve-zsync - PVE ZFS Replication Manager
1049
1050 =head1 SYNOPSIS
1051
1052 zfs-zsync <COMMAND> [ARGS] [OPTIONS]
1053
1054 zfs-zsync help <cmd> [OPTIONS]
1055
1056 Get help about specified command.
1057
1058 <cmd> string
1059
1060 Command name
1061
1062 -verbose boolean
1063
1064 Verbose output format.
1065
1066 zfs-zsync create -dest <string> -source <string> [OPTIONS]
1067
1068 Create a sync Job
1069
1070 -dest string
1071
1072 the destination target is like [IP]:<Pool>[/Path]
1073
1074 -interval integer
1075
1076 the interval in min in witch the zfs will sync, default is 15 min
1077
1078 -limit integer
1079
1080 max sync speed, default unlimited
1081
1082 -maxsnap string
1083
1084 how much snapshots will be kept before get erased, default 1
1085
1086 -name string
1087
1088 name of the sync job, if not set it is default
1089
1090 -skip boolean
1091
1092 if this flag is set it will skip the first sync
1093
1094 -source string
1095
1096 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1097
1098 zfs-zsync destroy -source <string> [OPTIONS]
1099
1100 remove a sync Job from the scheduler
1101
1102 -name string
1103
1104 name of the sync job, if not set it is default
1105
1106 -source string
1107
1108 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1109
1110 zfs-zsync list
1111
1112 Get a List of all scheduled Sync Jobs
1113
1114 zfs-zsync status
1115
1116 Get the status of all scheduled Sync Jobs
1117
1118 zfs-zsync sync -dest <string> -source <string> [OPTIONS]
1119
1120 will sync one time
1121
1122 -dest string
1123
1124 the destination target is like [IP:]<Pool>[/Path]
1125
1126 -limit integer
1127
1128 max sync speed in kBytes/s, default unlimited
1129
1130 -maxsnap integer
1131
1132 how much snapshots will be kept before get erased, default 1
1133
1134 -name string
1135
1136 name of the sync job, if not set it is default.
1137 It is only necessary if scheduler allready contains this source.
1138
1139 -source string
1140
1141 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1142
1143 =head1 DESCRIPTION
1144
1145 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1146 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1147
1148 =head2 PVE ZFS Storage sync Tool
1149
1150 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1151
1152 =head1 EXAMPLES
1153
1154 add sync job from local VM to remote ZFS Server
1155 zfs-zsync -source=100 -dest=192.168.1.2:zfspool
1156
1157 =head1 IMPORTANT FILES
1158
1159 Where the cron jobs are stored /etc/cron.d/pve-zsync
1160 Where the VM config get copied on the destination machine /var/pve-zsync
1161 Where the config is stored /var/pve-zsync
1162
1163 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1164
1165 This program is free software: you can redistribute it and/or modify it
1166 under the terms of the GNU Affero General Public License as published
1167 by the Free Software Foundation, either version 3 of the License, or
1168 (at your option) any later version.
1169
1170 This program is distributed in the hope that it will be useful, but
1171 WITHOUT ANY WARRANTY; without even the implied warranty of
1172 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1173 Affero General Public License for more details.
1174
1175 You should have received a copy of the GNU Affero General Public
1176 License along with this program. If not, see
1177 <http://www.gnu.org/licenses/>.