1-module(rabbit_prelaunch_conf). 2 3-include_lib("kernel/include/file.hrl"). 4-include_lib("kernel/include/logger.hrl"). 5-include_lib("stdlib/include/zip.hrl"). 6 7-include_lib("rabbit_common/include/rabbit.hrl"). 8-include_lib("rabbit_common/include/logging.hrl"). 9 10-export([setup/1, 11 get_config_state/0, 12 generate_config_from_cuttlefish_files/3, 13 decrypt_config/1]). 14 15-ifdef(TEST). 16-export([decrypt_config/2]). 17-endif. 18 19setup(Context) -> 20 ?LOG_DEBUG( 21 "\n== Configuration ==", 22 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 23 24 %% TODO: Check if directories/files are inside Mnesia dir. 25 26 ok = set_default_config(), 27 28 AdditionalConfigFiles = find_additional_config_files(Context), 29 AdvancedConfigFile = find_actual_advanced_config_file(Context), 30 State = case find_actual_main_config_file(Context) of 31 {MainConfigFile, erlang} -> 32 Config = load_cuttlefish_config_file(Context, 33 AdditionalConfigFiles, 34 MainConfigFile), 35 Apps = [App || {App, _} <- Config], 36 decrypt_config(Apps), 37 #{config_files => AdditionalConfigFiles, 38 config_advanced_file => MainConfigFile}; 39 {MainConfigFile, cuttlefish} -> 40 ConfigFiles = [MainConfigFile | AdditionalConfigFiles], 41 Config = load_cuttlefish_config_file(Context, 42 ConfigFiles, 43 AdvancedConfigFile), 44 Apps = [App || {App, _} <- Config], 45 decrypt_config(Apps), 46 #{config_files => ConfigFiles, 47 config_advanced_file => AdvancedConfigFile}; 48 undefined when AdditionalConfigFiles =/= [] -> 49 ConfigFiles = AdditionalConfigFiles, 50 Config = load_cuttlefish_config_file(Context, 51 ConfigFiles, 52 AdvancedConfigFile), 53 Apps = [App || {App, _} <- Config], 54 decrypt_config(Apps), 55 #{config_files => ConfigFiles, 56 config_advanced_file => AdvancedConfigFile}; 57 undefined when AdvancedConfigFile =/= undefined -> 58 ?LOG_WARNING( 59 "Using RABBITMQ_ADVANCED_CONFIG_FILE: ~s", 60 [AdvancedConfigFile], 61 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 62 Config = load_cuttlefish_config_file(Context, 63 AdditionalConfigFiles, 64 AdvancedConfigFile), 65 Apps = [App || {App, _} <- Config], 66 decrypt_config(Apps), 67 #{config_files => AdditionalConfigFiles, 68 config_advanced_file => AdvancedConfigFile}; 69 undefined -> 70 #{config_files => [], 71 config_advanced_file => undefined} 72 end, 73 ok = set_credentials_obfuscation_secret(), 74 ?LOG_DEBUG( 75 "Saving config state to application env: ~p", [State], 76 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 77 store_config_state(State). 78 79store_config_state(ConfigState) -> 80 persistent_term:put({rabbitmq_prelaunch, config_state}, ConfigState). 81 82get_config_state() -> 83 persistent_term:get({rabbitmq_prelaunch, config_state}, undefined). 84 85%% ------------------------------------------------------------------- 86%% Configuration loading. 87%% ------------------------------------------------------------------- 88 89set_default_config() -> 90 ?LOG_DEBUG("Setting default config", 91 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 92 Config = [ 93 {ra, 94 [ 95 {wal_max_size_bytes, 536870912}, %% 5 * 2 ^ 20 96 {wal_max_batch_size, 4096} 97 ]}, 98 {aten, 99 [ 100 %% a greater poll interval has shown to trigger fewer false 101 %% positive leader elections in quorum queues. The cost is slightly 102 %% longer detection time when a genuine network issue occurs. 103 %% Ra still uses erlang monitors of course so whenever a connection 104 %% goes down it is still immediately detected 105 {poll_interval, 5000} 106 ]}, 107 {syslog, 108 [{app_name, "rabbitmq-server"}]}, 109 {sysmon_handler, 110 [{process_limit, 100}, 111 {port_limit, 100}, 112 {gc_ms_limit, 0}, 113 {schedule_ms_limit, 0}, 114 {heap_word_limit, 0}, 115 {busy_port, false}, 116 {busy_dist_port, true}]} 117 ], 118 apply_erlang_term_based_config(Config). 119 120find_actual_main_config_file(#{main_config_file := File}) -> 121 case filelib:is_regular(File) of 122 true -> 123 Format = case filename:extension(File) of 124 ".conf" -> cuttlefish; 125 ".config" -> erlang; 126 _ -> determine_config_format(File) 127 end, 128 {File, Format}; 129 false -> 130 OldFormatFile = File ++ ".config", 131 NewFormatFile = File ++ ".conf", 132 case filelib:is_regular(OldFormatFile) of 133 true -> 134 case filelib:is_regular(NewFormatFile) of 135 true -> 136 ?LOG_WARNING( 137 "Both old (.config) and new (.conf) format " 138 "config files exist.", 139 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 140 ?LOG_WARNING( 141 "Using the old format config file: ~s", 142 [OldFormatFile], 143 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 144 ?LOG_WARNING( 145 "Please update your config files to the new " 146 "format and remove the old file.", 147 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 148 ok; 149 false -> 150 ok 151 end, 152 {OldFormatFile, erlang}; 153 false -> 154 case filelib:is_regular(NewFormatFile) of 155 true -> {NewFormatFile, cuttlefish}; 156 false -> undefined 157 end 158 end 159 end. 160 161find_additional_config_files(#{additional_config_files := Pattern}) 162 when Pattern =/= undefined -> 163 Pattern1 = case filelib:is_dir(Pattern) of 164 true -> filename:join(Pattern, "*"); 165 false -> Pattern 166 end, 167 OnlyFiles = [File || 168 File <- filelib:wildcard(Pattern1), 169 filelib:is_regular(File)], 170 lists:sort(OnlyFiles); 171find_additional_config_files(_) -> 172 []. 173 174find_actual_advanced_config_file(#{advanced_config_file := File}) -> 175 case filelib:is_regular(File) of 176 true -> File; 177 false -> undefined 178 end. 179 180determine_config_format(File) -> 181 case filelib:file_size(File) of 182 0 -> 183 cuttlefish; 184 _ -> 185 case file:consult(File) of 186 {ok, _} -> erlang; 187 _ -> cuttlefish 188 end 189 end. 190 191load_cuttlefish_config_file(Context, 192 ConfigFiles, 193 AdvancedConfigFile) -> 194 Config = generate_config_from_cuttlefish_files( 195 Context, ConfigFiles, AdvancedConfigFile), 196 apply_erlang_term_based_config(Config), 197 Config. 198 199generate_config_from_cuttlefish_files(Context, 200 ConfigFiles, 201 AdvancedConfigFile) -> 202 %% Load schemas. 203 SchemaFiles = find_cuttlefish_schemas(Context), 204 case SchemaFiles of 205 [] -> 206 ?LOG_ERROR( 207 "No configuration schema found", [], 208 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 209 throw({error, no_configuration_schema_found}); 210 _ -> 211 ?LOG_DEBUG( 212 "Configuration schemas found:~n", [], 213 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 214 lists:foreach( 215 fun(SchemaFile) -> 216 ?LOG_DEBUG(" - ~ts", [SchemaFile], 217 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}) 218 end, 219 SchemaFiles), 220 ok 221 end, 222 Schema = cuttlefish_schema:files(SchemaFiles), 223 224 %% Load configuration. 225 ?LOG_DEBUG( 226 "Loading configuration files (Cuttlefish based):", 227 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 228 lists:foreach( 229 fun(ConfigFile) -> 230 ?LOG_DEBUG(" - ~ts", [ConfigFile], 231 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}) 232 end, ConfigFiles), 233 case cuttlefish_conf:files(ConfigFiles) of 234 {errorlist, Errors} -> 235 ?LOG_ERROR("Error parsing configuration:", 236 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 237 lists:foreach( 238 fun(Error) -> 239 ?LOG_ERROR( 240 " - ~ts", 241 [cuttlefish_error:xlate(Error)], 242 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}) 243 end, Errors), 244 ?LOG_ERROR( 245 "Are these files using the Cuttlefish format?", 246 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 247 throw({error, failed_to_parse_configuration_file}); 248 Config0 -> 249 %% Finalize configuration, based on the schema. 250 Config = case cuttlefish_generator:map(Schema, Config0) of 251 {error, Phase, {errorlist, Errors}} -> 252 %% TODO 253 ?LOG_ERROR( 254 "Error preparing configuration in phase ~ts:", 255 [Phase], 256 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 257 lists:foreach( 258 fun(Error) -> 259 ?LOG_ERROR( 260 " - ~ts", 261 [cuttlefish_error:xlate(Error)], 262 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}) 263 end, Errors), 264 throw( 265 {error, failed_to_prepare_configuration}); 266 ValidConfig -> 267 proplists:delete(vm_args, ValidConfig) 268 end, 269 270 %% Apply advanced configuration overrides, if any. 271 override_with_advanced_config(Config, AdvancedConfigFile) 272 end. 273 274find_cuttlefish_schemas(Context) -> 275 Apps = list_apps(Context), 276 ?LOG_DEBUG( 277 "Looking up configuration schemas in the following applications:", 278 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 279 find_cuttlefish_schemas(Apps, []). 280 281find_cuttlefish_schemas([App | Rest], AllSchemas) -> 282 Schemas = list_schemas_in_app(App), 283 find_cuttlefish_schemas(Rest, AllSchemas ++ Schemas); 284find_cuttlefish_schemas([], AllSchemas) -> 285 lists:sort(fun(A,B) -> A < B end, AllSchemas). 286 287list_apps(#{os_type := {win32, _}, plugins_path := PluginsPath}) -> 288 PluginsDirs = lists:usort(string:lexemes(PluginsPath, ";")), 289 list_apps1(PluginsDirs, []); 290list_apps(#{plugins_path := PluginsPath}) -> 291 PluginsDirs = lists:usort(string:lexemes(PluginsPath, ":")), 292 list_apps1(PluginsDirs, []). 293 294 295list_apps1([Dir | Rest], Apps) -> 296 case file:list_dir(Dir) of 297 {ok, Filenames} -> 298 NewApps = [list_to_atom( 299 hd( 300 string:split(filename:basename(F, ".ex"), "-"))) 301 || F <- Filenames], 302 Apps1 = lists:umerge(Apps, lists:sort(NewApps)), 303 list_apps1(Rest, Apps1); 304 {error, Reason} -> 305 ?LOG_DEBUG( 306 "Failed to list directory \"~ts\" content: ~ts", 307 [Dir, file:format_error(Reason)], 308 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 309 list_apps1(Rest, Apps) 310 end; 311list_apps1([], AppInfos) -> 312 AppInfos. 313 314list_schemas_in_app(App) -> 315 {Loaded, Unload} = case application:load(App) of 316 ok -> {true, true}; 317 {error, {already_loaded, _}} -> {true, false}; 318 {error, Reason} -> {Reason, false} 319 end, 320 List = case Loaded of 321 true -> 322 case code:priv_dir(App) of 323 {error, bad_name} -> 324 ?LOG_DEBUG( 325 " [ ] ~s (no readable priv dir)", [App], 326 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 327 []; 328 PrivDir -> 329 SchemaDir = filename:join([PrivDir, "schema"]), 330 do_list_schemas_in_app(App, SchemaDir) 331 end; 332 Reason1 -> 333 ?LOG_DEBUG( 334 " [ ] ~s (failed to load application: ~p)", 335 [App, Reason1], 336 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 337 [] 338 end, 339 case Unload of 340 true -> _ = application:unload(App), 341 ok; 342 false -> ok 343 end, 344 List. 345 346do_list_schemas_in_app(App, SchemaDir) -> 347 case erl_prim_loader:list_dir(SchemaDir) of 348 {ok, Files} -> 349 ?LOG_DEBUG(" [x] ~s", [App], 350 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 351 [filename:join(SchemaDir, File) 352 || [C | _] = File <- Files, 353 C =/= $.]; 354 error -> 355 ?LOG_DEBUG( 356 " [ ] ~s (no readable schema dir)", [App], 357 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 358 [] 359 end. 360 361override_with_advanced_config(Config, undefined) -> 362 Config; 363override_with_advanced_config(Config, AdvancedConfigFile) -> 364 ?LOG_DEBUG( 365 "Override with advanced configuration file \"~ts\"", 366 [AdvancedConfigFile], 367 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 368 case file:consult(AdvancedConfigFile) of 369 {ok, [AdvancedConfig]} -> 370 cuttlefish_advanced:overlay(Config, AdvancedConfig); 371 {ok, OtherTerms} -> 372 ?LOG_ERROR( 373 "Failed to load advanced configuration file \"~ts\", " 374 "incorrect format: ~p", 375 [AdvancedConfigFile, OtherTerms], 376 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 377 throw({error, failed_to_parse_advanced_configuration_file}); 378 {error, Reason} -> 379 ?LOG_ERROR( 380 "Failed to load advanced configuration file \"~ts\": ~ts", 381 [AdvancedConfigFile, file:format_error(Reason)], 382 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 383 throw({error, failed_to_read_advanced_configuration_file}) 384 end. 385 386apply_erlang_term_based_config([{_, []} | Rest]) -> 387 apply_erlang_term_based_config(Rest); 388apply_erlang_term_based_config([{App, Vars} | Rest]) -> 389 ?LOG_DEBUG(" Applying configuration for '~s':", [App], 390 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 391 ok = apply_app_env_vars(App, Vars), 392 apply_erlang_term_based_config(Rest); 393apply_erlang_term_based_config([]) -> 394 ok. 395 396apply_app_env_vars(App, [{Var, Value} | Rest]) -> 397 ?LOG_DEBUG(" - ~s = ~p", [Var, Value], 398 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 399 ok = application:set_env(App, Var, Value, [{persistent, true}]), 400 apply_app_env_vars(App, Rest); 401apply_app_env_vars(_, []) -> 402 ok. 403 404set_credentials_obfuscation_secret() -> 405 ?LOG_DEBUG( 406 "Refreshing credentials obfuscation configuration from env: ~p", 407 [application:get_all_env(credentials_obfuscation)], 408 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 409 ok = credentials_obfuscation:refresh_config(), 410 CookieBin = rabbit_data_coercion:to_binary(erlang:get_cookie()), 411 ?LOG_DEBUG( 412 "Setting credentials obfuscation secret to '~s'", [CookieBin], 413 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 414 ok = credentials_obfuscation:set_secret(CookieBin). 415 416%% ------------------------------------------------------------------- 417%% Config decryption. 418%% ------------------------------------------------------------------- 419 420decrypt_config(Apps) -> 421 ?LOG_DEBUG("Decoding encrypted config values (if any)", 422 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 423 ConfigEntryDecoder = application:get_env(rabbit, config_entry_decoder, []), 424 decrypt_config(Apps, ConfigEntryDecoder). 425 426decrypt_config([], _) -> 427 ok; 428decrypt_config([App | Apps], Algo) -> 429 Algo1 = decrypt_app(App, application:get_all_env(App), Algo), 430 decrypt_config(Apps, Algo1). 431 432decrypt_app(_, [], Algo) -> 433 Algo; 434decrypt_app(App, [{Key, Value} | Tail], Algo) -> 435 Algo2 = try 436 case decrypt(Value, Algo) of 437 {Value, Algo1} -> 438 Algo1; 439 {NewValue, Algo1} -> 440 ?LOG_DEBUG( 441 "Value of `~s` decrypted", [Key], 442 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 443 ok = application:set_env(App, Key, NewValue, 444 [{persistent, true}]), 445 Algo1 446 end 447 catch 448 throw:{bad_config_entry_decoder, _} = Error -> 449 throw(Error); 450 _:Msg -> 451 throw({config_decryption_error, {key, Key}, Msg}) 452 end, 453 decrypt_app(App, Tail, Algo2). 454 455decrypt({encrypted, _} = EncValue, 456 {Cipher, Hash, Iterations, PassPhrase} = Algo) -> 457 {rabbit_pbe:decrypt_term(Cipher, Hash, Iterations, PassPhrase, EncValue), 458 Algo}; 459decrypt({encrypted, _} = EncValue, 460 ConfigEntryDecoder) 461 when is_list(ConfigEntryDecoder) -> 462 Algo = config_entry_decoder_to_algo(ConfigEntryDecoder), 463 decrypt(EncValue, Algo); 464decrypt(List, Algo) when is_list(List) -> 465 decrypt_list(List, Algo, []); 466decrypt(Value, Algo) -> 467 {Value, Algo}. 468 469%% We make no distinction between strings and other lists. 470%% When we receive a string, we loop through each element 471%% and ultimately return the string unmodified, as intended. 472decrypt_list([], Algo, Acc) -> 473 {lists:reverse(Acc), Algo}; 474decrypt_list([{Key, Value} | Tail], Algo, Acc) 475 when Key =/= encrypted -> 476 {Value1, Algo1} = decrypt(Value, Algo), 477 decrypt_list(Tail, Algo1, [{Key, Value1} | Acc]); 478decrypt_list([Value | Tail], Algo, Acc) -> 479 {Value1, Algo1} = decrypt(Value, Algo), 480 decrypt_list(Tail, Algo1, [Value1 | Acc]). 481 482config_entry_decoder_to_algo(ConfigEntryDecoder) -> 483 case get_passphrase(ConfigEntryDecoder) of 484 undefined -> 485 throw({bad_config_entry_decoder, missing_passphrase}); 486 PassPhrase -> 487 { 488 proplists:get_value( 489 cipher, ConfigEntryDecoder, rabbit_pbe:default_cipher()), 490 proplists:get_value( 491 hash, ConfigEntryDecoder, rabbit_pbe:default_hash()), 492 proplists:get_value( 493 iterations, ConfigEntryDecoder, 494 rabbit_pbe:default_iterations()), 495 PassPhrase 496 } 497 end. 498 499get_passphrase(ConfigEntryDecoder) -> 500 ?LOG_DEBUG("Getting encrypted config passphrase", 501 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), 502 case proplists:get_value(passphrase, ConfigEntryDecoder) of 503 prompt -> 504 IoDevice = get_input_iodevice(), 505 ok = io:setopts(IoDevice, [{echo, false}]), 506 PP = lists:droplast(io:get_line(IoDevice, 507 "\nPlease enter the passphrase to unlock encrypted " 508 "configuration entries.\n\nPassphrase: ")), 509 ok = io:setopts(IoDevice, [{echo, true}]), 510 io:format(IoDevice, "~n", []), 511 PP; 512 {file, Filename} -> 513 {ok, File} = file:read_file(Filename), 514 [PP|_] = binary:split(File, [<<"\r\n">>, <<"\n">>]), 515 PP; 516 PP -> 517 PP 518 end. 519 520%% This function retrieves the correct IoDevice for requesting 521%% input. The problem with using the default IoDevice is that 522%% the Erlang shell prevents us from getting the input. 523%% 524%% Instead we therefore look for the io process used by the 525%% shell and if it can't be found (because the shell is not 526%% started e.g with -noshell) we use the 'user' process. 527%% 528%% This function will not work when either -oldshell or -noinput 529%% options are passed to erl. 530get_input_iodevice() -> 531 case whereis(user) of 532 undefined -> 533 user; 534 User -> 535 case group:interfaces(User) of 536 [] -> 537 user; 538 [{user_drv, Drv}] -> 539 case user_drv:interfaces(Drv) of 540 [] -> user; 541 [{current_group, IoDevice}] -> IoDevice 542 end 543 end 544 end. 545