]>
Commit | Line | Data |
---|---|---|
c84106ed DC |
1 | package PVE::API2::Disks::ZFS; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Diskmanage; | |
7 | use PVE::JSONSchema qw(get_standard_option); | |
f720f6c4 | 8 | use PVE::Systemd; |
c84106ed DC |
9 | use PVE::API2::Storage::Config; |
10 | use PVE::Storage; | |
11 | use PVE::Tools qw(run_command lock_file trim); | |
12 | ||
13 | use PVE::RPCEnvironment; | |
14 | use PVE::RESTHandler; | |
15 | ||
16 | use base qw(PVE::RESTHandler); | |
17 | ||
18 | my $ZPOOL = '/sbin/zpool'; | |
19 | my $ZFS = '/sbin/zfs'; | |
20 | ||
55553bd4 AL |
21 | sub get_pool_data { |
22 | if (!-f $ZPOOL) { | |
23 | die "zfsutils-linux not installed\n"; | |
24 | } | |
25 | ||
26 | my $propnames = [qw(name size alloc free frag dedup health)]; | |
27 | my $numbers = { | |
28 | size => 1, | |
29 | alloc => 1, | |
30 | free => 1, | |
31 | frag => 1, | |
32 | dedup => 1, | |
33 | }; | |
34 | ||
35 | my $cmd = [$ZPOOL,'list', '-HpPLo', join(',', @$propnames)]; | |
36 | ||
37 | my $pools = []; | |
38 | ||
39 | run_command($cmd, outfunc => sub { | |
40 | my ($line) = @_; | |
41 | ||
42 | my @props = split('\s+', trim($line)); | |
43 | my $pool = {}; | |
44 | for (my $i = 0; $i < scalar(@$propnames); $i++) { | |
45 | if ($numbers->{$propnames->[$i]}) { | |
46 | $pool->{$propnames->[$i]} = $props[$i] + 0; | |
47 | } else { | |
48 | $pool->{$propnames->[$i]} = $props[$i]; | |
49 | } | |
50 | } | |
51 | ||
52 | push @$pools, $pool; | |
53 | }); | |
54 | ||
55 | return $pools; | |
56 | } | |
57 | ||
c84106ed DC |
58 | __PACKAGE__->register_method ({ |
59 | name => 'index', | |
60 | path => '', | |
61 | method => 'GET', | |
62 | proxyto => 'node', | |
63 | protected => 1, | |
64 | permissions => { | |
65 | check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1], | |
66 | }, | |
67 | description => "List Zpools.", | |
68 | parameters => { | |
69 | additionalProperties => 0, | |
70 | properties => { | |
71 | node => get_standard_option('pve-node'), | |
72 | }, | |
73 | }, | |
74 | returns => { | |
75 | type => 'array', | |
76 | items => { | |
77 | type => 'object', | |
78 | properties => { | |
79 | name => { | |
80 | type => 'string', | |
81 | description => "", | |
82 | }, | |
83 | size => { | |
84 | type => 'integer', | |
85 | description => "", | |
86 | }, | |
87 | alloc => { | |
88 | type => 'integer', | |
89 | description => "", | |
90 | }, | |
91 | free => { | |
92 | type => 'integer', | |
93 | description => "", | |
94 | }, | |
95 | frag => { | |
96 | type => 'integer', | |
97 | description => "", | |
98 | }, | |
99 | dedup => { | |
100 | type => 'number', | |
101 | description => "", | |
102 | }, | |
103 | health => { | |
104 | type => 'string', | |
105 | description => "", | |
106 | }, | |
107 | }, | |
108 | }, | |
109 | links => [ { rel => 'child', href => "{name}" } ], | |
110 | }, | |
111 | code => sub { | |
112 | my ($param) = @_; | |
113 | ||
55553bd4 | 114 | return get_pool_data(); |
c84106ed DC |
115 | }}); |
116 | ||
117 | sub preparetree { | |
118 | my ($el) = @_; | |
119 | delete $el->{lvl}; | |
120 | if ($el->{children} && scalar(@{$el->{children}})) { | |
121 | $el->{leaf} = 0; | |
122 | foreach my $child (@{$el->{children}}) { | |
123 | preparetree($child); | |
124 | } | |
125 | } else { | |
126 | $el->{leaf} = 1; | |
127 | } | |
128 | } | |
129 | ||
130 | ||
131 | __PACKAGE__->register_method ({ | |
132 | name => 'detail', | |
133 | path => '{name}', | |
134 | method => 'GET', | |
135 | proxyto => 'node', | |
136 | protected => 1, | |
137 | permissions => { | |
138 | check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1], | |
139 | }, | |
140 | description => "Get details about a zpool.", | |
141 | parameters => { | |
142 | additionalProperties => 0, | |
143 | properties => { | |
144 | node => get_standard_option('pve-node'), | |
145 | name => get_standard_option('pve-storage-id'), | |
146 | }, | |
147 | }, | |
148 | returns => { | |
4d12dbff DC |
149 | type => 'object', |
150 | properties => { | |
151 | name => { | |
152 | type => 'string', | |
153 | description => 'The name of the zpool.', | |
154 | }, | |
155 | state => { | |
156 | type => 'string', | |
157 | description => 'The state of the zpool.', | |
158 | }, | |
159 | status => { | |
160 | optional => 1, | |
161 | type => 'string', | |
162 | description => 'Information about the state of the zpool.', | |
163 | }, | |
164 | action => { | |
165 | optional => 1, | |
166 | type => 'string', | |
167 | description => 'Information about the recommended action to fix the state.', | |
168 | }, | |
169 | scan => { | |
977b80c8 | 170 | optional => 1, |
4d12dbff DC |
171 | type => 'string', |
172 | description => 'Information about the last/current scrub.', | |
173 | }, | |
b005f2f4 | 174 | errors => { |
4d12dbff DC |
175 | type => 'string', |
176 | description => 'Information about the errors on the zpool.', | |
177 | }, | |
178 | children => { | |
179 | type => 'array', | |
32f749b8 | 180 | description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.", |
4d12dbff DC |
181 | items => { |
182 | type => 'object', | |
183 | properties => { | |
184 | name => { | |
185 | type => 'string', | |
32f749b8 | 186 | description => 'The name of the vdev or section.', |
4d12dbff DC |
187 | }, |
188 | state => { | |
32f749b8 | 189 | optional => 1, |
4d12dbff DC |
190 | type => 'string', |
191 | description => 'The state of the vdev.', | |
192 | }, | |
193 | read => { | |
32f749b8 | 194 | optional => 1, |
4d12dbff DC |
195 | type => 'number', |
196 | }, | |
197 | write => { | |
32f749b8 | 198 | optional => 1, |
4d12dbff DC |
199 | type => 'number', |
200 | }, | |
201 | cksum => { | |
32f749b8 | 202 | optional => 1, |
4d12dbff DC |
203 | type => 'number', |
204 | }, | |
205 | msg => { | |
206 | type => 'string', | |
207 | description => 'An optional message about the vdev.' | |
208 | } | |
209 | }, | |
210 | }, | |
211 | }, | |
212 | }, | |
c84106ed DC |
213 | }, |
214 | code => sub { | |
215 | my ($param) = @_; | |
216 | ||
217 | if (!-f $ZPOOL) { | |
218 | die "zfsutils-linux not installed\n"; | |
219 | } | |
220 | ||
221 | my $cmd = [$ZPOOL, 'status', '-P', $param->{name}]; | |
222 | ||
223 | my $pool = { | |
224 | lvl => 0, | |
225 | }; | |
c84106ed DC |
226 | |
227 | my $curfield; | |
228 | my $config = 0; | |
229 | ||
230 | my $stack = [$pool]; | |
231 | my $curlvl = 0; | |
232 | ||
233 | run_command($cmd, outfunc => sub { | |
234 | my ($line) = @_; | |
235 | ||
236 | if ($line =~ m/^\s*(\S+): (\S+.*)$/) { | |
237 | $curfield = $1; | |
238 | $pool->{$curfield} = $2; | |
239 | ||
240 | $config = 0 if $curfield eq 'errors'; | |
241 | } elsif (!$config && $line =~ m/^\s+(\S+.*)$/) { | |
242 | $pool->{$curfield} .= " " . $1; | |
243 | } elsif (!$config && $line =~ m/^\s*config:/) { | |
244 | $config = 1; | |
a49fc735 | 245 | } elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) { |
c84106ed | 246 | my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7); |
576e143a FE |
247 | if ($name ne "NAME") { |
248 | my $lvl = int(length($space) / 2) + 1; # two spaces per level | |
c84106ed DC |
249 | my $vdev = { |
250 | name => $name, | |
c84106ed DC |
251 | msg => $msg, |
252 | lvl => $lvl, | |
253 | }; | |
8b6b7102 | 254 | |
a49fc735 TM |
255 | $vdev->{state} = $state if defined($state); |
256 | $vdev->{read} = $read + 0 if defined($read); | |
257 | $vdev->{write} = $write + 0 if defined($write); | |
258 | $vdev->{cksum} = $cksum + 0 if defined($cksum); | |
8b6b7102 | 259 | |
c84106ed DC |
260 | my $cur = pop @$stack; |
261 | ||
262 | if ($lvl > $curlvl) { | |
263 | $cur->{children} = [ $vdev ]; | |
c84106ed DC |
264 | } elsif ($lvl == $curlvl) { |
265 | $cur = pop @$stack; | |
266 | push @{$cur->{children}}, $vdev; | |
c84106ed | 267 | } else { |
8b6b7102 | 268 | while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) { |
c84106ed DC |
269 | $cur = pop @$stack; |
270 | } | |
271 | push @{$cur->{children}}, $vdev; | |
c84106ed | 272 | } |
8b6b7102 | 273 | |
a49fc735 TM |
274 | push @$stack, $cur; |
275 | push @$stack, $vdev; | |
c84106ed DC |
276 | $curlvl = $lvl; |
277 | } | |
278 | } | |
279 | }); | |
280 | ||
281 | # change treenodes for extjs tree | |
282 | $pool->{name} = delete $pool->{pool}; | |
283 | preparetree($pool); | |
284 | ||
285 | return $pool; | |
286 | }}); | |
287 | ||
288 | __PACKAGE__->register_method ({ | |
289 | name => 'create', | |
290 | path => '', | |
291 | method => 'POST', | |
292 | proxyto => 'node', | |
293 | protected => 1, | |
294 | permissions => { | |
295 | check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']], | |
296 | }, | |
fdc863c7 | 297 | description => "Create a ZFS pool.", |
c84106ed DC |
298 | parameters => { |
299 | additionalProperties => 0, | |
300 | properties => { | |
301 | node => get_standard_option('pve-node'), | |
302 | name => get_standard_option('pve-storage-id'), | |
303 | raidlevel => { | |
304 | type => 'string', | |
7058abe2 DC |
305 | description => 'The RAID level to use.', |
306 | enum => ['single', 'mirror', 'raid10', 'raidz', 'raidz2', 'raidz3'], | |
c84106ed DC |
307 | }, |
308 | devices => { | |
5be1a092 | 309 | type => 'string', format => 'string-list', |
fdc863c7 | 310 | description => 'The block devices you want to create the zpool on.', |
c84106ed DC |
311 | }, |
312 | ashift => { | |
313 | type => 'integer', | |
314 | minimum => 9, | |
315 | maximum => 16, | |
316 | optional => 1, | |
317 | default => 12, | |
7d597888 | 318 | description => 'Pool sector size exponent.', |
c84106ed DC |
319 | }, |
320 | compression => { | |
321 | type => 'string', | |
322 | description => 'The compression algorithm to use.', | |
ae098a19 | 323 | enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'], |
c84106ed DC |
324 | optional => 1, |
325 | default => 'on', | |
326 | }, | |
327 | add_storage => { | |
fdc863c7 | 328 | description => "Configure storage using the zpool.", |
c84106ed DC |
329 | type => 'boolean', |
330 | optional => 1, | |
331 | default => 0, | |
332 | }, | |
333 | }, | |
334 | }, | |
335 | returns => { type => 'string' }, | |
336 | code => sub { | |
337 | my ($param) = @_; | |
338 | ||
339 | my $rpcenv = PVE::RPCEnvironment::get(); | |
340 | my $user = $rpcenv->get_user(); | |
341 | ||
342 | my $name = $param->{name}; | |
55553bd4 | 343 | my $node = $param->{node}; |
c84106ed | 344 | my $devs = [PVE::Tools::split_list($param->{devices})]; |
38572a8f | 345 | my $raidlevel = $param->{raidlevel}; |
c84106ed DC |
346 | my $compression = $param->{compression} // 'on'; |
347 | ||
107208bd | 348 | for my $dev (@$devs) { |
c84106ed | 349 | $dev = PVE::Diskmanage::verify_blockdev_path($dev); |
0370861c | 350 | PVE::Diskmanage::assert_disk_unused($dev); |
bd485fd4 AL |
351 | |
352 | } | |
353 | my $storage_params = { | |
354 | type => 'zfspool', | |
355 | pool => $name, | |
356 | storage => $name, | |
357 | content => 'rootdir,images', | |
358 | nodes => $node, | |
359 | }; | |
360 | my $verify_params = [qw(pool)]; | |
361 | ||
362 | if ($param->{add_storage}) { | |
363 | PVE::API2::Storage::Config->create_or_update( | |
364 | $name, | |
365 | $node, | |
366 | $storage_params, | |
367 | $verify_params, | |
368 | 1, | |
369 | ); | |
c84106ed | 370 | } |
c84106ed | 371 | |
55553bd4 AL |
372 | my $pools = get_pool_data(); |
373 | die "pool '${name}' already exists on node '${node}'\n" | |
374 | if grep { $_->{name} eq $name } @{$pools}; | |
375 | ||
c84106ed DC |
376 | my $numdisks = scalar(@$devs); |
377 | my $mindisks = { | |
7058abe2 DC |
378 | single => 1, |
379 | mirror => 2, | |
c84106ed DC |
380 | raid10 => 4, |
381 | raidz => 3, | |
382 | raidz2 => 4, | |
383 | raidz3 => 5, | |
384 | }; | |
385 | ||
386 | # sanity checks | |
387 | die "raid10 needs an even number of disks\n" | |
38572a8f | 388 | if $raidlevel eq 'raid10' && $numdisks % 2 != 0; |
c84106ed | 389 | |
7058abe2 DC |
390 | die "please give only one disk for single disk mode\n" |
391 | if $raidlevel eq 'single' && $numdisks > 1; | |
392 | ||
38572a8f DC |
393 | die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n" |
394 | if $numdisks < $mindisks->{$raidlevel}; | |
c84106ed | 395 | |
107208bd TL |
396 | my $code = sub { |
397 | for my $dev (@$devs) { | |
398 | PVE::Diskmanage::assert_disk_unused($dev); | |
05d91712 | 399 | |
107208bd | 400 | my $is_partition = PVE::Diskmanage::is_partition($dev); |
a2c34371 | 401 | |
107208bd TL |
402 | if ($is_partition) { |
403 | eval { | |
404 | PVE::Diskmanage::change_parttype($dev, '6a898cc3-1dd2-11b2-99a6-080020736631'); | |
405 | }; | |
406 | warn $@ if $@; | |
407 | } | |
a2c34371 | 408 | |
107208bd TL |
409 | my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev; |
410 | ||
411 | $sysfsdev =~ s!^/dev/!/sys/block/!; | |
412 | if ($is_partition) { | |
413 | my $part = $dev =~ s!^/dev/!!r; | |
414 | $sysfsdev .= "/${part}"; | |
e99bc248 FE |
415 | } |
416 | ||
107208bd TL |
417 | my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev); |
418 | $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link}); | |
419 | } | |
c84106ed | 420 | |
107208bd TL |
421 | # create zpool with desired raidlevel |
422 | my $ashift = $param->{ashift} // 12; | |
c84106ed | 423 | |
107208bd TL |
424 | my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name]; |
425 | ||
426 | if ($raidlevel eq 'raid10') { | |
427 | for (my $i = 0; $i < @$devs; $i+=2) { | |
428 | push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1]; | |
c84106ed | 429 | } |
107208bd TL |
430 | } elsif ($raidlevel eq 'single') { |
431 | push @$cmd, $devs->[0]; | |
432 | } else { | |
433 | push @$cmd, $raidlevel, @$devs; | |
434 | } | |
c84106ed | 435 | |
107208bd TL |
436 | print "# ", join(' ', @$cmd), "\n"; |
437 | run_command($cmd); | |
438 | ||
439 | $cmd = [$ZFS, 'set', "compression=$compression", $name]; | |
440 | print "# ", join(' ', @$cmd), "\n"; | |
441 | run_command($cmd); | |
c84106ed | 442 | |
107208bd TL |
443 | if (-e '/lib/systemd/system/zfs-import@.service') { |
444 | my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service'; | |
445 | $cmd = ['systemctl', 'enable', $importunit]; | |
c84106ed DC |
446 | print "# ", join(' ', @$cmd), "\n"; |
447 | run_command($cmd); | |
107208bd | 448 | } |
c84106ed | 449 | |
107208bd | 450 | PVE::Diskmanage::udevadm_trigger($devs->@*); |
21a75847 | 451 | |
107208bd | 452 | if ($param->{add_storage}) { |
bd485fd4 AL |
453 | PVE::API2::Storage::Config->create_or_update( |
454 | $name, | |
455 | $node, | |
456 | $storage_params, | |
457 | $verify_params, | |
458 | ); | |
107208bd | 459 | } |
c84106ed DC |
460 | }; |
461 | ||
107208bd TL |
462 | return $rpcenv->fork_worker('zfscreate', $name, $user, sub { |
463 | PVE::Diskmanage::locked_disk_action($code); | |
464 | }); | |
c84106ed DC |
465 | }}); |
466 | ||
a83d8eb1 FE |
467 | __PACKAGE__->register_method ({ |
468 | name => 'delete', | |
469 | path => '{name}', | |
470 | method => 'DELETE', | |
471 | proxyto => 'node', | |
472 | protected => 1, | |
473 | permissions => { | |
474 | check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']], | |
475 | }, | |
476 | description => "Destroy a ZFS pool.", | |
477 | parameters => { | |
478 | additionalProperties => 0, | |
479 | properties => { | |
480 | node => get_standard_option('pve-node'), | |
481 | name => get_standard_option('pve-storage-id'), | |
cde43c48 FE |
482 | 'cleanup-config' => { |
483 | description => "Marks associated storage(s) as not available on this node anymore ". | |
484 | "or removes them from the configuration (if configured for this node only).", | |
485 | type => 'boolean', | |
486 | optional => 1, | |
487 | default => 0, | |
488 | }, | |
f81908eb FE |
489 | 'cleanup-disks' => { |
490 | description => "Also wipe disks so they can be repurposed afterwards.", | |
491 | type => 'boolean', | |
492 | optional => 1, | |
493 | default => 0, | |
494 | }, | |
a83d8eb1 FE |
495 | }, |
496 | }, | |
497 | returns => { type => 'string' }, | |
498 | code => sub { | |
499 | my ($param) = @_; | |
500 | ||
501 | my $rpcenv = PVE::RPCEnvironment::get(); | |
502 | my $user = $rpcenv->get_user(); | |
503 | ||
504 | my $name = $param->{name}; | |
cde43c48 | 505 | my $node = $param->{node}; |
a83d8eb1 FE |
506 | |
507 | my $worker = sub { | |
508 | PVE::Diskmanage::locked_disk_action(sub { | |
f81908eb FE |
509 | my $to_wipe = []; |
510 | if ($param->{'cleanup-disks'}) { | |
511 | # Using -o name does not only output the name in combination with -v. | |
512 | run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub { | |
513 | my ($line) = @_; | |
514 | ||
515 | my ($name) = PVE::Tools::split_list($line); | |
516 | return if $name !~ m|^/dev/.+|; | |
517 | ||
518 | my $dev = PVE::Diskmanage::verify_blockdev_path($name); | |
519 | my $wipe = $dev; | |
520 | ||
521 | $dev =~ s|^/dev/||; | |
522 | my $info = PVE::Diskmanage::get_disks($dev, 1, 1); | |
523 | die "unable to obtain information for disk '$dev'\n" if !$info->{$dev}; | |
524 | ||
525 | # Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved. | |
526 | my $parent = $info->{$dev}->{parent}; | |
527 | if ($parent && scalar(keys $info->%*) == 3) { | |
528 | $parent =~ s|^/dev/||; | |
529 | my $info9 = $info->{"${parent}9"}; | |
530 | ||
531 | $wipe = $info->{$dev}->{parent} # need leading /dev/ | |
532 | if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/; | |
533 | } | |
534 | ||
535 | push $to_wipe->@*, $wipe; | |
536 | }); | |
537 | } | |
538 | ||
a83d8eb1 FE |
539 | if (-e '/lib/systemd/system/zfs-import@.service') { |
540 | my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service'; | |
541 | run_command(['systemctl', 'disable', $importunit]); | |
542 | } | |
543 | ||
544 | run_command(['zpool', 'destroy', $name]); | |
f81908eb | 545 | |
cde43c48 FE |
546 | my $config_err; |
547 | if ($param->{'cleanup-config'}) { | |
548 | my $match = sub { | |
549 | my ($scfg) = @_; | |
550 | return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name; | |
551 | }; | |
552 | eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); }; | |
553 | warn $config_err = $@ if $@; | |
554 | } | |
555 | ||
f81908eb FE |
556 | eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; }; |
557 | my $err = $@; | |
558 | PVE::Diskmanage::udevadm_trigger($to_wipe->@*); | |
559 | die "cleanup failed - $err" if $err; | |
cde43c48 FE |
560 | |
561 | die "config cleanup failed - $config_err" if $config_err; | |
a83d8eb1 FE |
562 | }); |
563 | }; | |
564 | ||
565 | return $rpcenv->fork_worker('zfsremove', $name, $user, $worker); | |
566 | }}); | |
567 | ||
c84106ed | 568 | 1; |