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