diff --git a/apps/emqx/src/emqx_logger_jsonfmt.erl b/apps/emqx/src/emqx_logger_jsonfmt.erl index 387c9b4c3..32fd418f9 100644 --- a/apps/emqx/src/emqx_logger_jsonfmt.erl +++ b/apps/emqx/src/emqx_logger_jsonfmt.erl @@ -31,6 +31,9 @@ -export([format/2]). +%% For CLI outputs +-export([best_effort_json/1]). + -ifdef(TEST). -include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -51,6 +54,16 @@ -define(IS_STRING(String), (is_list(String) orelse is_binary(String))). +%% @doc Format a list() or map() to JSON object. +%% This is used for CLI result prints, +%% or HTTP API result formatting. +%% The JSON object is pretty-printed. +%% NOTE: do not use this function for logging. +best_effort_json(Input) -> + Config = #{depth => unlimited, single_line => true}, + JsonReady = best_effort_json_obj(Input, Config), + jsx:encode(JsonReady, [space, {indent, 4}]). + -spec format(logger:log_event(), config()) -> iodata(). format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) -> Config = add_default_config(Config0), diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 6e082e4ca..5885a2b17 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -225,47 +225,51 @@ if_valid_qos(QoS, Fun) -> end. plugins(["list"]) -> - lists:foreach(fun print/1, emqx_plugins:list()); - -plugins(["load", Name]) -> - case emqx_plugins:load(list_to_atom(Name)) of - ok -> - emqx_ctl:print("Plugin ~ts loaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Load plugin ~ts error: ~p.~n", [Name, Reason]) - end; - -plugins(["unload", "emqx_management"])-> - emqx_ctl:print("Plugin emqx_management can not be unloaded.~n"); - -plugins(["unload", Name]) -> - case emqx_plugins:unload(list_to_atom(Name)) of - ok -> - emqx_ctl:print("Plugin ~ts unloaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Unload plugin ~ts error: ~p.~n", [Name, Reason]) - end; - -plugins(["reload", Name]) -> - try list_to_existing_atom(Name) of - PluginName -> - case emqx_mgmt:reload_plugin(node(), PluginName) of - ok -> - emqx_ctl:print("Plugin ~ts reloaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Reload plugin ~ts error: ~p.~n", [Name, Reason]) - end - catch - error:badarg -> - emqx_ctl:print("Reload plugin ~ts error: The plugin doesn't exist.~n", [Name]) - end; - + emqx_plugins_cli:list(fun emqx_ctl:print/2); +plugins(["describe", NameVsn]) -> + emqx_plugins_cli:describe(NameVsn, fun emqx_ctl:print/2); +plugins(["install", NameVsn]) -> + emqx_plugins_cli:ensure_installed(NameVsn, fun emqx_ctl:print/2); +plugins(["uninstall", NameVsn])-> + emqx_plugins_cli:ensure_uninstalled(NameVsn, fun emqx_ctl:print/2); +plugins(["start", NameVsn]) -> + emqx_plugins_cli:ensure_started(NameVsn, fun emqx_ctl:print/2); +plugins(["stop", NameVsn]) -> + emqx_plugins_cli:ensure_stopped(NameVsn, fun emqx_ctl:print/2); +plugins(["restart", NameVsn]) -> + emqx_plugins_cli:restart(NameVsn, fun emqx_ctl:print/2); +plugins(["disable", NameVsn]) -> + emqx_plugins_cli:ensure_disabled(NameVsn, fun emqx_ctl:print/2); +plugins(["enable", NameVsn]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, no_move, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "front"]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, front, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "rear"]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, rear, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "before", Other]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, {before, Other}, fun emqx_ctl:print/2); plugins(_) -> - emqx_ctl:usage([{"plugins list", "Show loaded plugins"}, - {"plugins load ", "Load plugin"}, - {"plugins unload ", "Unload plugin"}, - {"plugins reload ", "Reload plugin"} - ]). + emqx_ctl:usage( + [{"plugins [Name-Vsn]", "e.g. 'start emqx_plugin_template-5.0-rc.1'"}, + {"plugins list", "List all installed plugins"}, + {"plugins describe Name-Vsn", "Describe an installed plugins"}, + {"plugins install Name-Vsn", "Install a plugin package placed\n" + "in plugin'sinstall_dir"}, + {"plugins uninstall Name-Vsn", "Uninstall a plugin. NOTE: it deletes\n" + "all files in install_dir/Name-Vsn"}, + {"plugins start Name-Vsn", "Start a plugin"}, + {"plugins stop Name-Vsn", "Stop a plugin"}, + {"plugins restart Name-Vsn", "Stop then start a plugin"}, + {"plugins disable Name-Vsn", "Disable auto-boot"}, + {"plugins enable Name-Vsn [Position]", + "Enable auto-boot at Position in the boot list, where Position could be\n" + "'front', 'rear', or 'before Other-Vsn' to specify a relative position.\n" + "The Position parameter can be used to adjust the boot order.\n" + "If no Position is given, an already configured plugin\n" + "will stary at is old position; a newly plugin is appended to the rear\n" + "e.g. plugins disable foo-0.1.0 front\n" + " plugins enable bar-0.2.0 before foo-0.1.0"} + ]). %%-------------------------------------------------------------------- %% @doc vm command diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index f01a7f362..44ac3075d 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -24,6 +24,7 @@ , ensure_enabled/1 , ensure_enabled/2 , ensure_disabled/1 + , delete_package/1 ]). -export([ ensure_started/0 @@ -32,7 +33,7 @@ , ensure_stopped/1 , restart/1 , list/0 - , delete_package/1 + , describe/1 ]). -export([ get_config/2 @@ -54,11 +55,16 @@ -type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0" -type plugin() :: map(). %% the parse result of the JSON info file +-type position() :: no_move | front | rear | {before, name_vsn()}. %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- +%% @doc Describe a plugin. +-spec describe(name_vsn()) -> {ok, plugin()} | {error, any()}. +describe(NameVsn) -> read_plugin(NameVsn). + %% @doc Install a .tar.gz package placed in install_dir. -spec ensure_installed(name_vsn()) -> ok | {error, any()}. ensure_installed(NameVsn) -> @@ -98,7 +104,7 @@ do_ensure_installed(NameVsn) -> -spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}. ensure_uninstalled(NameVsn) -> case read_plugin(NameVsn) of - {ok, #{running_status := RunningSt}} when RunningSt =/= not_loaded -> + {ok, #{running_status := RunningSt}} when RunningSt =/= stopped -> {error, #{reason => "bad_plugin_running_status", hint => "stop_the_plugin_first" }}; @@ -113,15 +119,17 @@ ensure_uninstalled(NameVsn) -> %% @doc Ensure a plugin is enabled to the end of the plugins list. -spec ensure_enabled(name_vsn()) -> ok | {error, any()}. ensure_enabled(NameVsn) -> - ensure_enabled(NameVsn, rear). + ensure_enabled(NameVsn, no_move). +%% @doc Ensure a plugin is enabled at the given position of the plugin list. +-spec ensure_enabled(name_vsn(), position()) -> ok | {error, any()}. ensure_enabled(NameVsn, Position) -> ensure_state(NameVsn, Position, true). %% @doc Ensure a plugin is disabled. -spec ensure_disabled(name_vsn()) -> ok | {error, any()}. ensure_disabled(NameVsn) -> - ensure_state(NameVsn, rear, false). + ensure_state(NameVsn, no_move, false). ensure_state(NameVsn, Position, State) when is_binary(NameVsn) -> ensure_state(binary_to_list(NameVsn), Position, State); @@ -147,6 +155,8 @@ ensure_configured(#{name_vsn := NameVsn} = Item, Position) -> end, ok = put_configured(NewConfigured). +add_new_configured(Configured, no_move, Item) -> + Configured ++ [Item]; add_new_configured(Configured, front, Item) -> [Item | Configured]; add_new_configured(Configured, rear, Item) -> @@ -232,16 +242,33 @@ restart(NameVsn) -> -spec list() -> [plugin()]. list() -> Pattern = filename:join([install_dir(), "*", "release.json"]), - lists:filtermap( - fun(JsonFile) -> - case read_plugin({file, JsonFile}) of - {ok, Info} -> - {true, Info}; - {error, Reason} -> - ?SLOG(warning, Reason), - false - end - end, filelib:wildcard(Pattern)). + All = lists:filtermap( + fun(JsonFile) -> + case read_plugin({file, JsonFile}) of + {ok, Info} -> + {true, Info}; + {error, Reason} -> + ?SLOG(warning, Reason), + false + end + end, filelib:wildcard(Pattern)), + list(configured(), All). + +%% Make sure configured ones are ordered in front. +list([], All) -> All; +list([#{name_vsn := NameVsn} | Rest], All) -> + SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> + bin([Name, "-", Vsn]) =/= bin(NameVsn) + end, + case lists:splitwith(SplitF, All) of + {_, []} -> + ?SLOG(warning, #{msg => "configured_plugin_not_installed", + name_vsn => NameVsn + }), + list(Rest, All); + {Front, [I | Rear]} -> + [I | list(Rest, Front ++ Rear)] + end. do_ensure_started(NameVsn) -> tryit("start_plugins", @@ -295,7 +322,7 @@ plugin_status(NameVsn) -> _ -> loaded end; undefined -> - not_loaded + stopped end, Configured = lists:filtermap( fun(#{name_vsn := Nv, enable := St}) -> diff --git a/apps/emqx_plugins/src/emqx_plugins_cli.erl b/apps/emqx_plugins/src/emqx_plugins_cli.erl new file mode 100644 index 000000000..694089624 --- /dev/null +++ b/apps/emqx_plugins/src/emqx_plugins_cli.erl @@ -0,0 +1,88 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_plugins_cli). + +-export([ list/1 + , describe/2 + , ensure_installed/2 + , ensure_uninstalled/2 + , ensure_started/2 + , ensure_stopped/2 + , restart/2 + , ensure_disabled/2 + , ensure_enabled/3 + ]). + +-include_lib("emqx/include/logger.hrl"). + +-define(PRINT(EXPR, LOG_FUN), + print(NameVsn, fun()-> EXPR end(), LOG_FUN, ?FUNCTION_NAME)). + +list(LogFun) -> + LogFun("~ts~n", [to_json(emqx_plugins:list())]). + +describe(NameVsn, LogFun) -> + case emqx_plugins:describe(NameVsn) of + {ok, Plugin} -> + LogFun("~ts~n", [to_json(Plugin)]); + {error, Reason} -> + %% this should not happend unless the package is manually installed + %% corrupted packages installed from emqx_plugins:ensure_installed + %% should not leave behind corrupted files + ?SLOG(error, #{msg => "failed_to_describe_plugin", + name_vsn => NameVsn, + cause => Reason}), + %% do nothing to the CLI console + ok + end. + +ensure_installed(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_installed(NameVsn), LogFun). + +ensure_uninstalled(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_uninstalled(NameVsn), LogFun). + +ensure_started(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_started(NameVsn), LogFun). + +ensure_stopped(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_stopped(NameVsn), LogFun). + +restart(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:restart(NameVsn), LogFun). + +ensure_enabled(NameVsn, Position, LogFun) -> + ?PRINT(emqx_plugins:ensure_enabled(NameVsn, Position), LogFun). + +ensure_disabled(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_disabled(NameVsn), LogFun). + +to_json(Input) -> + emqx_logger_jsonfmt:best_effort_json(Input). + +print(NameVsn, Res, LogFun, Action) -> + Obj = #{action => Action, + name_vsn => NameVsn}, + JsonReady = + case Res of + ok -> + Obj#{result => ok}; + {error, Reason} -> + Obj#{result => not_ok, + cause => Reason} + end, + LogFun("~ts~n", [to_json(JsonReady)]).