1 package PVE
::Ceph
::Tools
;
11 use PVE
::Tools
qw(run_command dir_glob_foreach extract_param);
12 use PVE
::Cluster
qw(cfs_read_file);
14 use PVE
::Ceph
::Services
;
17 my $ccname = 'ceph'; # ceph cluster name
18 my $ceph_cfgdir = "/etc/ceph";
19 my $pve_ceph_cfgpath = "/etc/pve/$ccname.conf";
20 my $ceph_cfgpath = "$ceph_cfgdir/$ccname.conf";
22 my $pve_mon_key_path = "/etc/pve/priv/$ccname.mon.keyring";
23 my $pve_ckeyring_path = "/etc/pve/priv/$ccname.client.admin.keyring";
24 my $ckeyring_path = "/etc/ceph/ceph.client.admin.keyring";
25 my $ceph_bootstrap_osd_keyring = "/var/lib/ceph/bootstrap-osd/$ccname.keyring";
26 my $ceph_bootstrap_mds_keyring = "/var/lib/ceph/bootstrap-mds/$ccname.keyring";
27 my $ceph_mds_data_dir = '/var/lib/ceph/mds';
30 ceph_bin
=> "/usr/bin/ceph",
31 ceph_mon
=> "/usr/bin/ceph-mon",
32 ceph_mgr
=> "/usr/bin/ceph-mgr",
33 ceph_osd
=> "/usr/bin/ceph-osd",
34 ceph_mds
=> "/usr/bin/ceph-mds",
35 ceph_volume
=> '/usr/sbin/ceph-volume',
40 pve_ceph_cfgpath
=> $pve_ceph_cfgpath,
41 pve_mon_key_path
=> $pve_mon_key_path,
42 pve_ckeyring_path
=> $pve_ckeyring_path,
43 ceph_bootstrap_osd_keyring
=> $ceph_bootstrap_osd_keyring,
44 ceph_bootstrap_mds_keyring
=> $ceph_bootstrap_mds_keyring,
45 ceph_mds_data_dir
=> $ceph_mds_data_dir,
46 long_rados_timeout
=> 60,
47 ceph_cfgpath
=> $ceph_cfgpath,
50 sub get_local_version
{
53 if (check_ceph_installed
('ceph_bin', $noerr)) {
56 [ $ceph_service->{ceph_bin
}, '--version' ],
58 outfunc
=> sub { $ceph_version = shift if !defined $ceph_version },
60 return undef if !defined $ceph_version;
62 if ($ceph_version =~ /^ceph.*\sv?(\d+(?:\.\d+)+(?:-pve\d+)?)\s+(?:\(([a-zA-Z0-9]+)\))?/) {
63 my ($version, $buildcommit) = ($1, $2);
64 my $subversions = [ split(/\.|-/, $version) ];
66 # return (version, buildid, major, minor, ...) : major;
68 ?
($version, $buildcommit, $subversions)
76 sub get_cluster_versions
{
77 my ($service, $noerr) = @_;
79 my $rados = PVE
::RADOS-
>new();
80 my $cmd = $service ?
"$service versions" : 'versions';
81 return $rados->mon_command({ prefix
=> $cmd });
87 my $value = $config_hash->{$key};
89 die "no such ceph config '$key'" if !$value;
94 sub purge_all_ceph_files
{
97 my $monlist = [ split(',', PVE
::CephConfig
::get_monaddr_list
($pve_ceph_cfgpath)) ];
99 foreach my $service (keys %$services) {
100 my $type = $services->{$service};
103 foreach my $name (keys %$type) {
104 my $dir_exists = $type->{$name}->{direxists
};
106 $is_local_mon = grep($type->{$name}->{addr
}, @$monlist)
107 if $service eq 'mon';
109 my $path = "/var/lib/ceph/$service";
110 $path = '/var/log/ceph' if $service eq 'logs';
113 File
::Path
::remove_tree
($path, {
117 warn "Error removing path, '$path'\n" if @$err;
122 if (scalar @$monlist > 0 && !$is_local_mon) {
123 warn "Foreign MON address in ceph.conf. Keeping config & keyrings\n"
125 print "Removing config & keyring files\n";
126 foreach my $file (%$config_hash) {
127 unlink $file if (-e
$file);
132 sub purge_all_ceph_services
{
135 foreach my $service (keys %$services) {
136 my $type = $services->{$service};
139 foreach my $name (keys %$type) {
140 my $service_exists = $type->{$name}->{service
};
142 if ($service_exists) {
143 eval { PVE
::Ceph
::Services
::ceph_service_cmd
('disable', "$service.$name") };
144 warn "Could not disable ceph-$service\@$name, error: $@\n" if $@;
146 eval { PVE
::Ceph
::Services
::ceph_service_cmd
('stop', "$service.$name") };
147 warn "Could not stop ceph-$service\@$name, error: $@\n" if $@;
153 sub ceph_install_flag_file
{ return '/run/pve-ceph-install-flag' };
155 sub check_ceph_installed
{
156 my ($service, $noerr) = @_;
158 $service = 'ceph_bin' if !defined($service);
160 # NOTE: the flag file is checked as on a new installation, the binary gets
161 # extracted by dpkg before the installation is finished
162 if (! -x
$ceph_service->{$service} || -f ceph_install_flag_file
()) {
163 die "binary not installed: $ceph_service->{$service}\n" if !$noerr;
171 sub check_ceph_configured
{
175 die "ceph not fully configured - missing '$pve_ckeyring_path'\n"
176 if ! -f
$pve_ckeyring_path;
181 sub check_ceph_inited
{
184 return undef if !check_ceph_installed
('ceph_mon', $noerr);
186 if (! -f
$pve_ceph_cfgpath) {
187 die "pveceph configuration not initialized\n" if !$noerr;
194 sub check_ceph_enabled
{
197 return undef if !check_ceph_inited
($noerr);
199 if (! -f
$ceph_cfgpath) {
200 die "pveceph configuration not enabled\n" if !$noerr;
207 my $set_pool_setting = sub {
208 my ($pool, $setting, $value, $rados) = @_;
211 if ($setting eq 'application') {
213 prefix
=> "osd pool application enable",
219 prefix
=> "osd pool set",
227 $rados = PVE
::RADOS-
>new() if !$rados;
228 eval { $rados->mon_command($command); };
229 return $@ ?
$@ : undef;
233 my ($pool, $param) = @_;
235 my $rados = PVE
::RADOS-
>new();
237 if (get_pool_type
($pool, $rados) eq 'erasure') {
238 #remove parameters that cannot be changed for erasure coded pools
239 my $ignore_params = ['size', 'crush_rule'];
240 for my $setting (@$ignore_params) {
241 if ($param->{$setting}) {
242 print "cannot set '${setting}' for erasure coded pool\n";
243 delete $param->{$setting};
247 # by default, pool size always resets min_size, so set it as first item
248 # https://tracker.ceph.com/issues/44862
249 my $keys = [ grep { $_ ne 'size' } sort keys %$param ];
250 unshift @$keys, 'size' if exists $param->{size
};
252 for my $setting (@$keys) {
253 my $value = $param->{$setting};
255 print "pool $pool: applying $setting = $value\n";
256 if (my $err = $set_pool_setting->($pool, $setting, $value, $rados)) {
259 delete $param->{$setting};
263 if (scalar(keys %$param) > 0) {
264 my $missing = join(', ', sort keys %$param );
265 die "Could not set: $missing\n";
270 sub get_pool_properties
{
271 my ($pool, $rados) = @_;
272 $rados = PVE
::RADOS-
>new() if !defined($rados);
274 prefix
=> "osd pool get",
279 return $rados->mon_command($command);
283 my ($pool, $rados) = @_;
284 $rados = PVE
::RADOS-
>new() if !defined($rados);
285 return 'erasure' if get_pool_properties
($pool, $rados)->{erasure_code_profile
};
290 my ($pool, $param, $rados) = @_;
291 $rados = PVE
::RADOS-
>new() if !defined($rados);
293 my $pg_num = $param->{pg_num
} || 128;
296 prefix
=> "osd pool create",
298 pg_num
=> int($pg_num),
301 $mon_params->{pool_type
} = extract_param
($param, 'pool_type') if $param->{pool_type
};
302 $mon_params->{erasure_code_profile
} = extract_param
($param, 'erasure_code_profile')
303 if $param->{erasure_code_profile
};
305 $rados->mon_command($mon_params);
307 set_pool
($pool, $param);
312 my ($pool, $rados) = @_;
313 $rados = PVE
::RADOS-
>new() if !defined($rados);
315 my $res = $rados->mon_command({ prefix
=> "osd lspools" });
321 my ($pool, $rados) = @_;
322 $rados = PVE
::RADOS-
>new() if !defined($rados);
324 # fixme: '--yes-i-really-really-mean-it'
325 $rados->mon_command({
326 prefix
=> "osd pool delete",
329 'yes_i_really_really_mean_it' => JSON
::true
,
334 # we get something like:
336 # 'metadata_pool_id' => 2,
337 # 'data_pool_ids' => [ 1 ],
338 # 'metadata_pool' => 'cephfs_metadata',
339 # 'data_pools' => [ 'cephfs_data' ],
340 # 'name' => 'cephfs',
344 $rados = PVE
::RADOS-
>new() if !defined($rados);
346 my $res = $rados->mon_command({ prefix
=> "fs ls" });
352 my ($fs, $param, $rados) = @_;
354 if (!defined($rados)) {
355 $rados = PVE
::RADOS-
>new();
358 $rados->mon_command({
361 metadata
=> $param->{pool_metadata
},
362 data
=> $param->{pool_data
},
368 my ($fs, $rados) = @_;
369 $rados = PVE
::RADOS-
>new() if !defined($rados);
371 $rados->mon_command({
374 'yes_i_really_mean_it' => JSON
::true
,
379 sub setup_pve_symlinks
{
380 # fail if we find a real file instead of a link
381 if (-f
$ceph_cfgpath) {
382 my $lnk = readlink($ceph_cfgpath);
383 die "file '$ceph_cfgpath' already exists and is not a symlink to $pve_ceph_cfgpath\n"
384 if !$lnk || $lnk ne $pve_ceph_cfgpath;
387 symlink($pve_ceph_cfgpath, $ceph_cfgpath) ||
388 die "unable to create symlink '$ceph_cfgpath' - $!\n";
390 my $ceph_uid = getpwnam('ceph');
391 my $ceph_gid = getgrnam('ceph');
392 chown $ceph_uid, $ceph_gid, $ceph_cfgdir;
395 sub get_or_create_admin_keyring
{
396 if (! -f
$pve_ckeyring_path) {
397 run_command
("ceph-authtool --create-keyring $pve_ckeyring_path " .
398 "--gen-key -n client.admin " .
399 "--cap mon 'allow *' " .
400 "--cap osd 'allow *' " .
401 "--cap mds 'allow *' " .
402 "--cap mgr 'allow *' ");
403 # we do not want to overwrite it
404 if (! -f
$ckeyring_path) {
405 run_command
("cp $pve_ckeyring_path $ckeyring_path");
406 run_command
("chown ceph:ceph $ckeyring_path");
409 return $pve_ckeyring_path;
412 # get ceph-volume managed osds
413 sub ceph_volume_list
{
416 if (!check_ceph_installed
('ceph_volume', 1)) {
421 my $cmd = [ $ceph_service->{ceph_volume
}, 'lvm', 'list', '--format', 'json' ];
422 run_command
($cmd, outfunc
=> sub { $output .= shift });
424 $result = eval { decode_json
($output) };
429 sub ceph_volume_zap
{
430 my ($osdid, $destroy) = @_;
432 die "no osdid given\n" if !defined($osdid);
434 my $cmd = [ $ceph_service->{ceph_volume
}, 'lvm', 'zap', '--osd-id', $osdid ];
435 push @$cmd, '--destroy' if $destroy;
440 sub get_db_wal_sizes
{
443 my $rados = PVE
::RADOS-
>new();
444 my $db_config = $rados->mon_command({ prefix
=> 'config-key dump', key
=> 'config/' });
446 $res->{db
} = $db_config->{"config/osd/bluestore_block_db_size"} //
447 $db_config->{"config/global/bluestore_block_db_size"};
449 $res->{wal
} = $db_config->{"config/osd/bluestore_block_wal_size"} //
450 $db_config->{"config/global/bluestore_block_wal_size"};
452 if (!$res->{db
} || !$res->{wal
}) {
453 my $cfg = cfs_read_file
('ceph.conf');
455 $res->{db
} = $cfg->{osd
}->{bluestore_block_db_size
} //
456 $cfg->{global
}->{bluestore_block_db_size
};
460 $res->{wal
} = $cfg->{osd
}->{bluestore_block_wal_size
} //
461 $cfg->{global
}->{bluestore_block_wal_size
};
467 sub get_possible_osd_flags
{
468 my $possible_flags = {
470 description
=> 'Pauses read and writes.',
475 description
=> 'OSDs are not allowed to start.',
480 description
=> 'OSD failure reports are being ignored, such that the monitors will not mark OSDs down.',
485 description
=> 'OSDs will not automatically be marked out after the configured interval.',
490 description
=> 'OSDs that were previously marked out will not be marked back in when they start.',
495 description
=> 'Backfilling of PGs is suspended.',
500 description
=> 'Rebalancing of PGs is suspended.',
505 description
=> 'Recovery of PGs is suspended.',
510 description
=> 'Scrubbing is disabled.',
515 description
=> 'Deep Scrubbing is disabled.',
520 description
=> 'Cache tiering activity is suspended.',
525 return $possible_flags;
528 sub get_real_flag_name
{
531 # the 'pause' flag gets always set to both 'pauserd' and 'pausewr'
532 # so decide that the 'pause' flag is set if we detect 'pauserd'
534 'pause' => 'pauserd',
537 return $flagmap->{$flag} // $flag;
540 sub ceph_cluster_status
{
542 $rados = PVE
::RADOS-
>new() if !$rados;
544 my $status = $rados->mon_command({ prefix
=> 'status' });
545 $status->{health
} = $rados->mon_command({ prefix
=> 'health', detail
=> 'detail' });
547 if (!exists $status->{monmap
}->{mons
}) { # octopus moved most info out of status, re-add
548 $status->{monmap
} = $rados->mon_command({ prefix
=> 'mon dump' });
549 $status->{mgrmap
} = $rados->mon_command({ prefix
=> 'mgr dump' });
555 sub ecprofile_exists
{
556 my ($name, $rados) = @_;
557 $rados = PVE
::RADOS-
>new() if !$rados;
559 my $res = $rados->mon_command({ prefix
=> 'osd erasure-code-profile ls' });
561 my $profiles = { map { $_ => 1 } @$res };
562 return $profiles->{$name};
565 sub create_ecprofile
{
566 my ($name, $k, $m, $failure_domain, $device_class, $rados) = @_;
567 $rados = PVE
::RADOS-
>new() if !$rados;
569 $failure_domain = 'host' if !$failure_domain;
572 "crush-failure-domain=${failure_domain}",
577 push(@$profile, "crush-device-class=${device_class}") if $device_class;
579 $rados->mon_command({
580 prefix
=> 'osd erasure-code-profile set',
586 sub destroy_ecprofile
{
587 my ($profile, $rados) = @_;
588 $rados = PVE
::RADOS-
>new() if !$rados;
591 prefix
=> 'osd erasure-code-profile rm',
595 return $rados->mon_command($command);
598 sub get_ecprofile_name
{
600 return "pve_ec_${name}";
603 sub destroy_crush_rule
{
604 my ($rule, $rados) = @_;
605 $rados = PVE
::RADOS-
>new() if !$rados;
608 prefix
=> 'osd crush rule rm',
612 return $rados->mon_command($command);