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