]>
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 | ||
21 | __PACKAGE__->register_method ({ | |
22 | name => 'index', | |
23 | path => '', | |
24 | method => 'GET', | |
25 | proxyto => 'node', | |
26 | protected => 1, | |
27 | permissions => { | |
28 | check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1], | |
29 | }, | |
30 | description => "List Zpools.", | |
31 | parameters => { | |
32 | additionalProperties => 0, | |
33 | properties => { | |
34 | node => get_standard_option('pve-node'), | |
35 | }, | |
36 | }, | |
37 | returns => { | |
38 | type => 'array', | |
39 | items => { | |
40 | type => 'object', | |
41 | properties => { | |
42 | name => { | |
43 | type => 'string', | |
44 | description => "", | |
45 | }, | |
46 | size => { | |
47 | type => 'integer', | |
48 | description => "", | |
49 | }, | |
50 | alloc => { | |
51 | type => 'integer', | |
52 | description => "", | |
53 | }, | |
54 | free => { | |
55 | type => 'integer', | |
56 | description => "", | |
57 | }, | |
58 | frag => { | |
59 | type => 'integer', | |
60 | description => "", | |
61 | }, | |
62 | dedup => { | |
63 | type => 'number', | |
64 | description => "", | |
65 | }, | |
66 | health => { | |
67 | type => 'string', | |
68 | description => "", | |
69 | }, | |
70 | }, | |
71 | }, | |
72 | links => [ { rel => 'child', href => "{name}" } ], | |
73 | }, | |
74 | code => sub { | |
75 | my ($param) = @_; | |
76 | ||
77 | if (!-f $ZPOOL) { | |
78 | die "zfsutils-linux not installed\n"; | |
79 | } | |
80 | ||
81 | my $propnames = [qw(name size alloc free frag dedup health)]; | |
82 | my $numbers = { | |
83 | size => 1, | |
84 | alloc => 1, | |
85 | free => 1, | |
86 | frag => 1, | |
87 | dedup => 1, | |
88 | }; | |
89 | ||
90 | my $cmd = [$ZPOOL,'list', '-HpPLo', join(',', @$propnames)]; | |
91 | ||
92 | my $pools = []; | |
93 | ||
94 | run_command($cmd, outfunc => sub { | |
95 | my ($line) = @_; | |
96 | ||
97 | my @props = split('\s+', trim($line)); | |
98 | my $pool = {}; | |
99 | for (my $i = 0; $i < scalar(@$propnames); $i++) { | |
100 | if ($numbers->{$propnames->[$i]}) { | |
101 | $pool->{$propnames->[$i]} = $props[$i] + 0; | |
102 | } else { | |
103 | $pool->{$propnames->[$i]} = $props[$i]; | |
104 | } | |
105 | } | |
106 | ||
107 | push @$pools, $pool; | |
108 | }); | |
109 | ||
110 | return $pools; | |
111 | }}); | |
112 | ||
113 | sub preparetree { | |
114 | my ($el) = @_; | |
115 | delete $el->{lvl}; | |
116 | if ($el->{children} && scalar(@{$el->{children}})) { | |
117 | $el->{leaf} = 0; | |
118 | foreach my $child (@{$el->{children}}) { | |
119 | preparetree($child); | |
120 | } | |
121 | } else { | |
122 | $el->{leaf} = 1; | |
123 | } | |
124 | } | |
125 | ||
126 | ||
127 | __PACKAGE__->register_method ({ | |
128 | name => 'detail', | |
129 | path => '{name}', | |
130 | method => 'GET', | |
131 | proxyto => 'node', | |
132 | protected => 1, | |
133 | permissions => { | |
134 | check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1], | |
135 | }, | |
136 | description => "Get details about a zpool.", | |
137 | parameters => { | |
138 | additionalProperties => 0, | |
139 | properties => { | |
140 | node => get_standard_option('pve-node'), | |
141 | name => get_standard_option('pve-storage-id'), | |
142 | }, | |
143 | }, | |
144 | returns => { | |
4d12dbff DC |
145 | type => 'object', |
146 | properties => { | |
147 | name => { | |
148 | type => 'string', | |
149 | description => 'The name of the zpool.', | |
150 | }, | |
151 | state => { | |
152 | type => 'string', | |
153 | description => 'The state of the zpool.', | |
154 | }, | |
155 | status => { | |
156 | optional => 1, | |
157 | type => 'string', | |
158 | description => 'Information about the state of the zpool.', | |
159 | }, | |
160 | action => { | |
161 | optional => 1, | |
162 | type => 'string', | |
163 | description => 'Information about the recommended action to fix the state.', | |
164 | }, | |
165 | scan => { | |
977b80c8 | 166 | optional => 1, |
4d12dbff DC |
167 | type => 'string', |
168 | description => 'Information about the last/current scrub.', | |
169 | }, | |
b005f2f4 | 170 | errors => { |
4d12dbff DC |
171 | type => 'string', |
172 | description => 'Information about the errors on the zpool.', | |
173 | }, | |
174 | children => { | |
175 | type => 'array', | |
32f749b8 | 176 | description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.", |
4d12dbff DC |
177 | items => { |
178 | type => 'object', | |
179 | properties => { | |
180 | name => { | |
181 | type => 'string', | |
32f749b8 | 182 | description => 'The name of the vdev or section.', |
4d12dbff DC |
183 | }, |
184 | state => { | |
32f749b8 | 185 | optional => 1, |
4d12dbff DC |
186 | type => 'string', |
187 | description => 'The state of the vdev.', | |
188 | }, | |
189 | read => { | |
32f749b8 | 190 | optional => 1, |
4d12dbff DC |
191 | type => 'number', |
192 | }, | |
193 | write => { | |
32f749b8 | 194 | optional => 1, |
4d12dbff DC |
195 | type => 'number', |
196 | }, | |
197 | cksum => { | |
32f749b8 | 198 | optional => 1, |
4d12dbff DC |
199 | type => 'number', |
200 | }, | |
201 | msg => { | |
202 | type => 'string', | |
203 | description => 'An optional message about the vdev.' | |
204 | } | |
205 | }, | |
206 | }, | |
207 | }, | |
208 | }, | |
c84106ed DC |
209 | }, |
210 | code => sub { | |
211 | my ($param) = @_; | |
212 | ||
213 | if (!-f $ZPOOL) { | |
214 | die "zfsutils-linux not installed\n"; | |
215 | } | |
216 | ||
217 | my $cmd = [$ZPOOL, 'status', '-P', $param->{name}]; | |
218 | ||
219 | my $pool = { | |
220 | lvl => 0, | |
221 | }; | |
c84106ed DC |
222 | |
223 | my $curfield; | |
224 | my $config = 0; | |
225 | ||
226 | my $stack = [$pool]; | |
227 | my $curlvl = 0; | |
228 | ||
229 | run_command($cmd, outfunc => sub { | |
230 | my ($line) = @_; | |
231 | ||
232 | if ($line =~ m/^\s*(\S+): (\S+.*)$/) { | |
233 | $curfield = $1; | |
234 | $pool->{$curfield} = $2; | |
235 | ||
236 | $config = 0 if $curfield eq 'errors'; | |
237 | } elsif (!$config && $line =~ m/^\s+(\S+.*)$/) { | |
238 | $pool->{$curfield} .= " " . $1; | |
239 | } elsif (!$config && $line =~ m/^\s*config:/) { | |
240 | $config = 1; | |
a49fc735 | 241 | } elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) { |
c84106ed | 242 | my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7); |
576e143a FE |
243 | if ($name ne "NAME") { |
244 | my $lvl = int(length($space) / 2) + 1; # two spaces per level | |
c84106ed DC |
245 | my $vdev = { |
246 | name => $name, | |
c84106ed DC |
247 | msg => $msg, |
248 | lvl => $lvl, | |
249 | }; | |
8b6b7102 | 250 | |
a49fc735 TM |
251 | $vdev->{state} = $state if defined($state); |
252 | $vdev->{read} = $read + 0 if defined($read); | |
253 | $vdev->{write} = $write + 0 if defined($write); | |
254 | $vdev->{cksum} = $cksum + 0 if defined($cksum); | |
8b6b7102 | 255 | |
c84106ed DC |
256 | my $cur = pop @$stack; |
257 | ||
258 | if ($lvl > $curlvl) { | |
259 | $cur->{children} = [ $vdev ]; | |
c84106ed DC |
260 | } elsif ($lvl == $curlvl) { |
261 | $cur = pop @$stack; | |
262 | push @{$cur->{children}}, $vdev; | |
c84106ed | 263 | } else { |
8b6b7102 | 264 | while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) { |
c84106ed DC |
265 | $cur = pop @$stack; |
266 | } | |
267 | push @{$cur->{children}}, $vdev; | |
c84106ed | 268 | } |
8b6b7102 | 269 | |
a49fc735 TM |
270 | push @$stack, $cur; |
271 | push @$stack, $vdev; | |
c84106ed DC |
272 | $curlvl = $lvl; |
273 | } | |
274 | } | |
275 | }); | |
276 | ||
277 | # change treenodes for extjs tree | |
278 | $pool->{name} = delete $pool->{pool}; | |
279 | preparetree($pool); | |
280 | ||
281 | return $pool; | |
282 | }}); | |
283 | ||
284 | __PACKAGE__->register_method ({ | |
285 | name => 'create', | |
286 | path => '', | |
287 | method => 'POST', | |
288 | proxyto => 'node', | |
289 | protected => 1, | |
290 | permissions => { | |
291 | check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']], | |
292 | }, | |
fdc863c7 | 293 | description => "Create a ZFS pool.", |
c84106ed DC |
294 | parameters => { |
295 | additionalProperties => 0, | |
296 | properties => { | |
297 | node => get_standard_option('pve-node'), | |
298 | name => get_standard_option('pve-storage-id'), | |
299 | raidlevel => { | |
300 | type => 'string', | |
7058abe2 DC |
301 | description => 'The RAID level to use.', |
302 | enum => ['single', 'mirror', 'raid10', 'raidz', 'raidz2', 'raidz3'], | |
c84106ed DC |
303 | }, |
304 | devices => { | |
5be1a092 | 305 | type => 'string', format => 'string-list', |
fdc863c7 | 306 | description => 'The block devices you want to create the zpool on.', |
c84106ed DC |
307 | }, |
308 | ashift => { | |
309 | type => 'integer', | |
310 | minimum => 9, | |
311 | maximum => 16, | |
312 | optional => 1, | |
313 | default => 12, | |
7d597888 | 314 | description => 'Pool sector size exponent.', |
c84106ed DC |
315 | }, |
316 | compression => { | |
317 | type => 'string', | |
318 | description => 'The compression algorithm to use.', | |
ae098a19 | 319 | enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'], |
c84106ed DC |
320 | optional => 1, |
321 | default => 'on', | |
322 | }, | |
323 | add_storage => { | |
fdc863c7 | 324 | description => "Configure storage using the zpool.", |
c84106ed DC |
325 | type => 'boolean', |
326 | optional => 1, | |
327 | default => 0, | |
328 | }, | |
329 | }, | |
330 | }, | |
331 | returns => { type => 'string' }, | |
332 | code => sub { | |
333 | my ($param) = @_; | |
334 | ||
335 | my $rpcenv = PVE::RPCEnvironment::get(); | |
336 | my $user = $rpcenv->get_user(); | |
337 | ||
338 | my $name = $param->{name}; | |
339 | my $devs = [PVE::Tools::split_list($param->{devices})]; | |
38572a8f | 340 | my $raidlevel = $param->{raidlevel}; |
c84106ed DC |
341 | my $node = $param->{node}; |
342 | my $ashift = $param->{ashift} // 12; | |
343 | my $compression = $param->{compression} // 'on'; | |
344 | ||
345 | foreach my $dev (@$devs) { | |
346 | $dev = PVE::Diskmanage::verify_blockdev_path($dev); | |
0370861c | 347 | PVE::Diskmanage::assert_disk_unused($dev); |
0f0d99a3 SI |
348 | my $sysfsdev = $dev =~ s!^/dev/!/sys/block/!r; |
349 | my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev); | |
350 | $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link}); | |
c84106ed DC |
351 | } |
352 | ||
9280153e | 353 | PVE::Storage::assert_sid_unused($name) if $param->{add_storage}; |
c84106ed DC |
354 | |
355 | my $numdisks = scalar(@$devs); | |
356 | my $mindisks = { | |
7058abe2 DC |
357 | single => 1, |
358 | mirror => 2, | |
c84106ed DC |
359 | raid10 => 4, |
360 | raidz => 3, | |
361 | raidz2 => 4, | |
362 | raidz3 => 5, | |
363 | }; | |
364 | ||
365 | # sanity checks | |
366 | die "raid10 needs an even number of disks\n" | |
38572a8f | 367 | if $raidlevel eq 'raid10' && $numdisks % 2 != 0; |
c84106ed | 368 | |
7058abe2 DC |
369 | die "please give only one disk for single disk mode\n" |
370 | if $raidlevel eq 'single' && $numdisks > 1; | |
371 | ||
38572a8f DC |
372 | die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n" |
373 | if $numdisks < $mindisks->{$raidlevel}; | |
c84106ed DC |
374 | |
375 | my $worker = sub { | |
e39e8ee2 | 376 | PVE::Diskmanage::locked_disk_action(sub { |
c84106ed DC |
377 | # create zpool with desired raidlevel |
378 | ||
379 | my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name]; | |
380 | ||
38572a8f | 381 | if ($raidlevel eq 'raid10') { |
c84106ed DC |
382 | for (my $i = 0; $i < @$devs; $i+=2) { |
383 | push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1]; | |
384 | } | |
7058abe2 DC |
385 | } elsif ($raidlevel eq 'single') { |
386 | push @$cmd, $devs->[0]; | |
c84106ed | 387 | } else { |
38572a8f | 388 | push @$cmd, $raidlevel, @$devs; |
c84106ed DC |
389 | } |
390 | ||
391 | print "# ", join(' ', @$cmd), "\n"; | |
392 | run_command($cmd); | |
393 | ||
394 | $cmd = [$ZFS, 'set', "compression=$compression", $name]; | |
395 | print "# ", join(' ', @$cmd), "\n"; | |
396 | run_command($cmd); | |
397 | ||
c9c90349 TL |
398 | if (-e '/lib/systemd/system/zfs-import@.service') { |
399 | my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service'; | |
400 | $cmd = ['systemctl', 'enable', $importunit]; | |
401 | print "# ", join(' ', @$cmd), "\n"; | |
402 | run_command($cmd); | |
403 | } | |
f720f6c4 | 404 | |
c84106ed DC |
405 | if ($param->{add_storage}) { |
406 | my $storage_params = { | |
407 | type => 'zfspool', | |
408 | pool => $name, | |
409 | storage => $name, | |
410 | content => 'rootdir,images', | |
411 | nodes => $node, | |
412 | }; | |
413 | ||
414 | PVE::API2::Storage::Config->create($storage_params); | |
415 | } | |
416 | }); | |
c84106ed DC |
417 | }; |
418 | ||
c84106ed DC |
419 | return $rpcenv->fork_worker('zfscreate', $name, $user, $worker); |
420 | }}); | |
421 | ||
422 | 1; |