]> git.proxmox.com Git - pve-storage.git/blame - PVE/API2/Disks/ZFS.pm
api: disks: delete: add flag for cleaning up storage config
[pve-storage.git] / PVE / API2 / Disks / ZFS.pm
CommitLineData
c84106ed
DC
1package PVE::API2::Disks::ZFS;
2
3use strict;
4use warnings;
5
6use PVE::Diskmanage;
7use PVE::JSONSchema qw(get_standard_option);
f720f6c4 8use PVE::Systemd;
c84106ed
DC
9use PVE::API2::Storage::Config;
10use PVE::Storage;
11use PVE::Tools qw(run_command lock_file trim);
12
13use PVE::RPCEnvironment;
14use PVE::RESTHandler;
15
16use base qw(PVE::RESTHandler);
17
18my $ZPOOL = '/sbin/zpool';
19my $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
113sub 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);
c84106ed
DC
348 }
349
9280153e 350 PVE::Storage::assert_sid_unused($name) if $param->{add_storage};
c84106ed
DC
351
352 my $numdisks = scalar(@$devs);
353 my $mindisks = {
7058abe2
DC
354 single => 1,
355 mirror => 2,
c84106ed
DC
356 raid10 => 4,
357 raidz => 3,
358 raidz2 => 4,
359 raidz3 => 5,
360 };
361
362 # sanity checks
363 die "raid10 needs an even number of disks\n"
38572a8f 364 if $raidlevel eq 'raid10' && $numdisks % 2 != 0;
c84106ed 365
7058abe2
DC
366 die "please give only one disk for single disk mode\n"
367 if $raidlevel eq 'single' && $numdisks > 1;
368
38572a8f
DC
369 die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n"
370 if $numdisks < $mindisks->{$raidlevel};
c84106ed
DC
371
372 my $worker = sub {
e39e8ee2 373 PVE::Diskmanage::locked_disk_action(sub {
e99bc248
FE
374 for my $dev (@$devs) {
375 PVE::Diskmanage::assert_disk_unused($dev);
a2c34371
FE
376
377 my $is_partition = PVE::Diskmanage::is_partition($dev);
05d91712
FE
378
379 if ($is_partition) {
380 eval {
381 PVE::Diskmanage::change_parttype(
382 $dev,
383 '6a898cc3-1dd2-11b2-99a6-080020736631',
384 );
385 };
386 warn $@ if $@;
387 }
388
a2c34371
FE
389 my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev;
390
391 $sysfsdev =~ s!^/dev/!/sys/block/!;
392 if ($is_partition) {
393 my $part = $dev =~ s!^/dev/!!r;
394 $sysfsdev .= "/${part}";
395 }
396
e99bc248
FE
397 my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev);
398 $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link});
399 }
400
c84106ed
DC
401 # create zpool with desired raidlevel
402
403 my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name];
404
38572a8f 405 if ($raidlevel eq 'raid10') {
c84106ed
DC
406 for (my $i = 0; $i < @$devs; $i+=2) {
407 push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1];
408 }
7058abe2
DC
409 } elsif ($raidlevel eq 'single') {
410 push @$cmd, $devs->[0];
c84106ed 411 } else {
38572a8f 412 push @$cmd, $raidlevel, @$devs;
c84106ed
DC
413 }
414
415 print "# ", join(' ', @$cmd), "\n";
416 run_command($cmd);
417
418 $cmd = [$ZFS, 'set', "compression=$compression", $name];
419 print "# ", join(' ', @$cmd), "\n";
420 run_command($cmd);
421
c9c90349
TL
422 if (-e '/lib/systemd/system/zfs-import@.service') {
423 my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service';
424 $cmd = ['systemctl', 'enable', $importunit];
425 print "# ", join(' ', @$cmd), "\n";
426 run_command($cmd);
427 }
f720f6c4 428
26082b7d 429 PVE::Diskmanage::udevadm_trigger($devs->@*);
21a75847 430
c84106ed
DC
431 if ($param->{add_storage}) {
432 my $storage_params = {
433 type => 'zfspool',
434 pool => $name,
435 storage => $name,
436 content => 'rootdir,images',
437 nodes => $node,
438 };
439
440 PVE::API2::Storage::Config->create($storage_params);
441 }
442 });
c84106ed
DC
443 };
444
c84106ed
DC
445 return $rpcenv->fork_worker('zfscreate', $name, $user, $worker);
446 }});
447
a83d8eb1
FE
448__PACKAGE__->register_method ({
449 name => 'delete',
450 path => '{name}',
451 method => 'DELETE',
452 proxyto => 'node',
453 protected => 1,
454 permissions => {
455 check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
456 },
457 description => "Destroy a ZFS pool.",
458 parameters => {
459 additionalProperties => 0,
460 properties => {
461 node => get_standard_option('pve-node'),
462 name => get_standard_option('pve-storage-id'),
cde43c48
FE
463 'cleanup-config' => {
464 description => "Marks associated storage(s) as not available on this node anymore ".
465 "or removes them from the configuration (if configured for this node only).",
466 type => 'boolean',
467 optional => 1,
468 default => 0,
469 },
f81908eb
FE
470 'cleanup-disks' => {
471 description => "Also wipe disks so they can be repurposed afterwards.",
472 type => 'boolean',
473 optional => 1,
474 default => 0,
475 },
a83d8eb1
FE
476 },
477 },
478 returns => { type => 'string' },
479 code => sub {
480 my ($param) = @_;
481
482 my $rpcenv = PVE::RPCEnvironment::get();
483 my $user = $rpcenv->get_user();
484
485 my $name = $param->{name};
cde43c48 486 my $node = $param->{node};
a83d8eb1
FE
487
488 my $worker = sub {
489 PVE::Diskmanage::locked_disk_action(sub {
f81908eb
FE
490 my $to_wipe = [];
491 if ($param->{'cleanup-disks'}) {
492 # Using -o name does not only output the name in combination with -v.
493 run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub {
494 my ($line) = @_;
495
496 my ($name) = PVE::Tools::split_list($line);
497 return if $name !~ m|^/dev/.+|;
498
499 my $dev = PVE::Diskmanage::verify_blockdev_path($name);
500 my $wipe = $dev;
501
502 $dev =~ s|^/dev/||;
503 my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
504 die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
505
506 # Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved.
507 my $parent = $info->{$dev}->{parent};
508 if ($parent && scalar(keys $info->%*) == 3) {
509 $parent =~ s|^/dev/||;
510 my $info9 = $info->{"${parent}9"};
511
512 $wipe = $info->{$dev}->{parent} # need leading /dev/
513 if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/;
514 }
515
516 push $to_wipe->@*, $wipe;
517 });
518 }
519
a83d8eb1
FE
520 if (-e '/lib/systemd/system/zfs-import@.service') {
521 my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service';
522 run_command(['systemctl', 'disable', $importunit]);
523 }
524
525 run_command(['zpool', 'destroy', $name]);
f81908eb 526
cde43c48
FE
527 my $config_err;
528 if ($param->{'cleanup-config'}) {
529 my $match = sub {
530 my ($scfg) = @_;
531 return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name;
532 };
533 eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
534 warn $config_err = $@ if $@;
535 }
536
f81908eb
FE
537 eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; };
538 my $err = $@;
539 PVE::Diskmanage::udevadm_trigger($to_wipe->@*);
540 die "cleanup failed - $err" if $err;
cde43c48
FE
541
542 die "config cleanup failed - $config_err" if $config_err;
a83d8eb1
FE
543 });
544 };
545
546 return $rpcenv->fork_worker('zfsremove', $name, $user, $worker);
547 }});
548
c84106ed 5491;