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