490 lines
16 KiB
Erlang
490 lines
16 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 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,
|
|
limiter/3
|
|
]).
|
|
|
|
-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"]).
|
|
|
|
-if(?EMQX_RELEASE_EDITION == ee).
|
|
-define(ROOT_KEYS_EE, [
|
|
<<"file_transfer">>
|
|
]).
|
|
-else.
|
|
-define(ROOT_KEYS_EE, []).
|
|
-endif.
|
|
|
|
-define(ROOT_KEYS, [
|
|
<<"dashboard">>,
|
|
<<"alarm">>,
|
|
<<"sys_topics">>,
|
|
<<"sysmon">>,
|
|
<<"log">>
|
|
| ?ROOT_KEYS_EE
|
|
]).
|
|
|
|
%% erlfmt-ignore
|
|
-define(SYSMON_EXAMPLE,
|
|
<<"""
|
|
sysmon {
|
|
os {
|
|
cpu_check_interval = 60s
|
|
cpu_high_watermark = 80%
|
|
cpu_low_watermark = 60%
|
|
mem_check_interval = 60s
|
|
procmem_high_watermark = 5%
|
|
sysmem_high_watermark = 70%
|
|
}
|
|
vm {
|
|
busy_dist_port = true
|
|
busy_port = true
|
|
large_heap = 32MB
|
|
long_gc = disabled
|
|
long_schedule = 240ms
|
|
process_check_interval = 30s
|
|
process_high_watermark = 80%
|
|
process_low_watermark = 60%
|
|
}
|
|
}
|
|
""">>
|
|
).
|
|
|
|
api_spec() ->
|
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
|
|
|
namespace() -> "configuration".
|
|
|
|
paths() ->
|
|
[
|
|
"/configs",
|
|
"/configs_reset/:rootname",
|
|
"/configs/global_zone",
|
|
"/configs/limiter"
|
|
] ++
|
|
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
|
|
|
schema("/configs") ->
|
|
#{
|
|
'operationId' => configs,
|
|
get => #{
|
|
tags => ?TAGS,
|
|
description => ?DESC(get_configs),
|
|
parameters => [
|
|
{key,
|
|
hoconsc:mk(
|
|
hoconsc:enum([binary_to_atom(K) || K <- emqx_conf_cli:keys()]),
|
|
#{in => query, example => <<"sysmon">>, required => false}
|
|
)},
|
|
{node,
|
|
hoconsc:mk(
|
|
typerefl:atom(),
|
|
#{
|
|
in => query,
|
|
required => false,
|
|
description => ?DESC(node_name),
|
|
hidden => true
|
|
}
|
|
)}
|
|
],
|
|
responses => #{
|
|
200 => #{
|
|
content =>
|
|
%% use proplists( not map) to make user text/plain is default in swagger
|
|
[
|
|
{'text/plain', #{
|
|
schema => #{type => string, example => ?SYSMON_EXAMPLE}
|
|
}},
|
|
{'application/json', #{
|
|
schema => #{type => object, example => #{<<"deprecated">> => true}}
|
|
}}
|
|
]
|
|
},
|
|
400 => emqx_dashboard_swagger:error_codes(['INVALID_ACCEPT']),
|
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND']),
|
|
500 => emqx_dashboard_swagger:error_codes(['BAD_NODE'])
|
|
}
|
|
},
|
|
put => #{
|
|
tags => ?TAGS,
|
|
description => ?DESC(update_configs),
|
|
parameters => [
|
|
{mode,
|
|
hoconsc:mk(
|
|
hoconsc:enum([replace, merge]),
|
|
#{in => query, default => merge, required => false}
|
|
)}
|
|
],
|
|
'requestBody' => #{
|
|
content =>
|
|
#{
|
|
'text/plain' =>
|
|
#{schema => #{type => string, example => ?SYSMON_EXAMPLE}}
|
|
}
|
|
},
|
|
responses => #{
|
|
200 => <<"Configurations updated">>,
|
|
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
|
|
}
|
|
}
|
|
};
|
|
schema("/configs_reset/:rootname") ->
|
|
Paths = lists:map(fun({Path, _}) -> binary_to_atom(Path) end, config_list()),
|
|
#{
|
|
'operationId' => config_reset,
|
|
post => #{
|
|
tags => ?TAGS,
|
|
description => ?DESC(rest_conf_query),
|
|
%% 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 => ?DESC(get_global_zone_configs),
|
|
responses => #{200 => Schema}
|
|
},
|
|
put => #{
|
|
tags => ?TAGS,
|
|
description => ?DESC(update_global_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") ->
|
|
#{
|
|
'operationId' => limiter,
|
|
get => #{
|
|
tags => ?TAGS,
|
|
hidden => true,
|
|
description => ?DESC(get_node_level_limiter_configs),
|
|
responses => #{
|
|
200 => hoconsc:mk(hoconsc:ref(emqx_limiter_schema, limiter)),
|
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
|
}
|
|
},
|
|
put => #{
|
|
tags => ?TAGS,
|
|
hidden => true,
|
|
description => ?DESC(update_node_level_limiter_configs),
|
|
'requestBody' => hoconsc:mk(hoconsc:ref(emqx_limiter_schema, limiter)),
|
|
responses => #{
|
|
200 => hoconsc:mk(hoconsc:ref(emqx_limiter_schema, limiter)),
|
|
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED']),
|
|
403 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
|
|
}
|
|
}
|
|
};
|
|
schema(Path) ->
|
|
{RootKey, {_Root, Schema}} = find_schema(Path),
|
|
GetDesc = iolist_to_binary([
|
|
<<"Get the sub-configurations under *">>,
|
|
RootKey,
|
|
<<"*">>
|
|
]),
|
|
PutDesc = iolist_to_binary([
|
|
<<"Update the sub-configurations under *">>,
|
|
RootKey,
|
|
<<"*">>
|
|
]),
|
|
#{
|
|
'operationId' => config,
|
|
get => #{
|
|
tags => ?TAGS,
|
|
desc => GetDesc,
|
|
summary => GetDesc,
|
|
responses => #{
|
|
200 => Schema,
|
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
|
}
|
|
},
|
|
put => #{
|
|
tags => ?TAGS,
|
|
desc => PutDesc,
|
|
summary => PutDesc,
|
|
'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),
|
|
{200, get_raw_config(Path)};
|
|
config(put, #{body := NewConf}, Req) ->
|
|
Path = conf_path(Req),
|
|
case emqx_conf:update(Path, NewConf, ?OPTS) of
|
|
{ok, #{raw_config := RawConf}} ->
|
|
{200, RawConf};
|
|
{error, Reason} ->
|
|
{400, #{code => 'UPDATE_FAILED', message => ?ERR_MSG(Reason)}}
|
|
end.
|
|
|
|
global_zone_configs(get, _Params, _Req) ->
|
|
{200, get_zones()};
|
|
global_zone_configs(put, #{body := Body}, _Req) ->
|
|
PrevZones = get_zones(),
|
|
Res =
|
|
maps:fold(
|
|
fun(Path, Value, Acc) ->
|
|
PrevValue = maps:get(Path, PrevZones),
|
|
case Value =/= PrevValue of
|
|
true ->
|
|
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;
|
|
false ->
|
|
Acc#{Path => Value}
|
|
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, 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, #{query_string := QueryStr, headers := Headers}, _Req) ->
|
|
%% Should deprecated json v1 since 5.2.0
|
|
case find_suitable_accept(Headers, [<<"text/plain">>, <<"application/json">>]) of
|
|
{ok, <<"application/json">>} -> get_configs_v1(QueryStr);
|
|
{ok, <<"text/plain">>} -> get_configs_v2(QueryStr);
|
|
{error, _} = Error -> {400, #{code => 'INVALID_ACCEPT', message => ?ERR_MSG(Error)}}
|
|
end;
|
|
configs(put, #{body := Conf, query_string := #{<<"mode">> := Mode}}, _Req) ->
|
|
case emqx_conf_cli:load_config(Conf, #{mode => Mode, log => none}) of
|
|
ok -> {200};
|
|
{error, Msg} -> {400, #{<<"content-type">> => <<"text/plain">>}, Msg}
|
|
end.
|
|
|
|
find_suitable_accept(Headers, Preferences) when is_list(Preferences), length(Preferences) > 0 ->
|
|
AcceptVal = maps:get(<<"accept">>, Headers, <<"*/*">>),
|
|
%% Multiple types, weighted with the quality value syntax:
|
|
%% Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
|
|
Accepts = lists:map(
|
|
fun(S) ->
|
|
[T | _] = binary:split(string:trim(S), <<";">>),
|
|
T
|
|
end,
|
|
re:split(AcceptVal, ",")
|
|
),
|
|
case lists:member(<<"*/*">>, Accepts) of
|
|
true ->
|
|
{ok, lists:nth(1, Preferences)};
|
|
false ->
|
|
Found = lists:filter(fun(Accept) -> lists:member(Accept, Accepts) end, Preferences),
|
|
case Found of
|
|
[] -> {error, no_suitable_accept};
|
|
_ -> {ok, lists:nth(1, Found)}
|
|
end
|
|
end.
|
|
|
|
%% To return a JSON formatted configuration file, which is used to be compatible with the already
|
|
%% implemented `GET /configs` in the old versions 5.0 and 5.1.
|
|
%%
|
|
%% In e5.1.1, we support to return a hocon configuration file by `get_configs_v2/1`. It's more
|
|
%% useful for the user to read or reload the configuration file via HTTP API.
|
|
%%
|
|
%% The `get_configs_v1/1` should be deprecated since 5.2.0.
|
|
get_configs_v1(QueryStr) ->
|
|
Node = maps:get(<<"node">>, QueryStr, node()),
|
|
case
|
|
lists:member(Node, emqx:running_nodes()) andalso
|
|
emqx_management_proto_v4: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.
|
|
|
|
get_configs_v2(QueryStr) ->
|
|
Node = maps:get(<<"node">>, QueryStr, node()),
|
|
Conf =
|
|
case maps:find(<<"key">>, QueryStr) of
|
|
error ->
|
|
emqx_conf_proto_v3:get_hocon_config(Node);
|
|
{ok, Key} ->
|
|
emqx_conf_proto_v3:get_hocon_config(Node, atom_to_binary(Key))
|
|
end,
|
|
{
|
|
200,
|
|
#{<<"content-type">> => <<"text/plain">>},
|
|
iolist_to_binary(hocon_pp:do(Conf, #{}))
|
|
}.
|
|
|
|
limiter(get, _Params, _Req) ->
|
|
{200, format_limiter_config(get_raw_config(limiter))};
|
|
limiter(put, #{body := NewConf}, _Req) ->
|
|
case emqx_conf:update([limiter], NewConf, ?OPTS) of
|
|
{ok, #{raw_config := RawConf}} ->
|
|
{200, format_limiter_config(RawConf)};
|
|
{error, {permission_denied, Reason}} ->
|
|
{403, #{code => 'UPDATE_FAILED', message => Reason}};
|
|
{error, Reason} ->
|
|
{400, #{code => 'UPDATE_FAILED', message => ?ERR_MSG(Reason)}}
|
|
end.
|
|
|
|
format_limiter_config(RawConf) ->
|
|
Shorts = lists:map(fun erlang:atom_to_binary/1, emqx_limiter_schema:short_paths()),
|
|
maps:with(Shorts, RawConf).
|
|
|
|
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:with(
|
|
?ROOT_KEYS,
|
|
emqx:get_raw_config([])
|
|
),
|
|
#{obfuscate_sensitive_values => true}
|
|
).
|
|
|
|
get_raw_config(Path) ->
|
|
#{Path := Conf} =
|
|
emqx_config:fill_defaults(
|
|
#{Path => emqx:get_raw_config([Path])},
|
|
#{obfuscate_sensitive_values => true}
|
|
),
|
|
Conf.
|
|
|
|
get_zones() ->
|
|
lists:foldl(
|
|
fun(Path, Acc) ->
|
|
maps:merge(Acc, get_config_with_default(Path))
|
|
end,
|
|
#{},
|
|
global_zone_roots()
|
|
).
|
|
|
|
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:keyfind(Key, 1, Roots) | Acc] end, [], ?ROOT_KEYS).
|
|
|
|
conf_path(Req) ->
|
|
<<"/api/v5", ?PREFIX, Path/binary>> = cowboy_req:path(Req),
|
|
string:lexemes(Path, "/ ").
|
|
|
|
global_zone_roots() ->
|
|
lists:map(fun({K, _}) -> list_to_binary(K) end, global_zone_schema()).
|
|
|
|
global_zone_schema() ->
|
|
emqx_zone_schema:global_zone_with_default().
|