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