]> git.proxmox.com Git - pve-storage.git/blob - PVE/API2/Disks/ZFS.pm
disks: allow add_storage for already configured local storage
[pve-storage.git] / PVE / API2 / Disks / ZFS.pm
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);
8 use PVE::Systemd;
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 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
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
114 return get_pool_data();
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 => {
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 => {
170 optional => 1,
171 type => 'string',
172 description => 'Information about the last/current scrub.',
173 },
174 errors => {
175 type => 'string',
176 description => 'Information about the errors on the zpool.',
177 },
178 children => {
179 type => 'array',
180 description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.",
181 items => {
182 type => 'object',
183 properties => {
184 name => {
185 type => 'string',
186 description => 'The name of the vdev or section.',
187 },
188 state => {
189 optional => 1,
190 type => 'string',
191 description => 'The state of the vdev.',
192 },
193 read => {
194 optional => 1,
195 type => 'number',
196 },
197 write => {
198 optional => 1,
199 type => 'number',
200 },
201 cksum => {
202 optional => 1,
203 type => 'number',
204 },
205 msg => {
206 type => 'string',
207 description => 'An optional message about the vdev.'
208 }
209 },
210 },
211 },
212 },
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 };
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;
245 } elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) {
246 my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7);
247 if ($name ne "NAME") {
248 my $lvl = int(length($space) / 2) + 1; # two spaces per level
249 my $vdev = {
250 name => $name,
251 msg => $msg,
252 lvl => $lvl,
253 };
254
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);
259
260 my $cur = pop @$stack;
261
262 if ($lvl > $curlvl) {
263 $cur->{children} = [ $vdev ];
264 } elsif ($lvl == $curlvl) {
265 $cur = pop @$stack;
266 push @{$cur->{children}}, $vdev;
267 } else {
268 while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) {
269 $cur = pop @$stack;
270 }
271 push @{$cur->{children}}, $vdev;
272 }
273
274 push @$stack, $cur;
275 push @$stack, $vdev;
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 },
297 description => "Create a ZFS pool.",
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',
305 description => 'The RAID level to use.',
306 enum => ['single', 'mirror', 'raid10', 'raidz', 'raidz2', 'raidz3'],
307 },
308 devices => {
309 type => 'string', format => 'string-list',
310 description => 'The block devices you want to create the zpool on.',
311 },
312 ashift => {
313 type => 'integer',
314 minimum => 9,
315 maximum => 16,
316 optional => 1,
317 default => 12,
318 description => 'Pool sector size exponent.',
319 },
320 compression => {
321 type => 'string',
322 description => 'The compression algorithm to use.',
323 enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'],
324 optional => 1,
325 default => 'on',
326 },
327 add_storage => {
328 description => "Configure storage using the zpool.",
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};
343 my $node = $param->{node};
344 my $devs = [PVE::Tools::split_list($param->{devices})];
345 my $node = $param->{node};
346 my $raidlevel = $param->{raidlevel};
347 my $compression = $param->{compression} // 'on';
348
349 for my $dev (@$devs) {
350 $dev = PVE::Diskmanage::verify_blockdev_path($dev);
351 PVE::Diskmanage::assert_disk_unused($dev);
352
353 }
354 my $storage_params = {
355 type => 'zfspool',
356 pool => $name,
357 storage => $name,
358 content => 'rootdir,images',
359 nodes => $node,
360 };
361 my $verify_params = [qw(pool)];
362
363 if ($param->{add_storage}) {
364 PVE::API2::Storage::Config->create_or_update(
365 $name,
366 $node,
367 $storage_params,
368 $verify_params,
369 1,
370 );
371 }
372
373 my $pools = get_pool_data();
374 die "pool '${name}' already exists on node '${node}'\n"
375 if grep { $_->{name} eq $name } @{$pools};
376
377 my $numdisks = scalar(@$devs);
378 my $mindisks = {
379 single => 1,
380 mirror => 2,
381 raid10 => 4,
382 raidz => 3,
383 raidz2 => 4,
384 raidz3 => 5,
385 };
386
387 # sanity checks
388 die "raid10 needs an even number of disks\n"
389 if $raidlevel eq 'raid10' && $numdisks % 2 != 0;
390
391 die "please give only one disk for single disk mode\n"
392 if $raidlevel eq 'single' && $numdisks > 1;
393
394 die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n"
395 if $numdisks < $mindisks->{$raidlevel};
396
397 my $code = sub {
398 for my $dev (@$devs) {
399 PVE::Diskmanage::assert_disk_unused($dev);
400
401 my $is_partition = PVE::Diskmanage::is_partition($dev);
402
403 if ($is_partition) {
404 eval {
405 PVE::Diskmanage::change_parttype($dev, '6a898cc3-1dd2-11b2-99a6-080020736631');
406 };
407 warn $@ if $@;
408 }
409
410 my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev;
411
412 $sysfsdev =~ s!^/dev/!/sys/block/!;
413 if ($is_partition) {
414 my $part = $dev =~ s!^/dev/!!r;
415 $sysfsdev .= "/${part}";
416 }
417
418 my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev);
419 $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link});
420 }
421
422 # create zpool with desired raidlevel
423 my $ashift = $param->{ashift} // 12;
424
425 my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name];
426
427 if ($raidlevel eq 'raid10') {
428 for (my $i = 0; $i < @$devs; $i+=2) {
429 push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1];
430 }
431 } elsif ($raidlevel eq 'single') {
432 push @$cmd, $devs->[0];
433 } else {
434 push @$cmd, $raidlevel, @$devs;
435 }
436
437 print "# ", join(' ', @$cmd), "\n";
438 run_command($cmd);
439
440 $cmd = [$ZFS, 'set', "compression=$compression", $name];
441 print "# ", join(' ', @$cmd), "\n";
442 run_command($cmd);
443
444 if (-e '/lib/systemd/system/zfs-import@.service') {
445 my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service';
446 $cmd = ['systemctl', 'enable', $importunit];
447 print "# ", join(' ', @$cmd), "\n";
448 run_command($cmd);
449 }
450
451 PVE::Diskmanage::udevadm_trigger($devs->@*);
452
453 if ($param->{add_storage}) {
454 PVE::API2::Storage::Config->create_or_update(
455 $name,
456 $node,
457 $storage_params,
458 $verify_params,
459 );
460 }
461 };
462
463 return $rpcenv->fork_worker('zfscreate', $name, $user, sub {
464 PVE::Diskmanage::locked_disk_action($code);
465 });
466 }});
467
468 __PACKAGE__->register_method ({
469 name => 'delete',
470 path => '{name}',
471 method => 'DELETE',
472 proxyto => 'node',
473 protected => 1,
474 permissions => {
475 check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
476 },
477 description => "Destroy a ZFS pool.",
478 parameters => {
479 additionalProperties => 0,
480 properties => {
481 node => get_standard_option('pve-node'),
482 name => get_standard_option('pve-storage-id'),
483 'cleanup-config' => {
484 description => "Marks associated storage(s) as not available on this node anymore ".
485 "or removes them from the configuration (if configured for this node only).",
486 type => 'boolean',
487 optional => 1,
488 default => 0,
489 },
490 'cleanup-disks' => {
491 description => "Also wipe disks so they can be repurposed afterwards.",
492 type => 'boolean',
493 optional => 1,
494 default => 0,
495 },
496 },
497 },
498 returns => { type => 'string' },
499 code => sub {
500 my ($param) = @_;
501
502 my $rpcenv = PVE::RPCEnvironment::get();
503 my $user = $rpcenv->get_user();
504
505 my $name = $param->{name};
506 my $node = $param->{node};
507
508 my $worker = sub {
509 PVE::Diskmanage::locked_disk_action(sub {
510 my $to_wipe = [];
511 if ($param->{'cleanup-disks'}) {
512 # Using -o name does not only output the name in combination with -v.
513 run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub {
514 my ($line) = @_;
515
516 my ($name) = PVE::Tools::split_list($line);
517 return if $name !~ m|^/dev/.+|;
518
519 my $dev = PVE::Diskmanage::verify_blockdev_path($name);
520 my $wipe = $dev;
521
522 $dev =~ s|^/dev/||;
523 my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
524 die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
525
526 # Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved.
527 my $parent = $info->{$dev}->{parent};
528 if ($parent && scalar(keys $info->%*) == 3) {
529 $parent =~ s|^/dev/||;
530 my $info9 = $info->{"${parent}9"};
531
532 $wipe = $info->{$dev}->{parent} # need leading /dev/
533 if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/;
534 }
535
536 push $to_wipe->@*, $wipe;
537 });
538 }
539
540 if (-e '/lib/systemd/system/zfs-import@.service') {
541 my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service';
542 run_command(['systemctl', 'disable', $importunit]);
543 }
544
545 run_command(['zpool', 'destroy', $name]);
546
547 my $config_err;
548 if ($param->{'cleanup-config'}) {
549 my $match = sub {
550 my ($scfg) = @_;
551 return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name;
552 };
553 eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
554 warn $config_err = $@ if $@;
555 }
556
557 eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; };
558 my $err = $@;
559 PVE::Diskmanage::udevadm_trigger($to_wipe->@*);
560 die "cleanup failed - $err" if $err;
561
562 die "config cleanup failed - $config_err" if $config_err;
563 });
564 };
565
566 return $rpcenv->fork_worker('zfsremove', $name, $user, $worker);
567 }});
568
569 1;