]> granicus.if.org Git - ejabberd/commitdiff
Add packaging support for contributed modules
authorChristophe Romain <christophe.romain@process-one.net>
Wed, 11 Mar 2015 13:14:28 +0000 (14:14 +0100)
committerChristophe Romain <christophe.romain@process-one.net>
Wed, 11 Mar 2015 13:14:28 +0000 (14:14 +0100)
This is a preliminary version that is tested to work with the packaging
branch of ejabberd-modules repository

This version lacks automatic configuration include at runtime

ejabberd.yml.example
ejabberdctl.cfg.example
include/jlib.hrl
src/ejabberd_app.erl
src/ext_mod.erl [new file with mode: 0644]

index 1a6d11b8960183a1522a7a00af1ee8c4abd2bead..3d69df236910bab754d8bdf024b28800c720fd35 100644 (file)
@@ -652,6 +652,14 @@ modules:
 ##       mod_echo:
 ##         host: "mirror.localhost"
 
+##
+## Enable modules management via ejabberdctl for installation and
+## uninstallation of public/private contributed modules
+## (enabled by default)
+##
+
+allow_contrib_modules: true
+
 ### Local Variables:
 ### mode: yaml
 ### End:
index 7bccb41837ae854629177aa7e4f06e202a6ec2f3..63c91735863a1140de39f5d1c75f8895ebb90e86 100644 (file)
 #
 #EJABBERD_CONFIG_PATH=/etc/ejabberd/ejabberd.yml
 
+#.
+#' CONTRIB_MODULES_PATH: contributed ejabberd modules path
+#
+# Specify the full path to the contributed ejabberd modules. If the path is not
+# defined, ejabberd will use ~/.ejabberd-modules in home of user running ejabberd.
+#
+# Default: $HOME/.ejabberd-modules
+#
+#CONTRIB_MODULES_PATH=/opt/ejabberd-modules
+
 #.
 #'
 # vim: foldmarker=#',#. foldmethod=marker:
index e4c7ca641e8d19b3b8bc332a37a53223a185d2fd..73b83b1c2b9e24c8812e6cda70a726ba41718322 100644 (file)
 %%%----------------------------------------------------------------------
 
 -include("ns.hrl").
+-ifdef(NO_EXT_LIB).
+-include("xml.hrl").
+-else.
 -include_lib("p1_xml/include/xml.hrl").
