]> git.proxmox.com Git - pve-manager.git/blame - PVE/CLI/pveceph.pm
fix #4364: pveceph: add confirmation dialogue for ceph installation
[pve-manager.git] / PVE / CLI / pveceph.pm
CommitLineData
3e8560ac
DM
1package PVE::CLI::pveceph;
2
3use strict;
4use warnings;
5
6use Fcntl ':flock';
7use File::Path;
8use IO::File;
9use JSON;
10use Data::Dumper;
11use LWP::UserAgent;
12
bbfbf0ef
TL
13use Proxmox::RS::Subscription;
14
3e8560ac
DM
15use PVE::SafeSyslog;
16use PVE::Cluster;
17use PVE::INotify;
18use PVE::RPCEnvironment;
19use PVE::Storage;
20use PVE::Tools qw(run_command);
21use PVE::JSONSchema qw(get_standard_option);
6fb08cb9 22use PVE::Ceph::Tools;
91dfa228 23use PVE::Ceph::Services;
3e8560ac 24use PVE::API2::Ceph;
7e1a9d25 25use PVE::API2::Ceph::FS;
b82649cc 26use PVE::API2::Ceph::MDS;
4fec2764 27use PVE::API2::Ceph::MGR;
98fe93ae 28use PVE::API2::Ceph::MON;
79fa41a2 29use PVE::API2::Ceph::OSD;
3e8560ac
DM
30
31use PVE::CLIHandler;
32
33use base qw(PVE::CLIHandler);
34
35my $nodename = PVE::INotify::nodename();
36
37my $upid_exit = sub {
38 my $upid = shift;
39 my $status = PVE::Tools::upid_read_status($upid);
4bb46baa 40 exit(PVE::Tools::upid_status_is_error($status) ? -1 : 0);
3e8560ac
DM
41};
42
7e017024
DM
43sub setup_environment {
44 PVE::RPCEnvironment->setup_default_cli_env();
45}
46
3e8560ac
DM
47__PACKAGE__->register_method ({
48 name => 'purge',
49 path => 'purge',
50 method => 'POST',
51 description => "Destroy ceph related data and configuration files.",
52 parameters => {
53 additionalProperties => 0,
54 properties => {
91dfa228
AA
55 logs => {
56 description => 'Additionally purge Ceph logs, /var/log/ceph.',
57 type => 'boolean',
58 optional => 1,
59 },
60 crash => {
61 description => 'Additionally purge Ceph crash logs, /var/lib/ceph/crash.',
62 type => 'boolean',
63 optional => 1,
64 },
3e8560ac
DM
65 },
66 },
67 returns => { type => 'null' },
68 code => sub {
69 my ($param) = @_;
70
91dfa228
AA
71 my $message;
72 my $pools = [];
73 my $monstat = {};
74 my $mdsstat = {};
75 my $osdstat = [];
3e8560ac
DM
76
77 eval {
78 my $rados = PVE::RADOS->new();
91dfa228
AA
79 $pools = PVE::Ceph::Tools::ls_pools(undef, $rados);
80 $monstat = PVE::Ceph::Services::get_services_info('mon', undef, $rados);
81 $mdsstat = PVE::Ceph::Services::get_services_info('mds', undef, $rados);
82 $osdstat = $rados->mon_command({ prefix => 'osd metadata' });
3e8560ac 83 };
c3a3f3ab 84 warn "Error gathering ceph info, already purged? Message: $@" if $@;
3e8560ac 85
91dfa228
AA
86 my $osd = grep { $_->{hostname} eq $nodename } @$osdstat;
87 my $mds = grep { $mdsstat->{$_}->{host} eq $nodename } keys %$mdsstat;
88 my $mon = grep { $monstat->{$_}->{host} eq $nodename } keys %$monstat;
3e8560ac 89
91dfa228
AA
90 # no pools = no data
91 $message .= "- remove pools, this will !!DESTROY DATA!!\n" if @$pools;
92 $message .= "- remove active OSD on $nodename\n" if $osd;
93 $message .= "- remove active MDS on $nodename\n" if $mds;
94 $message .= "- remove other MONs, $nodename is not the last MON\n"
95 if scalar(keys %$monstat) > 1 && $mon;
96
97 # display all steps at once
98 die "Unable to purge Ceph!\n\nTo continue:\n$message" if $message;
99
100 my $services = PVE::Ceph::Services::get_local_services();
101 $services->{mon} = $monstat if $mon;
102 $services->{crash}->{$nodename} = { direxists => 1 } if $param->{crash};
103 $services->{logs}->{$nodename} = { direxists => 1 } if $param->{logs};
104
105 PVE::Ceph::Tools::purge_all_ceph_services($services);
106 PVE::Ceph::Tools::purge_all_ceph_files($services);
3e8560ac
DM
107
108 return undef;
109 }});
110
bbfbf0ef
TL
111my sub has_valid_subscription {
112 my $info = eval { Proxmox::RS::Subscription::read_subscription('/etc/subscription') } // {};
113 warn "couldn't check subscription info - $@" if $@;
114 return $info->{status} && $info->{status} eq 'active'; # age check?
115}
116
74ea1fb2
TL
117my $supported_ceph_versions = ['quincy'];
118my $default_ceph_version = 'quincy';
983921b9 119
3e8560ac
DM
120__PACKAGE__->register_method ({
121 name => 'install',
122 path => 'install',
123 method => 'POST',
124 description => "Install ceph related packages.",
125 parameters => {
126 additionalProperties => 0,
127 properties => {
128 version => {
129 type => 'string',
983921b9
TL
130 enum => $supported_ceph_versions,
131 default => $default_ceph_version,
c3d9c698 132 description => "Ceph version to install.",
3e8560ac 133 optional => 1,
61819403 134 },
93542d77
TL
135 repository => {
136 type => 'string',
137 enum => ['enterprise', 'no-subscription', 'test'],
138 default => 'enterprise',
139 description => "Ceph repository to use.",
61819403 140 optional => 1,
61819403 141 },
93542d77 142 'allow-experimental' => {
3f7d054e
TL
143 type => 'boolean',
144 default => 0,
145 optional => 1,
93542d77 146 description => "Allow experimental versions. Use with care!",
3f7d054e 147 },
3e8560ac
DM
148 },
149 },
150 returns => { type => 'null' },
151 code => sub {
152 my ($param) = @_;
153
983921b9 154 my $cephver = $param->{version} || $default_ceph_version;
3e8560ac 155
93542d77
TL
156 my $repo = $param->{'repository'} // 'enterprise';
157 my $enterprise_repo = $repo eq 'enterprise';
158 my $cdn = $enterprise_repo ? 'https://enterprise.proxmox.com' : 'http://download.proxmox.com';
3f7d054e 159
bbfbf0ef
TL
160 if (has_valid_subscription()) {
161 warn "\nNOTE: The node has an active subscription but a non-production Ceph repository selected.\n\n"
162 if !$enterprise_repo;
163 } elsif ($enterprise_repo) {
164 warn "\nWARN: Enterprise repository selected, but no active subscription!\n\n";
165 } elsif ($repo eq 'no-subscription') {
166 warn "\nHINT: The no-subscription repository is not the best choice for production setups.\n"
167 ."Proxmox recommends using the enterprise repository with a valid subscription.\n";
168 } else {
169 warn "\nWARN: The test repository should only be used for test setups or after consulting"
170 ." the official Proxmox support!\n\n"
171 }
172
c3d9c698 173 my $repolist;
74ea1fb2 174 if ($cephver eq 'quincy') {
93542d77 175 $repolist = "deb ${cdn}/debian/ceph-quincy bookworm $repo\n";
caf37855 176 } else {
3f7d054e 177 die "unsupported ceph version: $cephver";
caf37855 178 }
d1c49f5b
MC
179
180 if (-t STDOUT && !$param->{version}) {
181 print "This will install Ceph " . ucfirst($cephver) . " - continue (y/N)? ";
182
183 my $answer = <STDIN>;
184 my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
185
186 die "Aborting installation as requested\n" if !$continue;
187 }
188
c3d9c698
TL
189 PVE::Tools::file_set_contents("/etc/apt/sources.list.d/ceph.list", $repolist);
190
7271e6f6
TL
191 my $supported_re = join('|', $supported_ceph_versions->@*);
192 warn "WARNING: installing non-default ceph release '$cephver'!\n" if $cephver !~ qr/^(?:$supported_re)$/;
3e8560ac 193
87166f36 194 local $ENV{DEBIAN_FRONTEND} = 'noninteractive';
3e8560ac 195 print "update available package list\n";
d26556c0
TL
196 eval {
197 run_command(
198 ['apt-get', '-q', 'update'],
199 outfunc => sub {},
200 errfunc => sub { print STDERR "$_[0]\n" },
201 )
202 };
3e8560ac 203
ecddd2e2 204 my @apt_install = qw(apt-get --no-install-recommends -o Dpkg::Options::=--force-confnew install --);
2ca75379
TL
205 my @ceph_packages = qw(
206 ceph
207 ceph-common
2ca75379 208 ceph-fuse
ddd89279
TL
209 ceph-mds
210 ceph-volume
2ca75379 211 gdisk
08e22c1e 212 nvme-cli
2ca75379
TL
213 );
214
55ab726e 215 print "start installation\n";
4dd27d50 216
d380d000 217 # this flag helps to determine when apt is actually done installing (vs. partial extracing)
9f6dc075 218 my $install_flag_fn = PVE::Ceph::Tools::ceph_install_flag_file();
d380d000 219 open(my $install_flag, '>', $install_flag_fn) or die "could not create install flag - $!\n";
4dd27d50
AL
220 close $install_flag;
221
2ca75379 222 if (system(@apt_install, @ceph_packages) != 0) {
d380d000 223 unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
2ca75379
TL
224 die "apt failed during ceph installation ($?)\n";
225 }
55ab726e 226
37bf860e 227 print "\ninstalled ceph $cephver successfully!\n";
d380d000
TL
228 # done: drop flag file so that the PVE::Ceph::Tools check returns Ok now.
229 unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
ece49b3c
TL
230
231 print "\nreloading API to load new Ceph RADOS library...\n";
232 run_command([
233 'systemctl', 'try-reload-or-restart', 'pvedaemon.service', 'pveproxy.service'
234 ]);
3e8560ac
DM
235
236 return undef;
237 }});
238
1baeb2d1
AL
239__PACKAGE__->register_method ({
240 name => 'status',
241 path => 'status',
242 method => 'GET',
243 description => "Get Ceph Status.",
244 parameters => {
245 additionalProperties => 0,
246 },
247 returns => { type => 'null' },
248 code => sub {
249 PVE::Ceph::Tools::check_ceph_inited();
250
251 run_command(
252 ['ceph', '-s'],
253 outfunc => sub { print "$_[0]\n" },
ab459f6d
TL
254 errfunc => sub { print STDERR "$_[0]\n" },
255 timeout => 15,
1baeb2d1
AL
256 );
257 return undef;
258 }});
259
02c1e98e
DC
260my $get_storages = sub {
261 my ($fs, $is_default) = @_;
262
263 my $cfg = PVE::Storage::config();
264
265 my $storages = $cfg->{ids};
266 my $res = {};
267 foreach my $storeid (keys %$storages) {
268 my $curr = $storages->{$storeid};
269 next if $curr->{type} ne 'cephfs';
270 my $cur_fs = $curr->{'fs-name'};
271 $res->{$storeid} = $storages->{$storeid}
272 if (!defined($cur_fs) && $is_default) || (defined($cur_fs) && $fs eq $cur_fs);
273 }
274
275 return $res;
276};
277
278__PACKAGE__->register_method ({
279 name => 'destroyfs',
280 path => 'destroyfs',
281 method => 'DELETE',
282 description => "Destroy a Ceph filesystem",
283 parameters => {
284 additionalProperties => 0,
285 properties => {
286 node => get_standard_option('pve-node'),
287 name => {
288 description => "The ceph filesystem name.",
289 type => 'string',
290 },
291 'remove-storages' => {
292 description => "Remove all pveceph-managed storages configured for this fs.",
293 type => 'boolean',
294 optional => 1,
295 default => 0,
296 },
297 'remove-pools' => {
298 description => "Remove data and metadata pools configured for this fs.",
299 type => 'boolean',
300 optional => 1,
301 default => 0,
302 },
303 },
304 },
305 returns => { type => 'string' },
306 code => sub {
307 my ($param) = @_;
308
309 PVE::Ceph::Tools::check_ceph_inited();
310
311 my $rpcenv = PVE::RPCEnvironment::get();
312 my $user = $rpcenv->get_user();
313
314 my $fs_name = $param->{name};
315
316 my $fs;
317 my $fs_list = PVE::Ceph::Tools::ls_fs();
318 for my $entry (@$fs_list) {
319 next if $entry->{name} ne $fs_name;
320 $fs = $entry;
321 last;
322 }
323 die "no such cephfs '$fs_name'\n" if !$fs;
324
325 my $worker = sub {
326 my $rados = PVE::RADOS->new();
327
328 if ($param->{'remove-storages'}) {
329 my $defaultfs;
330 my $fs_dump = $rados->mon_command({ prefix => "fs dump" });
331 for my $fs ($fs_dump->{filesystems}->@*) {
332 next if $fs->{id} != $fs_dump->{default_fscid};
333 $defaultfs = $fs->{mdsmap}->{fs_name};
334 }
335 warn "no default fs found, maybe not all relevant storages are removed\n"
336 if !defined($defaultfs);
337
338 my $storages = $get_storages->($fs_name, $fs_name eq ($defaultfs // ''));
339 for my $storeid (keys %$storages) {
340 my $store = $storages->{$storeid};
341 if (!$store->{disable}) {
342 die "storage '$storeid' is not disabled, make sure to disable ".
343 "and unmount the storage first\n";
344 }
345 }
346
347 my $err;
348 for my $storeid (keys %$storages) {
349 # skip external clusters, not managed by pveceph
350 next if $storages->{$storeid}->{monhost};
351 eval { PVE::API2::Storage::Config->delete({storage => $storeid}) };
352 if ($@) {
353 warn "failed to remove storage '$storeid': $@\n";
354 $err = 1;
355 }
356 }
357 die "failed to remove (some) storages - check log and remove manually!\n"
358 if $err;
359 }
360
361 PVE::Ceph::Tools::destroy_fs($fs_name, $rados);
362
363 if ($param->{'remove-pools'}) {
364 warn "removing metadata pool '$fs->{metadata_pool}'\n";
365 eval { PVE::Ceph::Tools::destroy_pool($fs->{metadata_pool}, $rados) };
366 warn "$@\n" if $@;
367
368 foreach my $pool ($fs->{data_pools}->@*) {
369 warn "removing data pool '$pool'\n";
370 eval { PVE::Ceph::Tools::destroy_pool($pool, $rados) };
371 warn "$@\n" if $@;
372 }
373 }
374
375 };
376 return $rpcenv->fork_worker('cephdestroyfs', $fs_name, $user, $worker);
377 }});
378
b48ca5a7 379__PACKAGE__->register_method ({
cf14758f
TL
380 name => 'osd-details',
381 path => 'osd-details',
b48ca5a7
AL
382 method => 'GET',
383 description => "Get OSD details.",
384 parameters => {
385 additionalProperties => 0,
386 properties => {
387 node => get_standard_option('pve-node'),
388 osdid => {
389 description => "ID of the OSD",
390 type => 'string',
391 },
392 verbose => {
393 description => "Print verbose information, same as json-pretty output format.",
394 type => 'boolean',
395 default => 0,
396 optional => 1,
397 },
398 },
399 },
400 returns => { type => 'object' },
401 code => sub {
402 my ($param) = @_;
cf14758f 403
b48ca5a7 404 PVE::Ceph::Tools::check_ceph_inited();
cf14758f 405
b48ca5a7 406 my $res = PVE::API2::Ceph::OSD->osddetails({
cf14758f
TL
407 osdid => $param->{osdid},
408 node => $param->{node},
409 });
410
411 for my $dev ($res->{devices}->@*) {
412 $dev->{"lv-info"} = PVE::API2::Ceph::OSD->osdvolume({
b48ca5a7
AL
413 osdid => $param->{osdid},
414 node => $param->{node},
cf14758f 415 type => $dev->{device},
b48ca5a7 416 });
b48ca5a7
AL
417 }
418 $res->{verbose} = 1 if $param->{verbose};
419 return $res;
420 }});
421
422my $format_osddetails = sub {
423 my ($data, $schema, $options) = @_;
cf14758f 424
b48ca5a7
AL
425 $options->{"output-format"} //= "text";
426
427 if ($data->{verbose}) {
428 $options->{"output-format"} = "json-pretty";
429 delete $data->{verbose};
430 }
431
432 if ($options->{"output-format"} eq "text") {
cf14758f
TL
433 for my $dev ($data->{devices}->@*) {
434 my ($disk, $type, $device) = $dev->@{'physical_device', 'type', 'device'};
435 my ($lv_size, $lv_ctime) = $dev->{'lv-info'}->@{'lv_size', 'creation_time'};
b48ca5a7 436
cf14758f 437 $data->{osd}->{$device} = "Disk: $disk, Type: $type, LV Size: $lv_size, LV Creation Time: $lv_ctime";
b48ca5a7
AL
438 }
439 PVE::CLIFormatter::print_api_result($data->{osd}, $schema, undef, $options);
440 } else {
441 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
442 }
443};
444
3e8560ac
DM
445our $cmddef = {
446 init => [ 'PVE::API2::Ceph', 'init', [], { node => $nodename } ],
8c476782 447 pool => {
a0665001 448 ls => [ 'PVE::API2::Ceph::Pool', 'lspools', [], { node => $nodename }, sub {
d4dba076
AA
449 my ($data, $schema, $options) = @_;
450 PVE::CLIFormatter::print_api_result($data, $schema,
451 [
452 'pool_name',
453 'size',
454 'min_size',
455 'pg_num',
5a3d7942
AA
456 'pg_num_min',
457 'pg_num_final',
d4dba076 458 'pg_autoscale_mode',
5a3d7942
AA
459 'target_size',
460 'target_size_ratio',
d4dba076
AA
461 'crush_rule_name',
462 'percent_used',
463 'bytes_used',
464 ],
465 $options);
466 }, $PVE::RESTHandler::standard_output_options],
a0665001
AL
467 create => [ 'PVE::API2::Ceph::Pool', 'createpool', ['name'], { node => $nodename }],
468 destroy => [ 'PVE::API2::Ceph::Pool', 'destroypool', ['name'], { node => $nodename } ],
469 set => [ 'PVE::API2::Ceph::Pool', 'setpool', ['name'], { node => $nodename } ],
470 get => [ 'PVE::API2::Ceph::Pool', 'getpool', ['name'], { node => $nodename }, sub {
54ba7dd9
AA
471 my ($data, $schema, $options) = @_;
472 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
473 }, $PVE::RESTHandler::standard_output_options],
8c476782
TL
474 },
475 lspools => { alias => 'pool ls' },
476 createpool => { alias => 'pool create' },
477 destroypool => { alias => 'pool destroy' },
478 fs => {
479 create => [ 'PVE::API2::Ceph::FS', 'createfs', [], { node => $nodename }],
02c1e98e 480 destroy => [ __PACKAGE__, 'destroyfs', ['name'], { node => $nodename }],
8c476782
TL
481 },
482 osd => {
79fa41a2
DC
483 create => [ 'PVE::API2::Ceph::OSD', 'createosd', ['dev'], { node => $nodename }, $upid_exit],
484 destroy => [ 'PVE::API2::Ceph::OSD', 'destroyosd', ['osdid'], { node => $nodename }, $upid_exit],
cf14758f
TL
485 details => [
486 __PACKAGE__, 'osd-details', ['osdid'], { node => $nodename }, $format_osddetails,
487 $PVE::RESTHandler::standard_output_options,
488 ],
8c476782
TL
489 },
490 createosd => { alias => 'osd create' },
491 destroyosd => { alias => 'osd destroy' },
492 mon => {
98fe93ae
DC
493 create => [ 'PVE::API2::Ceph::MON', 'createmon', [], { node => $nodename }, $upid_exit],
494 destroy => [ 'PVE::API2::Ceph::MON', 'destroymon', ['monid'], { node => $nodename }, $upid_exit],
8c476782
TL
495 },
496 createmon => { alias => 'mon create' },
497 destroymon => { alias => 'mon destroy' },
498 mgr => {
4fec2764
DC
499 create => [ 'PVE::API2::Ceph::MGR', 'createmgr', [], { node => $nodename }, $upid_exit],
500 destroy => [ 'PVE::API2::Ceph::MGR', 'destroymgr', ['id'], { node => $nodename }, $upid_exit],
8c476782
TL
501 },
502 createmgr => { alias => 'mgr create' },
503 destroymgr => { alias => 'mgr destroy' },
504 mds => {
505 create => [ 'PVE::API2::Ceph::MDS', 'createmds', [], { node => $nodename }, $upid_exit],
f16bb531 506 destroy => [ 'PVE::API2::Ceph::MDS', 'destroymds', ['name'], { node => $nodename }, $upid_exit],
8c476782 507 },
7da6ff26
DJ
508 start => [ 'PVE::API2::Ceph', 'start', [], { node => $nodename }, $upid_exit],
509 stop => [ 'PVE::API2::Ceph', 'stop', [], { node => $nodename }, $upid_exit],
3e8560ac
DM
510 install => [ __PACKAGE__, 'install', [] ],
511 purge => [ __PACKAGE__, 'purge', [] ],
1baeb2d1 512 status => [ __PACKAGE__, 'status', []],
3e8560ac
DM
513};
514
5151;