]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Ceph/Pools.pm
9c3b88844d6caa1e8d3c9c1a8f325ba671f1231b
[pve-manager.git] / PVE / API2 / Ceph / Pools.pm
1 package PVE::API2::Ceph::Pools;
2
3 use strict;
4 use warnings;
5
6 use PVE::Ceph::Tools;
7 use PVE::Ceph::Services;
8 use PVE::JSONSchema qw(get_standard_option parse_property_string);
9 use PVE::RADOS;
10 use PVE::RESTHandler;
11 use PVE::RPCEnvironment;
12 use PVE::Storage;
13 use PVE::Tools qw(extract_param);
14
15 use PVE::API2::Storage::Config;
16
17 use base qw(PVE::RESTHandler);
18
19 my $get_autoscale_status = sub {
20 my ($rados) = shift;
21
22 $rados = PVE::RADOS->new() if !defined($rados);
23
24 my $autoscale = $rados->mon_command({
25 prefix => 'osd pool autoscale-status'});
26
27 my $data;
28 foreach my $p (@$autoscale) {
29 $data->{$p->{pool_name}} = $p;
30 }
31
32 return $data;
33 };
34
35
36 __PACKAGE__->register_method ({
37 name => 'lspools',
38 path => '',
39 method => 'GET',
40 description => "List all pools.",
41 proxyto => 'node',
42 protected => 1,
43 permissions => {
44 check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
45 },
46 parameters => {
47 additionalProperties => 0,
48 properties => {
49 node => get_standard_option('pve-node'),
50 },
51 },
52 returns => {
53 type => 'array',
54 items => {
55 type => "object",
56 properties => {
57 pool => {
58 type => 'integer',
59 title => 'ID',
60 },
61 pool_name => {
62 type => 'string',
63 title => 'Name',
64 },
65 size => {
66 type => 'integer',
67 title => 'Size',
68 },
69 type => {
70 type => 'string',
71 title => 'Type',
72 enum => ['replicated', 'erasure', 'unknown'],
73 },
74 min_size => {
75 type => 'integer',
76 title => 'Min Size',
77 },
78 pg_num => {
79 type => 'integer',
80 title => 'PG Num',
81 },
82 pg_num_min => {
83 type => 'integer',
84 title => 'min. PG Num',
85 optional => 1,
86 },
87 pg_num_final => {
88 type => 'integer',
89 title => 'Optimal PG Num',
90 optional => 1,
91 },
92 pg_autoscale_mode => {
93 type => 'string',
94 title => 'PG Autoscale Mode',
95 optional => 1,
96 },
97 crush_rule => {
98 type => 'integer',
99 title => 'Crush Rule',
100 },
101 crush_rule_name => {
102 type => 'string',
103 title => 'Crush Rule Name',
104 },
105 percent_used => {
106 type => 'number',
107 title => '%-Used',
108 },
109 bytes_used => {
110 type => 'integer',
111 title => 'Used',
112 },
113 target_size => {
114 type => 'integer',
115 title => 'PG Autoscale Target Size',
116 optional => 1,
117 },
118 target_size_ratio => {
119 type => 'number',
120 title => 'PG Autoscale Target Ratio',
121 optional => 1,
122 },
123 autoscale_status => {
124 type => 'object',
125 title => 'Autoscale Status',
126 optional => 1,
127 },
128 },
129 },
130 links => [ { rel => 'child', href => "{pool_name}" } ],
131 },
132 code => sub {
133 my ($param) = @_;
134
135 PVE::Ceph::Tools::check_ceph_inited();
136
137 my $rados = PVE::RADOS->new();
138
139 my $stats = {};
140 my $res = $rados->mon_command({ prefix => 'df' });
141
142 foreach my $d (@{$res->{pools}}) {
143 next if !$d->{stats};
144 next if !defined($d->{id});
145 $stats->{$d->{id}} = $d->{stats};
146 }
147
148 $res = $rados->mon_command({ prefix => 'osd dump' });
149 my $rulestmp = $rados->mon_command({ prefix => 'osd crush rule dump'});
150
151 my $rules = {};
152 for my $rule (@$rulestmp) {
153 $rules->{$rule->{rule_id}} = $rule->{rule_name};
154 }
155
156 my $data = [];
157 my $attr_list = [
158 'pool',
159 'pool_name',
160 'size',
161 'min_size',
162 'pg_num',
163 'crush_rule',
164 'pg_autoscale_mode',
165 ];
166
167 # pg_autoscaler module is not enabled in Nautilus
168 my $autoscale = eval { $get_autoscale_status->($rados) };
169
170 foreach my $e (@{$res->{pools}}) {
171 my $d = {};
172 foreach my $attr (@$attr_list) {
173 $d->{$attr} = $e->{$attr} if defined($e->{$attr});
174 }
175
176 if ($autoscale) {
177 $d->{autoscale_status} = $autoscale->{$d->{pool_name}};
178 $d->{pg_num_final} = $d->{autoscale_status}->{pg_num_final};
179 # some info is nested under options instead
180 $d->{pg_num_min} = $e->{options}->{pg_num_min};
181 $d->{target_size} = $e->{options}->{target_size_bytes};
182 $d->{target_size_ratio} = $e->{options}->{target_size_ratio};
183 }
184
185 if (defined($d->{crush_rule}) && defined($rules->{$d->{crush_rule}})) {
186 $d->{crush_rule_name} = $rules->{$d->{crush_rule}};
187 }
188
189 if (my $s = $stats->{$d->{pool}}) {
190 $d->{bytes_used} = $s->{bytes_used};
191 $d->{percent_used} = $s->{percent_used};
192 }
193
194 # Cephs numerical pool types are barely documented. Found the following in the Ceph
195 # codebase: https://github.com/ceph/ceph/blob/ff144995a849407c258bcb763daa3e03cfce5059/src/osd/osd_types.h#L1221-L1233
196 if ($e->{type} == 1) {
197 $d->{type} = 'replicated';
198 } elsif ($e->{type} == 3) {
199 $d->{type} = 'erasure';
200 } else {
201 # we should never get here, but better be safe
202 $d->{type} = 'unknown';
203 }
204 push @$data, $d;
205 }
206
207
208 return $data;
209 }});
210
211
212 my $ceph_pool_common_options = sub {
213 my ($nodefault) = shift;
214 my $options = {
215 name => {
216 title => 'Name',
217 description => "The name of the pool. It must be unique.",
218 type => 'string',
219 },
220 size => {
221 title => 'Size',
222 description => 'Number of replicas per object',
223 type => 'integer',
224 default => 3,
225 optional => 1,
226 minimum => 1,
227 maximum => 7,
228 },
229 min_size => {
230 title => 'Min Size',
231 description => 'Minimum number of replicas per object',
232 type => 'integer',
233 default => 2,
234 optional => 1,
235 minimum => 1,
236 maximum => 7,
237 },
238 pg_num => {
239 title => 'PG Num',
240 description => "Number of placement groups.",
241 type => 'integer',
242 default => 128,
243 optional => 1,
244 minimum => 1,
245 maximum => 32768,
246 },
247 pg_num_min => {
248 title => 'min. PG Num',
249 description => "Minimal number of placement groups.",
250 type => 'integer',
251 optional => 1,
252 maximum => 32768,
253 },
254 crush_rule => {
255 title => 'Crush Rule Name',
256 description => "The rule to use for mapping object placement in the cluster.",
257 type => 'string',
258 optional => 1,
259 },
260 application => {
261 title => 'Application',
262 description => "The application of the pool.",
263 default => 'rbd',
264 type => 'string',
265 enum => ['rbd', 'cephfs', 'rgw'],
266 optional => 1,
267 },
268 pg_autoscale_mode => {
269 title => 'PG Autoscale Mode',
270 description => "The automatic PG scaling mode of the pool.",
271 type => 'string',
272 enum => ['on', 'off', 'warn'],
273 default => 'warn',
274 optional => 1,
275 },
276 target_size => {
277 description => "The estimated target size of the pool for the PG autoscaler.",
278 title => 'PG Autoscale Target Size',
279 type => 'string',
280 pattern => '^(\d+(\.\d+)?)([KMGT])?$',
281 optional => 1,
282 },
283 target_size_ratio => {
284 description => "The estimated target ratio of the pool for the PG autoscaler.",
285 title => 'PG Autoscale Target Ratio',
286 type => 'number',
287 optional => 1,
288 },
289 };
290
291 if ($nodefault) {
292 delete $options->{$_}->{default} for keys %$options;
293 }
294 return $options;
295 };
296
297
298 my $add_storage = sub {
299 my ($pool, $storeid, $ec_data_pool) = @_;
300
301 my $storage_params = {
302 type => 'rbd',
303 pool => $pool,
304 storage => $storeid,
305 krbd => 0,
306 content => 'rootdir,images',
307 };
308
309 $storage_params->{'data-pool'} = $ec_data_pool if $ec_data_pool;
310
311 PVE::API2::Storage::Config->create($storage_params);
312 };
313
314 my $get_storages = sub {
315 my ($pool) = @_;
316
317 my $cfg = PVE::Storage::config();
318
319 my $storages = $cfg->{ids};
320 my $res = {};
321 foreach my $storeid (keys %$storages) {
322 my $curr = $storages->{$storeid};
323 next if $curr->{type} ne 'rbd';
324 if (
325 $pool eq $curr->{pool} ||
326 (defined $curr->{'data-pool'} && $pool eq $curr->{'data-pool'})
327 ) {
328 $res->{$storeid} = $storages->{$storeid};
329 }
330 }
331
332 return $res;
333 };
334
335 my $ec_format = {
336 k => {
337 type => 'integer',
338 description => "Number of data chunks. Will create an erasure coded pool plus a"
339 ." replicated pool for metadata.",
340 minimum => 2,
341 },
342 m => {
343 type => 'integer',
344 description => "Number of coding chunks. Will create an erasure coded pool plus a"
345 ." replicated pool for metadata.",
346 minimum => 1,
347 },
348 'failure-domain' => {
349 type => 'string',
350 description => "CRUSH failure domain. Default is 'host'. Will create an erasure"
351 ." coded pool plus a replicated pool for metadata.",
352 format_description => 'domain',
353 optional => 1,
354 default => 'host',
355 },
356 'device-class' => {
357 type => 'string',
358 description => "CRUSH device class. Will create an erasure coded pool plus a"
359 ." replicated pool for metadata.",
360 format_description => 'class',
361 optional => 1,
362 },
363 profile => {
364 description => "Override the erasure code (EC) profile to use. Will create an"
365 ." erasure coded pool plus a replicated pool for metadata.",
366 type => 'string',
367 format_description => 'profile',
368 optional => 1,
369 },
370 };
371
372 sub ec_parse_and_check {
373 my ($property, $rados) = @_;
374 return if !$property;
375
376 my $ec = parse_property_string($ec_format, $property);
377
378 die "Erasure code profile '$ec->{profile}' does not exist.\n"
379 if $ec->{profile} && !PVE::Ceph::Tools::ecprofile_exists($ec->{profile}, $rados);
380
381 return $ec;
382 }
383
384
385 __PACKAGE__->register_method ({
386 name => 'createpool',
387 path => '',
388 method => 'POST',
389 description => "Create Ceph pool",
390 proxyto => 'node',
391 protected => 1,
392 permissions => {
393 check => ['perm', '/', [ 'Sys.Modify' ]],
394 },
395 parameters => {
396 additionalProperties => 0,
397 properties => {
398 node => get_standard_option('pve-node'),
399 add_storages => {
400 description => "Configure VM and CT storage using the new pool.",
401 type => 'boolean',
402 optional => 1,
403 default => "0; for erasure coded pools: 1",
404 },
405 'erasure-coding' => {
406 description => "Create an erasure coded pool for RBD with an"
407 ." accompaning replicated pool for metadata storage.",
408 type => 'string',
409 format => $ec_format,
410 optional => 1,
411 },
412 %{ $ceph_pool_common_options->() },
413 },
414 },
415 returns => { type => 'string' },
416 code => sub {
417 my ($param) = @_;
418
419 PVE::Cluster::check_cfs_quorum();
420 PVE::Ceph::Tools::check_ceph_configured();
421
422 my $pool = my $name = extract_param($param, 'name');
423 my $node = extract_param($param, 'node');
424 my $add_storages = extract_param($param, 'add_storages');
425
426 my $rpcenv = PVE::RPCEnvironment::get();
427 my $user = $rpcenv->get_user();
428 # Ceph uses target_size_bytes
429 if (defined($param->{'target_size'})) {
430 my $target_sizestr = extract_param($param, 'target_size');
431 $param->{target_size_bytes} = PVE::JSONSchema::parse_size($target_sizestr);
432 }
433
434 my $rados = PVE::RADOS->new();
435 my $ec = ec_parse_and_check(extract_param($param, 'erasure-coding'), $rados);
436 $add_storages = 1 if $ec && !defined($add_storages);
437
438 if ($add_storages) {
439 $rpcenv->check($user, '/storage', ['Datastore.Allocate']);
440 die "pool name contains characters which are illegal for storage naming\n"
441 if !PVE::JSONSchema::parse_storage_id($pool);
442 }
443
444 # pool defaults
445 $param->{pg_num} //= 128;
446 $param->{size} //= 3;
447 $param->{min_size} //= 2;
448 $param->{application} //= 'rbd';
449 $param->{pg_autoscale_mode} //= 'warn';
450
451 my $worker = sub {
452 # reopen with longer timeout
453 $rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
454
455 if ($ec) {
456 if (!$ec->{profile}) {
457 $ec->{profile} = PVE::Ceph::Tools::get_ecprofile_name($pool, $rados);
458 eval {
459 PVE::Ceph::Tools::create_ecprofile(
460 $ec->@{'profile', 'k', 'm', 'failure-domain', 'device-class'},
461 $rados,
462 );
463 };
464 die "could not create erasure code profile '$ec->{profile}': $@\n" if $@;
465 print "created new erasure code profile '$ec->{profile}'\n";
466 }
467
468 my $ec_data_param = {};
469 # copy all params, should be a flat hash
470 $ec_data_param = { map { $_ => $param->{$_} } keys %$param };
471
472 $ec_data_param->{pool_type} = 'erasure';
473 $ec_data_param->{allow_ec_overwrites} = 'true';
474 $ec_data_param->{erasure_code_profile} = $ec->{profile};
475 delete $ec_data_param->{size};
476 delete $ec_data_param->{min_size};
477 delete $ec_data_param->{crush_rule};
478
479 # metadata pool should be ok with 32 PGs
480 $param->{pg_num} = 32;
481
482 $pool = "${name}-metadata";
483 $ec->{data_pool} = "${name}-data";
484
485 PVE::Ceph::Tools::create_pool($ec->{data_pool}, $ec_data_param, $rados);
486 }
487
488 PVE::Ceph::Tools::create_pool($pool, $param, $rados);
489
490 if ($add_storages) {
491 eval { $add_storage->($pool, "${name}", $ec->{data_pool}) };
492 die "adding PVE storage for ceph pool '$name' failed: $@\n" if $@;
493 }
494 };
495
496 return $rpcenv->fork_worker('cephcreatepool', $pool, $user, $worker);
497 }});
498
499
500 __PACKAGE__->register_method ({
501 name => 'destroypool',
502 path => '{name}',
503 method => 'DELETE',
504 description => "Destroy pool",
505 proxyto => 'node',
506 protected => 1,
507 permissions => {
508 check => ['perm', '/', [ 'Sys.Modify' ]],
509 },
510 parameters => {
511 additionalProperties => 0,
512 properties => {
513 node => get_standard_option('pve-node'),
514 name => {
515 description => "The name of the pool. It must be unique.",
516 type => 'string',
517 },
518 force => {
519 description => "If true, destroys pool even if in use",
520 type => 'boolean',
521 optional => 1,
522 default => 0,
523 },
524 remove_storages => {
525 description => "Remove all pveceph-managed storages configured for this pool",
526 type => 'boolean',
527 optional => 1,
528 default => 0,
529 },
530 remove_ecprofile => {
531 description => "Remove the erasure code profile. Defaults to true, if applicable.",
532 type => 'boolean',
533 optional => 1,
534 default => 1,
535 },
536 },
537 },
538 returns => { type => 'string' },
539 code => sub {
540 my ($param) = @_;
541
542 PVE::Ceph::Tools::check_ceph_inited();
543
544 my $rpcenv = PVE::RPCEnvironment::get();
545 my $user = $rpcenv->get_user();
546 $rpcenv->check($user, '/storage', ['Datastore.Allocate'])
547 if $param->{remove_storages};
548
549 my $pool = $param->{name};
550
551 my $worker = sub {
552 my $storages = $get_storages->($pool);
553
554 # if not forced, destroy ceph pool only when no
555 # vm disks are on it anymore
556 if (!$param->{force}) {
557 my $storagecfg = PVE::Storage::config();
558 foreach my $storeid (keys %$storages) {
559 my $storage = $storages->{$storeid};
560
561 # check if any vm disks are on the pool
562 print "checking storage '$storeid' for RBD images..\n";
563 my $res = PVE::Storage::vdisk_list($storagecfg, $storeid);
564 die "ceph pool '$pool' still in use by storage '$storeid'\n"
565 if @{$res->{$storeid}} != 0;
566 }
567 }
568 my $rados = PVE::RADOS->new();
569
570 my $pool_properties = PVE::Ceph::Tools::get_pool_properties($pool, $rados);
571
572 PVE::Ceph::Tools::destroy_pool($pool, $rados);
573
574 if (my $ecprofile = $pool_properties->{erasure_code_profile}) {
575 print "found erasure coded profile '$ecprofile', destroying its CRUSH rule\n";
576 my $crush_rule = $pool_properties->{crush_rule};
577 eval { PVE::Ceph::Tools::destroy_crush_rule($crush_rule, $rados); };
578 warn "removing crush rule '${crush_rule}' failed: $@\n" if $@;
579
580 if ($param->{remove_ecprofile} // 1) {
581 print "destroying erasure coded profile '$ecprofile'\n";
582 eval { PVE::Ceph::Tools::destroy_ecprofile($ecprofile, $rados) };
583 warn "removing EC profile '${ecprofile}' failed: $@\n" if $@;
584 }
585 }
586
587 if ($param->{remove_storages}) {
588 my $err;
589 foreach my $storeid (keys %$storages) {
590 # skip external clusters, not managed by pveceph
591 next if $storages->{$storeid}->{monhost};
592 eval { PVE::API2::Storage::Config->delete({storage => $storeid}) };
593 if ($@) {
594 warn "failed to remove storage '$storeid': $@\n";
595 $err = 1;
596 }
597 }
598 die "failed to remove (some) storages - check log and remove manually!\n"
599 if $err;
600 }
601 };
602 return $rpcenv->fork_worker('cephdestroypool', $pool, $user, $worker);
603 }});
604
605
606 __PACKAGE__->register_method ({
607 name => 'setpool',
608 path => '{name}',
609 method => 'PUT',
610 description => "Change POOL settings",
611 proxyto => 'node',
612 protected => 1,
613 permissions => {
614 check => ['perm', '/', [ 'Sys.Modify' ]],
615 },
616 parameters => {
617 additionalProperties => 0,
618 properties => {
619 node => get_standard_option('pve-node'),
620 %{ $ceph_pool_common_options->('nodefault') },
621 },
622 },
623 returns => { type => 'string' },
624 code => sub {
625 my ($param) = @_;
626
627 PVE::Ceph::Tools::check_ceph_configured();
628
629 my $rpcenv = PVE::RPCEnvironment::get();
630 my $authuser = $rpcenv->get_user();
631
632 my $pool = extract_param($param, 'name');
633 my $node = extract_param($param, 'node');
634
635 # Ceph uses target_size_bytes
636 if (defined($param->{'target_size'})) {
637 my $target_sizestr = extract_param($param, 'target_size');
638 $param->{target_size_bytes} = PVE::JSONSchema::parse_size($target_sizestr);
639 }
640
641 my $worker = sub {
642 PVE::Ceph::Tools::set_pool($pool, $param);
643 };
644
645 return $rpcenv->fork_worker('cephsetpool', $pool, $authuser, $worker);
646 }});
647
648
649 __PACKAGE__->register_method ({
650 name => 'getpool',
651 path => '{name}',
652 method => 'GET',
653 description => "List pool settings.",
654 proxyto => 'node',
655 protected => 1,
656 permissions => {
657 check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
658 },
659 parameters => {
660 additionalProperties => 0,
661 properties => {
662 node => get_standard_option('pve-node'),
663 name => {
664 description => "The name of the pool. It must be unique.",
665 type => 'string',
666 },
667 verbose => {
668 type => 'boolean',
669 default => 0,
670 optional => 1,
671 description => "If enabled, will display additional data".
672 "(eg. statistics).",
673 },
674 },
675 },
676 returns => {
677 type => "object",
678 properties => {
679 id => { type => 'integer', title => 'ID' },
680 pgp_num => { type => 'integer', title => 'PGP num' },
681 noscrub => { type => 'boolean', title => 'noscrub' },
682 'nodeep-scrub' => { type => 'boolean', title => 'nodeep-scrub' },
683 nodelete => { type => 'boolean', title => 'nodelete' },
684 nopgchange => { type => 'boolean', title => 'nopgchange' },
685 nosizechange => { type => 'boolean', title => 'nosizechange' },
686 write_fadvise_dontneed => { type => 'boolean', title => 'write_fadvise_dontneed' },
687 hashpspool => { type => 'boolean', title => 'hashpspool' },
688 use_gmt_hitset => { type => 'boolean', title => 'use_gmt_hitset' },
689 fast_read => { type => 'boolean', title => 'Fast Read' },
690 application_list => { type => 'array', title => 'Application', optional => 1 },
691 statistics => { type => 'object', title => 'Statistics', optional => 1 },
692 autoscale_status => { type => 'object', title => 'Autoscale Status', optional => 1 },
693 %{ $ceph_pool_common_options->() },
694 },
695 },
696 code => sub {
697 my ($param) = @_;
698
699 PVE::Ceph::Tools::check_ceph_inited();
700
701 my $verbose = $param->{verbose};
702 my $pool = $param->{name};
703
704 my $rados = PVE::RADOS->new();
705 my $res = $rados->mon_command({
706 prefix => 'osd pool get',
707 pool => "$pool",
708 var => 'all',
709 });
710
711 my $data = {
712 id => $res->{pool_id},
713 name => $pool,
714 size => $res->{size},
715 min_size => $res->{min_size},
716 pg_num => $res->{pg_num},
717 pg_num_min => $res->{pg_num_min},
718 pgp_num => $res->{pgp_num},
719 crush_rule => $res->{crush_rule},
720 pg_autoscale_mode => $res->{pg_autoscale_mode},
721 noscrub => "$res->{noscrub}",
722 'nodeep-scrub' => "$res->{'nodeep-scrub'}",
723 nodelete => "$res->{nodelete}",
724 nopgchange => "$res->{nopgchange}",
725 nosizechange => "$res->{nosizechange}",
726 write_fadvise_dontneed => "$res->{write_fadvise_dontneed}",
727 hashpspool => "$res->{hashpspool}",
728 use_gmt_hitset => "$res->{use_gmt_hitset}",
729 fast_read => "$res->{fast_read}",
730 target_size => $res->{target_size_bytes},
731 target_size_ratio => $res->{target_size_ratio},
732 };
733
734 if ($verbose) {
735 my $stats;
736 my $res = $rados->mon_command({ prefix => 'df' });
737
738 # pg_autoscaler module is not enabled in Nautilus
739 # avoid partial read further down, use new rados instance
740 my $autoscale_status = eval { $get_autoscale_status->() };
741 $data->{autoscale_status} = $autoscale_status->{$pool};
742
743 foreach my $d (@{$res->{pools}}) {
744 next if !$d->{stats};
745 next if !defined($d->{name}) && !$d->{name} ne "$pool";
746 $data->{statistics} = $d->{stats};
747 }
748
749 my $apps = $rados->mon_command({ prefix => "osd pool application get", pool => "$pool", });
750 $data->{application_list} = [ keys %$apps ];
751 }
752
753 return $data;
754 }});
755
756
757 1;