]> git.proxmox.com Git - pve-manager.git/blame - PVE/Ceph/Tools.pm
ceph: introduce '/etc/pve/ceph'
[pve-manager.git] / PVE / Ceph / Tools.pm
CommitLineData
6fb08cb9 1package PVE::Ceph::Tools;
a34866f0
DM
2
3use strict;
4use warnings;
f8346b52 5
a34866f0 6use File::Path;
b436dca8 7use File::Basename;
f8346b52 8use IO::File;
48e8a06d 9use JSON;
a34866f0 10
3bd128d7 11use PVE::Tools qw(run_command dir_glob_foreach extract_param);
48e8a06d 12use PVE::Cluster qw(cfs_read_file);
f96d7012 13use PVE::RADOS;
91dfa228
AA
14use PVE::Ceph::Services;
15use PVE::CephConfig;
a34866f0
DM
16
17my $ccname = 'ceph'; # ceph cluster name
18my $ceph_cfgdir = "/etc/ceph";
19my $pve_ceph_cfgpath = "/etc/pve/$ccname.conf";
20my $ceph_cfgpath = "$ceph_cfgdir/$ccname.conf";
a51a28e3 21my $pve_ceph_cfgdir = "/etc/pve/ceph";
a34866f0
DM
22
23my $pve_mon_key_path = "/etc/pve/priv/$ccname.mon.keyring";
24my $pve_ckeyring_path = "/etc/pve/priv/$ccname.client.admin.keyring";
17709566 25my $ckeyring_path = "/etc/ceph/ceph.client.admin.keyring";
a34866f0
DM
26my $ceph_bootstrap_osd_keyring = "/var/lib/ceph/bootstrap-osd/$ccname.keyring";
27my $ceph_bootstrap_mds_keyring = "/var/lib/ceph/bootstrap-mds/$ccname.keyring";
b82649cc 28my $ceph_mds_data_dir = '/var/lib/ceph/mds';
a34866f0 29
c64c04dd
AA
30my $ceph_service = {
31 ceph_bin => "/usr/bin/ceph",
32 ceph_mon => "/usr/bin/ceph-mon",
33 ceph_mgr => "/usr/bin/ceph-mgr",
b82649cc
TL
34 ceph_osd => "/usr/bin/ceph-osd",
35 ceph_mds => "/usr/bin/ceph-mds",
48e8a06d 36 ceph_volume => '/usr/sbin/ceph-volume',
c64c04dd 37};
a34866f0 38
68b6c239 39my $config_values = {
a34866f0 40 ccname => $ccname,
a51a28e3 41 pve_ceph_cfgdir => $pve_ceph_cfgdir,
68b6c239
MC
42 ceph_mds_data_dir => $ceph_mds_data_dir,
43 long_rados_timeout => 60,
44};
45
46my $config_files = {
a34866f0
DM
47 pve_ceph_cfgpath => $pve_ceph_cfgpath,
48 pve_mon_key_path => $pve_mon_key_path,
49 pve_ckeyring_path => $pve_ckeyring_path,
50 ceph_bootstrap_osd_keyring => $ceph_bootstrap_osd_keyring,
51 ceph_bootstrap_mds_keyring => $ceph_bootstrap_mds_keyring,
91dfa228 52 ceph_cfgpath => $ceph_cfgpath,
a34866f0
DM
53};
54
c64c04dd
AA
55sub get_local_version {
56 my ($noerr) = @_;
57
6fb08cb9 58 if (check_ceph_installed('ceph_bin', $noerr)) {
c64c04dd 59 my $ceph_version;
3248590d
TL
60 run_command(
61 [ $ceph_service->{ceph_bin}, '--version' ],
62 noerr => $noerr,
63 outfunc => sub { $ceph_version = shift if !defined $ceph_version },
64 );
65 return undef if !defined $ceph_version;
66
2a080be1 67 if ($ceph_version =~ /^ceph.*\sv?(\d+(?:\.\d+)+(?:-pve\d+)?)\s+(?:\(([a-zA-Z0-9]+)\))?/) {
3248590d
TL
68 my ($version, $buildcommit) = ($1, $2);
69 my $subversions = [ split(/\.|-/, $version) ];
70
71 # return (version, buildid, major, minor, ...) : major;
72 return wantarray
73 ? ($version, $buildcommit, $subversions)
74 : $subversions->[0];
c64c04dd
AA
75 }
76 }
77
78 return undef;
79}
80
8ba0d0a0
FG
81sub get_cluster_versions {
82 my ($service, $noerr) = @_;
83
84 my $rados = PVE::RADOS->new();
85 my $cmd = $service ? "$service versions" : 'versions';
86 return $rados->mon_command({ prefix => $cmd });
87}
88
a34866f0
DM
89sub get_config {
90 my $key = shift;
91
04ed3d1b 92 my $value = $config_values->{$key} // $config_files->{$key};
a34866f0 93
68b6c239 94 die "no such ceph config '$key'" if ! defined($value);
a34866f0
DM
95
96 return $value;
97}
98
a34866f0 99sub purge_all_ceph_files {
91dfa228
AA
100 my ($services) = @_;
101 my $is_local_mon;
102 my $monlist = [ split(',', PVE::CephConfig::get_monaddr_list($pve_ceph_cfgpath)) ];
103
104 foreach my $service (keys %$services) {
105 my $type = $services->{$service};
106 next if (!%$type);
107
108 foreach my $name (keys %$type) {
109 my $dir_exists = $type->{$name}->{direxists};
110
111 $is_local_mon = grep($type->{$name}->{addr}, @$monlist)
112 if $service eq 'mon';
113
114 my $path = "/var/lib/ceph/$service";
115 $path = '/var/log/ceph' if $service eq 'logs';
116 if ($dir_exists) {
117 my $err;
118 File::Path::remove_tree($path, {
119 keep_root => 1,
120 error => \$err,
121 });
122 warn "Error removing path, '$path'\n" if @$err;
123 }
124 }
125 }
a34866f0 126
91dfa228
AA
127 if (scalar @$monlist > 0 && !$is_local_mon) {
128 warn "Foreign MON address in ceph.conf. Keeping config & keyrings\n"
129 } else {
130 print "Removing config & keyring files\n";
68b6c239 131 for my $file (%$config_files) {
91dfa228
AA
132 unlink $file if (-e $file);
133 }
134 }
135}
a34866f0 136
91dfa228
AA
137sub purge_all_ceph_services {
138 my ($services) = @_;
139
140 foreach my $service (keys %$services) {
141 my $type = $services->{$service};
142 next if (!%$type);
143
144 foreach my $name (keys %$type) {
145 my $service_exists = $type->{$name}->{service};
146
147 if ($service_exists) {
75cac279
TL
148 eval { PVE::Ceph::Services::ceph_service_cmd('disable', "$service.$name") };
149 warn "Could not disable ceph-$service\@$name, error: $@\n" if $@;
150
151 eval { PVE::Ceph::Services::ceph_service_cmd('stop', "$service.$name") };
152 warn "Could not stop ceph-$service\@$name, error: $@\n" if $@;
91dfa228
AA
153 }
154 }
155 }
a34866f0
DM
156}
157
9f6dc075
TL
158sub ceph_install_flag_file { return '/run/pve-ceph-install-flag' };
159
a34866f0 160sub check_ceph_installed {
c64c04dd
AA
161 my ($service, $noerr) = @_;
162
163 $service = 'ceph_bin' if !defined($service);
a34866f0 164
d380d000 165 # NOTE: the flag file is checked as on a new installation, the binary gets
4dd27d50 166 # extracted by dpkg before the installation is finished
9f6dc075 167 if (! -x $ceph_service->{$service} || -f ceph_install_flag_file()) {
c64c04dd 168 die "binary not installed: $ceph_service->{$service}\n" if !$noerr;
a34866f0
DM
169 return undef;
170 }
171
172 return 1;
173}
174
7ef69f33
TL
175
176sub check_ceph_configured {
177
178 check_ceph_inited();
179
180 die "ceph not fully configured - missing '$pve_ckeyring_path'\n"
181 if ! -f $pve_ckeyring_path;
182
183 return 1;
184}
185
a34866f0
DM
186sub check_ceph_inited {
187 my ($noerr) = @_;
188
315304f3 189 return undef if !check_ceph_installed('ceph_mon', $noerr);
6f8bf83d 190
a51a28e3
MC
191 my @errors;
192
193 push(@errors, "missing '$pve_ceph_cfgpath'") if ! -f $pve_ceph_cfgpath;
194 push(@errors, "missing '$pve_ceph_cfgdir'") if ! -d $pve_ceph_cfgdir;
195
196 if (@errors) {
197 my $err = 'pveceph configuration not initialized - ' . join(', ', @errors) . "\n";
198 die $err if !$noerr;
a34866f0
DM
199 return undef;
200 }
201
202 return 1;
203}
204
205sub check_ceph_enabled {
206 my ($noerr) = @_;
207
208 return undef if !check_ceph_inited($noerr);
209
210 if (! -f $ceph_cfgpath) {
211 die "pveceph configuration not enabled\n" if !$noerr;
212 return undef;
213 }
214
215 return 1;
216}
217
11d74274 218my $set_pool_setting = sub {
5f4efb88 219 my ($pool, $setting, $value, $rados) = @_;
11d74274
AA
220
221 my $command;
222 if ($setting eq 'application') {
223 $command = {
224 prefix => "osd pool application enable",
225 pool => "$pool",
226 app => "$value",
227 };
228 } else {
229 $command = {
230 prefix => "osd pool set",
231 pool => "$pool",
232 var => "$setting",
233 val => "$value",
234 format => 'plain',
235 };
236 }
237
5f4efb88 238 $rados = PVE::RADOS->new() if !$rados;
11d74274
AA
239 eval { $rados->mon_command($command); };
240 return $@ ? $@ : undef;
241};
242
50adb131
AA
243sub set_pool {
244 my ($pool, $param) = @_;
245
5f4efb88
AL
246 my $rados = PVE::RADOS->new();
247
248 if (get_pool_type($pool, $rados) eq 'erasure') {
249 #remove parameters that cannot be changed for erasure coded pools
250 my $ignore_params = ['size', 'crush_rule'];
251 for my $setting (@$ignore_params) {
252 if ($param->{$setting}) {
253 print "cannot set '${setting}' for erasure coded pool\n";
254 delete $param->{$setting};
255 }
256 }
257 }
69c0ff3e 258 # by default, pool size always resets min_size, so set it as first item
11d74274 259 # https://tracker.ceph.com/issues/44862
69c0ff3e
TL
260 my $keys = [ grep { $_ ne 'size' } sort keys %$param ];
261 unshift @$keys, 'size' if exists $param->{size};
11d74274 262
69c0ff3e 263 for my $setting (@$keys) {
11d74274 264 my $value = $param->{$setting};
50adb131 265
cb83113d 266 print "pool $pool: applying $setting = $value\n";
5f4efb88 267 if (my $err = $set_pool_setting->($pool, $setting, $value, $rados)) {
11d74274 268 print "$err";
50adb131
AA
269 } else {
270 delete $param->{$setting};
271 }
272 }
273
68f94af8
TL
274 if (scalar(keys %$param) > 0) {
275 my $missing = join(', ', sort keys %$param );
276 die "Could not set: $missing\n";
50adb131
AA
277 }
278
279}
280
34a2222d 281sub get_pool_properties {
23c407e5
TL
282 my ($pool, $rados) = @_;
283 $rados = PVE::RADOS->new() if !defined($rados);
34a2222d
AL
284 my $command = {
285 prefix => "osd pool get",
286 pool => "$pool",
287 var => "all",
288 format => 'json',
289 };
34a2222d
AL
290 return $rados->mon_command($command);
291}
292
5f4efb88
AL
293sub get_pool_type {
294 my ($pool, $rados) = @_;
295 $rados = PVE::RADOS->new() if !defined($rados);
296 return 'erasure' if get_pool_properties($pool, $rados)->{erasure_code_profile};
297 return 'replicated';
298}
299
f96d7012
TL
300sub create_pool {
301 my ($pool, $param, $rados) = @_;
24f3f2bc 302 $rados = PVE::RADOS->new() if !defined($rados);
f96d7012 303
6ad70a2b 304 my $pg_num = $param->{pg_num} || 128;
f96d7012 305
3bd128d7 306 my $mon_params = {
f96d7012
TL
307 prefix => "osd pool create",
308 pool => $pool,
309 pg_num => int($pg_num),
310 format => 'plain',
3bd128d7
AL
311 };
312 $mon_params->{pool_type} = extract_param($param, 'pool_type') if $param->{pool_type};
313 $mon_params->{erasure_code_profile} = extract_param($param, 'erasure_code_profile')
314 if $param->{erasure_code_profile};
315
316 $rados->mon_command($mon_params);
f96d7012 317
50adb131 318 set_pool($pool, $param);
f96d7012
TL
319
320}
321
7e1a9d25
TL
322sub ls_pools {
323 my ($pool, $rados) = @_;
24f3f2bc 324 $rados = PVE::RADOS->new() if !defined($rados);
7e1a9d25
TL
325
326 my $res = $rados->mon_command({ prefix => "osd lspools" });
327
328 return $res;
329}
330
f96d7012
TL
331sub destroy_pool {
332 my ($pool, $rados) = @_;
24f3f2bc 333 $rados = PVE::RADOS->new() if !defined($rados);
f96d7012
TL
334
335 # fixme: '--yes-i-really-really-mean-it'
336 $rados->mon_command({
337 prefix => "osd pool delete",
338 pool => $pool,
339 pool2 => $pool,
f8eade23 340 'yes_i_really_really_mean_it' => JSON::true,
f96d7012
TL
341 format => 'plain',
342 });
343}
344
0ab69d6e
DC
345# we get something like:
346#[{
347# 'metadata_pool_id' => 2,
348# 'data_pool_ids' => [ 1 ],
349# 'metadata_pool' => 'cephfs_metadata',
350# 'data_pools' => [ 'cephfs_data' ],
351# 'name' => 'cephfs',
352#}]
353sub ls_fs {
354 my ($rados) = @_;
24f3f2bc 355 $rados = PVE::RADOS->new() if !defined($rados);
0ab69d6e
DC
356
357 my $res = $rados->mon_command({ prefix => "fs ls" });
358
359 return $res;
360}
361
362sub create_fs {
363 my ($fs, $param, $rados) = @_;
364
365 if (!defined($rados)) {
366 $rados = PVE::RADOS->new();
367 }
368
369 $rados->mon_command({
370 prefix => "fs new",
371 fs_name => $fs,
372 metadata => $param->{pool_metadata},
373 data => $param->{pool_data},
374 format => 'plain',
375 });
376}
377
02c1e98e
DC
378sub destroy_fs {
379 my ($fs, $rados) = @_;
24f3f2bc 380 $rados = PVE::RADOS->new() if !defined($rados);
02c1e98e
DC
381
382 $rados->mon_command({
383 prefix => "fs rm",
384 fs_name => $fs,
385 'yes_i_really_mean_it' => JSON::true,
386 format => 'plain',
387 });
388}
389
a34866f0
DM
390sub setup_pve_symlinks {
391 # fail if we find a real file instead of a link
392 if (-f $ceph_cfgpath) {
393 my $lnk = readlink($ceph_cfgpath);
e881cf2a 394 die "file '$ceph_cfgpath' already exists and is not a symlink to $pve_ceph_cfgpath\n"
a34866f0
DM
395 if !$lnk || $lnk ne $pve_ceph_cfgpath;
396 } else {
c5a673ed 397 mkdir $ceph_cfgdir;
a34866f0
DM
398 symlink($pve_ceph_cfgpath, $ceph_cfgpath) ||
399 die "unable to create symlink '$ceph_cfgpath' - $!\n";
400 }
c18db15b
TL
401 my $ceph_uid = getpwnam('ceph');
402 my $ceph_gid = getgrnam('ceph');
403 chown $ceph_uid, $ceph_gid, $ceph_cfgdir;
a34866f0
DM
404}
405
d558d296
DC
406sub get_or_create_admin_keyring {
407 if (! -f $pve_ckeyring_path) {
408 run_command("ceph-authtool --create-keyring $pve_ckeyring_path " .
409 "--gen-key -n client.admin " .
410 "--cap mon 'allow *' " .
411 "--cap osd 'allow *' " .
412 "--cap mds 'allow *' " .
413 "--cap mgr 'allow *' ");
414 # we do not want to overwrite it
415 if (! -f $ckeyring_path) {
416 run_command("cp $pve_ckeyring_path $ckeyring_path");
ea60e3b7 417 run_command("chown ceph:ceph $ckeyring_path");
d558d296
DC
418 }
419 }
420 return $pve_ckeyring_path;
421}
422
48e8a06d
DC
423# get ceph-volume managed osds
424sub ceph_volume_list {
425 my $result = {};
48e8a06d
DC
426
427 if (!check_ceph_installed('ceph_volume', 1)) {
428 return $result;
429 }
430
d79e9eb5
TL
431 my $output = '';
432 my $cmd = [ $ceph_service->{ceph_volume}, 'lvm', 'list', '--format', 'json' ];
433 run_command($cmd, outfunc => sub { $output .= shift });
48e8a06d
DC
434
435 $result = eval { decode_json($output) };
436 warn $@ if $@;
437 return $result;
438}
439
440sub ceph_volume_zap {
441 my ($osdid, $destroy) = @_;
442
443 die "no osdid given\n" if !defined($osdid);
444
d79e9eb5 445 my $cmd = [ $ceph_service->{ceph_volume}, 'lvm', 'zap', '--osd-id', $osdid ];
48e8a06d
DC
446 push @$cmd, '--destroy' if $destroy;
447
448 run_command($cmd);
449}
450
d4e7f1bf
DC
451sub get_db_wal_sizes {
452 my $res = {};
453
454 my $rados = PVE::RADOS->new();
455 my $db_config = $rados->mon_command({ prefix => 'config-key dump', key => 'config/' });
456
457 $res->{db} = $db_config->{"config/osd/bluestore_block_db_size"} //
458 $db_config->{"config/global/bluestore_block_db_size"};
459
460 $res->{wal} = $db_config->{"config/osd/bluestore_block_wal_size"} //
461 $db_config->{"config/global/bluestore_block_wal_size"};
462
463 if (!$res->{db} || !$res->{wal}) {
464 my $cfg = cfs_read_file('ceph.conf');
465 if (!$res->{db}) {
466 $res->{db} = $cfg->{osd}->{bluestore_block_db_size} //
467 $cfg->{global}->{bluestore_block_db_size};
468 }
469
470 if (!$res->{wal}) {
471 $res->{wal} = $cfg->{osd}->{bluestore_block_wal_size} //
472 $cfg->{global}->{bluestore_block_wal_size};
473 }
474 }
475
476 return $res;
477}
735f24eb
TL
478sub get_possible_osd_flags {
479 my $possible_flags = {
480 pause => {
481 description => 'Pauses read and writes.',
482 type => 'boolean',
483 optional=> 1,
484 },
485 noup => {
486 description => 'OSDs are not allowed to start.',
487 type => 'boolean',
488 optional=> 1,
489 },
490 nodown => {
491 description => 'OSD failure reports are being ignored, such that the monitors will not mark OSDs down.',
492 type => 'boolean',
493 optional=> 1,
494 },
495 noout => {
496 description => 'OSDs will not automatically be marked out after the configured interval.',
497 type => 'boolean',
498 optional=> 1,
499 },
500 noin => {
501 description => 'OSDs that were previously marked out will not be marked back in when they start.',
502 type => 'boolean',
503 optional=> 1,
504 },
505 nobackfill => {
506 description => 'Backfilling of PGs is suspended.',
507 type => 'boolean',
508 optional=> 1,
509 },
510 norebalance => {
511 description => 'Rebalancing of PGs is suspended.',
512 type => 'boolean',
513 optional=> 1,
514 },
515 norecover => {
516 description => 'Recovery of PGs is suspended.',
517 type => 'boolean',
518 optional=> 1,
519 },
520 noscrub => {
521 description => 'Scrubbing is disabled.',
522 type => 'boolean',
523 optional=> 1,
524 },
525 'nodeep-scrub' => {
526 description => 'Deep Scrubbing is disabled.',
527 type => 'boolean',
528 optional=> 1,
529 },
530 notieragent => {
531 description => 'Cache tiering activity is suspended.',
532 type => 'boolean',
533 optional=> 1,
534 },
535 };
536 return $possible_flags;
537}
538
539sub get_real_flag_name {
540 my ($flag) = @_;
541
542 # the 'pause' flag gets always set to both 'pauserd' and 'pausewr'
543 # so decide that the 'pause' flag is set if we detect 'pauserd'
544 my $flagmap = {
545 'pause' => 'pauserd',
546 };
547
548 return $flagmap->{$flag} // $flag;
549}
d4e7f1bf 550
e25dda25
AA
551sub ceph_cluster_status {
552 my ($rados) = @_;
553 $rados = PVE::RADOS->new() if !$rados;
554
e25dda25 555 my $status = $rados->mon_command({ prefix => 'status' });
e25dda25
AA
556 $status->{health} = $rados->mon_command({ prefix => 'health', detail => 'detail' });
557
fdf79b4e 558 if (!exists $status->{monmap}->{mons}) { # octopus moved most info out of status, re-add
e25dda25
AA
559 $status->{monmap} = $rados->mon_command({ prefix => 'mon dump' });
560 $status->{mgrmap} = $rados->mon_command({ prefix => 'mgr dump' });
561 }
562
563 return $status;
564}
565
34a2222d 566sub ecprofile_exists {
23c407e5
TL
567 my ($name, $rados) = @_;
568 $rados = PVE::RADOS->new() if !$rados;
34a2222d 569
34a2222d
AL
570 my $res = $rados->mon_command({ prefix => 'osd erasure-code-profile ls' });
571
572 my $profiles = { map { $_ => 1 } @$res };
573 return $profiles->{$name};
574}
575
576sub create_ecprofile {
23c407e5
TL
577 my ($name, $k, $m, $failure_domain, $device_class, $rados) = @_;
578 $rados = PVE::RADOS->new() if !$rados;
34a2222d
AL
579
580 $failure_domain = 'host' if !$failure_domain;
581
582 my $profile = [
583 "crush-failure-domain=${failure_domain}",
584 "k=${k}",
585 "m=${m}",
586 ];
587
588 push(@$profile, "crush-device-class=${device_class}") if $device_class;
589
34a2222d
AL
590 $rados->mon_command({
591 prefix => 'osd erasure-code-profile set',
592 name => $name,
593 profile => $profile,
594 });
595}
596
597sub destroy_ecprofile {
23c407e5
TL
598 my ($profile, $rados) = @_;
599 $rados = PVE::RADOS->new() if !$rados;
34a2222d 600
34a2222d
AL
601 my $command = {
602 prefix => 'osd erasure-code-profile rm',
603 name => $profile,
604 format => 'plain',
605 };
606 return $rados->mon_command($command);
607}
608
609sub get_ecprofile_name {
610 my ($name) = @_;
611 return "pve_ec_${name}";
612}
613
614sub destroy_crush_rule {
23c407e5
TL
615 my ($rule, $rados) = @_;
616 $rados = PVE::RADOS->new() if !$rados;
617
34a2222d
AL
618 my $command = {
619 prefix => 'osd crush rule rm',
620 name => $rule,
621 format => 'plain',
622 };
623 return $rados->mon_command($command);
624}
625
a34866f0 6261;