+-endif.
 
 -define(STANZA_ERROR(Code, Type, Condition),
        #xmlel{name = <<"error">>,
index 15170daeeaf1dcf05dcf62c1035ae9e50a79b144..9bab858ae4590331873cb15978c7e25fdefcc176 100644 (file)
@@ -50,6 +50,7 @@ start(normal, _Args) ->
     ejabberd_commands:init(),
     ejabberd_admin:start(),
     gen_mod:start(),
+    ext_mod:start(),
     ejabberd_config:start(),
     set_loglevel_from_config(),
     acl:start(),
diff --git a/src/ext_mod.erl b/src/ext_mod.erl
new file mode 100644 (file)
index 0000000..9b1862f
--- /dev/null
@@ -0,0 +1,483 @@
+%%%----------------------------------------------------------------------
+%%% File    : ext_mod.erl
+%%% Author  : Christophe Romain <christophe.romain@process-one.net>
+%%% Purpose : external modules management
+%%% Created : 19 Feb 2015 by Christophe Romain <christophe.romain@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2006-2015   ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(ext_mod).
+-author("Christophe Romain <christophe.romain@process-one.net>").
+
+%% Packaging service
+-export([start/0, stop/0, update/0, check/1,
+         available_command/0, available/0, available/1,
+         installed_command/0, installed/0, installed/1,
+         install/1, uninstall/1,
+         upgrade/0, upgrade/1,
+         add_sources/2, del_sources/1]).
+
+-include("ejabberd_commands.hrl").
+
+-define(REPOS, "https://github.com/processone/ejabberd-contrib").
+
+%% -- ejabberd init and commands
+
+start() ->
+    case is_contrib_allowed() of
+        true ->
+            application:start(inets),
+            ejabberd_commands:register_commands(commands());
+        false ->
+            ok
+    end.
+
+stop() ->
+    ejabberd_commands:unregister_commands(commands()).
+
+commands() ->
+    [#ejabberd_commands{name = update_modules_specs,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = update,
+                        args = [],
+                        result = {res, integer}},
+     #ejabberd_commands{name = available_modules,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = available_command,
+                        args = [],
+                        result = {modules, {list,
+                                  {module, {tuple,
+                                   [{name, atom},
+                                    {summary, string}]}}}}},
+     #ejabberd_commands{name = installed_modules,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = installed_command,
+                        args = [],
+                        result = {modules, {list,
+                                  {module, {tuple,
+                                   [{name, atom},
+                                    {summary, string}]}}}}},
+     #ejabberd_commands{name = install_module,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = install,
+                        args = [{module, binary}],
+                        result = {res, integer}},
+     #ejabberd_commands{name = uninstall_module,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = uninstall,
+                        args = [{module, binary}],
+                        result = {res, integer}},
+     #ejabberd_commands{name = upgrade_module,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = upgrade,
+                        args = [{module, binary}],
+                        result = {res, integer}},
+     #ejabberd_commands{name = check_module,
+                        tags = [admin,modules],
+                        desc = "",
+                        longdesc = "",
+                        module = ?MODULE, function = check,
+                        args = [{module, binary}],
+                        result = {res, integer}}
+        ].
+%% -- public modules functions
+
+update() ->
+    add_sources(?REPOS),
+    lists:foreach(fun({Package, Spec}) ->
+                Path = proplists:get_value(url, Spec, ""),
+                add_sources(Package, Path)
+        end, modules_spec(sources_dir(), "*")).
+
+available() ->
+    Jungle = modules_spec(sources_dir(), "*/*"),
+    Standalone = modules_spec(sources_dir(), "*"),
+    lists:keysort(1,
+        lists:foldl(fun({Key, Val}, Acc) ->
+                lists:keystore(Key, 1, Acc, {Key, Val})
+            end, Jungle, Standalone)).
+available(Module) when is_atom(Module) ->
+    available(jlib:atom_to_binary(Module));
+available(Package) when is_binary(Package) ->
+    Available = [jlib:atom_to_binary(K) || K<-proplists:get_keys(available())],
+    lists:member(Package, Available).
+
+available_command() ->
+    [short_spec(Item) || Item <- available()].
+
+installed() ->
+    modules_spec(modules_dir(), "*").
+installed(Module) when is_atom(Module) ->
+    installed(jlib:atom_to_binary(Module));
+installed(Package) when is_binary(Package) ->
+    Installed = [jlib:atom_to_binary(K) || K<-proplists:get_keys(installed())],
+    lists:member(Package, Installed).
+
+installed_command() ->
+    [short_spec(Item) || Item <- installed()].
+
+install(Module) when is_atom(Module) ->
+    install(jlib:atom_to_binary(Module));
+install(Package) when is_binary(Package) ->
+    Spec = [S || {Mod, S} <- available(), jlib:atom_to_binary(Mod)==Package],
+    case {Spec, installed(Package), is_contrib_allowed()} of
+        {_, _, false} ->
+            {error, not_allowed};
+        {[], _, _} ->
+            {error, not_available};
+        {_, true, _} ->
+            {error, conflict};
+        {[Attrs], _, _} ->
+            Module = jlib:binary_to_atom(Package),
+            case compile_and_install(Module, Attrs) of
+                ok ->
+                    code:add_patha(module_ebin_dir(Module)),
+                    ok;
+                Error ->
+                    delete_path(module_lib_dir(Module)),
+                    Error
+            end
+    end.
+
+uninstall(Module) when is_atom(Module) ->
+    uninstall(jlib:atom_to_binary(Module));
+uninstall(Package) when is_binary(Package) ->
+    case installed(Package) of
+        true ->
+            Module = jlib:binary_to_atom(Package),
+            [catch gen_mod:stop_module(Host, Module)
+             || Host <- ejabberd_config:get_myhosts()],
+            code:purge(Module),
+            code:delete(Module),
+            code:del_path(module_ebin_dir(Module)),
+            delete_path(module_lib_dir(Module));
+        false ->
+            {error, not_installed}
+    end.
+
+upgrade() ->
+    [{Package, upgrade(Package)} || {Package, _Spec} <- installed()].
+upgrade(Module) when is_atom(Module) ->
+    upgrade(jlib:atom_to_binary(Module));
+upgrade(Package) when is_binary(Package) ->
+    uninstall(Package),
+    install(Package).
+
+add_sources(Path) when is_list(Path) ->
+    add_sources(module_name(Path), Path).
+add_sources(_, "") ->
+    {error, no_url};
+add_sources(Module, Path) when is_atom(Module), is_list(Path) ->
+    add_sources(jlib:atom_to_binary(Module), Path);
+add_sources(Package, Path) when is_binary(Package), is_list(Path) ->
+    DestDir = sources_dir(),
+    RepDir = filename:join(DestDir, module_name(Path)),
+    delete_path(RepDir),
+    case filelib:ensure_dir(RepDir) of
+        ok ->
+            case {string:left(Path, 4), string:right(Path, 2)} of
+                {"http", "ip"} -> extract(zip, geturl(Path), DestDir);
+                {"http", "gz"} -> extract(tar, geturl(Path), DestDir);
+                {"http", _} -> extract_url(Path, DestDir);
+                {"git@", _} -> extract_github_master(Path, DestDir);
+                {_, "ip"} -> extract(zip, Path, DestDir);
+                {_, "gz"} -> extract(tar, Path, DestDir);
+                _ -> {error, unsupported_source}
+            end;
+        Error ->
+            Error
+    end.
+
+del_sources(Module) when is_atom(Module) ->
+    del_sources(jlib:atom_to_binary(Module));
+del_sources(Package) when is_binary(Package) ->
+    case uninstall(Package) of
+        ok ->
+            SrcDir = module_src_dir(jlib:binary_to_atom(Package)),
+            delete_path(SrcDir);
+        Error ->
+            Error
+    end.
+
+check(Module) when is_atom(Module) ->
+    check(jlib:atom_to_binary(Module));
+check(Package) when is_binary(Package) ->
+    case {available(Package), installed(Package)} of
+        {false, _} ->
+            {error, not_available};
+        {_, false} ->
+            Status = install(Package),
+            uninstall(Package),
+            case Status of
+                ok -> check_sources(jlib:binary_to_atom(Package));
+                Error -> Error
+            end;
+        _ ->
+            check_sources(jlib:binary_to_atom(Package))
+    end.
+
+%% -- archives and variables functions
+
+geturl(Url) ->
+    geturl(Url, []).
+geturl(Url, UsrOpts) ->
+    geturl(Url, [], UsrOpts).
+geturl(Url, Hdrs, UsrOpts) ->
+    Host = case getenv("PROXY_SERVER", "", ":") of
+        [H, Port] -> [{proxy_host, H}, {proxy_port, list_to_integer(Port)}];
+        [H] -> [{proxy_host, H}, {proxy_port, 8080}];
+        _ -> []
+    end,
+    User = case getenv("PROXY_USER", "", [4]) of
+        [U, Pass] -> [{proxy_user, U}, {proxy_password, Pass}];
+        _ -> []
+    end,
+    case httpc:request(get, {Url, Hdrs}, Host++User++UsrOpts, []) of
+        {ok, {{_, 200, _}, Headers, Response}} ->
+            {ok, Headers, Response};
+        {ok, {{_, Code, _}, _Headers, Response}} ->
+            {error, {Code, Response}};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+getenv(Env) ->
+    getenv(Env, "").
+getenv(Env, Default) ->
+    case os:getenv(Env) of
+        false -> Default;
+        "" -> Default;
+        Value -> Value
+    end.
+getenv(Env, Default, Separator) ->
+    string:tokens(getenv(Env, Default), Separator).
+
+extract(zip, {ok, _, Body}, DestDir) ->
+    extract(zip, iolist_to_binary(Body), DestDir);
+extract(tar, {ok, _, Body}, DestDir) ->
+    extract(tar, {binary, iolist_to_binary(Body)}, DestDir);
+extract(_, {error, Reason}, _) ->
+    {error, Reason};
+extract(zip, Zip, DestDir) ->
+    case zip:extract(Zip, [{cwd, DestDir}]) of
+        {ok, _} -> ok;
+        Error -> Error
+    end;
+extract(tar, Tar, DestDir) ->
+    erl_tar:extract(Tar, [compressed, {cwd, DestDir}]);
+extract(_, _, _) ->
+    {error, unknown_format}.
+
+extract_url(Path, DestDir) ->
+    hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0]
+     ++[{error, unsupported_source}]).
+
+extract_github_master(Repos, DestDir) ->
+    case extract(zip, geturl(Repos ++ "/archive/master.zip"), DestDir) of
+        ok ->
+            RepDir = filename:join(DestDir, module_name(Repos)),
+            file:rename(RepDir++"-master", RepDir);
+        Error ->
+            Error
+    end.
+
+copy_file(From, To) ->
+    filelib:ensure_dir(To),
+    file:copy(From, To).
+
+delete_path(Path) ->
+    case filelib:is_dir(Path) of
+        true ->
+            [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")],
+            file:del_dir(Path);
+        false ->
+            file:delete(Path)
+    end.
+
+modules_dir() ->
+    DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"),
+    getenv("CONTRIB_MODULES_PATH", DefaultDir).
+
+sources_dir() ->
+    filename:join(modules_dir(), "sources").
+
+module_lib_dir(Package) ->
+    filename:join(modules_dir(), Package).
+
+module_ebin_dir(Package) ->
+    filename:join(module_lib_dir(Package), "ebin").
+
+module_src_dir(Package) ->
+    Rep = module_name(Package),
+    SrcDir = sources_dir(),
+    Standalone = filelib:wildcard(Rep, SrcDir),
+    Jungle = filelib:wildcard("*/"++Rep, SrcDir),
+    case Standalone++Jungle of
+        [RepDir|_] -> filename:join(SrcDir, RepDir);
+        _ -> filename:join(SrcDir, Rep)
+    end.
+
+module_name(Id) ->
+    filename:basename(filename:rootname(Id)).
+
+module(Id) ->
+    jlib:binary_to_atom(iolist_to_binary(module_name(Id))).
+
+module_spec(Spec) ->
+    [{path, filename:dirname(Spec)}
+      | case consult(Spec) of
+            {ok, Meta} -> Meta;
+            _ -> []
+        end].
+
+modules_spec(Dir, Path) ->
+    Wildcard = filename:join(Path, "*.spec"),
+    lists:sort(
+        [{module(Match), module_spec(filename:join(Dir, Match))}
+         || Match <- filelib:wildcard(Wildcard, Dir)]).
+
+short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) ->
+    {Module, proplists:get_value(summary, Attrs, "")}.
+
+is_contrib_allowed() ->
+    ejabberd_config:get_option(allow_contrib_modules,
+               fun(false) -> false;
+                  (no) -> false;
+                  (_) -> true
+            end, true).
+
+%% -- build functions
+
+check_sources(Module) ->
+    SrcDir = module_src_dir(Module),
+    SpecFile = filename:flatten([Module, ".spec"]),
+    {ok, Dir} = file:get_cwd(),
+    file:set_cwd(SrcDir),
+    HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of
+        true -> [];
+        false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}]
+    end,
+    DirCheck = lists:foldl(
+            fun({Type, Name}, Acc) ->
+                case filelib:Type(Name) of
+                    true -> Acc;
+                    false -> [{missing, Name}|Acc]
+                end
+            end, HaveSrc, [{is_file, "README.txt"},
+                           {is_file, "COPYING"},
+                           {is_file, SpecFile}]),
+    SpecCheck = case consult(SpecFile) of
+        {ok, Spec} ->
+            lists:foldl(
+                fun(Key, Acc) ->
+                    case lists:keysearch(Key, 1, Spec) of
+                        false -> [{missing_meta, Key}|Acc];
+                        {value, {Key, [_NoEmpty|_]}} -> Acc;
+                        {value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc]
+                    end
+                end, [], [author, summary, home, url]);
+        {error, enoent} ->
+            [];
+        {error, Error} ->
+            [{invalid_spec, Error}]
+    end,
+    file:set_cwd(Dir),
+    Result = DirCheck ++ SpecCheck,
+    case Result of
+        [] -> ok;
+        _ -> {error, Result}
+    end.
+
+compile_and_install(Module, Spec) ->
+    SrcDir = module_src_dir(Module),
+    LibDir = module_lib_dir(Module),
+    case filelib:is_dir(SrcDir) of
+        true ->
+            {ok, Dir} = file:get_cwd(),
+            file:set_cwd(SrcDir),
+            Result = case compile(Module, Spec, LibDir) of
+                ok -> install(Module, Spec, LibDir);
+                Error -> Error
+            end,
+            file:set_cwd(Dir),
+            Result;
+        false ->
+            Path = proplists:get_value(url, Spec, ""),
+            case add_sources(Module, Path) of
+                ok -> compile_and_install(Module, Spec);
+                Error -> Error
+            end
+    end.
+
+compile(_Module, _Spec, DestDir) ->
+    Ebin = filename:join(DestDir, "ebin"),
+    filelib:ensure_dir(filename:join(Ebin, ".")),
+    EjabBin = filename:dirname(code:which(ejabberd)),
+    EjabInc = filename:join(filename:dirname(EjabBin), "include"),
+    Options = [{outdir, Ebin}, {i, "include"}, {i, EjabInc},
+               {d, 'NO_EXT_LIB'},  %% use include instead of include_lib
+               verbose, report_errors, report_warnings],
+    Result = [case compile:file(File, Options) of
+            {ok, _} -> ok;
+            {ok, _, _} -> ok;
+            {ok, _, _, _} -> ok;
+            error -> {error, {compilation_failed, File}};
+            Error -> Error
+        end
+        || File <- filelib:wildcard("src/*.erl")],
+    case lists:dropwhile(
+            fun(ok) -> true;
+                (_) -> false
+            end, Result) of
+        [] -> ok;
+        [Error|_] -> Error
+    end.
+
+install(Module, _Spec, DestDir) ->
+    SpecFile = filename:flatten([Module, ".spec"]),
+    [copy_file(File, filename:join(DestDir, File))
+     || File <- [SpecFile | filelib:wildcard("{ebin,priv,conf,include}/**")]],
+    ok.
+
+%% -- YAML spec parser
+
+consult(File) ->
+    case p1_yaml:decode_from_file(File, [plain_as_atom]) of
+        {ok, []} -> {ok, []};
+        {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]};
+        {error, Err} -> {error, p1_yaml:format_error(Err)}
+    end.
+
+format({Key, Val}) when is_binary(Val) ->
+    {Key, binary_to_list(Val)};
+format({Key, Val}) -> % TODO: improve Yaml parsing
+    {Key, Val}.