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