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