emqx/apps/emqx_management/src/emqx_mgmt_api_configs.erl

397 lines
13 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 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_mgmt_api_configs).
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/logger.hrl").
-behaviour(minirest_api).
-export([api_spec/0, namespace/0]).
-export([paths/0, schema/1, fields/1]).
-export([
config/3,
config_reset/3,
configs/3,
get_full_config/0,
global_zone_configs/3
]).
-export([gen_schema/1]).
-define(PREFIX, "/configs/").
-define(PREFIX_RESET, "/configs_reset/").
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
-define(TAGS, ["Configs"]).
-define(EXCLUDES,
[
<<"exhook">>,
<<"gateway">>,
<<"plugins">>,
<<"bridges">>,
<<"rule_engine">>,
<<"authorization">>,
<<"authentication">>,
<<"rpc">>,
<<"db">>,
<<"connectors">>,
<<"slow_subs">>,
<<"psk_authentication">>,
<<"topic_metrics">>,
<<"rewrite">>,
<<"auto_subscribe">>,
<<"retainer">>,
<<"statsd">>,
<<"delayed">>,
<<"event_message">>,
<<"prometheus">>,
<<"telemetry">>,
<<"sys_topics">>,
<<"limiter">>,
<<"listeners">>
] ++ global_zone_roots()
).
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
namespace() -> "configuration".
paths() ->
[
"/configs",
"/configs_reset/:rootname",
"/configs/global_zone"
%% "/configs/limiter/:limiter_type"
] ++
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
schema("/configs") ->
#{
'operationId' => configs,
get => #{
tags => ?TAGS,
description =>
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
parameters => [
{node,
hoconsc:mk(
typerefl:atom(),
#{
in => query,
required => false,
example => <<"emqx@127.0.0.1">>,
desc =>
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>
}
)}
],
responses => #{
200 => lists:map(fun({_, Schema}) -> Schema end, config_list()),
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND']),
500 => emqx_dashboard_swagger:error_codes(['BAD_NODE'])
}
}
};
schema("/configs_reset/:rootname") ->
Paths = lists:map(fun({Path, _}) -> binary_to_atom(Path) end, config_list()),
#{
'operationId' => config_reset,
post => #{
tags => ?TAGS,
description =>
<<
"Reset the config entry specified by the query string parameter `conf_path`.<br/>"
"- For a config entry that has default value, this resets it to the default value;\n"
"- For a config entry that has no default value, an error 400 will be returned"
>>,
%% We only return "200" rather than the new configs that has been changed, as
%% the schema of the changed configs is depends on the request parameter
%% `conf_path`, it cannot be defined here.
parameters => [
{rootname,
hoconsc:mk(
hoconsc:enum(Paths),
#{in => path, example => <<"sysmon">>}
)},
{conf_path,
hoconsc:mk(
typerefl:binary(),
#{
in => query,
required => false,
example => <<"os.sysmem_high_watermark">>,
desc => <<"The config path separated by '.' character">>
}
)}
],
responses => #{
200 => <<"Rest config successfully">>,
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED']),
403 => emqx_dashboard_swagger:error_codes(['REST_FAILED'])
}
}
};
schema("/configs/global_zone") ->
Schema = global_zone_schema(),
#{
'operationId' => global_zone_configs,
get => #{
tags => ?TAGS,
description => <<"Get the global zone configs">>,
responses => #{200 => Schema}
},
put => #{
tags => ?TAGS,
description => <<"Update globbal zone configs">>,
'requestBody' => Schema,
responses => #{
200 => Schema,
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED']),
403 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
}
}
};
%% schema("/configs/limiter/:limiter_type") ->
%% Schema = hoconsc:ref(emqx_limiter_schema, limiter_opts),
%% Parameters = [
%% {limiter_type,
%% hoconsc:mk(
%% hoconsc:enum(emqx_limiter_schema:types()),
%% #{
%% in => path,
%% required => true,
%% example => <<"bytes_in">>,
%% desc => <<"The limiter type">>
%% }
%% )}
%% ],
%% #{
%% 'operationId' => config,
%% get => #{
%% tags => ?TAGS,
%% description => <<"Get config of this limiter">>,
%% parameters => Parameters,
%% responses => #{
%% 200 => Schema,
%% 404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
%% }
%% },
%% put => #{
%% tags => ?TAGS,
%% description => <<"Update config of this limiter">>,
%% parameters => Parameters,
%% 'requestBody' => Schema,
%% responses => #{
%% 200 => Schema,
%% 400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
%% }
%% }
%% };
schema(Path) ->
{RootKey, {_Root, Schema}} = find_schema(Path),
#{
'operationId' => config,
get => #{
tags => ?TAGS,
description => iolist_to_binary([
<<"Get the sub-configurations under *">>,
RootKey,
<<"*">>
]),
responses => #{
200 => Schema,
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
}
},
put => #{
tags => ?TAGS,
description => iolist_to_binary([
<<"Update the sub-configurations under *">>,
RootKey,
<<"*">>
]),
'requestBody' => Schema,
responses => #{
200 => Schema,
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED']),
403 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
}
}
}.
find_schema(Path) ->
[_, _Prefix, Root | _] = string:split(Path, "/", all),
Configs = config_list(),
lists:keyfind(list_to_binary(Root), 1, Configs).
%% we load all configs from emqx_*_schema, some of them are defined as local ref
%% we need redirect to emqx_*_schema.
%% such as hoconsc:ref("node") to hoconsc:ref(emqx_*_schema, "node")
fields(Field) ->
Mod = emqx_conf:schema_module(),
apply(Mod, fields, [Field]).
%%%==============================================================================================
%% HTTP API Callbacks
config(get, _Params, Req) ->
Path = conf_path(Req),
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
{200, Conf};
config(put, #{body := Body}, Req) ->
Path = conf_path(Req),
case emqx_conf:update(Path, Body, ?OPTS) of
{ok, #{raw_config := RawConf}} ->
{200, RawConf};
{error, {permission_denied, Reason}} ->
{403, #{code => 'UPDATE_FAILED', message => Reason}};
{error, Reason} ->
{400, #{code => 'UPDATE_FAILED', message => ?ERR_MSG(Reason)}}
end.
global_zone_configs(get, _Params, _Req) ->
Paths = global_zone_roots(),
Zones = lists:foldl(
fun(Path, Acc) -> maps:merge(Acc, get_config_with_default(Path)) end,
#{},
Paths
),
{200, Zones};
global_zone_configs(put, #{body := Body}, _Req) ->
Res =
maps:fold(
fun(Path, Value, Acc) ->
case emqx_conf:update([Path], Value, ?OPTS) of
{ok, #{raw_config := RawConf}} ->
Acc#{Path => RawConf};
{error, Reason} ->
?SLOG(error, #{
msg => "update global zone failed",
reason => Reason,
path => Path,
value => Value
}),
Acc
end
end,
#{},
Body
),
case maps:size(Res) =:= maps:size(Body) of
true -> {200, Res};
false -> {400, #{code => 'UPDATE_FAILED'}}
end.
config_reset(post, _Params, Req) ->
%% reset the config specified by the query string param 'conf_path'
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
case emqx_conf:reset(Path, ?OPTS) of
{ok, _} ->
{200};
{error, {permission_denied, Reason}} ->
{403, #{code => 'REST_FAILED', message => Reason}};
{error, no_default_value} ->
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
{error, Reason} ->
{400, #{code => 'REST_FAILED', message => ?ERR_MSG(Reason)}}
end.
configs(get, Params, _Req) ->
QS = maps:get(query_string, Params, #{}),
Node = maps:get(<<"node">>, QS, node()),
case
lists:member(Node, mria_mnesia:running_nodes()) andalso
emqx_management_proto_v2:get_full_config(Node)
of
false ->
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
{404, #{code => 'NOT_FOUND', message => Message}};
{badrpc, R} ->
Message = list_to_binary(io_lib:format("Bad node ~p, reason ~p", [Node, R])),
{500, #{code => 'BAD_NODE', message => Message}};
Res ->
{200, Res}
end.
conf_path_reset(Req) ->
<<"/api/v5", ?PREFIX_RESET, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
get_full_config() ->
emqx_config:fill_defaults(
maps:without(
?EXCLUDES,
emqx:get_raw_config([])
),
#{obfuscate_sensitive_values => true}
).
get_config_with_default(Path) ->
emqx_config:fill_defaults(#{Path => emqx:get_raw_config([Path])}).
conf_path_from_querystr(Req) ->
case proplists:get_value(<<"conf_path">>, cowboy_req:parse_qs(Req)) of
undefined -> [];
Path -> string:lexemes(Path, ". ")
end.
config_list() ->
Mod = emqx_conf:schema_module(),
Roots = hocon_schema:roots(Mod),
lists:foldl(fun(Key, Acc) -> lists:keydelete(Key, 1, Acc) end, Roots, ?EXCLUDES).
conf_path(Req) ->
<<"/api/v5", ?PREFIX, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
%% TODO: generate from hocon schema
gen_schema(Conf) when is_boolean(Conf) ->
with_default_value(#{type => boolean}, Conf);
gen_schema(Conf) when is_binary(Conf); is_atom(Conf) ->
with_default_value(#{type => string}, Conf);
gen_schema(Conf) when is_number(Conf) ->
with_default_value(#{type => number}, Conf);
gen_schema(Conf) when is_list(Conf) ->
case io_lib:printable_unicode_list(Conf) of
true ->
gen_schema(unicode:characters_to_binary(Conf));
false ->
#{type => array, items => gen_schema(hd(Conf))}
end;
gen_schema(Conf) when is_map(Conf) ->
#{
type => object,
properties =>
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)
};
gen_schema(_Conf) ->
%% the conf is not of JSON supported type, it may have been converted
%% by the hocon schema
#{type => string}.
with_default_value(Type, Value) ->
Type#{example => emqx_map_lib:binary_string(Value)}.
global_zone_roots() ->
lists:map(fun({K, _}) -> K end, global_zone_schema()).
global_zone_schema() ->
Roots = hocon_schema:roots(emqx_zone_schema),
lists:map(fun({RootKey, {_Root, Schema}}) -> {RootKey, Schema} end, Roots).