]> git.proxmox.com Git - pve-storage.git/blame - src/PVE/CephConfig.pm
cephconfig: align our parser with Ceph's parser
[pve-storage.git] / src / PVE / CephConfig.pm
CommitLineData
4050fcc1 1package PVE::CephConfig;
9b7ba1db
AA
2
3use strict;
4use warnings;
5use Net::IP;
6use PVE::Tools qw(run_command);
71be0113 7use PVE::Cluster qw(cfs_register_file);
9b7ba1db 8
71be0113
DC
9cfs_register_file('ceph.conf',
10 \&parse_ceph_config,
11 \&write_ceph_config);
e34ce144 12
f2c494b1
MC
13# For more information on how the Ceph parser works and how its grammar is
14# defined, see:
15# https://git.proxmox.com/?p=ceph.git;a=blob;f=ceph/src/common/ConfUtils.cc;h=2f78fd02bf9e27467275752e6f3bca0c5e3946ce;hb=e9fe820e7fffd1b7cde143a9f77653b73fcec748#l144
71be0113
DC
16sub parse_ceph_config {
17 my ($filename, $raw) = @_;
e34ce144 18
71be0113 19 my $cfg = {};
5b5534a9 20 return $cfg if !defined($raw);
e34ce144 21
f2c494b1
MC
22 # Note: According to Ceph's config grammar, a single key-value pair in a file
23 # (and nothing else!) is a valid config file and will be parsed by `ceph-conf`.
24 # We choose not to handle this case here because it doesn't seem to be used
25 # by Ceph at all (and otherwise doesn't really make sense anyway).
26
27 # Regexes ending with '_class' consist of only an extended character class
28 # each, which allows them to be interpolated into other ext. char classes.
29
30 my $re_leading_ws = qr/^\s+/;
31 my $re_trailing_ws = qr/\s+$/;
32
33 my $re_continue_marker = qr/\\/;
34 my $re_comment_class = qr/(?[ [ ; # ] ])/;
35 my $re_not_allowed_in_section_header_class = qr/(?[ [ \] ] + $re_comment_class ])/;
36
37 # Note: The Ceph config grammar defines keys the following way:
38 #
39 # key %= raw[+(text_char - char_("=[ ")) % +blank];
40 #
41 # The ' - char_("=[ ")' expression might lure you into thinking that keys
42 # may *not* contain spaces, but they can, due to the "% +blank" at the end!
43 #
44 # See: https://www.boost.org/doc/libs/1_42_0/libs/spirit/doc/html/spirit/qi/reference/operator/list.html
45 #
46 # Allowing spaces in this class and later squeezing whitespace as well as
47 # removing any leading and trailing whitespace from keys is just so much
48 # easier in our case.
49 my $re_not_allowed_in_keys_class = qr/(?[ [ = \[ ] + $re_comment_class ])/;
50
51 my $re_not_allowed_in_single_quoted_text_class = qr/(?[ [ ' ] + $re_comment_class ])/;
52 my $re_not_allowed_in_double_quoted_text_class = qr/(?[ [ " ] + $re_comment_class ])/;
53
54 my $re_text_char = qr/\\.|(?[ ! $re_comment_class ])/;
55 my $re_section_header_char = qr/\\.|(?[ ! $re_not_allowed_in_section_header_class ])/;
56
57 my $re_key_char = qr/\\.|(?[ ! $re_not_allowed_in_keys_class ])/;
58
59 my $re_single_quoted_text_char = qr/\\.|(?[ ! $re_not_allowed_in_single_quoted_text_class ])/;
60 my $re_double_quoted_text_char = qr/\\.|(?[ ! $re_not_allowed_in_double_quoted_text_class ])/;
61
62 my $re_single_quoted_value = qr/'(($re_single_quoted_text_char)*)'/;
63 my $re_double_quoted_value = qr/"(($re_double_quoted_text_char)*)"/;
64
65 my $re_key = qr/^(($re_key_char)+)/;
66 my $re_quoted_value = qr/$re_single_quoted_value|$re_double_quoted_value/;
67 my $re_unquoted_value = qr/(($re_text_char)*)/;
68 my $re_value = qr/($re_quoted_value|$re_unquoted_value)/;
69
70 my $re_kv_separator = qr/\s*(=)\s*/;
71
72 my $re_section_start = qr/\[/;
73 my $re_section_end = qr/\]/;
74 my $re_section_header = qr/$re_section_start(($re_section_header_char)+)$re_section_end/;
e34ce144
AA
75
76 my $section;
f2c494b1
MC
77 my @lines = split(/\n/, $raw);
78
79 my $parse_section_header = sub {
80 my ($section_line) = @_;
81
82 # continued lines in section headers are allowed
83 while ($section_line =~ s/$re_continue_marker$//) {
84 $section_line .= shift(@lines);
85 }
86
87 my $remainder = $section_line;
88
89 $remainder =~ s/$re_section_header//;
90 my $parsed_header = $1;
e34ce144 91
f2c494b1
MC
92 # Un-escape comment literals
93 $parsed_header =~ s/\\($re_comment_class)/$1/g;
94
95 if (!$parsed_header) {
96 die "failed to parse section - skip: $section_line\n";
97 }
98
99 # preserve Ceph's behaviour and disallow anything after the section header
100 # that's not whitespace or a comment
101 $remainder =~ s/$re_leading_ws//;
102 $remainder =~ s/^$re_comment_class.*$//;
103
104 if ($remainder) {
105 die "unexpected remainder after section - skip: $section_line\n";
106 }
107
108 return $parsed_header;
109 };
110
111 my $parse_key = sub {
112 my ($line) = @_;
113
114 my $remainder = $line;
115
116 my $key = '';
117 while ($remainder =~ s/$re_key//) {
118 $key .= $1;
119
120 while ($key =~ s/$re_continue_marker$//) {
121 $remainder = shift(@lines);
122 }
123 }
124
125 $key =~ s/$re_trailing_ws//;
126 $key =~ s/$re_leading_ws//;
127
128 $key =~ s/\s/ /;
129 while ($key =~ s/\s\s/ /) {} # squeeze repeated whitespace
130
131 # Ceph treats *single* spaces in keys the same as underscores,
132 # but we'll just use underscores for readability
133 $key =~ s/ /_/g;
134
135 # Un-escape comment literals
136 $key =~ s/\\($re_comment_class)/$1/g;
137
138 if ($key eq '') {
139 die "failed to parse key from line - skip: $line\n";
140 }
141
142 my $had_equals = $remainder =~ s/^$re_kv_separator//;
143
144 if (!$had_equals) {
145 die "expected '=' after key - skip: $line\n";
146 }
147
148 while ($remainder =~ s/^$re_continue_marker$//) {
149 # Whitespace and continuations after equals sign can be arbitrary
150 $remainder = shift(@lines);
151 $remainder =~ s/$re_leading_ws//;
152 }
153
154 return ($key, $remainder);
155 };
156
157 my $parse_value = sub {
158 my ($line, $remainder) = @_;
159
160 my $starts_with_quote = $remainder =~ m/^['"]/;
161 $remainder =~ s/$re_value//;
162 my $value = $1 // '';
163
164 if ($value eq '') {
165 die "failed to parse value - skip: $line\n";
166 }
167
168 if ($starts_with_quote) {
169 # If it started with a quote, the parsed value MUST end with a quote
170 my $is_single_quoted = $value =~ m/$re_single_quoted_value/;
171 $value = $1 if $is_single_quoted;
172 my $is_double_quoted = !$is_single_quoted && $value =~ m/$re_double_quoted_value/;
173 $value = $1 if $is_double_quoted;
174
175 if (!($is_single_quoted || $is_double_quoted)) {
176 die "failed to parse quoted value - skip: $line\n";
177 }
178
179 # Optionally, *only* line continuations may *only* follow right after
180 while ($remainder =~ s/^$re_continue_marker$//) {
181 $remainder .= shift(@lines);
182 }
183
184 # Nothing but whitespace or a comment may follow
185 $remainder =~ s/$re_leading_ws//;
186 $remainder =~ s/^$re_comment_class.*$//;
187
188 if ($remainder) {
189 die "unexpected remainder after value - skip: $line\n";
190 }
191
192 } else {
193 while ($value =~ s/$re_continue_marker$//) {
194 my $next_line = shift(@lines);
195
196 $next_line =~ s/$re_unquoted_value//;
197 my $value_part = $1 // '';
198 $value .= $value_part;
199 }
200
201 $value =~ s/$re_trailing_ws//;
202 }
203
204 # Un-escape comment literals
205 $value =~ s/\\($re_comment_class)/$1/g;
206
207 return $value;
208 };
209
210 while (scalar(@lines)) {
211 my $line = shift(@lines);
212
213 $line =~ s/^\s*(?<!\\)$re_comment_class.*$//;
214 $line =~ s/^\s*$//;
e34ce144 215 next if !$line;
f2c494b1
MC
216 next if $line =~ m/^$re_continue_marker$/;
217
218 if ($line =~ m/$re_section_start/) {
219 $section = undef;
220
221 eval { $section = $parse_section_header->($line) };
222 if ($@) {
223 warn "$@\n";
224 }
e34ce144 225
f2c494b1
MC
226 if (defined($section)) {
227 $cfg->{$section} = {} if !exists($cfg->{$section});
228 }
229
230 next;
231 }
232
233 if (!defined($section)) {
234 warn "no section header - skip: $line\n";
e34ce144
AA
235 next;
236 }
237
f2c494b1
MC
238 my ($key, $remainder) = eval { $parse_key->($line) };
239 if ($@) {
240 warn "$@\n";
241 next;
242 }
243
244 my $value = eval { $parse_value->($line, $remainder) };
245 if ($@) {
246 warn "$@\n";
247 next;
e34ce144
AA
248 }
249
f2c494b1 250 $cfg->{$section}->{$key} = $value;
e34ce144
AA
251 }
252
253 return $cfg;
71be0113
DC
254}
255
256my $parse_ceph_file = sub {
257 my ($filename) = @_;
258
259 my $cfg = {};
260
261 return $cfg if ! -f $filename;
262
263 my $content = PVE::Tools::file_get_contents($filename);
264
265 return parse_ceph_config($filename, $content);
e34ce144
AA
266};
267
71be0113
DC
268sub write_ceph_config {
269 my ($filename, $cfg) = @_;
270
8777f4a6 271 my $written_sections = {};
71be0113
DC
272 my $out = '';
273
274 my $cond_write_sec = sub {
275 my $re = shift;
276
e8dbfc50 277 for my $section (sort keys $cfg->%*) {
71be0113 278 next if $section !~ m/^$re$/;
8777f4a6 279 next if exists($written_sections->{$section});
e8dbfc50 280
71be0113 281 $out .= "[$section]\n";
e8dbfc50 282 for my $key (sort keys $cfg->{$section}->%*) {
d93f8aea 283 $out .= "\t$key = $cfg->{$section}->{$key}\n";
71be0113
DC
284 }
285 $out .= "\n";
8777f4a6
MC
286
287 $written_sections->{$section} = 1;
71be0113
DC
288 }
289 };
290
e8dbfc50
MC
291 my @rexprs = (
292 qr/global/,
f9d9bd32 293
e8dbfc50 294 qr/client/,
f9d9bd32 295 qr/client\..*/,
e8dbfc50
MC
296
297 qr/mds/,
e8dbfc50 298 qr/mds\..*/,
f9d9bd32
MC
299
300 qr/mon/,
e8dbfc50 301 qr/mon\..*/,
f9d9bd32
MC
302
303 qr/osd/,
e8dbfc50 304 qr/osd\..*/,
f9d9bd32
MC
305
306 qr/mgr/,
e8dbfc50 307 qr/mgr\..*/,
8777f4a6
MC
308
309 qr/.*/,
e8dbfc50 310 );
71be0113 311
e8dbfc50
MC
312 for my $re (@rexprs) {
313 $cond_write_sec->($re);
314 }
71be0113 315
ad6bcc9e
MC
316 # Escape comment literals that aren't escaped already
317 $out =~ s/(?<!\\)([;#])/\\$1/gm;
318
71be0113
DC
319 return $out;
320}
321
e34ce144
AA
322my $ceph_get_key = sub {
323 my ($keyfile, $username) = @_;
324
325 my $key = $parse_ceph_file->($keyfile);
326 my $secret = $key->{"client.$username"}->{key};
327
328 return $secret;
329};
330
c8a32345
DC
331my $get_host = sub {
332 my ($hostport) = @_;
333 my ($host, $port) = PVE::Tools::parse_host_and_port($hostport);
334 if (!defined($host)) {
335 return "";
336 }
337 $port = defined($port) ? ":$port" : '';
338 $host = "[$host]" if Net::IP::ip_is_ipv6($host);
339 return "${host}${port}";
340};
341
e34ce144
AA
342sub get_monaddr_list {
343 my ($configfile) = shift;
344
e34ce144
AA
345 if (!defined($configfile)) {
346 warn "No ceph config specified\n";
347 return;
348 }
349
350 my $config = $parse_ceph_file->($configfile);
e34ce144 351
5b79dac9 352 my $monhostlist = {};
4b3088a0 353
ffc31266 354 # get all ip addresses from mon_host
5b79dac9
DC
355 my $monhosts = [ split (/[ ,;]+/, $config->{global}->{mon_host} // "") ];
356
357 foreach my $monhost (@$monhosts) {
358 $monhost =~ s/^\[?v\d\://; # remove beginning of vector
359 $monhost =~ s|/\d+\]?||; # remove end of vector
360 my $host = $get_host->($monhost);
361 if ($host ne "") {
362 $monhostlist->{$host} = 1;
363 }
364 }
365
366 # then get all addrs from mon. sections
367 for my $section ( keys %$config ) {
368 next if $section !~ m/^mon\./;
369
370 if (my $addr = $config->{$section}->{mon_addr}) {
371 $monhostlist->{$addr} = 1;
372 }
373 }
374
375 return join(',', sort keys %$monhostlist);
376}
e34ce144 377
9b7ba1db
AA
378sub hostlist {
379 my ($list_text, $separator) = @_;
380
381 my @monhostlist = PVE::Tools::split_list($list_text);
c8a32345 382 return join($separator, map { $get_host->($_) } @monhostlist);
9b7ba1db
AA
383}
384
5527c824
TL
385my $ceph_check_keyfile = sub {
386 my ($filename, $type) = @_;
387
388 return if ! -f $filename;
389
390 my $content = PVE::Tools::file_get_contents($filename);
391 eval {
392 die if !$content;
393
394 if ($type eq 'rbd') {
395 die if $content !~ /\s*\[\S+\]\s*key\s*=\s*\S+==\s*$/m;
396 } elsif ($type eq 'cephfs') {
397 die if $content !~ /\S+==\s*$/;
398 }
399 };
400 die "Not a proper $type authentication file: $filename\n" if $@;
401
402 return undef;
403};
404
9b7ba1db
AA
405sub ceph_connect_option {
406 my ($scfg, $storeid, %options) = @_;
407
408 my $cmd_option = {};
9b7ba1db
AA
409 my $keyfile = "/etc/pve/priv/ceph/${storeid}.keyring";
410 $keyfile = "/etc/pve/priv/ceph/${storeid}.secret" if ($scfg->{type} eq 'cephfs');
411 my $pveceph_managed = !defined($scfg->{monhost});
412
428872eb 413 $cmd_option->{ceph_conf} = '/etc/pve/ceph.conf' if $pveceph_managed;
9b7ba1db 414
5527c824 415 $ceph_check_keyfile->($keyfile, $scfg->{type});
3e479172 416
428872eb
TL
417 if (-e "/etc/pve/priv/ceph/${storeid}.conf") {
418 # allow custom ceph configuration for external clusters
9b7ba1db
AA
419 if ($pveceph_managed) {
420 warn "ignoring custom ceph config for storage '$storeid', 'monhost' is not set (assuming pveceph managed cluster)!\n";
421 } else {
428872eb 422 $cmd_option->{ceph_conf} = "/etc/pve/priv/ceph/${storeid}.conf";
9b7ba1db
AA
423 }
424 }
425
426 $cmd_option->{keyring} = $keyfile if (-e $keyfile);
427 $cmd_option->{auth_supported} = (defined $cmd_option->{keyring}) ? 'cephx' : 'none';
428 $cmd_option->{userid} = $scfg->{username} ? $scfg->{username} : 'admin';
429 $cmd_option->{mon_host} = hostlist($scfg->{monhost}, ',') if (defined($scfg->{monhost}));
430
431 if (%options) {
432 foreach my $k (keys %options) {
433 $cmd_option->{$k} = $options{$k};
434 }
435 }
436
437 return $cmd_option;
438
439}
440
e34ce144 441sub ceph_create_keyfile {
a4a1fe64 442 my ($type, $storeid, $secret) = @_;
e34ce144
AA
443
444 my $extension = 'keyring';
445 $extension = 'secret' if ($type eq 'cephfs');
446
447 my $ceph_admin_keyring = '/etc/pve/priv/ceph.client.admin.keyring';
448 my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
449
450 die "ceph authx keyring file for storage '$storeid' already exists!\n"
a4a1fe64 451 if -e $ceph_storage_keyring && !defined($secret);
e34ce144 452
a4a1fe64 453 if (-e $ceph_admin_keyring || defined($secret)) {
e34ce144 454 eval {
a4a1fe64
AL
455 if (defined($secret)) {
456 mkdir '/etc/pve/priv/ceph';
e7bc1f03
AL
457 chomp $secret;
458 PVE::Tools::file_set_contents($ceph_storage_keyring, "${secret}\n", 0400);
a4a1fe64 459 } elsif ($type eq 'rbd') {
e34ce144
AA
460 mkdir '/etc/pve/priv/ceph';
461 PVE::Tools::file_copy($ceph_admin_keyring, $ceph_storage_keyring);
462 } elsif ($type eq 'cephfs') {
a4a1fe64 463 my $cephfs_secret = $ceph_get_key->($ceph_admin_keyring, 'admin');
e34ce144 464 mkdir '/etc/pve/priv/ceph';
e7bc1f03
AL
465 chomp $cephfs_secret;
466 PVE::Tools::file_set_contents($ceph_storage_keyring, "${cephfs_secret}\n", 0400);
e34ce144
AA
467 }
468 };
469 if (my $err = $@) {
470 unlink $ceph_storage_keyring;
471 die "failed to copy ceph authx $extension for storage '$storeid': $err\n";
472 }
473 } else {
474 warn "$ceph_admin_keyring not found, authentication is disabled.\n";
475 }
476}
477
478sub ceph_remove_keyfile {
479 my ($type, $storeid) = @_;
480
481 my $extension = 'keyring';
482 $extension = 'secret' if ($type eq 'cephfs');
483 my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
484
485 if (-f $ceph_storage_keyring) {
486 unlink($ceph_storage_keyring) or warn "removing keyring of storage failed: $!\n";
487 }
488}
489
e54c3e33
AA
490my $ceph_version_parser = sub {
491 my $ceph_version = shift;
492 # FIXME this is the same as pve-manager PVE::Ceph::Tools get_local_version
5c04a0b3 493 if ($ceph_version =~ /^ceph.*\sv?(\d+(?:\.\d+)+(?:-pve\d+)?)\s+(?:\(([a-zA-Z0-9]+)\))?/) {
e54c3e33
AA
494 my ($version, $buildcommit) = ($1, $2);
495 my $subversions = [ split(/\.|-/, $version) ];
496
497 return ($subversions, $version, $buildcommit);
498 }
499 warn "Could not parse Ceph version: '$ceph_version'\n";
500};
501
7435dc90 502sub local_ceph_version {
e54c3e33
AA
503 my ($cache) = @_;
504
505 my $version_string = $cache;
506 if (!defined($version_string)) {
507 run_command('ceph --version', outfunc => sub {
508 $version_string = shift;
509 });
510 }
511 return undef if !defined($version_string);
512 # subversion is an array ref. with the version parts from major to minor
513 # version is the filtered version string
514 my ($subversions, $version) = $ceph_version_parser->($version_string);
515
516 return wantarray ? ($subversions, $version) : $version;
517}
518
9b7ba1db 5191;