From c333cc0776a3670f5aa56169f3c663b8fe803a5e Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 17 May 2023 17:09:09 +0200 Subject: [PATCH] New option install_contrib_modules This option is read during ejabberd start or config reload. It installs the listed modules which aren't yet installed, as long as allow_contrib_modules is not disabled. Edit ejabberd.yml and configure the desired ejabberd-contrib modules, add them in the install_contrib_modules option, finally start ejabberd (or reload config). --- src/ejabberd_config.erl | 20 +++++++++++- src/ejabberd_option.erl | 5 +++ src/ejabberd_options.erl | 4 +++ src/ejabberd_options_doc.erl | 9 ++++++ src/ext_mod.erl | 59 ++++++++++++++++++++++++++++-------- 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 2745f5545..ff823554e 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -502,7 +502,13 @@ read_file(File, Opts) -> end, case Ret of {ok, Y} -> - validate(Y); + InstalledModules = maybe_install_contrib_modules(Y), + ValResult = validate(Y), + case InstalledModules of + [] -> ok; + _ -> spawn(fun() -> timer:sleep(5000), ?MODULE:reload() end) + end, + ValResult; Err -> Err end. @@ -527,6 +533,18 @@ read_erlang_file(File, _) -> Err end. +-spec maybe_install_contrib_modules(term()) -> [atom()]. +maybe_install_contrib_modules(Options) -> + case {lists:keysearch(allow_contrib_modules, 1, Options), + lists:keysearch(install_contrib_modules, 1, Options)} of + {Allow, {value, {_, InstallContribModules}}} + when (Allow == false) or + (Allow == {value, {allow_contrib_modules, true}}) -> + ext_mod:install_contrib_modules(InstallContribModules, Options); + _ -> + [] + end. + -spec validate(term()) -> {ok, [{atom(), term()}]} | error_return(). validate(Y1) -> case pre_validate(Y1) of diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl index b71f088c6..81f4bab7f 100644 --- a/src/ejabberd_option.erl +++ b/src/ejabberd_option.erl @@ -52,6 +52,7 @@ -export([host_config/0]). -export([hosts/0]). -export([include_config_file/0, include_config_file/1]). +-export([install_contrib_modules/0]). -export([jwt_auth_only_rule/0, jwt_auth_only_rule/1]). -export([jwt_jid_field/0, jwt_jid_field/1]). -export([jwt_key/0, jwt_key/1]). @@ -449,6 +450,10 @@ include_config_file() -> include_config_file(Host) -> ejabberd_config:get_option({include_config_file, Host}). +-spec install_contrib_modules() -> [atom()]. +install_contrib_modules() -> + ejabberd_config:get_option({install_contrib_modules, global}). + -spec jwt_auth_only_rule() -> atom(). jwt_auth_only_rule() -> jwt_auth_only_rule(global). diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl index c2af225cf..06087921d 100644 --- a/src/ejabberd_options.erl +++ b/src/ejabberd_options.erl @@ -183,6 +183,8 @@ opt_type(hosts) -> econf:non_empty(econf:list(econf:domain(), [unique])); opt_type(include_config_file) -> econf:any(); +opt_type(install_contrib_modules) -> + econf:list(econf:atom()); opt_type(language) -> econf:lang(); opt_type(ldap_backups) -> @@ -517,6 +519,7 @@ options() -> {access_rules, []}, {acme, #{}}, {allow_contrib_modules, true}, + {install_contrib_modules, []}, {allow_multiple_connections, false}, {anonymous_protocol, sasl_anon}, {api_permissions, @@ -736,6 +739,7 @@ globals() -> fqdn, hosts, host_config, + install_contrib_modules, listen, loglevel, log_rotate_count, diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl index 0e125e950..9d80e721b 100644 --- a/src/ejabberd_options_doc.erl +++ b/src/ejabberd_options_doc.erl @@ -303,6 +303,8 @@ doc() -> #{value => "true | false", desc => ?T("Whether to allow installation of third-party modules or not. " + "See https://docs.ejabberd.im/developer/extending-ejabberd/modules/#ejabberd-contrib" + "[ejabberd-contrib] documentation section. " "The default value is 'true'.")}}, {allow_multiple_connections, #{value => "true | false", @@ -670,6 +672,13 @@ doc() -> "file 'Filename'. The options that do not match this " "criteria are not accepted. The default value is to include " "all options.")}}]}, + {install_contrib_modules, + #{value => "[Module, ...]", + desc => + ?T("Modules to install from " + "https://docs.ejabberd.im/developer/extending-ejabberd/modules/#ejabberd-contrib" + "[ejabberd-contrib] at start time. " + "The default value is an empty list of modules: '[]'.")}}, {jwt_auth_only_rule, #{value => ?T("AccessName"), desc => diff --git a/src/ext_mod.erl b/src/ext_mod.erl index 2df0a4df9..34c37af19 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -33,6 +33,7 @@ installed_command/0, installed/0, installed/1, install/1, uninstall/1, upgrade/0, upgrade/1, add_paths/0, add_sources/1, add_sources/2, del_sources/1, modules_dir/0, + install_contrib_modules/2, config_dir/0, get_commands_spec/0]). -export([modules_configs/0, module_ebin_dir/1]). -export([compile_erlang_file/2, compile_elixir_file/2]). @@ -215,10 +216,13 @@ installed_command() -> [short_spec(Item) || Item <- installed()]. install(Module) when is_atom(Module) -> - install(misc:atom_to_binary(Module)); + install(misc:atom_to_binary(Module), undefined); install(Package) when is_binary(Package) -> + install(Package, undefined). + +install(Package, Config) when is_binary(Package) -> Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package], - case {Spec, installed(Package), is_contrib_allowed()} of + case {Spec, installed(Package), is_contrib_allowed(Config)} of {_, _, false} -> {error, not_allowed}; {[], _, _} -> @@ -227,10 +231,10 @@ install(Package) when is_binary(Package) -> {error, conflict}; {[Attrs], _, _} -> Module = misc:binary_to_atom(Package), - case compile_and_install(Module, Attrs) of + case compile_and_install(Module, Attrs, Config) of ok -> code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]), - ejabberd_config:reload(), + ejabberd_config_reload(Config), copy_commit_json(Package, Attrs), case erlang:function_exported(Module, post_install, 0) of true -> Module:post_install(); @@ -242,6 +246,14 @@ install(Package) when is_binary(Package) -> end end. +ejabberd_config_reload(Config) when is_list(Config) -> + %% Don't reload config when ejabberd is starting + %% because it will be reloaded after installing + %% all the external modules from install_contrib_modules + ok; +ejabberd_config_reload(undefined) -> + ejabberd_config:reload(). + uninstall(Module) when is_atom(Module) -> uninstall(misc:atom_to_binary(Module)); uninstall(Package) when is_binary(Package) -> @@ -483,7 +495,13 @@ modules_spec(Dir, Path) -> short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) -> {Module, proplists:get_value(summary, Attrs, "")}. -is_contrib_allowed() -> +is_contrib_allowed(Config) when is_list(Config) -> + case lists:keyfind(allow_contrib_modules, 1, Config) of + false -> true; + {_, false} -> false; + {_, true} -> true + end; +is_contrib_allowed(undefined) -> ejabberd_option:allow_contrib_modules(). %% -- build functions @@ -526,7 +544,7 @@ check_sources(Module) -> _ -> {error, Result} end. -compile_and_install(Module, Spec) -> +compile_and_install(Module, Spec, Config) -> SrcDir = module_src_dir(Module), LibDir = module_lib_dir(Module), case filelib:is_dir(SrcDir) of @@ -534,7 +552,7 @@ compile_and_install(Module, Spec) -> case compile_deps(SrcDir) of ok -> case compile(SrcDir) of - ok -> install(Module, Spec, SrcDir, LibDir); + ok -> install(Module, Spec, SrcDir, LibDir, Config); Error -> Error end; Error -> @@ -543,7 +561,7 @@ compile_and_install(Module, Spec) -> false -> Path = proplists:get_value(url, Spec, ""), case add_sources(Module, Path) of - ok -> compile_and_install(Module, Spec); + ok -> compile_and_install(Module, Spec, Config); Error -> Error end end. @@ -648,7 +666,7 @@ compile_elixir_file(_, File) -> {error, {compilation_failed, File}}. -endif. -install(Module, Spec, SrcDir, LibDir) -> +install(Module, Spec, SrcDir, LibDir, Config) -> {ok, CurDir} = file:get_cwd(), file:set_cwd(SrcDir), Files1 = [{File, copy(File, filename:join(LibDir, File))} @@ -660,7 +678,7 @@ install(Module, Spec, SrcDir, LibDir) -> Errors = lists:dropwhile(fun({_, ok}) -> true; (_) -> false end, Files1++Files2++Files3), - inform_module_configuration(Module, LibDir, Files1), + inform_module_configuration(Module, LibDir, Files1, Config), Result = case Errors of [{F, {error, E}}|_] -> {error, {F, E}}; @@ -672,11 +690,11 @@ install(Module, Spec, SrcDir, LibDir) -> file:set_cwd(CurDir), Result. -inform_module_configuration(Module, LibDir, Files1) -> +inform_module_configuration(Module, LibDir, Files1, Config) -> Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true; (_) -> false end, Files1), - AlreadyConfigured = lists:keymember(Module, 1, ejabberd_config:get_option(modules)), + AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)), case {Res, AlreadyConfigured} of {[{ConfigPath, ok}], false} -> FullConfigPath = filename:join(LibDir, ConfigPath), @@ -697,6 +715,12 @@ inform_module_configuration(Module, LibDir, Files1) -> [Module]) end. +get_modules(Config) when is_list(Config) -> + {modules, Modules} = lists:keyfind(modules, 1, Config), + Modules; +get_modules(undefined) -> + ejabberd_config:get_option(modules). + %% -- minimalist rebar spec parser, only support git fetch_rebar_deps(SrcDir) -> @@ -1220,3 +1244,14 @@ list_modules_parse_uninstall(Query) -> end, installed()), ok. + +install_contrib_modules(Modules, Config) -> + lists:filter(fun(Module) -> + case install(misc:atom_to_binary(Module), Config) of + {error, conflict} -> + false; + ok -> + true + end + end, + Modules).