]>
Commit | Line | Data |
---|---|---|
0bc3e510 WL |
1 | #!/usr/bin/perl |
2 | ||
3 | my $PROGNAME = "pve-zsync"; | |
4 | my $CONFIG_PATH = '/var/lib/'.$PROGNAME.'/'; | |
5 | my $CONFIG = "$PROGNAME.cfg"; | |
6 | my $CRONJOBS = '/etc/cron.d/'.$PROGNAME; | |
7 | my $VMCONFIG = '/var/lib/'.$PROGNAME.'/'; | |
8 | my $PATH = "/usr/sbin/"; | |
9 | my $QEMU_CONF = '/etc/pve/local/qemu-server/'; | |
10 | my $DEBUG = 0; | |
11 | ||
12 | use strict; | |
13 | use warnings; | |
14 | use Data::Dumper qw(Dumper); | |
15 | use Fcntl qw(:flock SEEK_END); | |
16 | use Getopt::Long; | |
17 | use Switch; | |
18 | ||
19 | check_bin ('cstream'); | |
20 | check_bin ('zfs'); | |
21 | check_bin ('ssh'); | |
22 | check_bin ('scp'); | |
23 | ||
24 | sub 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 | ||
37 | sub 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 | ||
55 | sub 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 | ||
62 | sub unlock { | |
63 | my ($fh) = @_; | |
64 | flock($fh, LOCK_UN) or die "Cannot unlock config- $!\n"; | |
65 | } | |
66 | ||
67 | sub 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 | ||
82 | sub check_pool_exsits { | |
83 | my ($pool, $ip) = @_; | |
84 | ||
85 | my $cmd = ''; | |
86 | $cmd = "ssh root\@$ip " if $ip; | |
87 | $cmd .= "zfs list $pool -H"; | |
88 | eval { | |
89 | run_cmd($cmd); | |
90 | }; | |
91 | ||
92 | if($@){ | |
93 | return 1; | |
94 | } | |
95 | return undef; | |
96 | } | |
97 | ||
98 | sub write_to_config { | |
99 | my ($cfg) = @_; | |
100 | ||
101 | open(my $fh, ">", "$CONFIG_PATH$CONFIG") | |
102 | or die "cannot open >$CONFIG_PATH$CONFIG: $!\n"; | |
103 | ||
104 | my $text = decode_config($cfg); | |
105 | ||
106 | print($fh $text); | |
107 | ||
108 | close($fh); | |
109 | } | |
110 | ||
111 | sub read_from_config { | |
112 | ||
113 | unless(-e "$CONFIG_PATH$CONFIG") { | |
114 | return undef; | |
115 | } | |
116 | ||
117 | open(my $fh, "<", "$CONFIG_PATH$CONFIG") | |
118 | or die "cannot open > $CONFIG_PATH$CONFIG: $!\n"; | |
119 | ||
120 | $/ = undef; | |
121 | ||
122 | my $text = <$fh>; | |
123 | ||
124 | unlock($fh); | |
125 | ||
1a7871e7 WL |
126 | close($fh); |
127 | ||
0bc3e510 WL |
128 | my $cfg = encode_config($text); |
129 | ||
130 | return $cfg; | |
131 | } | |
132 | ||
133 | sub 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 | ||
147 | sub 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 | ||
230 | sub 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 | ||
260 | sub 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 | ||
281 | sub 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 | ||
294 | sub 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 | ||
377 | sub 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 | ||
425 | sub 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 | ||
488 | sub 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 | ||
517 | sub 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 | ||
542 | sub 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 | ||
573 | sub 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 | ||
596 | sub 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 | ||
610 | sub 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 | ||
624 | sub 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 | ||
673 | sub 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 | ||
705 | sub 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 | ||
728 | sub 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 | ||
772 | sub 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 | ||
797 | sub 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 | ||
804 | sub 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 | ||
835 | my $command = $ARGV[0]; | |
836 | ||
837 | my $commands = {'destroy' => 1, | |
838 | 'create' => 1, | |
839 | 'sync' => 1, | |
840 | 'list' => 1, | |
841 | 'status' => 1, | |
842 | 'help' => 1}; | |
843 | ||
844 | if (!$command || !$commands->{$command}) { | |
845 | usage(); | |
846 | die "\n"; | |
847 | } | |
848 | ||
849 | my $dest = ''; | |
850 | my $source = ''; | |
851 | my $verbose = ''; | |
852 | my $interval = ''; | |
853 | my $limit = ''; | |
854 | my $maxsnap = ''; | |
855 | my $name = ''; | |
856 | my $skip = ''; | |
857 | ||
858 | my $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 | ||
872 | my $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 | ||
890 | my $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 | ||
897 | my $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 | ||
904 | my $help_list = "zfs-zsync list\n | |
905 | \tGet a List of all scheduled Sync Jobs\n"; | |
906 | ||
907 | my $help_status = "zfs-zsync status\n | |
908 | \tGet the status of all scheduled Sync Jobs\n"; | |
909 | ||
910 | sub 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 | ||
942 | my $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 | ||
951 | if ($err == 0) { | |
952 | die "can't parse options\n"; | |
953 | } | |
954 | ||
955 | my $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 | ||
965 | switch($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 | ||
1008 | sub 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 | ||
1021 | sub 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 | ||
1037 | pve-zsync - PVE ZFS Replication Manager | |
1038 | ||
1039 | =head1 SYNOPSIS | |
1040 | ||
1041 | zfs-zsync <COMMAND> [ARGS] [OPTIONS] | |
1042 | ||
1043 | zfs-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 | ||
1055 | zfs-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 | ||
1087 | zfs-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 | ||
1099 | zfs-zsync list | |
1100 | ||
1101 | Get a List of all scheduled Sync Jobs | |
1102 | ||
1103 | zfs-zsync status | |
1104 | ||
1105 | Get the status of all scheduled Sync Jobs | |
1106 | ||
1107 | zfs-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 | ||
1134 | This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers. | |
1135 | This 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 | ||
1139 | This Tool can get remote pool on other PVE or send Pool to others ZFS machines | |
1140 | ||
1141 | =head1 EXAMPLES | |
1142 | ||
1143 | add sync job from local VM to remote ZFS Server | |
1144 | zfs-zsync -source=100 -dest=192.168.1.2:zfspool | |
1145 | ||
1146 | =head1 IMPORTANT FILES | |
1147 | ||
1148 | Where the cron jobs are stored /etc/cron.d/pve-zsync | |
1149 | Where the VM config get copied on the destination machine /var/pve-zsync | |
1150 | Where the config is stored /var/pve-zsync | |
1151 | ||
1152 | Copyright (C) 2007-2015 Proxmox Server Solutions GmbH | |
1153 | ||
1154 | This program is free software: you can redistribute it and/or modify it | |
1155 | under the terms of the GNU Affero General Public License as published | |
1156 | by the Free Software Foundation, either version 3 of the License, or | |
1157 | (at your option) any later version. | |
1158 | ||
1159 | This program is distributed in the hope that it will be useful, but | |
1160 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
1161 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
1162 | Affero General Public License for more details. | |
1163 | ||
1164 | You should have received a copy of the GNU Affero General Public | |
1165 | License along with this program. If not, see | |
1166 | <http://www.gnu.org/licenses/>. |