]>
Commit | Line | Data |
---|---|---|
4050fcc1 | 1 | package PVE::CephConfig; |
9b7ba1db AA |
2 | |
3 | use strict; | |
4 | use warnings; | |
5 | use Net::IP; | |
6 | use PVE::Tools qw(run_command); | |
71be0113 | 7 | use PVE::Cluster qw(cfs_register_file); |
9b7ba1db | 8 | |
71be0113 DC |
9 | cfs_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 |
16 | sub 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 | ||
256 | my $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 |
268 | sub 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 |
322 | my $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 |
331 | my $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 |
342 | sub 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 |
378 | sub 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 |
385 | my $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 |
405 | sub 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 | 441 | sub 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 | ||
478 | sub 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 |
490 | my $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 | 502 | sub 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 | 519 | 1; |