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