]> git.proxmox.com Git - pve-manager.git/blame - PVE/API2/Ceph.pm
pass required node name parameter
[pve-manager.git] / PVE / API2 / Ceph.pm
CommitLineData
38db610a
DM
1package PVE::API2::Ceph;
2
3use strict;
4use warnings;
5use File::Basename;
6use File::Path;
7use POSIX qw (LONG_MAX);
8
9use PVE::SafeSyslog;
10use PVE::Tools qw(extract_param run_command);
11use PVE::Exception qw(raise raise_param_exc);
12use PVE::INotify;
13use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
14use PVE::AccessControl;
15use PVE::Storage;
16use PVE::RESTHandler;
17use PVE::RPCEnvironment;
18use PVE::JSONSchema qw(get_standard_option);
19use JSON;
20
21use base qw(PVE::RESTHandler);
22
23use Data::Dumper; # fixme: remove
24
25my $ccname = 'ceph'; # ceph cluster name
26my $ceph_cfgdir = "/etc/ceph";
27my $pve_ceph_cfgpath = "/etc/pve/$ccname.conf";
28my $ceph_cfgpath = "$ceph_cfgdir/$ccname.conf";
29my $pve_mon_key_path = "/etc/pve/priv/$ccname.mon.keyring";
30my $ceph_mon_key_path = "$ceph_cfgdir/$ccname.mon.keyring";
31my $pve_ckeyring_path = "/etc/pve/priv/$ccname.keyring";
32my $ceph_ckeyring_path = "$ceph_cfgdir/$ccname.client.admin.keyring";
33
34my $ceph_bootstrap_osd_keyring = "/var/lib/ceph/bootstrap-osd/$ccname.keyring";
35my $ceph_bootstrap_mds_keyring = "/var/lib/ceph/bootstrap-mds/$ccname.keyring";
36
37my $ceph_bin = "/usr/bin/ceph";
38
39sub purge_all_ceph_files {
40 # fixme: this is very dangerous - should we really support this function?
41
42 unlink $ceph_cfgpath;
43 unlink $ceph_mon_key_path;
44 unlink $ceph_ckeyring_path;
45
46 unlink $pve_ceph_cfgpath;
47 unlink $pve_ckeyring_path;
48 unlink $pve_mon_key_path;
49
50 unlink $ceph_bootstrap_osd_keyring;
51 unlink $ceph_bootstrap_mds_keyring;
52
53 system("rm -rf /var/lib/ceph/mon/ceph-*");
54
55 # remove osd?
56
57}
58
59my $check_ceph_installed = sub {
60 my ($noerr) = @_;
61
62 if (! -x $ceph_bin) {
63 die "ceph binaries not installed\n" if !$noerr;
64 return undef;
65 }
66
67 return 1;
68};
69
70my $check_ceph_inited = sub {
71 my ($noerr) = @_;
72
73 return undef if !&$check_ceph_installed($noerr);
74
75 if (! -f $pve_ceph_cfgpath) {
76 die "pveceph configuration not initialized\n" if !$noerr;
77 return undef;
78 }
79
80 return 1;
81};
82
83my $force_symlink = sub {
84 my ($old, $new) = @_;
85
86 return if (-l $new) && (readlink($new) eq $old);
87
88 unlink $new;
89 symlink($old, $new) ||
90 die "unable to create symlink '$new' - $!\n";
91};
92
93my $parse_ceph_config = sub {
94 my ($filename) = @_;
95
96 my $cfg = {};
97
98 return $cfg if ! -f $filename;
99
100 my $fh = IO::File->new($filename, "r") ||
101 die "unable to open '$filename' - $!\n";
102
103 my $section;
104
105 while (defined(my $line = <$fh>)) {
106 $line =~ s/[;#].*$//;
107 $line =~ s/^\s+//;
108 $line =~ s/\s+$//;
109 next if !$line;
110
111 $section = $1 if $line =~ m/^\[(\S+)\]$/;
112 if (!$section) {
113 warn "no section - skip: $line\n";
114 next;
115 }
116
117 if ($line =~ m/^(.*\S)\s*=\s*(\S.*)$/) {
118 $cfg->{$section}->{$1} = $2;
119 }
120
121 }
122
123 return $cfg;
124};
125
126my $run_ceph_cmd = sub {
127 my ($cmd, %params) = @_;
128
129 my $timeout = 3;
130
131 my $oldalarm;
132 eval {
133 local $SIG{ALRM} = sub { die "timeout\n" };
134 $oldalarm = alarm($timeout);
135 # Note: --connect-timeout does not work with current version
136 # '--connect-timeout', $timeout,
137
138 run_command(['ceph', '-c', $ceph_cfgpath, @$cmd], %params);
139 alarm(0);
140 };
141 my $err = $@;
142
143 alarm($oldalarm) if $oldalarm;
144
145 die $err if $err;
146
147};
148
149sub ceph_mon_status {
150 my ($quiet) = @_;
151
152 my $json = '';
153 my $parser = sub {
154 my $line = shift;
155 $json .= $line;
156 };
157
158 my $errfunc = sub {
159 my $line = shift;
160 print "$line\n" if !$quiet;
161 };
162
163 &$run_ceph_cmd(['mon_status'], outfunc => $parser, errfunc => $errfunc);
164
165 my $res = decode_json($json);
166
167 return $res;
168}
169
170my $ceph_osd_status = sub {
171 my ($quiet) = @_;
172
173 my $json = '';
174 my $parser = sub {
175 my $line = shift;
176 $json .= $line;
177 };
178
179 my $errfunc = sub {
180 my $line = shift;
181 print "$line\n" if !$quiet;
182 };
183
184 &$run_ceph_cmd(['osd', 'dump', '--format', 'json'],
185 outfunc => $parser, errfunc => $errfunc);
186
187 my $res = decode_json($json);
188
189 return $res;
190};
191
192my $write_ceph_config = sub {
193 my ($cfg) = @_;
194
195 my $out = '';
196 foreach my $section (keys %$cfg) {
197 $out .= "[$section]\n";
198 foreach my $key (sort keys %{$cfg->{$section}}) {
199 $out .= "\t $key = $cfg->{$section}->{$key}\n";
200 }
201 $out .= "\n";
202 }
203
204 PVE::Tools::file_set_contents($pve_ceph_cfgpath, $out);
205};
206
207my $setup_pve_symlinks = sub {
208 # fail if we find a real file instead of a link
209 if (-f $ceph_cfgpath) {
210 my $lnk = readlink($ceph_cfgpath);
211 die "file '$ceph_cfgpath' already exists\n"
212 if !$lnk || $lnk ne $pve_ceph_cfgpath;
213 }
214
215 # now assume we are allowed to setup/overwrite content
216 &$force_symlink($pve_ceph_cfgpath, $ceph_cfgpath);
217 &$force_symlink($pve_mon_key_path, $ceph_mon_key_path);
218 &$force_symlink($pve_ckeyring_path, $ceph_ckeyring_path);
219};
220
221my $ceph_service_cmd = sub {
222 run_command(['service', 'ceph', '-c', $ceph_cfgpath, @_]);
223};
224
225__PACKAGE__->register_method ({
226 name => 'index',
227 path => '',
228 method => 'GET',
229 description => "Directory index.",
230 permissions => { user => 'all' },
231 parameters => {
232 additionalProperties => 0,
233 properties => {
234 node => get_standard_option('pve-node'),
235 },
236 },
237 returns => {
238 type => 'array',
239 items => {
240 type => "object",
241 properties => {},
242 },
243 links => [ { rel => 'child', href => "{name}" } ],
244 },
245 code => sub {
246 my ($param) = @_;
247
248 my $result = [
249 { name => 'init' },
250 { name => 'createmon' },
251 { name => 'destroymon' },
252 { name => 'createosd' },
253 { name => 'destroyosd' },
254 { name => 'stop' },
255 { name => 'start' },
256 { name => 'status' },
257 ];
258
259 return $result;
260 }});
261
262__PACKAGE__->register_method ({
263 name => 'init',
264 path => 'init',
265 method => 'POST',
266 description => "Create initial ceph configuration.",
267 proxyto => 'node',
268 protected => 1,
269 parameters => {
270 additionalProperties => 0,
271 properties => {
272 node => get_standard_option('pve-node'),
273 size => {
274 description => 'Number of replicas per object',
275 type => 'integer',
276 default => 2,
277 optional => 1,
278 minimum => 1,
279 maximum => 3,
280 },
281 pg_bits => {
282 description => "Placement group bits, used to specify the default number of placement groups (Note: 'osd pool default pg num' does not work for deafult pools)",
283 type => 'integer',
284 default => 9,
285 optional => 1,
286 minimum => 6,
287 maximum => 14,
288 },
289 },
290 },
291 returns => { type => 'null' },
292 code => sub {
293 my ($param) = @_;
294
295 &$check_ceph_installed();
296
297 -f $pve_ceph_cfgpath &&
298 die "configuration file '$pve_ceph_cfgpath' already exists.\n";
299
300 my $pg_bits = $param->{pg_bits} || 9;
301 my $size = $param->{size} || 2;
302
303 my $global = {
304 'auth supported' => 'cephx',
305 'auth cluster required' => 'cephx',
306 'auth service required' => 'cephx',
307 'auth client required' => 'cephx',
308 'filestore xattr use omap' => 'true',
309 'osd journal size' => '1024',
310 'osd pool default size' => $size,
311 'osd pool default min size' => 1,
312 'osd pg bits' => $pg_bits,
313 'osd pgp bits' => $pg_bits,
314 };
315
316 # this does not work for default pools
317 #'osd pool default pg num' => $pg_num,
318 #'osd pool default pgp num' => $pg_num,
319
320 &$write_ceph_config({global => $global});
321
322 &$setup_pve_symlinks();
323
324 return undef;
325 }});
326
327__PACKAGE__->register_method ({
328 name => 'createmon',
329 path => 'createmon',
330 method => 'POST',
331 description => "Create Ceph Monitor",
332 proxyto => 'node',
333 protected => 1,
334 parameters => {
335 additionalProperties => 0,
336 properties => {
337 node => get_standard_option('pve-node'),
338 },
339 },
340 returns => { type => 'null' },
341 code => sub {
342 my ($param) = @_;
343
344 &$check_ceph_inited();
345
346 &$setup_pve_symlinks();
347
348 if (! -f $pve_ckeyring_path) {
349 run_command("ceph-authtool $pve_ckeyring_path --create-keyring " .
350 "--gen-key -n client.admin");
351 }
352
353 if (! -f $pve_mon_key_path) {
354 run_command("cp $pve_ckeyring_path $pve_mon_key_path.tmp");
355 run_command("ceph-authtool $pve_mon_key_path.tmp -n client.admin --set-uid=0 " .
356 "--cap mds 'allow *' " .
357 "--cap osd 'allow *' " .
358 "--cap mon 'allow *'");
359 run_command("ceph-authtool $pve_mon_key_path.tmp --gen-key -n mon. --cap mon 'allow *'");
360 run_command("mv $pve_mon_key_path.tmp $pve_mon_key_path");
361 }
362
363
364 my $cfg = &$parse_ceph_config($pve_ceph_cfgpath);
365
366 my $moncount = 0;
367
368 my $monaddrhash = {};
369
370 foreach my $section (keys %$cfg) {
371 next if $section eq 'global';
372 my $d = $cfg->{$section};
373 if ($section =~ m/^mon\./) {
374 $moncount++;
375 if ($d->{'mon addr'}) {
376 $monaddrhash->{$d->{'mon addr'}} = $section;
377 }
378 }
379 }
380
381 my $monid;
382 for (my $i = 0; $i < 7; $i++) {
383 if (!$cfg->{"mon.$i"}) {
384 $monid = $i;
385 last;
386 }
387 }
388 die "unable to find usable monitor id\n" if !defined($monid);
389
390 my $monsection = "mon.$monid";
391 my $monaddr = PVE::Cluster::remote_node_ip($param->{node}) . ":6789";
392 my $monname = $param->{node};
393
394 die "monitor '$monsection' already exists\n" if $cfg->{$monsection};
395 die "monitor address '$monaddr' already in use by '$monaddrhash->{$monaddr}'\n"
396 if $monaddrhash->{$monaddr};
397
398
399 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
400 -d $mondir && die "monitor filesystem '$mondir' already exist\n";
401
402 my $monmap = "/tmp/monmap";
403
404 eval {
405 mkdir $mondir;
406
407 if ($moncount > 0) {
408 my $monstat = ceph_mon_status(); # online test
409 &$run_ceph_cmd(['mon', 'getmap', '-o', $monmap]);
410 } else {
411 run_command("monmaptool --create --clobber --add $monid $monaddr --print $monmap");
412 }
413
414 run_command("ceph-mon --mkfs -i $monid --monmap $monmap --keyring $pve_mon_key_path");
415 };
416 my $err = $@;
417 unlink $monmap;
418 if ($err) {
419 File::Path::remove_tree($mondir);
420 die $err;
421 }
422
423 $cfg->{$monsection} = {
424 'host' => $monname,
425 'mon addr' => $monaddr,
426 };
427
428 &$write_ceph_config($cfg);
429
430 &$ceph_service_cmd('start', $monsection);
431
432 return undef;
433
434 }});
435
436__PACKAGE__->register_method ({
437 name => 'destroymon',
438 path => 'destroymon',
439 method => 'POST',
440 description => "Destroy Ceph monitor.",
441 proxyto => 'node',
442 protected => 1,
443 parameters => {
444 additionalProperties => 0,
445 properties => {
446 node => get_standard_option('pve-node'),
447 monid => {
448 description => 'Monitor ID',
449 type => 'integer',
450 },
451 },
452 },
453 returns => { type => 'null' },
454 code => sub {
455 my ($param) = @_;
456
457 &$check_ceph_inited();
458
459 my $cfg = &$parse_ceph_config($pve_ceph_cfgpath);
460
461 my $monid = $param->{monid};
462 my $monsection = "mon.$monid";
463
464 my $monstat = ceph_mon_status();
465 my $monlist = $monstat->{monmap}->{mons};
466
467 die "no such monitor id '$monid'\n"
468 if !defined($cfg->{$monsection});
469
470
471 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
472 -d $mondir || die "monitor filesystem '$mondir' does not exist on this node\n";
473
474 die "can't remove last monitor\n" if scalar(@$monlist) <= 1;
475
476 &$run_ceph_cmd(['mon', 'remove', $monid]);
477
478 eval { &$ceph_service_cmd('stop', $monsection); };
479 warn $@ if $@;
480
481 delete $cfg->{$monsection};
482 &$write_ceph_config($cfg);
483 File::Path::remove_tree($mondir);
484
485 return undef;
486 }});
487
488__PACKAGE__->register_method ({
489 name => 'stop',
490 path => 'stop',
491 method => 'POST',
492 description => "Stop ceph services.",
493 proxyto => 'node',
494 protected => 1,
495 parameters => {
496 additionalProperties => 0,
497 properties => {
498 node => get_standard_option('pve-node'),
499 },
500 },
501 returns => { type => 'null' },
502 code => sub {
503 my ($param) = @_;
504
505 &$check_ceph_inited();
506
507 my $cfg = &$parse_ceph_config($pve_ceph_cfgpath);
508 scalar(keys %$cfg) || die "no configuration\n";
509
510 &$ceph_service_cmd('stop');
511
512 return undef;
513 }});
514
515__PACKAGE__->register_method ({
516 name => 'start',
517 path => 'start',
518 method => 'POST',
519 description => "Start ceph services.",
520 proxyto => 'node',
521 protected => 1,
522 parameters => {
523 additionalProperties => 0,
524 properties => {
525 node => get_standard_option('pve-node'),
526 },
527 },
528 returns => { type => 'null' },
529 code => sub {
530 my ($param) = @_;
531
532 &$check_ceph_inited();
533
534 my $cfg = &$parse_ceph_config($pve_ceph_cfgpath);
535 scalar(keys %$cfg) || die "no configuration\n";
536
537 &$ceph_service_cmd('start');
538
539 return undef;
540 }});
541
542__PACKAGE__->register_method ({
543 name => 'status',
544 path => 'status',
545 method => 'GET',
546 description => "Get ceph status.",
547 proxyto => 'node',
548 protected => 1,
549 parameters => {
550 additionalProperties => 0,
551 properties => {
552 node => get_standard_option('pve-node'),
553 },
554 },
555 returns => { type => 'object' },
556 code => sub {
557 my ($param) = @_;
558
559 my $res = { status => 'unknown' };
560
561 eval {
562 if (!&$check_ceph_installed(1)) {
563 $res->{status} = 'notinstalled';
564 }
565 if (! -f $ceph_cfgpath) {
566 $res->{status} = 'notconfigured';
567 return;
568 } else {
569 $res->{status} = 'configured';
570 }
571
572 my $cfg = &$parse_ceph_config($pve_ceph_cfgpath);
573 $res->{config} = $cfg;
574
575 eval {
576 my $monstat = ceph_mon_status(1);
577 $res->{monstat} = $monstat;
578 };
579 warn $@ if $@;
580
581 eval {
582 my $osdstat = &$ceph_osd_status(1);
583 $res->{osdstat} = $osdstat;
584 };
585 warn $@ if $@;
586
587 };
588 warn $@ if $@;
589
590 return $res;
591 }});
592
593__PACKAGE__->register_method ({
594 name => 'createosd',
595 path => 'createosd',
596 method => 'POST',
597 description => "Create OSD",
598 proxyto => 'node',
599 protected => 1,
600 parameters => {
601 additionalProperties => 0,
602 properties => {
603 node => get_standard_option('pve-node'),
604 dev => {
605 description => "Block device name.",
606 type => 'string',
607 }
608 },
609 },
610 returns => { type => 'null' },
611 code => sub {
612 my ($param) = @_;
613
614 &$check_ceph_inited();
615
616 die "not fully configured - missing '$pve_ckeyring_path'\n"
617 if ! -f $pve_ckeyring_path;
618
619 &$setup_pve_symlinks();
620
621 print "create OSD on $param->{dev}\n";
622
623 -b $param->{dev} || die "no such block device '$param->{dev}'\n";
624
625 my $monstat = ceph_mon_status(1);
626 die "unable to get fsid\n" if !$monstat->{monmap} || !$monstat->{monmap}->{fsid};
627 my $fsid = $monstat->{monmap}->{fsid};
628
629 if (! -f $ceph_bootstrap_osd_keyring) {
630 &$run_ceph_cmd(['auth', 'get', 'client.bootstrap-osd', '-o', $ceph_bootstrap_osd_keyring]);
631 };
632
633 run_command(['ceph-disk', 'prepare', '--zap-disk', '--fs-type', 'xfs',
634 '--cluster', $ccname, '--cluster-uuid', $fsid,
635 '--', $param->{dev}]);
636
637 return undef;
638 }});
639
640__PACKAGE__->register_method ({
641 name => 'destroyosd',
642 path => 'destroyosd',
643 method => 'POST',
644 description => "Destroy OSD",
645 proxyto => 'node',
646 protected => 1,
647 parameters => {
648 additionalProperties => 0,
649 properties => {
650 node => get_standard_option('pve-node'),
651 osdid => {
652 description => 'OSD ID',
653 type => 'integer',
654 },
655 },
656 },
657 returns => { type => 'null' },
658 code => sub {
659 my ($param) = @_;
660
661 &$check_ceph_inited();
662
663 my $osdid = $param->{osdid};
664
665 print "destroy OSD $param->{osdid}\n";
666
667 # fixme: not sure what we should do here
668
669 my $stat = &$ceph_osd_status();
670
671 my $osdlist = $stat->{osds} || [];
672
673 my $osdstat;
674 foreach my $d (@$osdlist) {
675 if ($d->{osd} == $osdid) {
676 $osdstat = $d;
677 last;
678 }
679 }
680 die "no such OSD '$osdid'\n" if !$osdstat;
681
682 die "osd is in use (in == 1)\n" if $osdstat->{in};
683 #&$run_ceph_cmd(['osd', 'out', $osdid]);
684
685 die "osd is still runnung (up == 1)\n" if $osdstat->{up};
686
687 my $osdsection = "osd.$osdid";
688
689 eval { &$ceph_service_cmd('stop', $osdsection); };
690 warn $@ if $@;
691
692 print "Remove $osdsection from the CRUSH map\n";
693 &$run_ceph_cmd(['osd', 'crush', 'remove', $osdid]);
694
695 print "Remove the $osdsection authentication key.\n";
696 &$run_ceph_cmd(['auth', 'del', $osdsection]);
697
698 print "Remove OSD $osdsection\n";
699 &$run_ceph_cmd(['osd', 'rm', $osdid]);
700
701 return undef;
702 }});