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