Merge branch 'master' into hot-confs-sys-topics-limiter
This commit is contained in:
commit
ba24f0309d
|
|
@ -1,4 +1,5 @@
|
||||||
.eunit
|
.eunit
|
||||||
|
*.conf.all
|
||||||
test-data/
|
test-data/
|
||||||
deps
|
deps
|
||||||
!deps/.placeholder
|
!deps/.placeholder
|
||||||
|
|
|
||||||
5
Makefile
5
Makefile
|
|
@ -8,7 +8,7 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-10:1.13.3-24.2.1-1-a
|
||||||
export EMQX_DEFAULT_RUNNER = alpine:3.15.1
|
export EMQX_DEFAULT_RUNNER = alpine:3.15.1
|
||||||
export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh)
|
export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh)
|
||||||
export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh)
|
export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh)
|
||||||
export EMQX_DASHBOARD_VERSION ?= v0.28.0
|
export EMQX_DASHBOARD_VERSION ?= v0.29.0
|
||||||
export DOCKERFILE := deploy/docker/Dockerfile
|
export DOCKERFILE := deploy/docker/Dockerfile
|
||||||
export EMQX_REL_FORM ?= tgz
|
export EMQX_REL_FORM ?= tgz
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
|
|
@ -61,7 +61,7 @@ get-dashboard:
|
||||||
@$(SCRIPTS)/get-dashboard.sh
|
@$(SCRIPTS)/get-dashboard.sh
|
||||||
|
|
||||||
.PHONY: eunit
|
.PHONY: eunit
|
||||||
eunit: $(REBAR)
|
eunit: $(REBAR) conf-segs
|
||||||
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
|
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
|
||||||
|
|
||||||
.PHONY: proper
|
.PHONY: proper
|
||||||
|
|
@ -218,6 +218,7 @@ $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
|
||||||
.PHONY:
|
.PHONY:
|
||||||
conf-segs:
|
conf-segs:
|
||||||
@scripts/merge-config.escript
|
@scripts/merge-config.escript
|
||||||
|
@scripts/merge-i18n.escript
|
||||||
|
|
||||||
## elixir target is to create release packages using Elixir's Mix
|
## elixir target is to create release packages using Elixir's Mix
|
||||||
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
|
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,12 @@
|
||||||
{deps, [
|
{deps, [
|
||||||
{lc, {git, "https://github.com/emqx/lc.git", {tag, "0.2.1"}}},
|
{lc, {git, "https://github.com/emqx/lc.git", {tag, "0.2.1"}}},
|
||||||
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}},
|
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}},
|
||||||
{typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.8.6"}}},
|
|
||||||
{jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}},
|
{jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}},
|
||||||
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}},
|
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}},
|
||||||
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
|
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
|
||||||
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
|
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
|
||||||
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
|
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
|
||||||
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}},
|
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}},
|
||||||
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
||||||
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
|
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
|
||||||
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,6 @@ parse_packet(
|
||||||
(PacketId =/= undefined) andalso
|
(PacketId =/= undefined) andalso
|
||||||
StrictMode andalso validate_packet_id(PacketId),
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
{Properties, Payload} = parse_properties(Rest1, Ver, StrictMode),
|
{Properties, Payload} = parse_properties(Rest1, Ver, StrictMode),
|
||||||
ok = ensure_topic_name_valid(StrictMode, TopicName, Properties),
|
|
||||||
Publish = #mqtt_packet_publish{
|
Publish = #mqtt_packet_publish{
|
||||||
topic_name = TopicName,
|
topic_name = TopicName,
|
||||||
packet_id = PacketId,
|
packet_id = PacketId,
|
||||||
|
|
@ -425,7 +424,6 @@ parse_will_message(
|
||||||
{Props, Rest} = parse_properties(Bin, Ver, StrictMode),
|
{Props, Rest} = parse_properties(Bin, Ver, StrictMode),
|
||||||
{Topic, Rest1} = parse_utf8_string(Rest, StrictMode),
|
{Topic, Rest1} = parse_utf8_string(Rest, StrictMode),
|
||||||
{Payload, Rest2} = parse_binary_data(Rest1),
|
{Payload, Rest2} = parse_binary_data(Rest1),
|
||||||
ok = ensure_topic_name_valid(StrictMode, Topic, Props),
|
|
||||||
{
|
{
|
||||||
Packet#mqtt_packet_connect{
|
Packet#mqtt_packet_connect{
|
||||||
will_props = Props,
|
will_props = Props,
|
||||||
|
|
@ -623,15 +621,6 @@ parse_binary_data(Bin) when
|
||||||
->
|
->
|
||||||
?PARSE_ERR(malformed_binary_data_length).
|
?PARSE_ERR(malformed_binary_data_length).
|
||||||
|
|
||||||
ensure_topic_name_valid(false, _TopicName, _Properties) ->
|
|
||||||
ok;
|
|
||||||
ensure_topic_name_valid(true, TopicName, _Properties) when TopicName =/= <<>> ->
|
|
||||||
ok;
|
|
||||||
ensure_topic_name_valid(true, <<>>, #{'Topic-Alias' := _}) ->
|
|
||||||
ok;
|
|
||||||
ensure_topic_name_valid(true, <<>>, _) ->
|
|
||||||
error(empty_topic_name).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Serialize MQTT Packet
|
%% Serialize MQTT Packet
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,22 @@ do_list_raw() ->
|
||||||
Listeners = maps:to_list(RawWithDefault),
|
Listeners = maps:to_list(RawWithDefault),
|
||||||
lists:flatmap(fun format_raw_listeners/1, Listeners).
|
lists:flatmap(fun format_raw_listeners/1, Listeners).
|
||||||
|
|
||||||
format_raw_listeners({Type, Conf}) ->
|
format_raw_listeners({Type0, Conf}) ->
|
||||||
|
Type = binary_to_atom(Type0),
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({LName, LConf0}) when is_map(LConf0) ->
|
fun({LName, LConf0}) when is_map(LConf0) ->
|
||||||
Running = is_running(binary_to_atom(Type), listener_id(Type, LName), LConf0),
|
Bind = parse_bind(LConf0),
|
||||||
|
Running = is_running(Type, listener_id(Type, LName), LConf0#{bind => Bind}),
|
||||||
LConf1 = maps:remove(<<"authentication">>, LConf0),
|
LConf1 = maps:remove(<<"authentication">>, LConf0),
|
||||||
LConf2 = maps:put(<<"running">>, Running, LConf1),
|
LConf2 = maps:remove(<<"limiter">>, LConf1),
|
||||||
{Type, LName, LConf2}
|
LConf3 = maps:put(<<"running">>, Running, LConf2),
|
||||||
|
CurrConn =
|
||||||
|
case Running of
|
||||||
|
true -> current_conns(Type, LName, Bind);
|
||||||
|
false -> 0
|
||||||
|
end,
|
||||||
|
LConf4 = maps:put(<<"current_connections">>, CurrConn, LConf3),
|
||||||
|
{Type0, LName, LConf4}
|
||||||
end,
|
end,
|
||||||
maps:to_list(Conf)
|
maps:to_list(Conf)
|
||||||
).
|
).
|
||||||
|
|
@ -112,16 +121,7 @@ is_running(ListenerId) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
is_running(Type, ListenerId, Conf) when Type =:= tcp; Type =:= ssl ->
|
is_running(Type, ListenerId, Conf) when Type =:= tcp; Type =:= ssl ->
|
||||||
ListenOn =
|
#{bind := ListenOn} = Conf,
|
||||||
case Conf of
|
|
||||||
#{bind := Bind} ->
|
|
||||||
Bind;
|
|
||||||
#{<<"bind">> := Bind} ->
|
|
||||||
case emqx_schema:to_ip_port(binary_to_list(Bind)) of
|
|
||||||
{ok, L} -> L;
|
|
||||||
{error, _} -> binary_to_integer(Bind)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
try esockd:listener({ListenerId, ListenOn}) of
|
try esockd:listener({ListenerId, ListenOn}) of
|
||||||
Pid when is_pid(Pid) ->
|
Pid when is_pid(Pid) ->
|
||||||
true
|
true
|
||||||
|
|
@ -545,3 +545,10 @@ str(B) when is_binary(B) ->
|
||||||
binary_to_list(B);
|
binary_to_list(B);
|
||||||
str(S) when is_list(S) ->
|
str(S) when is_list(S) ->
|
||||||
S.
|
S.
|
||||||
|
|
||||||
|
parse_bind(#{<<"bind">> := Bind}) when is_integer(Bind) -> Bind;
|
||||||
|
parse_bind(#{<<"bind">> := Bind}) ->
|
||||||
|
case emqx_schema:to_ip_port(binary_to_list(Bind)) of
|
||||||
|
{ok, L} -> L;
|
||||||
|
{error, _} -> binary_to_integer(Bind)
|
||||||
|
end.
|
||||||
|
|
|
||||||
|
|
@ -157,17 +157,6 @@ t_parse_malformed_utf8_string(_) ->
|
||||||
ParseState = emqx_frame:initial_parse_state(#{strict_mode => true}),
|
ParseState = emqx_frame:initial_parse_state(#{strict_mode => true}),
|
||||||
?ASSERT_FRAME_THROW(utf8_string_invalid, emqx_frame:parse(MalformedPacket, ParseState)).
|
?ASSERT_FRAME_THROW(utf8_string_invalid, emqx_frame:parse(MalformedPacket, ParseState)).
|
||||||
|
|
||||||
t_parse_empty_topic_name(_) ->
|
|
||||||
Packet = ?PUBLISH_PACKET(?QOS_1, <<>>, 1, #{}, <<>>),
|
|
||||||
?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => false})),
|
|
||||||
?ASSERT_FRAME_THROW(empty_topic_name, parse_serialize(Packet, #{strict_mode => true})).
|
|
||||||
|
|
||||||
t_parse_empty_topic_name_with_alias(_) ->
|
|
||||||
Props = #{'Topic-Alias' => 16#AB},
|
|
||||||
Packet = ?PUBLISH_PACKET(?QOS_1, <<>>, 1, Props, <<>>),
|
|
||||||
?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => false})),
|
|
||||||
?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => true})).
|
|
||||||
|
|
||||||
t_serialize_parse_v3_connect(_) ->
|
t_serialize_parse_v3_connect(_) ->
|
||||||
Bin =
|
Bin =
|
||||||
<<16, 37, 0, 6, 77, 81, 73, 115, 100, 112, 3, 2, 0, 60, 0, 23, 109, 111, 115, 113, 112, 117,
|
<<16, 37, 0, 6, 77, 81, 73, 115, 100, 112, 3, 2, 0, 60, 0, 23, 109, 111, 115, 113, 112, 117,
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,8 @@ fields(response_users) ->
|
||||||
paginated_list_type(ref(response_user));
|
paginated_list_type(ref(response_user));
|
||||||
fields(pagination_meta) ->
|
fields(pagination_meta) ->
|
||||||
[
|
[
|
||||||
{page, non_neg_integer()},
|
{page, pos_integer()},
|
||||||
{limit, non_neg_integer()},
|
{limit, pos_integer()},
|
||||||
{count, non_neg_integer()}
|
{count, non_neg_integer()}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
|
@ -431,8 +431,10 @@ schema("/authentication/:id/users") ->
|
||||||
description => <<"List users in authenticator in global authentication chain">>,
|
description => <<"List users in authenticator in global authentication chain">>,
|
||||||
parameters => [
|
parameters => [
|
||||||
param_auth_id(),
|
param_auth_id(),
|
||||||
{page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})},
|
{page,
|
||||||
{limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})},
|
mk(pos_integer(), #{in => query, desc => <<"Page Index">>, required => false})},
|
||||||
|
{limit,
|
||||||
|
mk(pos_integer(), #{in => query, desc => <<"Page Limit">>, required => false})},
|
||||||
{like_username,
|
{like_username,
|
||||||
mk(binary(), #{
|
mk(binary(), #{
|
||||||
in => query,
|
in => query,
|
||||||
|
|
@ -481,8 +483,10 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
|
||||||
parameters => [
|
parameters => [
|
||||||
param_listener_id(),
|
param_listener_id(),
|
||||||
param_auth_id(),
|
param_auth_id(),
|
||||||
{page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})},
|
{page,
|
||||||
{limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})}
|
mk(pos_integer(), #{in => query, desc => <<"Page Index">>, required => false})},
|
||||||
|
{limit,
|
||||||
|
mk(pos_integer(), #{in => query, desc => <<"Page Limit">>, required => false})}
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => emqx_dashboard_swagger:schema_with_example(
|
200 => emqx_dashboard_swagger:schema_with_example(
|
||||||
|
|
@ -1158,8 +1162,17 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_users(ChainName, AuthenticatorID, QueryString) ->
|
list_users(ChainName, AuthenticatorID, QueryString) ->
|
||||||
Response = emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString),
|
case emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString) of
|
||||||
emqx_mgmt_util:generate_response(Response).
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{400, #{
|
||||||
|
code => <<"INVALID_PARAMETER">>,
|
||||||
|
message => list_to_binary(io_lib:format("Reason ~p", [Reason]))
|
||||||
|
}};
|
||||||
|
Result ->
|
||||||
|
{200, Result}
|
||||||
|
end.
|
||||||
|
|
||||||
update_config(Path, ConfigRequest) ->
|
update_config(Path, ConfigRequest) ->
|
||||||
emqx_conf:update(Path, ConfigRequest, #{
|
emqx_conf:update(Path, ConfigRequest, #{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_api_cache).
|
||||||
|
|
||||||
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
api_spec/0,
|
||||||
|
paths/0,
|
||||||
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
clean_cache/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(BAD_REQUEST, 'BAD_REQUEST').
|
||||||
|
|
||||||
|
api_spec() ->
|
||||||
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
|
paths() ->
|
||||||
|
[
|
||||||
|
"/authorization/cache"
|
||||||
|
].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Schema for each URI
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
schema("/authorization/cache") ->
|
||||||
|
#{
|
||||||
|
'operationId' => clean_cache,
|
||||||
|
delete =>
|
||||||
|
#{
|
||||||
|
description => <<"Clean all authorization cache in the cluster.">>,
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"No Content">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
clean_cache(delete, _) ->
|
||||||
|
case emqx_mgmt:clean_authz_cache_all() of
|
||||||
|
ok ->
|
||||||
|
{204};
|
||||||
|
{error, Reason} ->
|
||||||
|
{400, #{
|
||||||
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
|
||||||
|
|
@ -405,14 +405,23 @@ fields(meta) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
users(get, #{query_string := QueryString}) ->
|
users(get, #{query_string := QueryString}) ->
|
||||||
Response = emqx_mgmt_api:node_query(
|
case
|
||||||
node(),
|
emqx_mgmt_api:node_query(
|
||||||
QueryString,
|
node(),
|
||||||
?ACL_TABLE,
|
QueryString,
|
||||||
?ACL_USERNAME_QSCHEMA,
|
?ACL_TABLE,
|
||||||
?QUERY_USERNAME_FUN
|
?ACL_USERNAME_QSCHEMA,
|
||||||
),
|
?QUERY_USERNAME_FUN
|
||||||
emqx_mgmt_util:generate_response(Response);
|
)
|
||||||
|
of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Result ->
|
||||||
|
{200, Result}
|
||||||
|
end;
|
||||||
users(post, #{body := Body}) when is_list(Body) ->
|
users(post, #{body := Body}) when is_list(Body) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
||||||
|
|
@ -423,14 +432,23 @@ users(post, #{body := Body}) when is_list(Body) ->
|
||||||
{204}.
|
{204}.
|
||||||
|
|
||||||
clients(get, #{query_string := QueryString}) ->
|
clients(get, #{query_string := QueryString}) ->
|
||||||
Response = emqx_mgmt_api:node_query(
|
case
|
||||||
node(),
|
emqx_mgmt_api:node_query(
|
||||||
QueryString,
|
node(),
|
||||||
?ACL_TABLE,
|
QueryString,
|
||||||
?ACL_CLIENTID_QSCHEMA,
|
?ACL_TABLE,
|
||||||
?QUERY_CLIENTID_FUN
|
?ACL_CLIENTID_QSCHEMA,
|
||||||
),
|
?QUERY_CLIENTID_FUN
|
||||||
emqx_mgmt_util:generate_response(Response);
|
)
|
||||||
|
of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Result ->
|
||||||
|
{200, Result}
|
||||||
|
end;
|
||||||
clients(post, #{body := Body}) when is_list(Body) ->
|
clients(post, #{body := Body}) when is_list(Body) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
|
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_api_cache_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-import(emqx_dashboard_api_test_helpers, [request/2, uri/1]).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
|
[emqx_conf, emqx_authz, emqx_dashboard, emqx_management],
|
||||||
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
{ok, _} = emqx:update_config(
|
||||||
|
[authorization],
|
||||||
|
#{
|
||||||
|
<<"no_match">> => <<"allow">>,
|
||||||
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf, emqx_management]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
emqx_dashboard_api_test_helpers:set_default_config();
|
||||||
|
set_special_configs(emqx_authz) ->
|
||||||
|
{ok, _} = emqx:update_config([authorization, cache, enable], true),
|
||||||
|
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
||||||
|
{ok, _} = emqx:update_config([authorization, sources], []),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_clean_cahce(_) ->
|
||||||
|
{ok, C} = emqtt:start_link([{clientid, <<"emqx0">>}, {username, <<"emqx0">>}]),
|
||||||
|
{ok, _} = emqtt:connect(C),
|
||||||
|
{ok, _, _} = emqtt:subscribe(C, <<"a/b/c">>, 0),
|
||||||
|
ok = emqtt:publish(C, <<"a/b/c">>, <<"{\"x\":1,\"y\":1}">>, 0),
|
||||||
|
|
||||||
|
{ok, 200, Result3} = request(get, uri(["clients", "emqx0", "authorization", "cache"])),
|
||||||
|
?assertEqual(2, length(jsx:decode(Result3))),
|
||||||
|
|
||||||
|
request(delete, uri(["authorization", "cache"])),
|
||||||
|
|
||||||
|
{ok, 200, Result4} = request(get, uri(["clients", "emqx0", "authorization", "cache"])),
|
||||||
|
?assertEqual(0, length(jsx:decode(Result4))),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
|
stop_apps(Apps) ->
|
||||||
|
lists:foreach(fun application:stop/1, Apps).
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
emqx_auto_subscribe_api {
|
||||||
|
list_auto_subscribe_api {
|
||||||
|
desc {
|
||||||
|
en: """Get auto subscribe topic list"""
|
||||||
|
zh: """获取自动订阅主题列表"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_auto_subscribe_api {
|
||||||
|
desc {
|
||||||
|
en: """Update auto subscribe topic list"""
|
||||||
|
zh: """更新自动订阅主题列表"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_auto_subscribe_api_response409 {
|
||||||
|
desc {
|
||||||
|
en: """Auto Subscribe topics max limit"""
|
||||||
|
zh: """超出自定订阅主题列表长度限制"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
emqx_auto_subscribe_schema {
|
||||||
|
auto_subscribe {
|
||||||
|
desc {
|
||||||
|
en: """After the device logs in successfully, the subscription is automatically completed for the device through the pre-defined subscription representation. Supports the use of placeholders."""
|
||||||
|
zh: """设备登陆成功之后,通过预设的订阅表示符,为设备自动完成订阅。支持使用占位符。"""
|
||||||
|
}
|
||||||
|
lable {
|
||||||
|
en: """Auto Subscribe"""
|
||||||
|
zh: """自动订阅"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topic {
|
||||||
|
desc {
|
||||||
|
en: """Topic name, placeholders are supported. For example: client/${clientid}/username/${username}/host/${host}/port/${port}
|
||||||
|
Required field, and cannot be empty string"""
|
||||||
|
zh: """订阅标识符,支持使用占位符,例如 client/${clientid}/username/${username}/host/${host}/port/${port}
|
||||||
|
必填,且不可为空字符串"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Topic"""
|
||||||
|
zh: """订阅标识符"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qos {
|
||||||
|
desc {
|
||||||
|
en: """Default value 0. Quality of service.
|
||||||
|
At most once (0)
|
||||||
|
At least once (1)
|
||||||
|
Exactly once (2)"""
|
||||||
|
zh: """缺省值为 0,服务质量,
|
||||||
|
QoS 0:消息最多传递一次,如果当时客户端不可用,则会丢失该消息。
|
||||||
|
QoS 1:消息传递至少 1 次。
|
||||||
|
QoS 2:消息仅传送一次。"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Quality of Service"""
|
||||||
|
zh: """服务质量"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rh {
|
||||||
|
desc {
|
||||||
|
en: """Default value 0. This option is used to specify whether the server forwards the retained message to the client when establishing a subscription.
|
||||||
|
Retain Handling is equal to 0, as long as the client successfully subscribes, the server will send the retained message.
|
||||||
|
Retain Handling is equal to 1, if the client successfully subscribes and this subscription does not exist previously, the server sends the retained message. After all, sometimes the client re-initiate the subscription just to change the QoS, but it does not mean that it wants to receive the reserved messages again.
|
||||||
|
Retain Handling is equal to 2, even if the client successfully subscribes, the server does not send the retained message."""
|
||||||
|
zh: """指定订阅建立时服务端是否向客户端发送保留消息,
|
||||||
|
可选值 0:只要客户端订阅成功,服务端就发送保留消息。
|
||||||
|
可选值 1:客户端订阅成功且该订阅此前不存在,服务端才发送保留消息。毕竟有些时候客户端重新发起订阅可能只是为了改变一下 QoS,并不意味着它想再次接收保留消息。
|
||||||
|
可选值 2:即便客户订阅成功,服务端也不会发送保留消息。"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Retain Handling"""
|
||||||
|
zh: """Retain Handling"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rap {
|
||||||
|
desc {
|
||||||
|
en: """Default value 0. This option is used to specify whether the server retains the RETAIN mark when forwarding messages to the client, and this option does not affect the RETAIN mark in the retained message. Therefore, when the option Retain As Publish is set to 0, the client will directly distinguish whether this is a normal forwarded message or a retained message according to the RETAIN mark in the message, instead of judging whether this message is the first received after subscribing(the forwarded message may be sent before the retained message, which depends on the specific implementation of different brokers)."""
|
||||||
|
zh: """缺省值为 0,这一选项用来指定服务端向客户端转发消息时是否要保留其中的 RETAIN 标识,注意这一选项不会影响保留消息中的 RETAIN 标识。因此当 Retain As Publish 选项被设置为 0 时,客户端直接依靠消息中的 RETAIN 标识来区分这是一个正常的转发消息还是一个保留消息,而不是去判断消息是否是自己订阅后收到的第一个消息(转发消息甚至可能会先于保留消息被发送,视不同 Broker 的具体实现而定)。"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Retain As Publish"""
|
||||||
|
zh: """Retain As Publish"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nl {
|
||||||
|
desc {
|
||||||
|
en: """Default value 0.
|
||||||
|
MQTT v3.1.1: if you subscribe to the topic published by yourself, you will receive all messages that you published.
|
||||||
|
MQTT v5: if you set this option as 1 when subscribing, the server will not forward the message you published to you."""
|
||||||
|
zh: """缺省值为0,
|
||||||
|
MQTT v3.1.1:如果设备订阅了自己发布消息的主题,那么将收到自己发布的所有消息。
|
||||||
|
MQTT v5:如果设备在订阅时将此选项设置为 1,那么服务端将不会向设备转发自己发布的消息"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """No Local"""
|
||||||
|
zh: """No Local"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_auto_subscribe,
|
{application, emqx_auto_subscribe, [
|
||||||
[{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_auto_subscribe_app, []}},
|
{mod, {emqx_auto_subscribe_app, []}},
|
||||||
{applications,
|
{applications, [
|
||||||
[kernel,
|
kernel,
|
||||||
stdlib
|
stdlib,
|
||||||
]},
|
emqx
|
||||||
{env,[]},
|
]},
|
||||||
{modules, []},
|
{env, []},
|
||||||
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache 2.0"]},
|
{licenses, ["Apache 2.0"]},
|
||||||
{links, []}
|
{links, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,21 @@
|
||||||
|
|
||||||
-define(MAX_AUTO_SUBSCRIBE, 20).
|
-define(MAX_AUTO_SUBSCRIBE, 20).
|
||||||
|
|
||||||
-export([load/0, unload/0]). %
|
%
|
||||||
|
-export([load/0, unload/0]).
|
||||||
|
|
||||||
-export([ max_limit/0
|
-export([
|
||||||
, list/0
|
max_limit/0,
|
||||||
, update/1
|
list/0,
|
||||||
, post_config_update/5
|
update/1,
|
||||||
]).
|
post_config_update/5
|
||||||
|
]).
|
||||||
|
|
||||||
%% hook callback
|
%% hook callback
|
||||||
-export([on_client_connected/3]).
|
-export([on_client_connected/3]).
|
||||||
|
|
||||||
load() ->
|
load() ->
|
||||||
emqx_conf:add_handler([auto_subscribe, topics], ?MODULE),
|
ok = emqx_conf:add_handler([auto_subscribe, topics], ?MODULE),
|
||||||
update_hook().
|
update_hook().
|
||||||
|
|
||||||
unload() ->
|
unload() ->
|
||||||
|
|
@ -56,7 +58,8 @@ post_config_update(_KeyPath, _Req, NewTopics, _OldConf, _AppEnvs) ->
|
||||||
|
|
||||||
on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) ->
|
on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) ->
|
||||||
case erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]) of
|
case erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]) of
|
||||||
[] -> ok;
|
[] ->
|
||||||
|
ok;
|
||||||
TopicTables ->
|
TopicTables ->
|
||||||
_ = self() ! {subscribe, TopicTables},
|
_ = self() ! {subscribe, TopicTables},
|
||||||
ok
|
ok
|
||||||
|
|
@ -71,17 +74,21 @@ format(Rules) when is_list(Rules) ->
|
||||||
[format(Rule) || Rule <- Rules];
|
[format(Rule) || Rule <- Rules];
|
||||||
format(Rule = #{topic := Topic}) when is_map(Rule) ->
|
format(Rule = #{topic := Topic}) when is_map(Rule) ->
|
||||||
#{
|
#{
|
||||||
topic => Topic,
|
topic => Topic,
|
||||||
qos => maps:get(qos, Rule, 0),
|
qos => maps:get(qos, Rule, 0),
|
||||||
rh => maps:get(rh, Rule, 0),
|
rh => maps:get(rh, Rule, 0),
|
||||||
rap => maps:get(rap, Rule, 0),
|
rap => maps:get(rap, Rule, 0),
|
||||||
nl => maps:get(nl, Rule, 0)
|
nl => maps:get(nl, Rule, 0)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE ->
|
update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE ->
|
||||||
case emqx_conf:update([auto_subscribe, topics],
|
case
|
||||||
Topics,
|
emqx_conf:update(
|
||||||
#{rawconf_with_defaults => true, override_to => cluster}) of
|
[auto_subscribe, topics],
|
||||||
|
Topics,
|
||||||
|
#{rawconf_with_defaults => true, override_to => cluster}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, #{raw_config := NewTopics}} ->
|
{ok, #{raw_config := NewTopics}} ->
|
||||||
{ok, NewTopics};
|
{ok, NewTopics};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
-define(EXCEED_LIMIT, 'EXCEED_LIMIT').
|
-define(EXCEED_LIMIT, 'EXCEED_LIMIT').
|
||||||
-define(BAD_REQUEST, 'BAD_REQUEST').
|
-define(BAD_REQUEST, 'BAD_REQUEST').
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
|
|
@ -41,20 +42,21 @@ schema("/mqtt/auto_subscribe") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => auto_subscribe,
|
'operationId' => auto_subscribe,
|
||||||
get => #{
|
get => #{
|
||||||
description => <<"Auto subscribe list">>,
|
description => ?DESC(list_auto_subscribe_api),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe")
|
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
put => #{
|
put => #{
|
||||||
description => <<"Update auto subscribe topic list">>,
|
description => ?DESC(update_auto_subscribe_api),
|
||||||
'requestBody' => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
|
'requestBody' => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
|
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
|
||||||
400 => emqx_mgmt_util:error_schema(
|
409 => emqx_dashboard_swagger:error_codes(
|
||||||
<<"Request body required">>, [?BAD_REQUEST]),
|
[?EXCEED_LIMIT],
|
||||||
409 => emqx_mgmt_util:error_schema(
|
?DESC(update_auto_subscribe_api_response409))
|
||||||
<<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}}
|
}
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%%==============================================================================================
|
%%%==============================================================================================
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
|
|
@ -32,34 +33,31 @@ roots() ->
|
||||||
|
|
||||||
fields("auto_subscribe") ->
|
fields("auto_subscribe") ->
|
||||||
[ {topics, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "topic")),
|
[ {topics, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "topic")),
|
||||||
#{desc => "List of auto-subscribe topics."})}
|
#{desc => ?DESC(auto_subscribe)})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("topic") ->
|
fields("topic") ->
|
||||||
[ {topic, sc(binary(), #{
|
[ {topic, sc(binary(), #{
|
||||||
|
required => true,
|
||||||
example => topic_example(),
|
example => topic_example(),
|
||||||
desc => "Topic name, placeholders is supported. For example: "
|
desc => ?DESC("topic")})}
|
||||||
++ binary_to_list(topic_example())})}
|
|
||||||
, {qos, sc(emqx_schema:qos(), #{
|
, {qos, sc(emqx_schema:qos(), #{
|
||||||
default => 0,
|
default => 0,
|
||||||
desc => "Quality of service. MQTT definition."})}
|
desc => ?DESC("qos")})}
|
||||||
, {rh, sc(range(0,2), #{
|
, {rh, sc(range(0,2), #{
|
||||||
default => 0,
|
default => 0,
|
||||||
desc => "Retain handling. MQTT 5.0 definition."})}
|
desc => ?DESC("rh")})}
|
||||||
, {rap, sc(range(0, 1), #{
|
, {rap, sc(range(0, 1), #{
|
||||||
default => 0,
|
default => 0,
|
||||||
desc => "Retain as Published. MQTT 5.0 definition."})}
|
desc => ?DESC("rap")})}
|
||||||
, {nl, sc(range(0, 1), #{
|
, {nl, sc(range(0, 1), #{
|
||||||
default => 0,
|
default => 0,
|
||||||
desc => "Not local. MQTT 5.0 definition."})}
|
desc => ?DESC(nl)})}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc("auto_subscribe") ->
|
desc("auto_subscribe") -> ?DESC("auto_subscribe");
|
||||||
"Configuration for `auto_subscribe` feature.";
|
desc("topic") -> ?DESC("topic");
|
||||||
desc("topic") ->
|
desc(_) -> undefined.
|
||||||
"";
|
|
||||||
desc(_) ->
|
|
||||||
undefined.
|
|
||||||
|
|
||||||
topic_example() ->
|
topic_example() ->
|
||||||
<<"/clientid/", ?PH_S_CLIENTID,
|
<<"/clientid/", ?PH_S_CLIENTID,
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@
|
||||||
|
|
||||||
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
|
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
|
||||||
|
|
||||||
-define(ENSURE_TOPICS , [<<"/c/auto_sub_c">>
|
-define(ENSURE_TOPICS, [
|
||||||
, <<"/u/auto_sub_u">>
|
<<"/c/auto_sub_c">>,
|
||||||
, ?TOPIC_S]).
|
<<"/u/auto_sub_u">>,
|
||||||
|
?TOPIC_S
|
||||||
|
]).
|
||||||
|
|
||||||
-define(CLIENT_ID, <<"auto_sub_c">>).
|
-define(CLIENT_ID, <<"auto_sub_c">>).
|
||||||
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
|
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
|
||||||
|
|
@ -45,60 +47,58 @@ init_per_suite(Config) ->
|
||||||
mria:start(),
|
mria:start(),
|
||||||
application:stop(?APP),
|
application:stop(?APP),
|
||||||
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx_schema, fields, fun("auto_subscribe") ->
|
meck:expect(emqx_schema, fields, fun
|
||||||
meck:passthrough(["auto_subscribe"]) ++
|
("auto_subscribe") ->
|
||||||
emqx_auto_subscribe_schema:fields("auto_subscribe");
|
meck:passthrough(["auto_subscribe"]) ++
|
||||||
(F) -> meck:passthrough([F])
|
emqx_auto_subscribe_schema:fields("auto_subscribe");
|
||||||
end),
|
(F) ->
|
||||||
|
meck:passthrough([F])
|
||||||
|
end),
|
||||||
|
|
||||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
|
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
|
||||||
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
|
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
|
||||||
meck:expect(emqx_resource, remove, fun(_) -> ok end ),
|
meck:expect(emqx_resource, remove, fun(_) -> ok end),
|
||||||
|
|
||||||
application:load(emqx_dashboard),
|
application:load(emqx_dashboard),
|
||||||
application:load(?APP),
|
application:load(?APP),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_auto_subscribe_schema,
|
ok = emqx_common_test_helpers:load_config(
|
||||||
<<"auto_subscribe {
|
emqx_auto_subscribe_schema,
|
||||||
topics = [
|
<<"auto_subscribe {\n"
|
||||||
{
|
" topics = [\n"
|
||||||
topic = \"/c/${clientid}\"
|
" {\n"
|
||||||
},
|
" topic = \"/c/${clientid}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/u/${username}\"
|
" {\n"
|
||||||
},
|
" topic = \"/u/${username}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/h/${host}\"
|
" {\n"
|
||||||
},
|
" topic = \"/h/${host}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/p/${port}\"
|
" {\n"
|
||||||
},
|
" topic = \"/p/${port}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"
|
" {\n"
|
||||||
},
|
" topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/topic/simple\"
|
" {\n"
|
||||||
qos = 1
|
" topic = \"/topic/simple\"\n"
|
||||||
rh = 0
|
" qos = 1\n"
|
||||||
rap = 0
|
" rh = 0\n"
|
||||||
nl = 0
|
" rap = 0\n"
|
||||||
}
|
" nl = 0\n"
|
||||||
]
|
" }\n"
|
||||||
}">>),
|
" ]\n"
|
||||||
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard, ?APP],
|
" }">>
|
||||||
fun set_special_configs/1),
|
),
|
||||||
|
emqx_common_test_helpers:start_apps(
|
||||||
|
[emqx_conf, emqx_dashboard, ?APP],
|
||||||
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -106,10 +106,10 @@ set_special_configs(_) ->
|
||||||
topic_config(T) ->
|
topic_config(T) ->
|
||||||
#{
|
#{
|
||||||
topic => T,
|
topic => T,
|
||||||
qos => 0,
|
qos => 0,
|
||||||
rh => 0,
|
rh => 0,
|
||||||
rap => 0,
|
rap => 0,
|
||||||
nl => 0
|
nl => 0
|
||||||
}.
|
}.
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
|
|
@ -148,7 +148,6 @@ t_update(_) ->
|
||||||
?assertEqual(1, erlang:length(GETResponseMap)),
|
?assertEqual(1, erlang:length(GETResponseMap)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
check_subs(Count) ->
|
check_subs(Count) ->
|
||||||
Subs = ets:tab2list(emqx_suboption),
|
Subs = ets:tab2list(emqx_suboption),
|
||||||
ct:pal("---> ~p ~p ~n", [Subs, Count]),
|
ct:pal("---> ~p ~p ~n", [Subs, Count]),
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,12 @@ on_message_publish(Message = #message{topic = Topic, flags = Flags}) ->
|
||||||
send_to_matched_egress_bridges(Topic, Msg) ->
|
send_to_matched_egress_bridges(Topic, Msg) ->
|
||||||
lists:foreach(fun (Id) ->
|
lists:foreach(fun (Id) ->
|
||||||
try send_message(Id, Msg) of
|
try send_message(Id, Msg) of
|
||||||
ok -> ok;
|
{error, Reason} ->
|
||||||
Error -> ?SLOG(error, #{msg => "send_message_to_bridge_failed",
|
?SLOG(error, #{msg => "send_message_to_bridge_failed",
|
||||||
bridge => Id, error => Error})
|
bridge => Id, error => Reason});
|
||||||
|
_ -> ok
|
||||||
catch Err:Reason:ST ->
|
catch Err:Reason:ST ->
|
||||||
?SLOG(error, #{msg => "send_message_to_bridge_crash",
|
?SLOG(error, #{msg => "send_message_to_bridge_exception",
|
||||||
bridge => Id, error => Err, reason => Reason,
|
bridge => Id, error => Err, reason => Reason,
|
||||||
stacktrace => ST})
|
stacktrace => ST})
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
-export([update/3, update/4]).
|
-export([update/3, update/4]).
|
||||||
-export([remove/2, remove/3]).
|
-export([remove/2, remove/3]).
|
||||||
-export([reset/2, reset/3]).
|
-export([reset/2, reset/3]).
|
||||||
-export([dump_schema/1, dump_schema/2]).
|
-export([dump_schema/1, dump_schema/3]).
|
||||||
-export([schema_module/0]).
|
-export([schema_module/0]).
|
||||||
|
|
||||||
%% for rpc
|
%% for rpc
|
||||||
|
|
@ -80,15 +80,22 @@ get_node_and_config(KeyPath) ->
|
||||||
{node(), emqx:get_config(KeyPath, config_not_found)}.
|
{node(), emqx:get_config(KeyPath, config_not_found)}.
|
||||||
|
|
||||||
%% @doc Update all value of key path in cluster-override.conf or local-override.conf.
|
%% @doc Update all value of key path in cluster-override.conf or local-override.conf.
|
||||||
-spec update(emqx_map_lib:config_key_path(), emqx_config:update_request(),
|
-spec update(
|
||||||
emqx_config:update_opts()) ->
|
emqx_map_lib:config_key_path(),
|
||||||
|
emqx_config:update_request(),
|
||||||
|
emqx_config:update_opts()
|
||||||
|
) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
||||||
update(KeyPath, UpdateReq, Opts) ->
|
update(KeyPath, UpdateReq, Opts) ->
|
||||||
check_cluster_rpc_result(emqx_conf_proto_v1:update(KeyPath, UpdateReq, Opts)).
|
check_cluster_rpc_result(emqx_conf_proto_v1:update(KeyPath, UpdateReq, Opts)).
|
||||||
|
|
||||||
%% @doc Update the specified node's key path in local-override.conf.
|
%% @doc Update the specified node's key path in local-override.conf.
|
||||||
-spec update(node(), emqx_map_lib:config_key_path(), emqx_config:update_request(),
|
-spec update(
|
||||||
emqx_config:update_opts()) ->
|
node(),
|
||||||
|
emqx_map_lib:config_key_path(),
|
||||||
|
emqx_config:update_request(),
|
||||||
|
emqx_config:update_opts()
|
||||||
|
) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()} | emqx_rpc:badrpc().
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()} | emqx_rpc:badrpc().
|
||||||
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
|
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
|
||||||
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
|
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
|
||||||
|
|
@ -126,25 +133,41 @@ reset(Node, KeyPath, Opts) ->
|
||||||
%% @doc Called from build script.
|
%% @doc Called from build script.
|
||||||
-spec dump_schema(file:name_all()) -> ok.
|
-spec dump_schema(file:name_all()) -> ok.
|
||||||
dump_schema(Dir) ->
|
dump_schema(Dir) ->
|
||||||
dump_schema(Dir, emqx_conf_schema).
|
I18nFile = emqx:etc_file("i18n.conf"),
|
||||||
|
dump_schema(Dir, emqx_conf_schema, I18nFile).
|
||||||
|
|
||||||
dump_schema(Dir, SchemaModule) ->
|
dump_schema(Dir, SchemaModule, I18nFile) ->
|
||||||
SchemaMdFile = filename:join([Dir, "config.md"]),
|
lists:foreach(
|
||||||
io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]),
|
fun(Lang) ->
|
||||||
ok = gen_doc(SchemaMdFile, SchemaModule),
|
gen_config_md(Dir, I18nFile, SchemaModule, Lang),
|
||||||
|
gen_hot_conf_schema_json(Dir, I18nFile, Lang)
|
||||||
|
end,
|
||||||
|
[en, zh]
|
||||||
|
),
|
||||||
|
gen_schema_json(Dir, I18nFile, SchemaModule).
|
||||||
|
|
||||||
%% for scripts/spellcheck.
|
%% for scripts/spellcheck.
|
||||||
|
gen_schema_json(Dir, I18nFile, SchemaModule) ->
|
||||||
SchemaJsonFile = filename:join([Dir, "schema.json"]),
|
SchemaJsonFile = filename:join([Dir, "schema.json"]),
|
||||||
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
|
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
|
||||||
JsonMap = hocon_schema_json:gen(SchemaModule),
|
Opts = #{desc_file => I18nFile, lang => "en"},
|
||||||
|
JsonMap = hocon_schema_json:gen(SchemaModule, Opts),
|
||||||
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
|
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
|
||||||
ok = file:write_file(SchemaJsonFile, IoData),
|
ok = file:write_file(SchemaJsonFile, IoData).
|
||||||
|
|
||||||
%% hot-update configuration schema
|
gen_hot_conf_schema_json(Dir, I18nFile, Lang) ->
|
||||||
HotConfigSchemaFile = filename:join([Dir, "hot-config-schema.json"]),
|
emqx_dashboard:init_i18n(I18nFile, Lang),
|
||||||
|
JsonFile = "hot-config-schema-" ++ atom_to_list(Lang) ++ ".json",
|
||||||
|
HotConfigSchemaFile = filename:join([Dir, JsonFile]),
|
||||||
io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]),
|
io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]),
|
||||||
ok = gen_hot_conf_schema(HotConfigSchemaFile),
|
ok = gen_hot_conf_schema(HotConfigSchemaFile),
|
||||||
ok.
|
emqx_dashboard:clear_i18n().
|
||||||
|
|
||||||
|
gen_config_md(Dir, I18nFile, SchemaModule, Lang0) ->
|
||||||
|
Lang = atom_to_list(Lang0),
|
||||||
|
SchemaMdFile = filename:join([Dir, "config-" ++ Lang ++ ".md"]),
|
||||||
|
io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
|
||||||
|
ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang).
|
||||||
|
|
||||||
%% @doc return the root schema module.
|
%% @doc return the root schema module.
|
||||||
-spec schema_module() -> module().
|
-spec schema_module() -> module().
|
||||||
|
|
@ -158,63 +181,96 @@ schema_module() ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec gen_doc(file:name_all(), module()) -> ok.
|
-spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok.
|
||||||
gen_doc(File, SchemaModule) ->
|
gen_doc(File, SchemaModule, I18nFile, Lang) ->
|
||||||
Version = emqx_release:version(),
|
Version = emqx_release:version(),
|
||||||
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
|
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
|
||||||
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
|
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
|
||||||
{ok, Body} = file:read_file(BodyFile),
|
{ok, Body} = file:read_file(BodyFile),
|
||||||
Doc = hocon_schema_md:gen(SchemaModule, #{title => Title, body => Body}),
|
Opts = #{title => Title, body => Body, desc_file => I18nFile, lang => Lang},
|
||||||
|
Doc = hocon_schema_md:gen(SchemaModule, Opts),
|
||||||
file:write_file(File, Doc).
|
file:write_file(File, Doc).
|
||||||
|
|
||||||
check_cluster_rpc_result(Result) ->
|
check_cluster_rpc_result(Result) ->
|
||||||
case Result of
|
case Result of
|
||||||
{ok, _TnxId, Res} -> Res;
|
{ok, _TnxId, Res} ->
|
||||||
|
Res;
|
||||||
{retry, TnxId, Res, Nodes} ->
|
{retry, TnxId, Res, Nodes} ->
|
||||||
%% The init MFA return ok, but other nodes failed.
|
%% The init MFA return ok, but other nodes failed.
|
||||||
%% We return ok and alert an alarm.
|
%% We return ok and alert an alarm.
|
||||||
?SLOG(error, #{msg => "failed_to_update_config_in_cluster", nodes => Nodes,
|
?SLOG(error, #{
|
||||||
tnx_id => TnxId}),
|
msg => "failed_to_update_config_in_cluster",
|
||||||
|
nodes => Nodes,
|
||||||
|
tnx_id => TnxId
|
||||||
|
}),
|
||||||
Res;
|
Res;
|
||||||
{error, Error} -> %% all MFA return not ok or {ok, term()}.
|
%% all MFA return not ok or {ok, term()}.
|
||||||
|
{error, Error} ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Only gen hot_conf schema, not all configuration fields.
|
%% Only gen hot_conf schema, not all configuration fields.
|
||||||
gen_hot_conf_schema(File) ->
|
gen_hot_conf_schema(File) ->
|
||||||
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs,
|
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
|
||||||
#{schema_converter => fun hocon_schema_to_spec/2}),
|
emqx_mgmt_api_configs,
|
||||||
ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) ->
|
#{schema_converter => fun hocon_schema_to_spec/2}
|
||||||
NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) ->
|
),
|
||||||
case Responses of
|
ApiSpec = lists:foldl(
|
||||||
#{<<"200">> :=
|
fun({Path, Spec, _, _}, Acc) ->
|
||||||
#{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} ->
|
NewSpec = maps:fold(
|
||||||
SubAcc#{Method => Schema};
|
fun(Method, #{responses := Responses}, SubAcc) ->
|
||||||
_ -> SubAcc
|
case Responses of
|
||||||
end
|
#{
|
||||||
end, #{}, Spec),
|
<<"200">> :=
|
||||||
Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0),
|
#{
|
||||||
|
<<"content">> := #{
|
||||||
|
<<"application/json">> := #{<<"schema">> := Schema}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
SubAcc#{Method => Schema};
|
||||||
|
_ ->
|
||||||
|
SubAcc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Spec
|
||||||
|
),
|
||||||
|
Acc#{list_to_atom(Path) => NewSpec}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
ApiSpec0
|
||||||
|
),
|
||||||
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
|
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
|
||||||
IoData = jsx:encode(#{
|
IoData = jsx:encode(
|
||||||
info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
|
#{
|
||||||
paths => ApiSpec,
|
info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
|
||||||
components => #{schemas => Components}
|
paths => ApiSpec,
|
||||||
}, [space, {indent, 4}]),
|
components => #{schemas => Components}
|
||||||
|
},
|
||||||
|
[space, {indent, 4}]
|
||||||
|
),
|
||||||
file:write_file(File, IoData).
|
file:write_file(File, IoData).
|
||||||
|
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{},
|
-define(INIT_SCHEMA, #{
|
||||||
validations => [], namespace => undefined}).
|
fields => #{},
|
||||||
|
translations => #{},
|
||||||
|
validations => [],
|
||||||
|
namespace => undefined
|
||||||
|
}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
||||||
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
|
||||||
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
|
iolist_to_binary([
|
||||||
|
<<"#/components/schemas/">>,
|
||||||
|
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
|
||||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
|
||||||
[{Module, StructName}]};
|
|
||||||
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
|
||||||
[{LocalModule, StructName}]};
|
|
||||||
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||||
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
|
|
@ -226,50 +282,97 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => enum, symbols => Items}, []};
|
{#{type => enum, symbols => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
{#{<<"type">> => object,
|
{
|
||||||
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
|
#{
|
||||||
SubRefs};
|
<<"type">> => object,
|
||||||
|
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
|
||||||
|
},
|
||||||
|
SubRefs
|
||||||
|
};
|
||||||
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
|
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
|
||||||
{OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
|
{OneOf, Refs} = lists:foldl(
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
fun(Type, {Acc, RefsAcc}) ->
|
||||||
{[Schema | Acc], SubRefs ++ RefsAcc}
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
end, {[], []}, Types),
|
{[Schema | Acc], SubRefs ++ RefsAcc}
|
||||||
|
end,
|
||||||
|
{[], []},
|
||||||
|
Types
|
||||||
|
),
|
||||||
{#{<<"oneOf">> => OneOf}, Refs};
|
{#{<<"oneOf">> => OneOf}, Refs};
|
||||||
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
||||||
{#{type => enum, symbols => [Atom]}, []}.
|
{#{type => enum, symbols => [Atom]}, []}.
|
||||||
|
|
||||||
typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]};
|
typename_to_spec("user_id_type()", _Mod) ->
|
||||||
typename_to_spec("term()", _Mod) -> #{type => string};
|
#{type => enum, symbols => [clientid, username]};
|
||||||
typename_to_spec("boolean()", _Mod) -> #{type => boolean};
|
typename_to_spec("term()", _Mod) ->
|
||||||
typename_to_spec("binary()", _Mod) -> #{type => string};
|
#{type => string};
|
||||||
typename_to_spec("float()", _Mod) -> #{type => number};
|
typename_to_spec("boolean()", _Mod) ->
|
||||||
typename_to_spec("integer()", _Mod) -> #{type => number};
|
#{type => boolean};
|
||||||
typename_to_spec("non_neg_integer()", _Mod) -> #{type => number, minimum => 1};
|
typename_to_spec("binary()", _Mod) ->
|
||||||
typename_to_spec("number()", _Mod) -> #{type => number};
|
#{type => string};
|
||||||
typename_to_spec("string()", _Mod) -> #{type => string};
|
typename_to_spec("float()", _Mod) ->
|
||||||
typename_to_spec("atom()", _Mod) -> #{type => string};
|
#{type => number};
|
||||||
|
typename_to_spec("integer()", _Mod) ->
|
||||||
typename_to_spec("duration()", _Mod) -> #{type => duration};
|
#{type => number};
|
||||||
typename_to_spec("duration_s()", _Mod) -> #{type => duration};
|
typename_to_spec("non_neg_integer()", _Mod) ->
|
||||||
typename_to_spec("duration_ms()", _Mod) -> #{type => duration};
|
#{type => number, minimum => 1};
|
||||||
typename_to_spec("percent()", _Mod) -> #{type => percent};
|
typename_to_spec("number()", _Mod) ->
|
||||||
typename_to_spec("file()", _Mod) -> #{type => string};
|
#{type => number};
|
||||||
typename_to_spec("ip_port()", _Mod) -> #{type => ip_port};
|
typename_to_spec("string()", _Mod) ->
|
||||||
typename_to_spec("url()", _Mod) -> #{type => url};
|
#{type => string};
|
||||||
typename_to_spec("bytesize()", _Mod) -> #{type => 'byteSize'};
|
typename_to_spec("atom()", _Mod) ->
|
||||||
typename_to_spec("wordsize()", _Mod) -> #{type => 'byteSize'};
|
#{type => string};
|
||||||
typename_to_spec("qos()", _Mod) -> #{type => enum, symbols => [0, 1, 2]};
|
typename_to_spec("duration()", _Mod) ->
|
||||||
typename_to_spec("comma_separated_list()", _Mod) -> #{type => comma_separated_string};
|
#{type => duration};
|
||||||
typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => comma_separated_string};
|
typename_to_spec("duration_s()", _Mod) ->
|
||||||
typename_to_spec("pool_type()", _Mod) -> #{type => enum, symbols => [random, hash]};
|
#{type => duration};
|
||||||
|
typename_to_spec("duration_ms()", _Mod) ->
|
||||||
|
#{type => duration};
|
||||||
|
typename_to_spec("percent()", _Mod) ->
|
||||||
|
#{type => percent};
|
||||||
|
typename_to_spec("file()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
typename_to_spec("ip_port()", _Mod) ->
|
||||||
|
#{type => ip_port};
|
||||||
|
typename_to_spec("url()", _Mod) ->
|
||||||
|
#{type => url};
|
||||||
|
typename_to_spec("bytesize()", _Mod) ->
|
||||||
|
#{type => 'byteSize'};
|
||||||
|
typename_to_spec("wordsize()", _Mod) ->
|
||||||
|
#{type => 'byteSize'};
|
||||||
|
typename_to_spec("qos()", _Mod) ->
|
||||||
|
#{type => enum, symbols => [0, 1, 2]};
|
||||||
|
typename_to_spec("comma_separated_list()", _Mod) ->
|
||||||
|
#{type => comma_separated_string};
|
||||||
|
typename_to_spec("comma_separated_atoms()", _Mod) ->
|
||||||
|
#{type => comma_separated_string};
|
||||||
|
typename_to_spec("pool_type()", _Mod) ->
|
||||||
|
#{type => enum, symbols => [random, hash]};
|
||||||
typename_to_spec("log_level()", _Mod) ->
|
typename_to_spec("log_level()", _Mod) ->
|
||||||
#{type => enum, symbols => [debug, info, notice, warning, error,
|
#{
|
||||||
critical, alert, emergency, all]};
|
type => enum,
|
||||||
typename_to_spec("rate()", _Mod) -> #{type => string};
|
symbols => [
|
||||||
typename_to_spec("capacity()", _Mod) -> #{type => string};
|
debug,
|
||||||
typename_to_spec("burst_rate()", _Mod) -> #{type => string};
|
info,
|
||||||
typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]};
|
notice,
|
||||||
typename_to_spec("initial()", _Mod) -> #{type => string};
|
warning,
|
||||||
|
error,
|
||||||
|
critical,
|
||||||
|
alert,
|
||||||
|
emergency,
|
||||||
|
all
|
||||||
|
]
|
||||||
|
};
|
||||||
|
typename_to_spec("rate()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
typename_to_spec("capacity()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
typename_to_spec("burst_rate()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
typename_to_spec("failure_strategy()", _Mod) ->
|
||||||
|
#{type => enum, symbols => [force, drop, throw]};
|
||||||
|
typename_to_spec("initial()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
typename_to_spec(Name, Mod) ->
|
typename_to_spec(Name, Mod) ->
|
||||||
Spec = range(Name),
|
Spec = range(Name),
|
||||||
Spec1 = remote_module_type(Spec, Name, Mod),
|
Spec1 = remote_module_type(Spec, Name, Mod),
|
||||||
|
|
@ -282,11 +385,13 @@ default_type(Type) -> Type.
|
||||||
|
|
||||||
range(Name) ->
|
range(Name) ->
|
||||||
case string:split(Name, "..") of
|
case string:split(Name, "..") of
|
||||||
[MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
|
%% 1..10 1..inf -inf..10
|
||||||
|
[MinStr, MaxStr] ->
|
||||||
Schema = #{type => number},
|
Schema = #{type => number},
|
||||||
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
||||||
add_integer_prop(Schema1, maximum, MaxStr);
|
add_integer_prop(Schema1, maximum, MaxStr);
|
||||||
_ -> nomatch
|
_ ->
|
||||||
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Module:Type
|
%% Module:Type
|
||||||
|
|
@ -295,21 +400,25 @@ remote_module_type(nomatch, Name, Mod) ->
|
||||||
[_Module, Type] -> typename_to_spec(Type, Mod);
|
[_Module, Type] -> typename_to_spec(Type, Mod);
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
remote_module_type(Spec, _Name, _Mod) -> Spec.
|
remote_module_type(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% [string()] or [integer()] or [xxx].
|
%% [string()] or [integer()] or [xxx].
|
||||||
typerefl_array(nomatch, Name, Mod) ->
|
typerefl_array(nomatch, Name, Mod) ->
|
||||||
case string:trim(Name, leading, "[") of
|
case string:trim(Name, leading, "[") of
|
||||||
Name -> nomatch;
|
Name ->
|
||||||
|
nomatch;
|
||||||
Name1 ->
|
Name1 ->
|
||||||
case string:trim(Name1, trailing, "]") of
|
case string:trim(Name1, trailing, "]") of
|
||||||
Name1 -> notmatch;
|
Name1 ->
|
||||||
|
notmatch;
|
||||||
Name2 ->
|
Name2 ->
|
||||||
Schema = typename_to_spec(Name2, Mod),
|
Schema = typename_to_spec(Name2, Mod),
|
||||||
#{type => array, items => Schema}
|
#{type => array, items => Schema}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
typerefl_array(Spec, _Name, _Mod) -> Spec.
|
typerefl_array(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% integer(1)
|
%% integer(1)
|
||||||
integer(nomatch, Name) ->
|
integer(nomatch, Name) ->
|
||||||
|
|
@ -317,12 +426,13 @@ integer(nomatch, Name) ->
|
||||||
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
|
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
integer(Spec, _Name) -> Spec.
|
integer(Spec, _Name) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
add_integer_prop(Schema, Key, Value) ->
|
add_integer_prop(Schema, Key, Value) ->
|
||||||
case string:to_integer(Value) of
|
case string:to_integer(Value) of
|
||||||
{error, no_integer} -> Schema;
|
{error, no_integer} -> Schema;
|
||||||
{Int, []}when Key =:= minimum -> Schema#{Key => Int};
|
{Int, []} when Key =:= minimum -> Schema#{Key => Int};
|
||||||
{Int, []} -> Schema#{Key => Int}
|
{Int, []} -> Schema#{Key => Int}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -333,4 +443,5 @@ to_bin(List) when is_list(List) ->
|
||||||
end;
|
end;
|
||||||
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
||||||
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
||||||
to_bin(X) -> X.
|
to_bin(X) ->
|
||||||
|
X.
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@
|
||||||
doc_gen_test() ->
|
doc_gen_test() ->
|
||||||
Dir = "tmp",
|
Dir = "tmp",
|
||||||
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
||||||
_ = emqx_conf:dump_schema(Dir),
|
I18nFile = filename:join(["_build", "test", "lib", "emqx_dashboard", "etc", "i18n.conf.all"]),
|
||||||
|
_ = emqx_conf:dump_schema(Dir, emqx_conf_schema, I18nFile),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ For example: `http://localhost:9901/`
|
||||||
, desc => "The type of the pool. Can be one of `random`, `hash`."
|
, desc => "The type of the pool. Can be one of `random`, `hash`."
|
||||||
})}
|
})}
|
||||||
, {pool_size,
|
, {pool_size,
|
||||||
sc(non_neg_integer(),
|
sc(pos_integer(),
|
||||||
#{ default => 8
|
#{ default => 8
|
||||||
, desc => "The pool size."
|
, desc => "The pool size."
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ fields(sharded) ->
|
||||||
, {w_mode, fun w_mode/1}
|
, {w_mode, fun w_mode/1}
|
||||||
] ++ mongo_fields();
|
] ++ mongo_fields();
|
||||||
fields(topology) ->
|
fields(topology) ->
|
||||||
[ {pool_size, fun internal_pool_size/1}
|
[ {pool_size, fun emqx_connector_schema_lib:pool_size/1}
|
||||||
, {max_overflow, fun emqx_connector_schema_lib:pool_size/1}
|
, {max_overflow, fun max_overflow/1}
|
||||||
, {overflow_ttl, fun duration/1}
|
, {overflow_ttl, fun duration/1}
|
||||||
, {overflow_check_period, fun duration/1}
|
, {overflow_check_period, fun duration/1}
|
||||||
, {local_threshold_ms, fun duration/1}
|
, {local_threshold_ms, fun duration/1}
|
||||||
|
|
@ -114,12 +114,6 @@ mongo_fields() ->
|
||||||
] ++
|
] ++
|
||||||
emqx_connector_schema_lib:ssl_fields().
|
emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
internal_pool_size(type) -> integer();
|
|
||||||
internal_pool_size(desc) -> "Pool size on start.";
|
|
||||||
internal_pool_size(default) -> 1;
|
|
||||||
internal_pool_size(validator) -> [?MIN(1)];
|
|
||||||
internal_pool_size(_) -> undefined.
|
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
|
|
||||||
on_start(InstId, Config = #{mongo_type := Type,
|
on_start(InstId, Config = #{mongo_type := Type,
|
||||||
|
|
@ -334,6 +328,11 @@ duration(desc) -> "Time interval, such as timeout or TTL.";
|
||||||
duration(required) -> false;
|
duration(required) -> false;
|
||||||
duration(_) -> undefined.
|
duration(_) -> undefined.
|
||||||
|
|
||||||
|
max_overflow(type) -> non_neg_integer();
|
||||||
|
max_overflow(desc) -> "Max Overflow.";
|
||||||
|
max_overflow(default) -> 0;
|
||||||
|
max_overflow(_) -> undefined.
|
||||||
|
|
||||||
replica_set_name(type) -> binary();
|
replica_set_name(type) -> binary();
|
||||||
replica_set_name(desc) -> "Name of the replica set.";
|
replica_set_name(desc) -> "Name of the replica set.";
|
||||||
replica_set_name(required) -> false;
|
replica_set_name(required) -> false;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-type database() :: binary().
|
-type database() :: binary().
|
||||||
-type pool_size() :: integer().
|
-type pool_size() :: pos_integer().
|
||||||
-type username() :: binary().
|
-type username() :: binary().
|
||||||
-type password() :: binary().
|
-type password() :: binary().
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ database(required) -> true;
|
||||||
database(validator) -> [?NOT_EMPTY("the value of the field 'database' cannot be empty")];
|
database(validator) -> [?NOT_EMPTY("the value of the field 'database' cannot be empty")];
|
||||||
database(_) -> undefined.
|
database(_) -> undefined.
|
||||||
|
|
||||||
pool_size(type) -> integer();
|
pool_size(type) -> pos_integer();
|
||||||
pool_size(desc) -> "Size of the connection pool.";
|
pool_size(desc) -> "Size of the connection pool.";
|
||||||
pool_size(default) -> 8;
|
pool_size(default) -> 8;
|
||||||
pool_size(validator) -> [?MIN(1)];
|
pool_size(validator) -> [?MIN(1)];
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ topic filters for 'remote_topic' of ingress connections.
|
||||||
"messages in case of ACK not received.",
|
"messages in case of ACK not received.",
|
||||||
#{default => "15s"})}
|
#{default => "15s"})}
|
||||||
, {max_inflight,
|
, {max_inflight,
|
||||||
sc(integer(),
|
sc(non_neg_integer(),
|
||||||
#{ default => 32
|
#{ default => 32
|
||||||
, desc => "Max inflight (sent, but un-acked) messages of the MQTT protocol"
|
, desc => "Max inflight (sent, but un-acked) messages of the MQTT protocol"
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -10,34 +10,29 @@ dashboard {
|
||||||
sample_interval = 10s
|
sample_interval = 10s
|
||||||
## JWT token expiration time.
|
## JWT token expiration time.
|
||||||
token_expired_time = 60m
|
token_expired_time = 60m
|
||||||
listeners = [
|
listeners.http {
|
||||||
{
|
num_acceptors = 4
|
||||||
protocol = http
|
max_connections = 512
|
||||||
num_acceptors = 4
|
bind = 18083
|
||||||
max_connections = 512
|
backlog = 512
|
||||||
bind = 18083
|
send_timeout = 5s
|
||||||
backlog = 512
|
inet6 = false
|
||||||
send_timeout = 5s
|
ipv6_v6only = false
|
||||||
inet6 = false
|
}
|
||||||
ipv6_v6only = false
|
#listeners.https {
|
||||||
}
|
# bind = "127.0.0.1:18084"
|
||||||
# ,
|
# num_acceptors = 4
|
||||||
# {
|
# backlog = 512
|
||||||
# protocol = https
|
# send_timeout = 5s
|
||||||
# bind = "127.0.0.1:18084"
|
# inet6 = false
|
||||||
# num_acceptors = 2
|
# ipv6_v6only = false
|
||||||
# backlog = 512
|
# certfile = "etc/certs/cert.pem"
|
||||||
# send_timeout = 5s
|
# keyfile = "etc/certs/key.pem"
|
||||||
# inet6 = false
|
# cacertfile = "etc/certs/cacert.pem"
|
||||||
# ipv6_v6only = false
|
# verify = verify_peer
|
||||||
# certfile = "etc/certs/cert.pem"
|
# versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"]
|
||||||
# keyfile = "etc/certs/key.pem"
|
# ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"]
|
||||||
# cacertfile = "etc/certs/cacert.pem"
|
#}
|
||||||
# verify = verify_peer
|
|
||||||
# versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"]
|
|
||||||
# ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"]
|
|
||||||
# }
|
|
||||||
]
|
|
||||||
|
|
||||||
## CORS Support. don't set cors true if you don't know what it means.
|
## CORS Support. don't set cors true if you don't know what it means.
|
||||||
# cors = false
|
# cors = false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
emqx_dashboard_schema {
|
||||||
|
protocol {
|
||||||
|
desc {
|
||||||
|
en: "Protocol Name"
|
||||||
|
zh: "协议名"
|
||||||
|
}
|
||||||
|
label: {
|
||||||
|
en: "Protocol"
|
||||||
|
zh: "协议"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps,
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
[ {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.8.6"}}}
|
|
||||||
, {emqx, {path, "../emqx"}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
{erl_opts, [warn_unused_vars,
|
{erl_opts, [warn_unused_vars,
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,19 @@
|
||||||
|
|
||||||
-define(APP, ?MODULE).
|
-define(APP, ?MODULE).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
start_listeners/0,
|
||||||
|
start_listeners/1,
|
||||||
|
stop_listeners/1,
|
||||||
|
stop_listeners/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ start_listeners/0
|
-export([
|
||||||
, start_listeners/1
|
init_i18n/2,
|
||||||
, stop_listeners/1
|
init_i18n/0,
|
||||||
, stop_listeners/0]).
|
get_i18n/0,
|
||||||
|
clear_i18n/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% Authorization
|
%% Authorization
|
||||||
-export([authorize/1]).
|
-export([authorize/1]).
|
||||||
|
|
@ -48,6 +56,7 @@ stop_listeners() ->
|
||||||
|
|
||||||
start_listeners(Listeners) ->
|
start_listeners(Listeners) ->
|
||||||
{ok, _} = application:ensure_all_started(minirest),
|
{ok, _} = application:ensure_all_started(minirest),
|
||||||
|
init_i18n(),
|
||||||
Authorization = {?MODULE, authorize},
|
Authorization = {?MODULE, authorize},
|
||||||
GlobalSpec = #{
|
GlobalSpec = #{
|
||||||
openapi => "3.0.0",
|
openapi => "3.0.0",
|
||||||
|
|
@ -58,12 +67,15 @@ start_listeners(Listeners) ->
|
||||||
'securitySchemes' => #{
|
'securitySchemes' => #{
|
||||||
'basicAuth' => #{type => http, scheme => basic},
|
'basicAuth' => #{type => http, scheme => basic},
|
||||||
'bearerAuth' => #{type => http, scheme => bearer}
|
'bearerAuth' => #{type => http, scheme => bearer}
|
||||||
}}},
|
}
|
||||||
Dispatch = [ {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
}
|
||||||
, {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}
|
},
|
||||||
, {?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []}
|
Dispatch = [
|
||||||
, {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}},
|
||||||
],
|
{"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}},
|
||||||
|
{?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []},
|
||||||
|
{'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
||||||
|
],
|
||||||
BaseMinirest = #{
|
BaseMinirest = #{
|
||||||
base_path => ?BASE_PATH,
|
base_path => ?BASE_PATH,
|
||||||
modules => minirest_api:find_api_modules(apps()),
|
modules => minirest_api:find_api_modules(apps()),
|
||||||
|
|
@ -74,75 +86,109 @@ start_listeners(Listeners) ->
|
||||||
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
||||||
},
|
},
|
||||||
Res =
|
Res =
|
||||||
lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
|
lists:foldl(
|
||||||
Minirest = BaseMinirest#{protocol => Protocol},
|
fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
|
||||||
case minirest:start(Name, RanchOptions, Minirest) of
|
Minirest = BaseMinirest#{protocol => Protocol},
|
||||||
{ok, _} ->
|
case minirest:start(Name, RanchOptions, Minirest) of
|
||||||
?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]),
|
{ok, _} ->
|
||||||
Acc;
|
?ULOG("Start listener ~ts on ~ts successfully.~n", [
|
||||||
{error, _Reason} ->
|
Name, emqx_listeners:format_addr(Bind)
|
||||||
%% Don't record the reason because minirest already does(too much logs noise).
|
]),
|
||||||
[Name | Acc]
|
Acc;
|
||||||
end
|
{error, _Reason} ->
|
||||||
end, [], listeners(Listeners)),
|
%% Don't record the reason because minirest already does(too much logs noise).
|
||||||
|
[Name | Acc]
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
listeners(Listeners)
|
||||||
|
),
|
||||||
|
clear_i18n(),
|
||||||
case Res of
|
case Res of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
_ -> {error, Res}
|
_ -> {error, Res}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
stop_listeners(Listeners) ->
|
stop_listeners(Listeners) ->
|
||||||
[begin
|
[
|
||||||
case minirest:stop(Name) of
|
begin
|
||||||
ok ->
|
case minirest:stop(Name) of
|
||||||
?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]);
|
ok ->
|
||||||
{error, not_found} ->
|
?ULOG("Stop listener ~ts on ~ts successfully.~n", [
|
||||||
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
Name, emqx_listeners:format_addr(Port)
|
||||||
|
]);
|
||||||
|
{error, not_found} ->
|
||||||
|
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end || {Name, _, Port, _} <- listeners(Listeners)],
|
|| {Name, _, Port, _} <- listeners(Listeners)
|
||||||
|
],
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
get_i18n() ->
|
||||||
|
application:get_env(emqx_dashboard, i18n).
|
||||||
|
|
||||||
|
init_i18n(File, Lang) ->
|
||||||
|
Cache = hocon_schema:new_cache(File),
|
||||||
|
application:set_env(emqx_dashboard, i18n, #{lang => atom_to_binary(Lang), cache => Cache}).
|
||||||
|
|
||||||
|
clear_i18n() ->
|
||||||
|
case application:get_env(emqx_dashboard, i18n) of
|
||||||
|
{ok, #{cache := Cache}} ->
|
||||||
|
hocon_schema:delete_cache(Cache),
|
||||||
|
application:unset_env(emqx_dashboard, i18n);
|
||||||
|
undefined ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% internal
|
%% internal
|
||||||
|
|
||||||
apps() ->
|
apps() ->
|
||||||
[App || {App, _, _} <- application:loaded_applications(),
|
[
|
||||||
|
App
|
||||||
|
|| {App, _, _} <- application:loaded_applications(),
|
||||||
case re:run(atom_to_list(App), "^emqx") of
|
case re:run(atom_to_list(App), "^emqx") of
|
||||||
{match,[{0,4}]} -> true;
|
{match, [{0, 4}]} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end].
|
end
|
||||||
|
].
|
||||||
|
|
||||||
listeners(Listeners) ->
|
listeners(Listeners) ->
|
||||||
[begin
|
lists:map(fun({Protocol, Conf}) ->
|
||||||
Protocol = maps:get(protocol, ListenerOption0, http),
|
{Conf1, Bind} = ip_port(Conf),
|
||||||
{ListenerOption, Bind} = ip_port(ListenerOption0),
|
{listener_name(Protocol, Conf1), Protocol, Bind, ranch_opts(Conf1)}
|
||||||
Name = listener_name(Protocol, ListenerOption),
|
end, maps:to_list(Listeners)).
|
||||||
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
|
|
||||||
{Name, Protocol, Bind, RanchOptions}
|
|
||||||
end || ListenerOption0 <- Listeners].
|
|
||||||
|
|
||||||
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
||||||
|
|
||||||
ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
||||||
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
||||||
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
||||||
|
|
||||||
|
init_i18n() ->
|
||||||
|
File = i18n_file(),
|
||||||
|
Lang = emqx_conf:get([dashboard, i18n_lang], en),
|
||||||
|
init_i18n(File, Lang).
|
||||||
|
|
||||||
ranch_opts(RanchOptions) ->
|
ranch_opts(RanchOptions) ->
|
||||||
Keys = [ {ack_timeout, handshake_timeout}
|
Keys = [
|
||||||
, connection_type
|
{ack_timeout, handshake_timeout},
|
||||||
, max_connections
|
connection_type,
|
||||||
, num_acceptors
|
max_connections,
|
||||||
, shutdown
|
num_acceptors,
|
||||||
, socket],
|
shutdown,
|
||||||
|
socket
|
||||||
|
],
|
||||||
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
|
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
|
||||||
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
|
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
|
||||||
|
|
||||||
|
key_take(Key, {All, R}) ->
|
||||||
key_take(Key, {All, R}) ->
|
{K, KX} =
|
||||||
{K, KX} = case Key of
|
case Key of
|
||||||
{K1, K2} -> {K1, K2};
|
{K1, K2} -> {K1, K2};
|
||||||
_ -> {Key, Key}
|
_ -> {Key, Key}
|
||||||
end,
|
end,
|
||||||
case maps:get(K, All, undefined) of
|
case maps:get(K, All, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{All, R};
|
{All, R};
|
||||||
|
|
@ -150,20 +196,22 @@ key_take(Key, {All, R}) ->
|
||||||
{maps:remove(K, All), R#{KX => V}}
|
{maps:remove(K, All), R#{KX => V}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
key_only(K , true , S) -> [K | S];
|
key_only(K, true, S) -> [K | S];
|
||||||
key_only(_K, false, S) -> S;
|
key_only(_K, false, S) -> S;
|
||||||
key_only(K , V , S) -> [{K, V} | S].
|
key_only(K, V, S) -> [{K, V} | S].
|
||||||
|
|
||||||
listener_name(Protocol, #{port := Port, ip := IP}) ->
|
listener_name(Protocol, #{port := Port, ip := IP}) ->
|
||||||
Name = "dashboard:"
|
Name =
|
||||||
++ atom_to_list(Protocol) ++ ":"
|
"dashboard:" ++
|
||||||
++ inet:ntoa(IP) ++ ":"
|
atom_to_list(Protocol) ++ ":" ++
|
||||||
++ integer_to_list(Port),
|
inet:ntoa(IP) ++ ":" ++
|
||||||
|
integer_to_list(Port),
|
||||||
list_to_atom(Name);
|
list_to_atom(Name);
|
||||||
listener_name(Protocol, #{port := Port}) ->
|
listener_name(Protocol, #{port := Port}) ->
|
||||||
Name = "dashboard:"
|
Name =
|
||||||
++ atom_to_list(Protocol) ++ ":"
|
"dashboard:" ++
|
||||||
++ integer_to_list(Port),
|
atom_to_list(Protocol) ++ ":" ++
|
||||||
|
integer_to_list(Port),
|
||||||
list_to_atom(Name).
|
list_to_atom(Name).
|
||||||
|
|
||||||
authorize(Req) ->
|
authorize(Req) ->
|
||||||
|
|
@ -180,11 +228,13 @@ authorize(Req) ->
|
||||||
{error, <<"not_allowed">>} ->
|
{error, <<"not_allowed">>} ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
?WRONG_USERNAME_OR_PWD,
|
?WRONG_USERNAME_OR_PWD,
|
||||||
<<"Check username/password">>);
|
<<"Check username/password">>
|
||||||
|
);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET,
|
?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET,
|
||||||
<<"Check username/password or api_key/api_secret">>)
|
<<"Check username/password or api_key/api_secret">>
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
|
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
|
||||||
|
|
@ -199,12 +249,22 @@ authorize(Req) ->
|
||||||
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
return_unauthorized(<<"AUTHORIZATION_HEADER_ERROR">>,
|
return_unauthorized(
|
||||||
<<"Support authorization: basic/bearer ">>)
|
<<"AUTHORIZATION_HEADER_ERROR">>,
|
||||||
|
<<"Support authorization: basic/bearer ">>
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
return_unauthorized(Code, Message) ->
|
return_unauthorized(Code, Message) ->
|
||||||
{401, #{<<"WWW-Authenticate">> =>
|
{401,
|
||||||
<<"Basic Realm=\"minirest-server\"">>},
|
#{
|
||||||
#{code => Code, message => Message}
|
<<"WWW-Authenticate">> =>
|
||||||
}.
|
<<"Basic Realm=\"minirest-server\"">>
|
||||||
|
},
|
||||||
|
#{code => Code, message => Message}}.
|
||||||
|
|
||||||
|
i18n_file() ->
|
||||||
|
case application:get_env(emqx_dashboard, i18n_file) of
|
||||||
|
undefined -> emqx:etc_file("i18n.conf");
|
||||||
|
{ok, File} -> File
|
||||||
|
end.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
-include("emqx_dashboard.hrl").
|
-include("emqx_dashboard.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
@ -128,7 +130,16 @@ current_rate() ->
|
||||||
current_rate(all) ->
|
current_rate(all) ->
|
||||||
current_rate();
|
current_rate();
|
||||||
current_rate(Node) when Node == node() ->
|
current_rate(Node) when Node == node() ->
|
||||||
do_call(current_rate);
|
try
|
||||||
|
{ok, Rate} = do_call(current_rate),
|
||||||
|
{ok, Rate}
|
||||||
|
catch _E:R ->
|
||||||
|
?SLOG(warning, #{msg => "Dashboard monitor error", reason => R}),
|
||||||
|
%% Rate map 0, ensure api will not crash.
|
||||||
|
%% When joining cluster, dashboard monitor restart.
|
||||||
|
Rate0 = [{Key, 0} || Key <- ?GAUGE_SAMPLER_LIST ++ maps:values(?DELTA_SAMPLER_RATE_MAP)],
|
||||||
|
{ok, maps:from_list(Rate0)}
|
||||||
|
end;
|
||||||
current_rate(Node) ->
|
current_rate(Node) ->
|
||||||
case emqx_dashboard_proto_v1:current_rate(Node) of
|
case emqx_dashboard_proto_v1:current_rate(Node) of
|
||||||
{badrpc, Reason} ->
|
{badrpc, Reason} ->
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ fields(sampler) ->
|
||||||
Samplers =
|
Samplers =
|
||||||
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
|
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
|
||||||
|| SamplerName <- ?SAMPLER_LIST],
|
|| SamplerName <- ?SAMPLER_LIST],
|
||||||
[{time_stamp, hoconsc:mk(integer(), #{desc => <<"Timestamp">>})} | Samplers];
|
[{time_stamp, hoconsc:mk(non_neg_integer(), #{desc => <<"Timestamp">>})} | Samplers];
|
||||||
|
|
||||||
fields(sampler_current) ->
|
fields(sampler_current) ->
|
||||||
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
|
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
|
||||||
|
|
|
||||||
|
|
@ -15,90 +15,140 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_dashboard_schema).
|
-module(emqx_dashboard_schema).
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
, namespace/0
|
fields/1,
|
||||||
, desc/1
|
namespace/0,
|
||||||
]).
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
namespace() -> <<"dashboard">>.
|
namespace() -> <<"dashboard">>.
|
||||||
roots() -> ["dashboard"].
|
roots() -> ["dashboard"].
|
||||||
|
|
||||||
fields("dashboard") ->
|
fields("dashboard") ->
|
||||||
[ {listeners,
|
[
|
||||||
sc(hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"),
|
{listeners,
|
||||||
hoconsc:ref(?MODULE, "https")])),
|
sc(
|
||||||
#{ desc =>
|
ref("listeners"),
|
||||||
"HTTP(s) listeners are identified by their protocol type and are
|
#{
|
||||||
used to serve dashboard UI and restful HTTP API.<br>
|
desc =>
|
||||||
Listeners must have a unique combination of port number and IP address.<br>
|
"HTTP(s) listeners are identified by their protocol type and are\n"
|
||||||
For example, an HTTP listener can listen on all configured IP addresses
|
"used to serve dashboard UI and restful HTTP API.<br>\n"
|
||||||
on a given port for a machine by specifying the IP address 0.0.0.0.<br>
|
"Listeners must have a unique combination of port number and IP address.<br>\n"
|
||||||
Alternatively, the HTTP listener can specify a unique IP address for each listener,
|
"For example, an HTTP listener can listen on all configured IP addresses\n"
|
||||||
but use the same port."})}
|
"on a given port for a machine by specifying the IP address 0.0.0.0.<br>\n"
|
||||||
, {default_username, fun default_username/1}
|
"Alternatively, the HTTP listener can specify a unique IP address for each listener,\n"
|
||||||
, {default_password, fun default_password/1}
|
"but use the same port."
|
||||||
, {sample_interval, sc(emqx_schema:duration_s(),
|
}
|
||||||
#{ default => "10s"
|
)},
|
||||||
, desc => "How often to update metrics displayed in the dashboard.<br/>"
|
{default_username, fun default_username/1},
|
||||||
"Note: `sample_interval` should be a divisor of 60."
|
{default_password, fun default_password/1},
|
||||||
})}
|
{sample_interval,
|
||||||
, {token_expired_time, sc(emqx_schema:duration(),
|
sc(
|
||||||
#{ default => "30m"
|
emqx_schema:duration_s(),
|
||||||
, desc => "JWT token expiration time."
|
#{
|
||||||
})}
|
default => "10s",
|
||||||
, {cors, fun cors/1}
|
desc =>
|
||||||
|
"How often to update metrics displayed in the dashboard.<br/>"
|
||||||
|
"Note: `sample_interval` should be a divisor of 60."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{token_expired_time,
|
||||||
|
sc(
|
||||||
|
emqx_schema:duration(),
|
||||||
|
#{
|
||||||
|
default => "30m",
|
||||||
|
desc => "JWT token expiration time."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{cors, fun cors/1},
|
||||||
|
{i18n_lang, fun i18n_lang/1}
|
||||||
|
];
|
||||||
|
fields("listeners") ->
|
||||||
|
[
|
||||||
|
{"http",
|
||||||
|
sc(
|
||||||
|
ref("http"),
|
||||||
|
#{
|
||||||
|
desc => "TCP listeners",
|
||||||
|
required => {false, recursively}
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"https",
|
||||||
|
sc(
|
||||||
|
ref("https"),
|
||||||
|
#{
|
||||||
|
desc => "SSL listeners",
|
||||||
|
required => {false, recursively}
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("http") ->
|
fields("http") ->
|
||||||
[ {"protocol", sc(
|
[
|
||||||
hoconsc:enum([http, https]),
|
{"bind", fun bind/1},
|
||||||
#{ desc => "HTTP/HTTPS protocol."
|
{"num_acceptors",
|
||||||
, required => true
|
sc(
|
||||||
, default => http
|
integer(),
|
||||||
})}
|
#{
|
||||||
, {"bind", fun bind/1}
|
default => 4,
|
||||||
, {"num_acceptors", sc(
|
desc => "Socket acceptor pool size for TCP protocols."
|
||||||
integer(),
|
}
|
||||||
#{ default => 4
|
)},
|
||||||
, desc => "Socket acceptor pool size for TCP protocols."
|
{"max_connections",
|
||||||
})}
|
sc(
|
||||||
, {"max_connections",
|
integer(),
|
||||||
sc(integer(),
|
#{
|
||||||
#{ default => 512
|
default => 512,
|
||||||
, desc => "Maximum number of simultaneous connections."
|
desc => "Maximum number of simultaneous connections."
|
||||||
})}
|
}
|
||||||
, {"backlog",
|
)},
|
||||||
sc(integer(),
|
{"backlog",
|
||||||
#{ default => 1024
|
sc(
|
||||||
, desc => "Defines the maximum length that the queue of pending connections can grow to."
|
integer(),
|
||||||
})}
|
#{
|
||||||
, {"send_timeout",
|
default => 1024,
|
||||||
sc(emqx_schema:duration(),
|
desc =>
|
||||||
#{ default => "5s"
|
"Defines the maximum length that the queue of pending connections can grow to."
|
||||||
, desc => "Send timeout for the socket."
|
}
|
||||||
})}
|
)},
|
||||||
, {"inet6",
|
{"send_timeout",
|
||||||
sc(boolean(),
|
sc(
|
||||||
#{ default => false
|
emqx_schema:duration(),
|
||||||
, desc => "Sets up the listener for IPv6."
|
#{
|
||||||
})}
|
default => "5s",
|
||||||
, {"ipv6_v6only",
|
desc => "Send timeout for the socket."
|
||||||
sc(boolean(),
|
}
|
||||||
#{ default => false
|
)},
|
||||||
, desc => "Disable IPv4-to-IPv6 mapping for the listener."
|
{"inet6",
|
||||||
})}
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
desc => "Sets up the listener for IPv6."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"ipv6_v6only",
|
||||||
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
desc => "Disable IPv4-to-IPv6 mapping for the listener."
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("https") ->
|
fields("https") ->
|
||||||
fields("http") ++
|
fields("http") ++
|
||||||
proplists:delete("fail_if_no_peer_cert",
|
proplists:delete(
|
||||||
emqx_schema:server_ssl_opts_schema(#{}, true)).
|
"fail_if_no_peer_cert",
|
||||||
|
emqx_schema:server_ssl_opts_schema(#{}, true)
|
||||||
|
).
|
||||||
|
|
||||||
desc("dashboard") ->
|
desc("dashboard") ->
|
||||||
"Configuration for EMQX dashboard.";
|
"Configuration for EMQX dashboard.";
|
||||||
|
desc("listeners") ->
|
||||||
|
"Configuration for the dashboard listener.";
|
||||||
desc("http") ->
|
desc("http") ->
|
||||||
"Configuration for the dashboard listener (plaintext).";
|
"Configuration for the dashboard listener (plaintext).";
|
||||||
desc("https") ->
|
desc("https") ->
|
||||||
|
|
@ -119,23 +169,44 @@ default_username(desc) -> "The default username of the automatically created das
|
||||||
default_username('readOnly') -> true;
|
default_username('readOnly') -> true;
|
||||||
default_username(_) -> undefined.
|
default_username(_) -> undefined.
|
||||||
|
|
||||||
default_password(type) -> string();
|
default_password(type) ->
|
||||||
default_password(default) -> "public";
|
string();
|
||||||
default_password(required) -> true;
|
default_password(default) ->
|
||||||
default_password('readOnly') -> true;
|
"public";
|
||||||
default_password(sensitive) -> true;
|
default_password(required) ->
|
||||||
default_password(desc) -> """
|
true;
|
||||||
The initial default password for dashboard 'admin' user.
|
default_password('readOnly') ->
|
||||||
For safety, it should be changed as soon as possible.""";
|
true;
|
||||||
default_password(_) -> undefined.
|
default_password(sensitive) ->
|
||||||
|
true;
|
||||||
|
default_password(desc) ->
|
||||||
|
""
|
||||||
|
"\n"
|
||||||
|
"The initial default password for dashboard 'admin' user.\n"
|
||||||
|
"For safety, it should be changed as soon as possible."
|
||||||
|
"";
|
||||||
|
default_password(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
cors(type) -> boolean();
|
cors(type) ->
|
||||||
cors(default) -> false;
|
boolean();
|
||||||
cors(required) -> false;
|
cors(default) ->
|
||||||
|
false;
|
||||||
|
cors(required) ->
|
||||||
|
false;
|
||||||
cors(desc) ->
|
cors(desc) ->
|
||||||
"Support Cross-Origin Resource Sharing (CORS).
|
"Support Cross-Origin Resource Sharing (CORS).\n"
|
||||||
Allows a server to indicate any origins (domain, scheme, or port) other than
|
"Allows a server to indicate any origins (domain, scheme, or port) other than\n"
|
||||||
its own from which a browser should permit loading resources.";
|
"its own from which a browser should permit loading resources.";
|
||||||
cors(_) -> undefined.
|
cors(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
i18n_lang(type) -> ?ENUM([en, zh]);
|
||||||
|
i18n_lang(default) -> en;
|
||||||
|
i18n_lang('readOnly') -> true;
|
||||||
|
i18n_lang(desc) -> "Internationalization language support.";
|
||||||
|
i18n_lang(_) -> undefined.
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
|
|
||||||
|
ref(Field) -> hoconsc:ref(?MODULE, Field).
|
||||||
|
|
|
||||||
|
|
@ -28,101 +28,136 @@
|
||||||
-export([filter_check_request/2, filter_check_request_and_translate_body/2]).
|
-export([filter_check_request/2, filter_check_request_and_translate_body/2]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-export([ parse_spec_ref/3
|
-export([
|
||||||
, components/2
|
parse_spec_ref/3,
|
||||||
]).
|
components/2
|
||||||
|
]).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
||||||
|
|
||||||
-define(DEFAULT_FIELDS, [example, allowReserved, style, format, readOnly,
|
-define(DEFAULT_FIELDS, [
|
||||||
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
|
example,
|
||||||
|
allowReserved,
|
||||||
|
style,
|
||||||
|
format,
|
||||||
|
readOnly,
|
||||||
|
explode,
|
||||||
|
maxLength,
|
||||||
|
allowEmptyValue,
|
||||||
|
deprecated,
|
||||||
|
minimum,
|
||||||
|
maximum
|
||||||
|
]).
|
||||||
|
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{},
|
-define(INIT_SCHEMA, #{
|
||||||
validations => [], namespace => undefined}).
|
fields => #{},
|
||||||
|
translations => #{},
|
||||||
|
validations => [],
|
||||||
|
namespace => undefined
|
||||||
|
}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
||||||
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
|
||||||
?TO_REF(namespace(_M_), _F_)])).
|
iolist_to_binary([
|
||||||
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>,
|
<<"#/components/schemas/">>,
|
||||||
?TO_REF(namespace(_M_), _F_)])).
|
?TO_REF(namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
-define(TO_COMPONENTS_PARAM(_M_, _F_),
|
||||||
|
iolist_to_binary([
|
||||||
|
<<"#/components/parameters/">>,
|
||||||
|
?TO_REF(namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
|
||||||
-define(MAX_ROW_LIMIT, 1000).
|
-define(MAX_ROW_LIMIT, 1000).
|
||||||
-define(DEFAULT_ROW, 100).
|
-define(DEFAULT_ROW, 100).
|
||||||
|
|
||||||
-type(request() :: #{bindings => map(), query_string => map(), body => map()}).
|
-type request() :: #{bindings => map(), query_string => map(), body => map()}.
|
||||||
-type(request_meta() :: #{module => module(), path => string(), method => atom()}).
|
-type request_meta() :: #{module => module(), path => string(), method => atom()}.
|
||||||
|
|
||||||
-type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}).
|
-type filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}.
|
||||||
-type(filter() :: fun((request(), request_meta()) -> filter_result())).
|
-type filter() :: fun((request(), request_meta()) -> filter_result()).
|
||||||
|
|
||||||
-type(spec_opts() :: #{check_schema => boolean() | filter(),
|
-type spec_opts() :: #{
|
||||||
translate_body => boolean(),
|
check_schema => boolean() | filter(),
|
||||||
schema_converter => fun((hocon_schema:schema(), Module::atom()) -> map())
|
translate_body => boolean(),
|
||||||
}).
|
schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map())
|
||||||
|
}.
|
||||||
|
|
||||||
-type(route_path() :: string() | binary()).
|
-type route_path() :: string() | binary().
|
||||||
-type(route_methods() :: map()).
|
-type route_methods() :: map().
|
||||||
-type(route_handler() :: atom()).
|
-type route_handler() :: atom().
|
||||||
-type(route_options() :: #{filter => filter() | undefined}).
|
-type route_options() :: #{filter => filter() | undefined}.
|
||||||
|
|
||||||
-type(api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}).
|
-type api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}.
|
||||||
-type(api_spec_component() :: map()).
|
-type api_spec_component() :: map().
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @equiv spec(Module, #{check_schema => false})
|
%% @equiv spec(Module, #{check_schema => false})
|
||||||
-spec(spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
-spec spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}.
|
||||||
spec(Module) -> spec(Module, #{check_schema => false}).
|
spec(Module) -> spec(Module, #{check_schema => false}).
|
||||||
|
|
||||||
-spec(spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
-spec spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}.
|
||||||
spec(Module, Options) ->
|
spec(Module, Options) ->
|
||||||
Paths = apply(Module, paths, []),
|
Paths = apply(Module, paths, []),
|
||||||
{ApiSpec, AllRefs} =
|
{ApiSpec, AllRefs} =
|
||||||
lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) ->
|
lists:foldl(
|
||||||
{OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
|
fun(Path, {AllAcc, AllRefsAcc}) ->
|
||||||
CheckSchema = support_check_schema(Options),
|
{OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
|
||||||
{[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
|
CheckSchema = support_check_schema(Options),
|
||||||
Refs ++ AllRefsAcc}
|
{
|
||||||
end, {[], []}, Paths),
|
[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
|
||||||
|
Refs ++ AllRefsAcc
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
{[], []},
|
||||||
|
Paths
|
||||||
|
),
|
||||||
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
||||||
|
|
||||||
-spec(namespace() -> hocon_schema:name()).
|
-spec namespace() -> hocon_schema:name().
|
||||||
namespace() -> "public".
|
namespace() -> "public".
|
||||||
|
|
||||||
-spec(fields(hocon_schema:name()) -> hocon_schema:fields()).
|
-spec fields(hocon_schema:name()) -> hocon_schema:fields().
|
||||||
fields(page) ->
|
fields(page) ->
|
||||||
Desc = <<"Page number of the results to fetch.">>,
|
Desc = <<"Page number of the results to fetch.">>,
|
||||||
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
||||||
[{page, hoconsc:mk(integer(), Meta)}];
|
[{page, hoconsc:mk(pos_integer(), Meta)}];
|
||||||
fields(limit) ->
|
fields(limit) ->
|
||||||
Desc = iolist_to_binary([<<"Results per page(max ">>,
|
Desc = iolist_to_binary([
|
||||||
integer_to_binary(?MAX_ROW_LIMIT), <<")">>]),
|
<<"Results per page(max ">>,
|
||||||
|
integer_to_binary(?MAX_ROW_LIMIT),
|
||||||
|
<<")">>
|
||||||
|
]),
|
||||||
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
|
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
|
||||||
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
||||||
|
|
||||||
-spec(schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map()).
|
-spec schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map().
|
||||||
schema_with_example(Type, Example) ->
|
schema_with_example(Type, Example) ->
|
||||||
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
|
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
|
||||||
|
|
||||||
-spec(schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map()).
|
-spec schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map().
|
||||||
schema_with_examples(Type, Examples) ->
|
schema_with_examples(Type, Examples) ->
|
||||||
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
|
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
|
||||||
|
|
||||||
-spec(error_codes(list(atom())) -> hocon_schema:fields()).
|
-spec error_codes(list(atom())) -> hocon_schema:fields().
|
||||||
error_codes(Codes) ->
|
error_codes(Codes) ->
|
||||||
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
||||||
|
|
||||||
-spec(error_codes(nonempty_list(atom()), binary()) -> hocon_schema:fields()).
|
-spec error_codes(nonempty_list(atom()), binary() | {desc, module(), term()}) ->
|
||||||
error_codes(Codes = [_ | _], MsgExample) ->
|
hocon_schema:fields().
|
||||||
|
error_codes(Codes = [_ | _], MsgDesc) ->
|
||||||
[
|
[
|
||||||
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
||||||
{message, hoconsc:mk(string(), #{
|
{message,
|
||||||
desc => <<"Details description of the error.">>,
|
hoconsc:mk(string(), #{
|
||||||
example => MsgExample
|
desc => MsgDesc
|
||||||
})}
|
})}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -143,10 +178,13 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
|
||||||
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
||||||
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
||||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
||||||
catch throw:{_, ValidErrors} ->
|
catch
|
||||||
Msg = [io_lib:format("~ts : ~p", [Key, Reason]) ||
|
throw:{_, ValidErrors} ->
|
||||||
{validation_error, #{path := Key, reason := Reason}} <- ValidErrors],
|
Msg = [
|
||||||
{400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
|
io_lib:format("~ts : ~p", [Key, Reason])
|
||||||
|
|| {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
|
||||||
|
],
|
||||||
|
{400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_and_translate(Schema, Map, Opts) ->
|
check_and_translate(Schema, Map, Opts) ->
|
||||||
|
|
@ -169,30 +207,51 @@ parse_spec_ref(Module, Path, Options) ->
|
||||||
Schema =
|
Schema =
|
||||||
try
|
try
|
||||||
erlang:apply(Module, schema, [Path])
|
erlang:apply(Module, schema, [Path])
|
||||||
catch error: Reason -> %% better error message
|
%% better error message
|
||||||
throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
|
catch
|
||||||
|
error:Reason ->
|
||||||
|
throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
|
||||||
end,
|
end,
|
||||||
{Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
|
{Specs, Refs} = maps:fold(
|
||||||
(not lists:member(Method, ?METHODS))
|
fun(Method, Meta, {Acc, RefsAcc}) ->
|
||||||
andalso throw({error, #{module => Module, path => Path, method => Method}}),
|
(not lists:member(Method, ?METHODS)) andalso
|
||||||
{Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
|
throw({error, #{module => Module, path => Path, method => Method}}),
|
||||||
{Acc#{Method => Spec}, SubRefs ++ RefsAcc}
|
{Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
|
||||||
end, {#{}, []},
|
{Acc#{Method => Spec}, SubRefs ++ RefsAcc}
|
||||||
maps:without(['operationId'], Schema)),
|
end,
|
||||||
|
{#{}, []},
|
||||||
|
maps:without(['operationId'], Schema)
|
||||||
|
),
|
||||||
{maps:get('operationId', Schema), Specs, Refs}.
|
{maps:get('operationId', Schema), Specs, Refs}.
|
||||||
|
|
||||||
check_parameters(Request, Spec, Module) ->
|
check_parameters(Request, Spec, Module) ->
|
||||||
#{bindings := Bindings, query_string := QueryStr} = Request,
|
#{bindings := Bindings, query_string := QueryStr} = Request,
|
||||||
BindingsBin = maps:fold(fun(Key, Value, Acc) ->
|
BindingsBin = maps:fold(
|
||||||
Acc#{atom_to_binary(Key) => Value}
|
fun(Key, Value, Acc) ->
|
||||||
end, #{}, Bindings),
|
Acc#{atom_to_binary(Key) => Value}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Bindings
|
||||||
|
),
|
||||||
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
|
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
|
||||||
|
|
||||||
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
||||||
check_parameter([?R_REF(LocalMod, Fields) | Spec],
|
check_parameter(
|
||||||
Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
[?R_REF(LocalMod, Fields) | Spec],
|
||||||
check_parameter([?R_REF(Module, Fields) | Spec],
|
Bindings,
|
||||||
Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
QueryStr,
|
||||||
|
LocalMod,
|
||||||
|
BindingsAcc,
|
||||||
|
QueryStrAcc
|
||||||
|
);
|
||||||
|
check_parameter(
|
||||||
|
[?R_REF(Module, Fields) | Spec],
|
||||||
|
Bindings,
|
||||||
|
QueryStr,
|
||||||
|
LocalMod,
|
||||||
|
BindingsAcc,
|
||||||
|
QueryStrAcc
|
||||||
|
) ->
|
||||||
Params = apply(Module, fields, [Fields]),
|
Params = apply(Module, fields, [Fields]),
|
||||||
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
||||||
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
|
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
|
||||||
|
|
@ -209,7 +268,7 @@ check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc,
|
||||||
Option = #{},
|
Option = #{},
|
||||||
NewQueryStr = hocon_tconf:check_plain(Schema, QueryStr, Option),
|
NewQueryStr = hocon_tconf:check_plain(Schema, QueryStr, Option),
|
||||||
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
||||||
check_parameter(Spec, Bindings, QueryStr, Module,BindingsAcc, NewQueryStrAcc)
|
check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
|
|
@ -230,21 +289,24 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
|
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
|
||||||
%% ]}
|
%% ]}
|
||||||
%% ]
|
%% ]
|
||||||
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false)when is_list(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
|
||||||
lists:foldl(fun({Name, Type}, Acc) ->
|
lists:foldl(
|
||||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
fun({Name, Type}, Acc) ->
|
||||||
maps:merge(Acc, CheckFun(Schema, Body, #{}))
|
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||||
end, #{}, Spec);
|
maps:merge(Acc, CheckFun(Schema, Body, #{}))
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Spec
|
||||||
|
);
|
||||||
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
||||||
%% #{schema => #{ type => string, format => binary}}}
|
%% #{schema => #{ type => string, format => binary}}}
|
||||||
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
|
||||||
Body.
|
Body.
|
||||||
|
|
||||||
%% tags, description, summary, security, deprecated
|
%% tags, description, summary, security, deprecated
|
||||||
meta_to_spec(Meta, Module, Options) ->
|
meta_to_spec(Meta, Module, Options) ->
|
||||||
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module),
|
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module),
|
||||||
{RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module),
|
{RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module, Options),
|
||||||
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
|
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
|
||||||
{
|
{
|
||||||
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
|
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
|
||||||
|
|
@ -258,95 +320,132 @@ to_spec(Meta, Params, RequestBody, Responses) ->
|
||||||
Spec = to_spec(Meta, Params, [], Responses),
|
Spec = to_spec(Meta, Params, [], Responses),
|
||||||
maps:put('requestBody', RequestBody, Spec).
|
maps:put('requestBody', RequestBody, Spec).
|
||||||
|
|
||||||
generate_method_desc(Spec0 = #{desc := Desc}) ->
|
generate_method_desc(Spec = #{desc := _Desc}) ->
|
||||||
Spec = maps:remove(desc, Spec0),
|
trans_description(maps:remove(desc, Spec), Spec);
|
||||||
Spec#{description => to_bin(Desc)};
|
generate_method_desc(Spec = #{description := _Desc}) ->
|
||||||
generate_method_desc(Spec = #{description := Desc}) ->
|
trans_description(Spec, Spec);
|
||||||
Spec#{description => to_bin(Desc)};
|
|
||||||
generate_method_desc(Spec) ->
|
generate_method_desc(Spec) ->
|
||||||
Spec.
|
Spec.
|
||||||
|
|
||||||
parameters(Params, Module) ->
|
parameters(Params, Module) ->
|
||||||
{SpecList, AllRefs} =
|
{SpecList, AllRefs} =
|
||||||
lists:foldl(fun(Param, {Acc, RefsAcc}) ->
|
lists:foldl(
|
||||||
case Param of
|
fun(Param, {Acc, RefsAcc}) ->
|
||||||
?REF(StructName) -> to_ref(Module, StructName, Acc, RefsAcc);
|
case Param of
|
||||||
?R_REF(RModule, StructName) -> to_ref(RModule, StructName, Acc, RefsAcc);
|
?REF(StructName) ->
|
||||||
{Name, Type} ->
|
to_ref(Module, StructName, Acc, RefsAcc);
|
||||||
In = hocon_schema:field_schema(Type, in),
|
?R_REF(RModule, StructName) ->
|
||||||
In =:= undefined andalso
|
to_ref(RModule, StructName, Acc, RefsAcc);
|
||||||
throw({error, <<"missing in:path/query field in parameters">>}),
|
{Name, Type} ->
|
||||||
Required = hocon_schema:field_schema(Type, required),
|
In = hocon_schema:field_schema(Type, in),
|
||||||
Default = hocon_schema:field_schema(Type, default),
|
In =:= undefined andalso
|
||||||
HoconType = hocon_schema:field_schema(Type, type),
|
throw({error, <<"missing in:path/query field in parameters">>}),
|
||||||
Meta = init_meta(Default),
|
Required = hocon_schema:field_schema(Type, required),
|
||||||
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
Default = hocon_schema:field_schema(Type, default),
|
||||||
Spec0 = init_prop([required | ?DEFAULT_FIELDS],
|
HoconType = hocon_schema:field_schema(Type, type),
|
||||||
#{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
|
Meta = init_meta(Default),
|
||||||
Spec1 = trans_required(Spec0, Required, In),
|
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
||||||
Spec2 = trans_desc(Spec1, Type),
|
Spec0 = init_prop(
|
||||||
{[Spec2 | Acc], Refs ++ RefsAcc}
|
[required | ?DEFAULT_FIELDS],
|
||||||
end
|
#{schema => maps:merge(ParamType, Meta), name => Name, in => In},
|
||||||
end, {[], []}, Params),
|
Type
|
||||||
|
),
|
||||||
|
Spec1 = trans_required(Spec0, Required, In),
|
||||||
|
Spec2 = trans_description(Spec1, Type),
|
||||||
|
{[Spec2 | Acc], Refs ++ RefsAcc}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{[], []},
|
||||||
|
Params
|
||||||
|
),
|
||||||
{lists:reverse(SpecList), AllRefs}.
|
{lists:reverse(SpecList), AllRefs}.
|
||||||
|
|
||||||
init_meta(undefined) -> #{};
|
init_meta(undefined) -> #{};
|
||||||
init_meta(Default) -> #{default => Default}.
|
init_meta(Default) -> #{default => Default}.
|
||||||
|
|
||||||
init_prop(Keys, Init, Type) ->
|
init_prop(Keys, Init, Type) ->
|
||||||
lists:foldl(fun(Key, Acc) ->
|
lists:foldl(
|
||||||
case hocon_schema:field_schema(Type, Key) of
|
fun(Key, Acc) ->
|
||||||
undefined -> Acc;
|
case hocon_schema:field_schema(Type, Key) of
|
||||||
Schema -> Acc#{Key => to_bin(Schema)}
|
undefined -> Acc;
|
||||||
end
|
Schema -> Acc#{Key => to_bin(Schema)}
|
||||||
end, Init, Keys).
|
end
|
||||||
|
end,
|
||||||
|
Init,
|
||||||
|
Keys
|
||||||
|
).
|
||||||
|
|
||||||
trans_required(Spec, true, _) -> Spec#{required => true};
|
trans_required(Spec, true, _) -> Spec#{required => true};
|
||||||
trans_required(Spec, _, path) -> Spec#{required => true};
|
trans_required(Spec, _, path) -> Spec#{required => true};
|
||||||
trans_required(Spec, _, _) -> Spec.
|
trans_required(Spec, _, _) -> Spec.
|
||||||
|
|
||||||
trans_desc(Init, Hocon, Func, Name) ->
|
trans_desc(Init, Hocon, Func, Name) ->
|
||||||
Spec0 = trans_desc(Init, Hocon),
|
Spec0 = trans_description(Init, Hocon),
|
||||||
case Func =:= fun hocon_schema_to_spec/2 of
|
case Func =:= fun hocon_schema_to_spec/2 of
|
||||||
true -> Spec0;
|
true ->
|
||||||
|
Spec0;
|
||||||
false ->
|
false ->
|
||||||
Spec1 = Spec0#{label => Name},
|
Spec1 = trans_label(Spec0, Hocon, Name),
|
||||||
case Spec1 of
|
case Spec1 of
|
||||||
#{description := _} -> Spec1;
|
#{description := _} -> Spec1;
|
||||||
_ -> Spec1#{description => <<Name/binary, " Description">>}
|
_ -> Spec1#{description => <<Name/binary, " Description">>}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
trans_desc(Spec, Hocon) ->
|
trans_description(Spec, Hocon) ->
|
||||||
case hocon_schema:field_schema(Hocon, desc) of
|
case trans_desc(<<"desc">>, Hocon, undefined) of
|
||||||
undefined ->
|
undefined -> Spec;
|
||||||
case hocon_schema:field_schema(Hocon, description) of
|
Value -> Spec#{description => Value}
|
||||||
undefined ->
|
|
||||||
Spec;
|
|
||||||
Desc ->
|
|
||||||
Spec#{description => to_bin(Desc)}
|
|
||||||
end;
|
|
||||||
Desc -> Spec#{description => to_bin(Desc)}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
request_body(#{content := _} = Content, _Module) -> {Content, []};
|
trans_label(Spec, Hocon, Default) ->
|
||||||
request_body([], _Module) -> {[], []};
|
Label = trans_desc(<<"label">>, Hocon, Default),
|
||||||
request_body(Schema, Module) ->
|
Spec#{label => Label}.
|
||||||
|
|
||||||
|
trans_desc(Key, Hocon, Default) ->
|
||||||
|
case resolve_desc(Key, desc_struct(Hocon)) of
|
||||||
|
undefined -> Default;
|
||||||
|
Value -> to_bin(Value)
|
||||||
|
end.
|
||||||
|
|
||||||
|
desc_struct(Hocon) ->
|
||||||
|
case hocon_schema:field_schema(Hocon, desc) of
|
||||||
|
undefined -> hocon_schema:field_schema(Hocon, description);
|
||||||
|
Struct -> Struct
|
||||||
|
end.
|
||||||
|
|
||||||
|
resolve_desc(_Key, Bin) when is_binary(Bin) -> Bin;
|
||||||
|
resolve_desc(Key, Struct) ->
|
||||||
|
{ok, #{cache := Cache, lang := Lang}} = emqx_dashboard:get_i18n(),
|
||||||
|
Desc = hocon_schema:resolve_schema(Struct, Cache),
|
||||||
|
case is_map(Desc) of
|
||||||
|
true -> emqx_map_lib:deep_get([Key, Lang], Desc, undefined);
|
||||||
|
false -> Desc
|
||||||
|
end.
|
||||||
|
|
||||||
|
request_body(#{content := _} = Content, _Module, _Options) ->
|
||||||
|
{Content, []};
|
||||||
|
request_body([], _Module, _Options) ->
|
||||||
|
{[], []};
|
||||||
|
request_body(Schema, Module, Options) ->
|
||||||
{{Props, Refs}, Examples} =
|
{{Props, Refs}, Examples} =
|
||||||
case hoconsc:is_schema(Schema) of
|
case hoconsc:is_schema(Schema) of
|
||||||
true ->
|
true ->
|
||||||
HoconSchema = hocon_schema:field_schema(Schema, type),
|
HoconSchema = hocon_schema:field_schema(Schema, type),
|
||||||
SchemaExamples = hocon_schema:field_schema(Schema, examples),
|
SchemaExamples = hocon_schema:field_schema(Schema, examples),
|
||||||
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
|
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
|
||||||
false -> {parse_object(Schema, Module, #{}), undefined}
|
false ->
|
||||||
|
{parse_object(Schema, Module, Options), undefined}
|
||||||
end,
|
end,
|
||||||
{#{<<"content">> => content(Props, Examples)},
|
{#{<<"content">> => content(Props, Examples)}, Refs}.
|
||||||
Refs}.
|
|
||||||
|
|
||||||
responses(Responses, Module, Options) ->
|
responses(Responses, Module, Options) ->
|
||||||
{Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
|
{Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
|
||||||
{Spec, Refs}.
|
{Spec, Refs}.
|
||||||
|
|
||||||
|
response(Status, ?DESC(_Mod, _Id) = Schema, {Acc, RefsAcc, Module, Options}) ->
|
||||||
|
Desc = trans_description(#{}, #{desc => Schema}),
|
||||||
|
{Acc#{integer_to_binary(Status) => Desc}, RefsAcc, Module, Options};
|
||||||
response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
|
response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
|
||||||
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
|
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
|
||||||
%% Support swagger raw object(file download).
|
%% Support swagger raw object(file download).
|
||||||
|
|
@ -359,33 +458,49 @@ response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) ->
|
||||||
SchemaToSpec = schema_converter(Options),
|
SchemaToSpec = schema_converter(Options),
|
||||||
{Spec, Refs} = SchemaToSpec(RRef, Module),
|
{Spec, Refs} = SchemaToSpec(RRef, Module),
|
||||||
Content = content(Spec),
|
Content = content(Spec),
|
||||||
{Acc#{integer_to_binary(Status) =>
|
{
|
||||||
#{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options};
|
Acc#{
|
||||||
|
integer_to_binary(Status) =>
|
||||||
|
#{<<"content">> => Content}
|
||||||
|
},
|
||||||
|
Refs ++ RefsAcc,
|
||||||
|
Module,
|
||||||
|
Options
|
||||||
|
};
|
||||||
response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
|
response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
|
||||||
case hoconsc:is_schema(Schema) of
|
case hoconsc:is_schema(Schema) of
|
||||||
true ->
|
true ->
|
||||||
Hocon = hocon_schema:field_schema(Schema, type),
|
Hocon = hocon_schema:field_schema(Schema, type),
|
||||||
Examples = hocon_schema:field_schema(Schema, examples),
|
Examples = hocon_schema:field_schema(Schema, examples),
|
||||||
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
||||||
Init = trans_desc(#{}, Schema),
|
Init = trans_description(#{}, Schema),
|
||||||
Content = content(Spec, Examples),
|
Content = content(Spec, Examples),
|
||||||
{
|
{
|
||||||
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
|
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
|
||||||
Refs ++ RefsAcc, Module, Options
|
Refs ++ RefsAcc,
|
||||||
|
Module,
|
||||||
|
Options
|
||||||
};
|
};
|
||||||
false ->
|
false ->
|
||||||
{Props, Refs} = parse_object(Schema, Module, Options),
|
{Props, Refs} = parse_object(Schema, Module, Options),
|
||||||
Init = trans_desc(#{}, Schema),
|
Init = trans_description(#{}, Schema),
|
||||||
Content = Init#{<<"content">> => content(Props)},
|
Content = Init#{<<"content">> => content(Props)},
|
||||||
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
|
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
components(Refs, Options) ->
|
components(Refs, Options) ->
|
||||||
lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
|
lists:sort(
|
||||||
components(Options, Refs, #{}, []))).
|
maps:fold(
|
||||||
|
fun(K, V, Acc) -> [#{K => V} | Acc] end,
|
||||||
|
[],
|
||||||
|
components(Options, Refs, #{}, [])
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
components(_Options, [], SpecAcc, []) -> SpecAcc;
|
components(_Options, [], SpecAcc, []) ->
|
||||||
components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []);
|
SpecAcc;
|
||||||
|
components(Options, [], SpecAcc, SubRefAcc) ->
|
||||||
|
components(Options, SubRefAcc, SpecAcc, []);
|
||||||
components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
|
components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
|
||||||
Props = hocon_schema_fields(Module, Field),
|
Props = hocon_schema_fields(Module, Field),
|
||||||
Namespace = namespace(Module),
|
Namespace = namespace(Module),
|
||||||
|
|
@ -404,7 +519,9 @@ hocon_schema_fields(Module, StructName) ->
|
||||||
case apply(Module, fields, [StructName]) of
|
case apply(Module, fields, [StructName]) of
|
||||||
#{fields := Fields, desc := _} ->
|
#{fields := Fields, desc := _} ->
|
||||||
%% evil here, as it's match hocon_schema's internal representation
|
%% evil here, as it's match hocon_schema's internal representation
|
||||||
Fields; %% TODO: make use of desc ?
|
|
||||||
|
%% TODO: make use of desc ?
|
||||||
|
Fields;
|
||||||
Other ->
|
Other ->
|
||||||
Other
|
Other
|
||||||
end.
|
end.
|
||||||
|
|
@ -415,15 +532,13 @@ hocon_schema_fields(Module, StructName) ->
|
||||||
namespace(Module) ->
|
namespace(Module) ->
|
||||||
case hocon_schema:namespace(Module) of
|
case hocon_schema:namespace(Module) of
|
||||||
undefined -> Module;
|
undefined -> Module;
|
||||||
NameSpace -> re:replace(to_bin(NameSpace), ":","-",[global])
|
NameSpace -> re:replace(to_bin(NameSpace), ":", "-", [global])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
|
||||||
[{Module, StructName}]};
|
|
||||||
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
|
||||||
[{LocalModule, StructName}]};
|
|
||||||
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||||
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
|
|
@ -435,66 +550,111 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => string, enum => Items}, []};
|
{#{type => string, enum => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
{#{<<"type">> => object,
|
{
|
||||||
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
|
#{
|
||||||
SubRefs};
|
<<"type">> => object,
|
||||||
|
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
|
||||||
|
},
|
||||||
|
SubRefs
|
||||||
|
};
|
||||||
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
|
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
|
||||||
{OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
|
{OneOf, Refs} = lists:foldl(
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
fun(Type, {Acc, RefsAcc}) ->
|
||||||
{[Schema | Acc], SubRefs ++ RefsAcc}
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
end, {[], []}, Types),
|
{[Schema | Acc], SubRefs ++ RefsAcc}
|
||||||
|
end,
|
||||||
|
{[], []},
|
||||||
|
Types
|
||||||
|
),
|
||||||
{#{<<"oneOf">> => OneOf}, Refs};
|
{#{<<"oneOf">> => OneOf}, Refs};
|
||||||
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
||||||
{#{type => string, enum => [Atom]}, []}.
|
{#{type => string, enum => [Atom]}, []}.
|
||||||
|
|
||||||
%% todo: Find a way to fetch enum value from user_id_type().
|
%% todo: Find a way to fetch enum value from user_id_type().
|
||||||
typename_to_spec("user_id_type()", _Mod) -> #{type => string, enum => [clientid, username]};
|
typename_to_spec("user_id_type()", _Mod) ->
|
||||||
typename_to_spec("term()", _Mod) -> #{type => string, example => "any"};
|
#{type => string, enum => [clientid, username]};
|
||||||
typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true};
|
typename_to_spec("term()", _Mod) ->
|
||||||
typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>};
|
#{type => string};
|
||||||
typename_to_spec("float()", _Mod) -> #{type => number, example => 3.14159};
|
typename_to_spec("boolean()", _Mod) ->
|
||||||
typename_to_spec("integer()", _Mod) -> #{type => integer, example => 100};
|
#{type => boolean};
|
||||||
typename_to_spec("non_neg_integer()", _Mod) -> #{type => integer, minimum => 1, example => 100};
|
typename_to_spec("binary()", _Mod) ->
|
||||||
typename_to_spec("number()", _Mod) -> #{type => number, example => 42};
|
#{type => string};
|
||||||
typename_to_spec("string()", _Mod) -> #{type => string, example => <<"string-example">>};
|
typename_to_spec("float()", _Mod) ->
|
||||||
typename_to_spec("atom()", _Mod) -> #{type => string, example => atom};
|
#{type => number};
|
||||||
|
typename_to_spec("integer()", _Mod) ->
|
||||||
|
#{type => integer};
|
||||||
|
typename_to_spec("non_neg_integer()", _Mod) ->
|
||||||
|
#{type => integer, minimum => 0};
|
||||||
|
typename_to_spec("pos_integer()", _Mod) ->
|
||||||
|
#{type => integer, minimum => 1};
|
||||||
|
typename_to_spec("number()", _Mod) ->
|
||||||
|
#{type => number};
|
||||||
|
typename_to_spec("string()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
typename_to_spec("atom()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
typename_to_spec("epoch_second()", _Mod) ->
|
typename_to_spec("epoch_second()", _Mod) ->
|
||||||
#{<<"oneOf">> => [
|
#{
|
||||||
#{type => integer, example => 1640995200, description => <<"epoch-second">>},
|
<<"oneOf">> => [
|
||||||
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
|
#{type => integer, example => 1640995200, description => <<"epoch-second">>},
|
||||||
};
|
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
|
||||||
typename_to_spec("epoch_millisecond()", _Mod) ->
|
]
|
||||||
#{<<"oneOf">> => [
|
|
||||||
#{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
|
|
||||||
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
|
|
||||||
};
|
};
|
||||||
typename_to_spec("duration()", _Mod) -> #{type => string, example => <<"12m">>};
|
typename_to_spec("epoch_millisecond()", _Mod) ->
|
||||||
typename_to_spec("duration_s()", _Mod) -> #{type => string, example => <<"1h">>};
|
#{
|
||||||
typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>};
|
<<"oneOf">> => [
|
||||||
typename_to_spec("percent()", _Mod) -> #{type => number, example => <<"12%">>};
|
#{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
|
||||||
typename_to_spec("file()", _Mod) -> #{type => string, example => <<"/path/to/file">>};
|
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
|
||||||
typename_to_spec("ip_port()", _Mod) -> #{type => string, example => <<"127.0.0.1:80">>};
|
]
|
||||||
|
};
|
||||||
|
typename_to_spec("duration()", _Mod) ->
|
||||||
|
#{type => string, example => <<"12m">>};
|
||||||
|
typename_to_spec("duration_s()", _Mod) ->
|
||||||
|
#{type => string, example => <<"1h">>};
|
||||||
|
typename_to_spec("duration_ms()", _Mod) ->
|
||||||
|
#{type => string, example => <<"32s">>};
|
||||||
|
typename_to_spec("percent()", _Mod) ->
|
||||||
|
#{type => number, example => <<"12%">>};
|
||||||
|
typename_to_spec("file()", _Mod) ->
|
||||||
|
#{type => string, example => <<"/path/to/file">>};
|
||||||
|
typename_to_spec("ip_port()", _Mod) ->
|
||||||
|
#{type => string, example => <<"127.0.0.1:80">>};
|
||||||
typename_to_spec("ip_ports()", _Mod) ->
|
typename_to_spec("ip_ports()", _Mod) ->
|
||||||
#{type => string, example => <<"127.0.0.1:80, 127.0.0.2:80">>};
|
#{type => string, example => <<"127.0.0.1:80, 127.0.0.2:80">>};
|
||||||
typename_to_spec("url()", _Mod) -> #{type => string, example => <<"http://127.0.0.1">>};
|
typename_to_spec("url()", _Mod) ->
|
||||||
typename_to_spec("connect_timeout()", Mod) -> typename_to_spec("timeout()", Mod);
|
#{type => string, example => <<"http://127.0.0.1">>};
|
||||||
typename_to_spec("timeout()", _Mod) -> #{<<"oneOf">> => [#{type => string, example => infinity},
|
typename_to_spec("connect_timeout()", Mod) ->
|
||||||
#{type => integer, example => 100}], example => infinity};
|
typename_to_spec("timeout()", Mod);
|
||||||
typename_to_spec("bytesize()", _Mod) -> #{type => string, example => <<"32MB">>};
|
typename_to_spec("timeout()", _Mod) ->
|
||||||
typename_to_spec("wordsize()", _Mod) -> #{type => string, example => <<"1024KB">>};
|
#{
|
||||||
typename_to_spec("map()", _Mod) -> #{type => object, example => #{}};
|
<<"oneOf">> => [
|
||||||
typename_to_spec("#{" ++ _, Mod) -> typename_to_spec("map()", Mod);
|
#{type => string, example => infinity},
|
||||||
typename_to_spec("qos()", _Mod) -> #{type => string, enum => [0, 1, 2], example => 0};
|
#{type => integer}
|
||||||
typename_to_spec("{binary(), binary()}", _Mod) -> #{type => object, example => #{}};
|
],
|
||||||
|
example => infinity
|
||||||
|
};
|
||||||
|
typename_to_spec("bytesize()", _Mod) ->
|
||||||
|
#{type => string, example => <<"32MB">>};
|
||||||
|
typename_to_spec("wordsize()", _Mod) ->
|
||||||
|
#{type => string, example => <<"1024KB">>};
|
||||||
|
typename_to_spec("map()", _Mod) ->
|
||||||
|
#{type => object, example => #{}};
|
||||||
|
typename_to_spec("#{" ++ _, Mod) ->
|
||||||
|
typename_to_spec("map()", Mod);
|
||||||
|
typename_to_spec("qos()", _Mod) ->
|
||||||
|
#{type => string, enum => [0, 1, 2]};
|
||||||
|
typename_to_spec("{binary(), binary()}", _Mod) ->
|
||||||
|
#{type => object, example => #{}};
|
||||||
typename_to_spec("comma_separated_list()", _Mod) ->
|
typename_to_spec("comma_separated_list()", _Mod) ->
|
||||||
#{type => string, example => <<"item1,item2">>};
|
#{type => string, example => <<"item1,item2">>};
|
||||||
typename_to_spec("comma_separated_atoms()", _Mod) ->
|
typename_to_spec("comma_separated_atoms()", _Mod) ->
|
||||||
#{type => string, example => <<"item1,item2">>};
|
#{type => string, example => <<"item1,item2">>};
|
||||||
typename_to_spec("pool_type()", _Mod) ->
|
typename_to_spec("pool_type()", _Mod) ->
|
||||||
#{type => string, enum => [random, hash], example => hash};
|
#{type => string, enum => [random, hash]};
|
||||||
typename_to_spec("log_level()", _Mod) ->
|
typename_to_spec("log_level()", _Mod) ->
|
||||||
#{ type => string,
|
#{
|
||||||
enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
|
type => string,
|
||||||
|
enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
|
||||||
};
|
};
|
||||||
typename_to_spec("rate()", _Mod) ->
|
typename_to_spec("rate()", _Mod) ->
|
||||||
#{type => string, example => <<"10M/s">>};
|
#{type => string, example => <<"10M/s">>};
|
||||||
|
|
@ -515,16 +675,18 @@ typename_to_spec(Name, Mod) ->
|
||||||
Spec2 = typerefl_array(Spec1, Name, Mod),
|
Spec2 = typerefl_array(Spec1, Name, Mod),
|
||||||
Spec3 = integer(Spec2, Name),
|
Spec3 = integer(Spec2, Name),
|
||||||
Spec3 =:= nomatch andalso
|
Spec3 =:= nomatch andalso
|
||||||
throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
|
throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
|
||||||
Spec3.
|
Spec3.
|
||||||
|
|
||||||
range(Name) ->
|
range(Name) ->
|
||||||
case string:split(Name, "..") of
|
case string:split(Name, "..") of
|
||||||
[MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
|
%% 1..10 1..inf -inf..10
|
||||||
|
[MinStr, MaxStr] ->
|
||||||
Schema = #{type => integer},
|
Schema = #{type => integer},
|
||||||
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
||||||
add_integer_prop(Schema1, maximum, MaxStr);
|
add_integer_prop(Schema1, maximum, MaxStr);
|
||||||
_ -> nomatch
|
_ ->
|
||||||
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Module:Type
|
%% Module:Type
|
||||||
|
|
@ -533,34 +695,39 @@ remote_module_type(nomatch, Name, Mod) ->
|
||||||
[_Module, Type] -> typename_to_spec(Type, Mod);
|
[_Module, Type] -> typename_to_spec(Type, Mod);
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
remote_module_type(Spec, _Name, _Mod) -> Spec.
|
remote_module_type(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% [string()] or [integer()] or [xxx].
|
%% [string()] or [integer()] or [xxx].
|
||||||
typerefl_array(nomatch, Name, Mod) ->
|
typerefl_array(nomatch, Name, Mod) ->
|
||||||
case string:trim(Name, leading, "[") of
|
case string:trim(Name, leading, "[") of
|
||||||
Name -> nomatch;
|
Name ->
|
||||||
|
nomatch;
|
||||||
Name1 ->
|
Name1 ->
|
||||||
case string:trim(Name1, trailing, "]") of
|
case string:trim(Name1, trailing, "]") of
|
||||||
Name1 -> notmatch;
|
Name1 ->
|
||||||
|
notmatch;
|
||||||
Name2 ->
|
Name2 ->
|
||||||
Schema = typename_to_spec(Name2, Mod),
|
Schema = typename_to_spec(Name2, Mod),
|
||||||
#{type => array, items => Schema}
|
#{type => array, items => Schema}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
typerefl_array(Spec, _Name, _Mod) -> Spec.
|
typerefl_array(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% integer(1)
|
%% integer(1)
|
||||||
integer(nomatch, Name) ->
|
integer(nomatch, Name) ->
|
||||||
case string:to_integer(Name) of
|
case string:to_integer(Name) of
|
||||||
{Int, []} -> #{type => integer, enum => [Int], example => Int, default => Int};
|
{Int, []} -> #{type => integer, enum => [Int], default => Int};
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
integer(Spec, _Name) -> Spec.
|
integer(Spec, _Name) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
add_integer_prop(Schema, Key, Value) ->
|
add_integer_prop(Schema, Key, Value) ->
|
||||||
case string:to_integer(Value) of
|
case string:to_integer(Value) of
|
||||||
{error, no_integer} -> Schema;
|
{error, no_integer} -> Schema;
|
||||||
{Int, []}when Key =:= minimum -> Schema#{Key => Int, example => Int};
|
{Int, []} when Key =:= minimum -> Schema#{Key => Int};
|
||||||
{Int, []} -> Schema#{Key => Int}
|
{Int, []} -> Schema#{Key => Int}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -571,39 +738,53 @@ to_bin(List) when is_list(List) ->
|
||||||
end;
|
end;
|
||||||
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
||||||
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
||||||
to_bin(X) -> X.
|
to_bin(X) ->
|
||||||
|
X.
|
||||||
|
|
||||||
parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
|
parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
|
||||||
{Props, Required, Refs} =
|
{Props, Required, Refs} =
|
||||||
lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
|
lists:foldl(
|
||||||
NameBin = to_bin(Name),
|
fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
|
||||||
case hoconsc:is_schema(Hocon) of
|
NameBin = to_bin(Name),
|
||||||
true ->
|
case hoconsc:is_schema(Hocon) of
|
||||||
HoconType = hocon_schema:field_schema(Hocon, type),
|
true ->
|
||||||
Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
|
HoconType = hocon_schema:field_schema(Hocon, type),
|
||||||
SchemaToSpec = schema_converter(Options),
|
Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
|
||||||
Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
|
SchemaToSpec = schema_converter(Options),
|
||||||
{Prop, Refs1} = SchemaToSpec(HoconType, Module),
|
Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
|
||||||
NewRequiredAcc =
|
{Prop, Refs1} = SchemaToSpec(HoconType, Module),
|
||||||
case is_required(Hocon) of
|
NewRequiredAcc =
|
||||||
true -> [NameBin | RequiredAcc];
|
case is_required(Hocon) of
|
||||||
false -> RequiredAcc
|
true -> [NameBin | RequiredAcc];
|
||||||
end,
|
false -> RequiredAcc
|
||||||
{[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc};
|
end,
|
||||||
false ->
|
{
|
||||||
{SubObject, SubRefs} = parse_object(Hocon, Module, Options),
|
[{NameBin, maps:merge(Prop, Init)} | Acc],
|
||||||
{[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
|
NewRequiredAcc,
|
||||||
end
|
Refs1 ++ RefsAcc
|
||||||
end, {[], [], []}, PropList),
|
};
|
||||||
|
false ->
|
||||||
|
{SubObject, SubRefs} = parse_object(Hocon, Module, Options),
|
||||||
|
{[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{[], [], []},
|
||||||
|
PropList
|
||||||
|
),
|
||||||
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
|
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
|
||||||
case Required of
|
case Required of
|
||||||
[] -> {Object, Refs};
|
[] -> {Object, Refs};
|
||||||
_ -> {maps:put(required, Required, Object), Refs}
|
_ -> {maps:put(required, Required, Object), Refs}
|
||||||
end;
|
end;
|
||||||
parse_object(Other, Module, Options) ->
|
parse_object(Other, Module, Options) ->
|
||||||
erlang:throw({error,
|
erlang:throw(
|
||||||
#{msg => <<"Object only supports not empty proplists">>,
|
{error, #{
|
||||||
args => Other, module => Module, options => Options}}).
|
msg => <<"Object only supports not empty proplists">>,
|
||||||
|
args => Other,
|
||||||
|
module => Module,
|
||||||
|
options => Options
|
||||||
|
}}
|
||||||
|
).
|
||||||
|
|
||||||
is_required(Hocon) ->
|
is_required(Hocon) ->
|
||||||
hocon_schema:field_schema(Hocon, required) =:= true.
|
hocon_schema:field_schema(Hocon, required) =:= true.
|
||||||
|
|
|
||||||
|
|
@ -75,18 +75,13 @@ end_per_suite(_Config) ->
|
||||||
mria:stop().
|
mria:stop().
|
||||||
|
|
||||||
set_special_configs(emqx_management) ->
|
set_special_configs(emqx_management) ->
|
||||||
Listeners = [#{protocol => http, port => 8081}],
|
Listeners = #{http => #{port => 8081}},
|
||||||
Config = #{listeners => Listeners,
|
Config = #{listeners => Listeners,
|
||||||
applications => [#{id => "admin", secret => "public"}]},
|
applications => [#{id => "admin", secret => "public"}]},
|
||||||
emqx_config:put([emqx_management], Config),
|
emqx_config:put([emqx_management], Config),
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Listeners = [#{protocol => http, port => 18083}],
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
Config = #{listeners => Listeners,
|
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,22 @@ set_default_config() ->
|
||||||
set_default_config(<<"admin">>).
|
set_default_config(<<"admin">>).
|
||||||
|
|
||||||
set_default_config(DefaultUsername) ->
|
set_default_config(DefaultUsername) ->
|
||||||
Config = #{listeners => [#{protocol => http,
|
Config = #{listeners => #{
|
||||||
port => 18083}],
|
http => #{
|
||||||
|
port => 18083
|
||||||
|
}
|
||||||
|
},
|
||||||
default_username => DefaultUsername,
|
default_username => DefaultUsername,
|
||||||
default_password => <<"public">>
|
default_password => <<"public">>,
|
||||||
|
i18n_lang => en
|
||||||
},
|
},
|
||||||
emqx_config:put([dashboard], Config),
|
emqx_config:put([dashboard], Config),
|
||||||
|
I18nFile = filename:join([
|
||||||
|
filename:dirname(code:priv_dir(emqx_dashboard)),
|
||||||
|
"etc",
|
||||||
|
"i18n.conf.all"
|
||||||
|
]),
|
||||||
|
application:set_env(emqx_dashboard, i18n_file, I18nFile),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
request(Method, Url) ->
|
request(Method, Url) ->
|
||||||
|
|
|
||||||
|
|
@ -35,15 +35,7 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -35,15 +35,7 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,7 @@ end_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||||
|
-export([init_per_suite/1, end_per_suite/1]).
|
||||||
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
||||||
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
||||||
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
||||||
|
|
@ -26,6 +27,27 @@ groups() -> [
|
||||||
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
mria:start(),
|
||||||
|
application:load(emqx_dashboard),
|
||||||
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
|
emqx_dashboard:init_i18n(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_in_path(_Config) ->
|
t_in_path(_Config) ->
|
||||||
Expect =
|
Expect =
|
||||||
[#{description => <<"Indicates which sorts of issues to return">>,
|
[#{description => <<"Indicates which sorts of issues to return">>,
|
||||||
|
|
@ -40,9 +62,9 @@ t_in_query(_Config) ->
|
||||||
Expect =
|
Expect =
|
||||||
[#{description => <<"results per page (max 100)">>,
|
[#{description => <<"results per page (max 100)">>,
|
||||||
example => 1, in => query, name => per_page,
|
example => 1, in => query, name => per_page,
|
||||||
schema => #{example => 1, maximum => 100, minimum => 1, type => integer}},
|
schema => #{maximum => 100, minimum => 1, type => integer}},
|
||||||
#{description => <<"QOS">>, in => query, name => qos,
|
#{description => <<"QOS">>, in => query, name => qos,
|
||||||
schema => #{enum => [0, 1, 2], example => 0, type => string}}],
|
schema => #{enum => [0, 1, 2], type => string}}],
|
||||||
validate("/test/in/query", Expect),
|
validate("/test/in/query", Expect),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -74,12 +96,12 @@ t_public_ref(_Config) ->
|
||||||
], Refs),
|
], Refs),
|
||||||
ExpectRefs = [
|
ExpectRefs = [
|
||||||
#{<<"public.limit">> => #{description => <<"Results per page(max 1000)">>,
|
#{<<"public.limit">> => #{description => <<"Results per page(max 1000)">>,
|
||||||
example => 50,in => query,name => limit,
|
in => query,name => limit, example => 50,
|
||||||
schema => #{default => 100,example => 1,maximum => 1000,
|
schema => #{default => 100,maximum => 1000,
|
||||||
minimum => 1,type => integer}}},
|
minimum => 1,type => integer}}},
|
||||||
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
|
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
|
||||||
example => 1,in => query,name => page,
|
in => query,name => page,example => 1,
|
||||||
schema => #{default => 1,example => 100,type => integer}}}],
|
schema => #{default => 1,minimum => 1,type => integer}}}],
|
||||||
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})),
|
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -92,11 +114,11 @@ t_in_mix(_Config) ->
|
||||||
example => <<"12m">>,in => path,name => state,required => true,
|
example => <<"12m">>,in => path,name => state,required => true,
|
||||||
schema => #{example => <<"1h">>,type => string}},
|
schema => #{example => <<"1h">>,type => string}},
|
||||||
#{example => 10,in => query,name => per_page, required => false,
|
#{example => 10,in => query,name => per_page, required => false,
|
||||||
schema => #{default => 5,example => 1,maximum => 50,minimum => 1, type => integer}},
|
schema => #{default => 5,maximum => 50,minimum => 1, type => integer}},
|
||||||
#{in => query,name => is_admin, schema => #{example => true,type => boolean}},
|
#{in => query,name => is_admin, schema => #{type => boolean}},
|
||||||
#{in => query,name => timeout,
|
#{in => query,name => timeout,
|
||||||
schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
|
schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
|
||||||
#{example => 30,maximum => 60,minimum => 30, type => integer}]}}],
|
#{maximum => 60,minimum => 30, type => integer}]}}],
|
||||||
ExpectMeta = #{
|
ExpectMeta = #{
|
||||||
tags => [tags, good],
|
tags => [tags, good],
|
||||||
description => <<"good description">>,
|
description => <<"good description">>,
|
||||||
|
|
@ -116,15 +138,15 @@ t_without_in(_Config) ->
|
||||||
t_require(_Config) ->
|
t_require(_Config) ->
|
||||||
ExpectSpec = [#{
|
ExpectSpec = [#{
|
||||||
in => query,name => userid, required => false,
|
in => query,name => userid, required => false,
|
||||||
schema => #{example => <<"binary-example">>, type => string}}],
|
schema => #{type => string}}],
|
||||||
validate("/required/false", ExpectSpec),
|
validate("/required/false", ExpectSpec),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_nullable(_Config) ->
|
t_nullable(_Config) ->
|
||||||
NullableFalse = [#{in => query,name => userid, required => true,
|
NullableFalse = [#{in => query,name => userid, required => true,
|
||||||
schema => #{example => <<"binary-example">>, type => string}}],
|
schema => #{type => string}}],
|
||||||
NullableTrue = [#{in => query,name => userid,
|
NullableTrue = [#{in => query,name => userid,
|
||||||
schema => #{example => <<"binary-example">>, type => string}, required => false}],
|
schema => #{type => string}, required => false}],
|
||||||
validate("/nullable/false", NullableFalse),
|
validate("/nullable/false", NullableFalse),
|
||||||
validate("/nullable/true", NullableTrue),
|
validate("/nullable/true", NullableTrue),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,36 @@
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
%% API
|
-compile(nowarn_export_all).
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-compile(export_all).
|
||||||
-export([t_object/1, t_nest_object/1, t_api_spec/1,
|
|
||||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_sub_fields/1
|
|
||||||
]).
|
|
||||||
-export([
|
|
||||||
t_object_trans/1, t_object_notrans/1, t_nest_object_trans/1, t_local_ref_trans/1,
|
|
||||||
t_remote_ref_trans/1, t_nest_ref_trans/1,
|
|
||||||
t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
|
|
||||||
t_ref_trans_error/1, t_object_trans_error/1
|
|
||||||
]).
|
|
||||||
-export([all/0, suite/0, groups/0]).
|
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-import(hoconsc, [mk/2]).
|
-import(hoconsc, [mk/2]).
|
||||||
|
|
||||||
all() -> [{group, spec}, {group, validation}].
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
init_per_suite(Config) ->
|
||||||
groups() -> [
|
mria:start(),
|
||||||
{spec, [parallel], [
|
application:load(emqx_dashboard),
|
||||||
t_api_spec, t_object, t_nest_object,
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
emqx_dashboard:init_i18n(),
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
|
Config.
|
||||||
{validation, [parallel],
|
|
||||||
[
|
set_special_configs(emqx_dashboard) ->
|
||||||
t_object_trans, t_object_notrans, t_local_ref_trans, t_remote_ref_trans,
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
|
ok;
|
||||||
t_ref_trans_error, t_object_trans_error
|
set_special_configs(_) ->
|
||||||
%% t_nest_object_trans,
|
ok.
|
||||||
]}
|
|
||||||
].
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_object(_Config) ->
|
t_object(_Config) ->
|
||||||
Spec = #{
|
Spec = #{
|
||||||
|
|
@ -48,7 +43,7 @@ t_object(_Config) ->
|
||||||
#{required => [<<"timeout">>, <<"per_page">>],
|
#{required => [<<"timeout">>, <<"per_page">>],
|
||||||
<<"properties">> =>[
|
<<"properties">> =>[
|
||||||
{<<"per_page">>, #{description => <<"good per page desc">>,
|
{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string},
|
[#{example => <<"1h">>, type => string},
|
||||||
#{enum => [infinity], type => string}]}},
|
#{enum => [infinity], type => string}]}},
|
||||||
|
|
@ -69,13 +64,13 @@ t_nest_object(_Config) ->
|
||||||
#{required => [<<"timeout">>],
|
#{required => [<<"timeout">>],
|
||||||
<<"properties">> =>
|
<<"properties">> =>
|
||||||
[{<<"per_page">>, #{description => <<"good per page desc">>,
|
[{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string},
|
[#{example => <<"1h">>, type => string},
|
||||||
#{enum => [infinity], type => string}]}},
|
#{enum => [infinity], type => string}]}},
|
||||||
{<<"nest_object">>,
|
{<<"nest_object">>,
|
||||||
#{<<"properties">> =>
|
#{<<"properties">> =>
|
||||||
[{<<"good_nest_1">>, #{example => 100, type => integer}},
|
[{<<"good_nest_1">>, #{type => integer}},
|
||||||
{<<"good_nest_2">>, #{<<"$ref">> =>
|
{<<"good_nest_2">>, #{<<"$ref">> =>
|
||||||
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
||||||
<<"type">> => object}},
|
<<"type">> => object}},
|
||||||
|
|
@ -110,7 +105,7 @@ t_remote_ref(_Config) ->
|
||||||
{_, Components} = validate("/ref/remote", Spec, Refs),
|
{_, Components} = validate("/ref/remote", Spec, Refs),
|
||||||
ExpectComponents = [
|
ExpectComponents = [
|
||||||
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"properties">> => [
|
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"properties">> => [
|
||||||
{<<"page">>, #{description => <<"good page">>,example => 1,
|
{<<"page">>, #{description => <<"good page">>,
|
||||||
maximum => 100,minimum => 1,type => integer}},
|
maximum => 100,minimum => 1,type => integer}},
|
||||||
{<<"another_ref">>, #{<<"$ref">> =>
|
{<<"another_ref">>, #{<<"$ref">> =>
|
||||||
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
|
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
|
||||||
|
|
@ -141,8 +136,7 @@ t_nest_ref(_Config) ->
|
||||||
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>,
|
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>,
|
||||||
example => <<"127.0.0.1:80">>,type => string}},
|
example => <<"127.0.0.1:80">>,type => string}},
|
||||||
{<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
|
{<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
|
||||||
{<<"tag">>, #{description => <<"tag">>,
|
{<<"tag">>, #{description => <<"tag">>,type => string}}],
|
||||||
example => <<"binary-example">>,type => string}}],
|
|
||||||
<<"type">> => object}}]),
|
<<"type">> => object}}]),
|
||||||
{_, Components} = validate("/ref/nest/ref", Spec, Refs),
|
{_, Components} = validate("/ref/nest/ref", Spec, Refs),
|
||||||
?assertEqual(ExpectComponents, Components),
|
?assertEqual(ExpectComponents, Components),
|
||||||
|
|
@ -186,7 +180,7 @@ t_ref_array_with_key(_Config) ->
|
||||||
<<"type">> => object, <<"properties">> =>
|
<<"type">> => object, <<"properties">> =>
|
||||||
[
|
[
|
||||||
{<<"per_page">>, #{description => <<"good per page desc">>,
|
{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string},
|
[#{example => <<"1h">>, type => string},
|
||||||
#{enum => [infinity], type => string}]}},
|
#{enum => [infinity], type => string}]}},
|
||||||
|
|
@ -281,7 +275,7 @@ t_object_notrans(_Config) ->
|
||||||
?assertEqual(Body, ActualBody),
|
?assertEqual(Body, ActualBody),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_nest_object_trans(_Config) ->
|
todo_t_nest_object_check(_Config) ->
|
||||||
Path = "/nest/object",
|
Path = "/nest/object",
|
||||||
Body = #{
|
Body = #{
|
||||||
<<"timeout">> => "10m",
|
<<"timeout">> => "10m",
|
||||||
|
|
@ -306,7 +300,7 @@ t_nest_object_trans(_Config) ->
|
||||||
body => #{<<"per_page">> => 10,
|
body => #{<<"per_page">> => 10,
|
||||||
<<"timeout">> => 600}
|
<<"timeout">> => 600}
|
||||||
},
|
},
|
||||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
{ok, NewRequest} = check_requestBody(Path, Body),
|
||||||
?assertEqual(Expect, NewRequest),
|
?assertEqual(Expect, NewRequest),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -487,6 +481,10 @@ trans_requestBody(Path, Body) ->
|
||||||
trans_requestBody(Path, Body,
|
trans_requestBody(Path, Body,
|
||||||
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
|
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
|
||||||
|
|
||||||
|
check_requestBody(Path, Body) ->
|
||||||
|
trans_requestBody(Path, Body,
|
||||||
|
fun emqx_dashboard_swagger:filter_check_request/2).
|
||||||
|
|
||||||
trans_requestBody(Path, Body, Filter) ->
|
trans_requestBody(Path, Body, Filter) ->
|
||||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||||
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,31 @@
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-import(hoconsc, [mk/2]).
|
-import(hoconsc, [mk/2]).
|
||||||
|
|
||||||
-export([all/0, suite/0, groups/0]).
|
-compile(nowarn_export_all).
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-compile(export_all).
|
||||||
-export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1,
|
|
||||||
t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1, t_complicated_type/1,
|
|
||||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1, t_sub_fields/1,
|
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
|
|
||||||
|
|
||||||
all() -> [{group, spec}].
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
|
||||||
groups() -> [
|
init_per_suite(Config) ->
|
||||||
{spec, [parallel], [
|
mria:start(),
|
||||||
t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_complicated_type,
|
application:load(emqx_dashboard),
|
||||||
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref, t_sub_fields,
|
emqx_dashboard:init_i18n(),
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
|
Config.
|
||||||
].
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_simple_binary(_config) ->
|
t_simple_binary(_config) ->
|
||||||
Path = "/simple/bin",
|
Path = "/simple/bin",
|
||||||
|
|
@ -41,7 +50,7 @@ t_object(_config) ->
|
||||||
#{<<"schema">> => #{required => [<<"timeout">>, <<"per_page">>],
|
#{<<"schema">> => #{required => [<<"timeout">>, <<"per_page">>],
|
||||||
<<"properties">> => [
|
<<"properties">> => [
|
||||||
{<<"per_page">>, #{description => <<"good per page desc">>,
|
{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||||
{<<"inner_ref">>, #{<<"$ref">> =>
|
{<<"inner_ref">>, #{<<"$ref">> =>
|
||||||
|
|
@ -58,16 +67,15 @@ t_error(_Config) ->
|
||||||
<<"properties">> =>
|
<<"properties">> =>
|
||||||
[
|
[
|
||||||
{<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
|
{<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
|
||||||
{<<"message">>, #{description => <<"Details description of the error.">>,
|
{<<"message">>, #{description => <<"Bad request desc">>, type => string}}]
|
||||||
example => <<"Bad request desc">>, type => string}}]
|
|
||||||
}}}},
|
}}}},
|
||||||
Error404 = #{<<"content">> =>
|
Error404 = #{<<"content">> =>
|
||||||
#{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
|
#{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
|
||||||
<<"properties">> =>
|
<<"properties">> =>
|
||||||
[
|
[
|
||||||
{<<"code">>, #{enum => ['Not-Found'], type => string}},
|
{<<"code">>, #{enum => ['Not-Found'], type => string}},
|
||||||
{<<"message">>, #{description => <<"Details description of the error.">>,
|
{<<"message">>, #{
|
||||||
example => <<"Error code to troubleshoot problems.">>, type => string}}]
|
description => <<"Error code to troubleshoot problems.">>, type => string}}]
|
||||||
}}}},
|
}}}},
|
||||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
|
||||||
?assertEqual(test, OperationId),
|
?assertEqual(test, OperationId),
|
||||||
|
|
@ -83,12 +91,12 @@ t_nest_object(_Config) ->
|
||||||
Object =
|
Object =
|
||||||
#{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
#{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||||
#{required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
#{required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
||||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1,
|
{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||||
{<<"nest_object">>, #{<<"type">> => object, <<"properties">> => [
|
{<<"nest_object">>, #{<<"type">> => object, <<"properties">> => [
|
||||||
{<<"good_nest_1">>, #{example => 100, type => integer}},
|
{<<"good_nest_1">>, #{type => integer}},
|
||||||
{<<"good_nest_2">>, #{<<"$ref">> =>
|
{<<"good_nest_2">>, #{<<"$ref">> =>
|
||||||
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}
|
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}
|
||||||
}]}},
|
}]}},
|
||||||
|
|
@ -176,16 +184,15 @@ t_complicated_type(_Config) ->
|
||||||
Object = #{<<"content">> => #{<<"application/json">> =>
|
Object = #{<<"content">> => #{<<"application/json">> =>
|
||||||
#{<<"schema">> => #{<<"properties">> =>
|
#{<<"schema">> => #{<<"properties">> =>
|
||||||
[
|
[
|
||||||
{<<"no_neg_integer">>, #{example => 100, minimum => 1, type => integer}},
|
{<<"no_neg_integer">>, #{minimum => 0, type => integer}},
|
||||||
{<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
|
{<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
|
||||||
{<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
|
{<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
|
||||||
{<<"connect_timeout">>, #{example => infinity, <<"oneOf">> => [
|
{<<"connect_timeout">>, #{example => infinity, <<"oneOf">> => [
|
||||||
#{example => infinity, type => string},
|
#{example => infinity, type => string},
|
||||||
#{example => 100, type => integer}]}},
|
#{type => integer}]}},
|
||||||
{<<"pool_type">>, #{enum => [random, hash], example => hash, type => string}},
|
{<<"pool_type">>, #{enum => [random, hash], type => string}},
|
||||||
{<<"timeout">>, #{example => infinity,
|
{<<"timeout">>, #{example => infinity,
|
||||||
<<"oneOf">> =>
|
<<"oneOf">> => [#{example => infinity, type => string}, #{type => integer}]}},
|
||||||
[#{example => infinity, type => string}, #{example => 100, type => integer}]}},
|
|
||||||
{<<"bytesize">>, #{example => <<"32MB">>, type => string}},
|
{<<"bytesize">>, #{example => <<"32MB">>, type => string}},
|
||||||
{<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
|
{<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
|
||||||
{<<"maps">>, #{example => #{}, type => object}},
|
{<<"maps">>, #{example => #{}, type => object}},
|
||||||
|
|
@ -194,7 +201,7 @@ t_complicated_type(_Config) ->
|
||||||
{<<"log_level">>,
|
{<<"log_level">>,
|
||||||
#{enum => [debug, info, notice, warning, error, critical, alert, emergency, all],
|
#{enum => [debug, info, notice, warning, error, critical, alert, emergency, all],
|
||||||
type => string}},
|
type => string}},
|
||||||
{<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}}
|
{<<"fix_integer">>, #{default => 100, enum => [100],type => integer}}
|
||||||
],
|
],
|
||||||
<<"type">> => object}}}},
|
<<"type">> => object}}}},
|
||||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
|
||||||
|
|
@ -210,17 +217,16 @@ t_ref_array_with_key(_Config) ->
|
||||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||||
required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
||||||
{<<"per_page">>, #{description => <<"good per page desc">>,
|
{<<"per_page">>, #{description => <<"good per page desc">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||||
{<<"assert">>, #{description => <<"money">>, example => 3.14159, type => number}},
|
{<<"assert">>, #{description => <<"money">>, type => number}},
|
||||||
{<<"number_ex">>, #{description => <<"number example">>,
|
{<<"number_ex">>, #{description => <<"number example">>, type => number}},
|
||||||
example => 42, type => number}},
|
|
||||||
{<<"percent_ex">>, #{description => <<"percent example">>,
|
{<<"percent_ex">>, #{description => <<"percent example">>,
|
||||||
example => <<"12%">>, type => number}},
|
example => <<"12%">>, type => number}},
|
||||||
{<<"duration_ms_ex">>, #{description => <<"duration ms example">>,
|
{<<"duration_ms_ex">>, #{description => <<"duration ms example">>,
|
||||||
example => <<"32s">>, type => string}},
|
example => <<"32s">>, type => string}},
|
||||||
{<<"atom_ex">>, #{description => <<"atom ex">>, example => atom, type => string}},
|
{<<"atom_ex">>, #{description => <<"atom ex">>, type => string}},
|
||||||
{<<"array_refs">>, #{items => #{<<"$ref">> =>
|
{<<"array_refs">>, #{items => #{<<"$ref">> =>
|
||||||
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}, type => array}}
|
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}, type => array}}
|
||||||
]}
|
]}
|
||||||
|
|
@ -245,12 +251,12 @@ t_hocon_schema_function(_Config) ->
|
||||||
#{<<"emqx_swagger_remote_schema.ref1">> => #{<<"type">> => object,
|
#{<<"emqx_swagger_remote_schema.ref1">> => #{<<"type">> => object,
|
||||||
<<"properties">> => [
|
<<"properties">> => [
|
||||||
{<<"protocol">>, #{enum => [http, https], type => string}},
|
{<<"protocol">>, #{enum => [http, https], type => string}},
|
||||||
{<<"port">>, #{default => 18083, example => 100, type => integer}}]
|
{<<"port">>, #{default => 18083, type => integer}}]
|
||||||
}},
|
}},
|
||||||
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"type">> => object,
|
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"type">> => object,
|
||||||
<<"properties">> => [
|
<<"properties">> => [
|
||||||
{<<"page">>, #{description => <<"good page">>,
|
{<<"page">>, #{description => <<"good page">>,
|
||||||
example => 1, maximum => 100, minimum => 1, type => integer}},
|
maximum => 100, minimum => 1, type => integer}},
|
||||||
{<<"another_ref">>, #{<<"$ref">> =>
|
{<<"another_ref">>, #{<<"$ref">> =>
|
||||||
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}
|
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}
|
||||||
]
|
]
|
||||||
|
|
@ -270,9 +276,9 @@ t_hocon_schema_function(_Config) ->
|
||||||
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]},
|
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]},
|
||||||
type => array}},
|
type => array}},
|
||||||
{<<"default_username">>,
|
{<<"default_username">>,
|
||||||
#{default => <<"admin">>, example => <<"string-example">>, type => string}},
|
#{default => <<"admin">>, type => string}},
|
||||||
{<<"default_password">>,
|
{<<"default_password">>,
|
||||||
#{default => <<"public">>, example => <<"string-example">>, type => string}},
|
#{default => <<"public">>, type => string}},
|
||||||
{<<"sample_interval">>,
|
{<<"sample_interval">>,
|
||||||
#{default => <<"10s">>, example => <<"1h">>, type => string}},
|
#{default => <<"10s">>, example => <<"1h">>, type => string}},
|
||||||
{<<"token_expired_time">>,
|
{<<"token_expired_time">>,
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ fields(node_metrics) ->
|
||||||
fields(node_status) ->
|
fields(node_status) ->
|
||||||
[
|
[
|
||||||
{node, mk(string(), #{})},
|
{node, mk(string(), #{})},
|
||||||
{status, mk(enum([running, waiting, stopped, error]), #{})}
|
{status, mk(enum([connected, connecting, unconnected, disable, error]), #{})}
|
||||||
];
|
];
|
||||||
fields(hook_info) ->
|
fields(hook_info) ->
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -63,21 +63,25 @@
|
||||||
-export([roots/0]).
|
-export([roots/0]).
|
||||||
|
|
||||||
%% Running servers
|
%% Running servers
|
||||||
-type state() :: #{
|
-type state() :: #{servers := servers()}.
|
||||||
running := servers(),
|
|
||||||
%% Wait to reload servers
|
|
||||||
waiting := servers(),
|
|
||||||
%% Marked stopped servers
|
|
||||||
stopped := servers(),
|
|
||||||
%% Timer references
|
|
||||||
trefs := map(),
|
|
||||||
orders := orders()
|
|
||||||
}.
|
|
||||||
|
|
||||||
-type server_name() :: binary().
|
|
||||||
-type servers() :: #{server_name() => server()}.
|
|
||||||
-type server() :: server_options().
|
|
||||||
-type server_options() :: map().
|
-type server_options() :: map().
|
||||||
|
-type server_name() :: binary().
|
||||||
|
|
||||||
|
-type status() ::
|
||||||
|
connected
|
||||||
|
| connecting
|
||||||
|
| unconnected
|
||||||
|
| disable.
|
||||||
|
|
||||||
|
-type server() :: #{
|
||||||
|
status := status(),
|
||||||
|
timer := reference(),
|
||||||
|
order := integer(),
|
||||||
|
%% include the content of server_options
|
||||||
|
atom() => any()
|
||||||
|
}.
|
||||||
|
-type servers() :: #{server_name() => server()}.
|
||||||
|
|
||||||
-type position() ::
|
-type position() ::
|
||||||
front
|
front
|
||||||
|
|
@ -85,19 +89,10 @@
|
||||||
| {before, binary()}
|
| {before, binary()}
|
||||||
| {'after', binary()}.
|
| {'after', binary()}.
|
||||||
|
|
||||||
-type orders() :: #{server_name() => integer()}.
|
|
||||||
|
|
||||||
-type server_info() :: #{
|
|
||||||
name := server_name(),
|
|
||||||
status := running | waiting | stopped,
|
|
||||||
|
|
||||||
atom() => term()
|
|
||||||
}.
|
|
||||||
|
|
||||||
-define(DEFAULT_TIMEOUT, 60000).
|
-define(DEFAULT_TIMEOUT, 60000).
|
||||||
-define(REFRESH_INTERVAL, timer:seconds(5)).
|
-define(REFRESH_INTERVAL, timer:seconds(5)).
|
||||||
|
|
||||||
-export_type([servers/0, server/0, server_info/0]).
|
-export_type([servers/0, server/0]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
|
|
@ -113,7 +108,7 @@ start_link() ->
|
||||||
list() ->
|
list() ->
|
||||||
call(list).
|
call(list).
|
||||||
|
|
||||||
-spec lookup(server_name()) -> not_found | server_info().
|
-spec lookup(server_name()) -> not_found | server().
|
||||||
lookup(Name) ->
|
lookup(Name) ->
|
||||||
call({lookup, Name}).
|
call({lookup, Name}).
|
||||||
|
|
||||||
|
|
@ -195,104 +190,56 @@ init([]) ->
|
||||||
process_flag(trap_exit, true),
|
process_flag(trap_exit, true),
|
||||||
emqx_conf:add_handler([exhook, servers], ?MODULE),
|
emqx_conf:add_handler([exhook, servers], ?MODULE),
|
||||||
ServerL = emqx:get_config([exhook, servers]),
|
ServerL = emqx:get_config([exhook, servers]),
|
||||||
{Waiting, Running, Stopped} = load_all_servers(ServerL),
|
Servers = load_all_servers(ServerL),
|
||||||
Orders = reorder(ServerL),
|
Servers2 = reorder(ServerL, Servers),
|
||||||
refresh_tick(),
|
refresh_tick(),
|
||||||
{ok,
|
{ok, #{servers => Servers2}}.
|
||||||
ensure_reload_timer(
|
|
||||||
#{
|
|
||||||
waiting => Waiting,
|
|
||||||
running => Running,
|
|
||||||
stopped => Stopped,
|
|
||||||
trefs => #{},
|
|
||||||
orders => Orders
|
|
||||||
}
|
|
||||||
)}.
|
|
||||||
|
|
||||||
-spec load_all_servers(list(server_options())) -> {servers(), servers(), servers()}.
|
-spec load_all_servers(list(server_options())) -> servers().
|
||||||
load_all_servers(ServerL) ->
|
load_all_servers(ServerL) ->
|
||||||
load_all_servers(ServerL, #{}, #{}, #{}).
|
load_all_servers(ServerL, #{}).
|
||||||
|
|
||||||
load_all_servers([#{name := Name} = Options | More], Waiting, Running, Stopped) ->
|
load_all_servers([#{name := Name} = Options | More], Servers) ->
|
||||||
case emqx_exhook_server:load(Name, Options) of
|
{_, Server} = do_load_server(options_to_server(Options)),
|
||||||
{ok, ServerState} ->
|
load_all_servers(More, Servers#{Name => Server});
|
||||||
save(Name, ServerState),
|
load_all_servers([], Servers) ->
|
||||||
load_all_servers(More, Waiting, Running#{Name => Options}, Stopped);
|
Servers.
|
||||||
{error, _} ->
|
|
||||||
load_all_servers(More, Waiting#{Name => Options}, Running, Stopped);
|
|
||||||
disable ->
|
|
||||||
load_all_servers(More, Waiting, Running, Stopped#{Name => Options})
|
|
||||||
end;
|
|
||||||
load_all_servers([], Waiting, Running, Stopped) ->
|
|
||||||
{Waiting, Running, Stopped}.
|
|
||||||
|
|
||||||
handle_call(
|
handle_call(
|
||||||
list,
|
list,
|
||||||
_From,
|
_From,
|
||||||
State = #{
|
State = #{servers := Servers}
|
||||||
running := Running,
|
|
||||||
waiting := Waiting,
|
|
||||||
stopped := Stopped,
|
|
||||||
orders := Orders
|
|
||||||
}
|
|
||||||
) ->
|
) ->
|
||||||
R = get_servers_info(running, Running),
|
Infos = get_servers_info(Servers),
|
||||||
W = get_servers_info(waiting, Waiting),
|
OrderServers = sort_name_by_order(Infos, Servers),
|
||||||
S = get_servers_info(stopped, Stopped),
|
|
||||||
|
|
||||||
Servers = R ++ W ++ S,
|
|
||||||
OrderServers = sort_name_by_order(Servers, Orders),
|
|
||||||
|
|
||||||
{reply, OrderServers, State};
|
{reply, OrderServers, State};
|
||||||
handle_call(
|
handle_call(
|
||||||
{update_config, {move, _Name, _Position}, NewConfL},
|
{update_config, {move, _Name, _Position}, NewConfL},
|
||||||
_From,
|
_From,
|
||||||
State
|
#{servers := Servers} = State
|
||||||
) ->
|
) ->
|
||||||
Orders = reorder(NewConfL),
|
Servers2 = reorder(NewConfL, Servers),
|
||||||
{reply, ok, State#{orders := Orders}};
|
{reply, ok, State#{servers := Servers2}};
|
||||||
handle_call({update_config, {delete, ToDelete}, _}, _From, State) ->
|
handle_call({update_config, {delete, ToDelete}, _}, _From, State) ->
|
||||||
{ok,
|
|
||||||
#{
|
|
||||||
orders := Orders,
|
|
||||||
stopped := Stopped
|
|
||||||
} = State2} = do_unload_server(ToDelete, State),
|
|
||||||
|
|
||||||
State3 = State2#{
|
|
||||||
stopped := maps:remove(ToDelete, Stopped),
|
|
||||||
orders := maps:remove(ToDelete, Orders)
|
|
||||||
},
|
|
||||||
|
|
||||||
emqx_exhook_metrics:on_server_deleted(ToDelete),
|
emqx_exhook_metrics:on_server_deleted(ToDelete),
|
||||||
|
|
||||||
{reply, ok, State3};
|
#{servers := Servers} = State2 = do_unload_server(ToDelete, State),
|
||||||
|
|
||||||
|
Servers2 = maps:remove(ToDelete, Servers),
|
||||||
|
|
||||||
|
{reply, ok, update_servers(Servers2, State2)};
|
||||||
handle_call(
|
handle_call(
|
||||||
{update_config, {add, RawConf}, NewConfL},
|
{update_config, {add, RawConf}, NewConfL},
|
||||||
_From,
|
_From,
|
||||||
#{running := Running, waiting := Waitting, stopped := Stopped} = State
|
#{servers := Servers} = State
|
||||||
) ->
|
) ->
|
||||||
{_, #{name := Name} = Conf} = emqx_config:check_config(?MODULE, RawConf),
|
{_, #{name := Name} = Conf} = emqx_config:check_config(?MODULE, RawConf),
|
||||||
|
{Result, Server} = do_load_server(options_to_server(Conf)),
|
||||||
case emqx_exhook_server:load(Name, Conf) of
|
Servers2 = Servers#{Name => Server},
|
||||||
{ok, ServerState} ->
|
Servers3 = reorder(NewConfL, Servers2),
|
||||||
save(Name, ServerState),
|
{reply, Result, State#{servers := Servers3}};
|
||||||
State2 = State#{running := Running#{Name => Conf}};
|
|
||||||
{error, _} ->
|
|
||||||
StateT = State#{waiting := Waitting#{Name => Conf}},
|
|
||||||
State2 = ensure_reload_timer(StateT);
|
|
||||||
disable ->
|
|
||||||
State2 = State#{stopped := Stopped#{Name => Conf}}
|
|
||||||
end,
|
|
||||||
Orders = reorder(NewConfL),
|
|
||||||
{reply, ok, State2#{orders := Orders}};
|
|
||||||
handle_call({lookup, Name}, _From, State) ->
|
handle_call({lookup, Name}, _From, State) ->
|
||||||
case where_is_server(Name, State) of
|
{reply, where_is_server(Name, State), State};
|
||||||
not_found ->
|
|
||||||
Result = not_found;
|
|
||||||
{Where, #{Name := Conf}} ->
|
|
||||||
Result = maps:merge(Conf, #{status => Where})
|
|
||||||
end,
|
|
||||||
{reply, Result, State};
|
|
||||||
handle_call({update_config, {update, Name, _Conf}, NewConfL}, _From, State) ->
|
handle_call({update_config, {update, Name, _Conf}, NewConfL}, _From, State) ->
|
||||||
{Result, State2} = restart_server(Name, NewConfL, State),
|
{Result, State2} = restart_server(Name, NewConfL, State),
|
||||||
{reply, Result, State2};
|
{reply, Result, State2};
|
||||||
|
|
@ -303,7 +250,7 @@ handle_call({server_info, Name}, _From, State) ->
|
||||||
case where_is_server(Name, State) of
|
case where_is_server(Name, State) of
|
||||||
not_found ->
|
not_found ->
|
||||||
Result = not_found;
|
Result = not_found;
|
||||||
{Status, _} ->
|
#{status := Status} ->
|
||||||
HooksMetrics = emqx_exhook_metrics:server_metrics(Name),
|
HooksMetrics = emqx_exhook_metrics:server_metrics(Name),
|
||||||
Result = #{
|
Result = #{
|
||||||
status => Status,
|
status => Status,
|
||||||
|
|
@ -314,25 +261,9 @@ handle_call({server_info, Name}, _From, State) ->
|
||||||
handle_call(
|
handle_call(
|
||||||
all_servers_info,
|
all_servers_info,
|
||||||
_From,
|
_From,
|
||||||
#{
|
#{servers := Servers} = State
|
||||||
running := Running,
|
|
||||||
waiting := Waiting,
|
|
||||||
stopped := Stopped
|
|
||||||
} = State
|
|
||||||
) ->
|
) ->
|
||||||
MakeStatus = fun(Status, Servers, Acc) ->
|
Status = maps:map(fun(_Name, #{status := Status}) -> Status end, Servers),
|
||||||
lists:foldl(
|
|
||||||
fun(Name, IAcc) -> IAcc#{Name => Status} end,
|
|
||||||
Acc,
|
|
||||||
maps:keys(Servers)
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
Status = lists:foldl(
|
|
||||||
fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end,
|
|
||||||
#{},
|
|
||||||
[{running, Running}, {waiting, Waiting}, {stopped, Stopped}]
|
|
||||||
),
|
|
||||||
|
|
||||||
Metrics = emqx_exhook_metrics:servers_metrics(),
|
Metrics = emqx_exhook_metrics:servers_metrics(),
|
||||||
|
|
||||||
Result = #{
|
Result = #{
|
||||||
|
|
@ -352,23 +283,8 @@ handle_cast(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, _Ref, {reload, Name}}, State) ->
|
handle_info({timeout, _Ref, {reload, Name}}, State) ->
|
||||||
{Result, NState} = do_load_server(Name, State),
|
{_, NState} = do_reload_server(Name, State),
|
||||||
case Result of
|
{noreply, NState};
|
||||||
ok ->
|
|
||||||
{noreply, NState};
|
|
||||||
{error, not_found} ->
|
|
||||||
{noreply, NState};
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(
|
|
||||||
warning,
|
|
||||||
#{
|
|
||||||
msg => "failed_to_reload_exhook_callback_server",
|
|
||||||
reason => Reason,
|
|
||||||
name => Name
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{noreply, ensure_reload_timer(NState)}
|
|
||||||
end;
|
|
||||||
handle_info(refresh_tick, State) ->
|
handle_info(refresh_tick, State) ->
|
||||||
refresh_tick(),
|
refresh_tick(),
|
||||||
emqx_exhook_metrics:update(?REFRESH_INTERVAL),
|
emqx_exhook_metrics:update(?REFRESH_INTERVAL),
|
||||||
|
|
@ -376,14 +292,13 @@ handle_info(refresh_tick, State) ->
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, State = #{running := Running}) ->
|
terminate(_Reason, State = #{servers := Servers}) ->
|
||||||
_ = maps:fold(
|
_ = maps:fold(
|
||||||
fun(Name, _, AccIn) ->
|
fun(Name, _, AccIn) ->
|
||||||
{ok, NAccIn} = do_unload_server(Name, AccIn),
|
do_unload_server(Name, AccIn)
|
||||||
NAccIn
|
|
||||||
end,
|
end,
|
||||||
State,
|
State,
|
||||||
Running
|
Servers
|
||||||
),
|
),
|
||||||
_ = unload_exhooks(),
|
_ = unload_exhooks(),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -401,122 +316,83 @@ unload_exhooks() ->
|
||||||
|| {Name, {M, F, _A}} <- ?ENABLED_HOOKS
|
|| {Name, {M, F, _A}} <- ?ENABLED_HOOKS
|
||||||
].
|
].
|
||||||
|
|
||||||
-spec do_load_server(server_name(), state()) ->
|
do_load_server(#{name := Name} = Server) ->
|
||||||
{{error, not_found}, state()}
|
case emqx_exhook_server:load(Name, Server) of
|
||||||
| {{error, already_started}, state()}
|
{ok, ServerState} ->
|
||||||
| {ok, state()}.
|
save(Name, ServerState),
|
||||||
do_load_server(Name, State = #{orders := Orders}) ->
|
{ok, Server#{status => connected}};
|
||||||
case where_is_server(Name, State) of
|
disable ->
|
||||||
not_found ->
|
{ok, set_disable(Server)};
|
||||||
{{error, not_found}, State};
|
{ErrorType, Reason} = Error ->
|
||||||
{running, _} ->
|
?SLOG(
|
||||||
{ok, State};
|
error,
|
||||||
{Where, Map} ->
|
#{
|
||||||
State2 = clean_reload_timer(Name, State),
|
msg => "failed_to_load_exhook_callback_server",
|
||||||
{Options, Map2} = maps:take(Name, Map),
|
reason => Reason,
|
||||||
State3 = State2#{Where := Map2},
|
name => Name
|
||||||
#{
|
}
|
||||||
running := Running,
|
),
|
||||||
stopped := Stopped
|
case ErrorType of
|
||||||
} = State3,
|
load_error ->
|
||||||
case emqx_exhook_server:load(Name, Options) of
|
{ok, ensure_reload_timer(Server)};
|
||||||
{ok, ServerState} ->
|
_ ->
|
||||||
save(Name, ServerState),
|
{Error, Server#{status => unconnected}}
|
||||||
update_order(Orders),
|
|
||||||
?SLOG(info, #{
|
|
||||||
msg => "load_exhook_callback_server_ok",
|
|
||||||
name => Name
|
|
||||||
}),
|
|
||||||
{ok, State3#{running := maps:put(Name, Options, Running)}};
|
|
||||||
{error, Reason} ->
|
|
||||||
{{error, Reason}, State};
|
|
||||||
disable ->
|
|
||||||
{ok, State3#{stopped := Stopped#{Name => Options}}}
|
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec do_unload_server(server_name(), state()) -> {ok, state()}.
|
do_load_server(#{name := Name} = Server, #{servers := Servers} = State) ->
|
||||||
do_unload_server(Name, #{stopped := Stopped} = State) ->
|
{Result, Server2} = do_load_server(Server),
|
||||||
|
{Result, update_servers(Servers#{Name => Server2}, State)}.
|
||||||
|
|
||||||
|
-spec do_reload_server(server_name(), state()) ->
|
||||||
|
{{error, term()}, state()}
|
||||||
|
| {ok, state()}.
|
||||||
|
do_reload_server(Name, State = #{servers := Servers}) ->
|
||||||
case where_is_server(Name, State) of
|
case where_is_server(Name, State) of
|
||||||
{stopped, _} ->
|
|
||||||
{ok, State};
|
|
||||||
{waiting, Waiting} ->
|
|
||||||
{Options, Waiting2} = maps:take(Name, Waiting),
|
|
||||||
{ok,
|
|
||||||
clean_reload_timer(
|
|
||||||
Name,
|
|
||||||
State#{
|
|
||||||
waiting := Waiting2,
|
|
||||||
stopped := maps:put(Name, Options, Stopped)
|
|
||||||
}
|
|
||||||
)};
|
|
||||||
{running, Running} ->
|
|
||||||
Service = server(Name),
|
|
||||||
ok = unsave(Name),
|
|
||||||
ok = emqx_exhook_server:unload(Service),
|
|
||||||
{Options, Running2} = maps:take(Name, Running),
|
|
||||||
{ok, State#{
|
|
||||||
running := Running2,
|
|
||||||
stopped := maps:put(Name, Options, Stopped)
|
|
||||||
}};
|
|
||||||
not_found ->
|
not_found ->
|
||||||
{ok, State}
|
{{error, not_found}, State};
|
||||||
|
#{timer := undefined} ->
|
||||||
|
{ok, State};
|
||||||
|
Server ->
|
||||||
|
clean_reload_timer(Server),
|
||||||
|
do_load_server(Server, Servers)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec ensure_reload_timer(state()) -> state().
|
-spec do_unload_server(server_name(), state()) -> state().
|
||||||
ensure_reload_timer(
|
do_unload_server(Name, #{servers := Servers} = State) ->
|
||||||
State = #{
|
case where_is_server(Name, State) of
|
||||||
waiting := Waiting,
|
not_found ->
|
||||||
stopped := Stopped,
|
|
||||||
trefs := TRefs
|
|
||||||
}
|
|
||||||
) ->
|
|
||||||
Iter = maps:iterator(Waiting),
|
|
||||||
|
|
||||||
{Waitting2, Stopped2, TRefs2} =
|
|
||||||
ensure_reload_timer(maps:next(Iter), Waiting, Stopped, TRefs),
|
|
||||||
|
|
||||||
State#{
|
|
||||||
waiting := Waitting2,
|
|
||||||
stopped := Stopped2,
|
|
||||||
trefs := TRefs2
|
|
||||||
}.
|
|
||||||
|
|
||||||
ensure_reload_timer(none, Waiting, Stopped, TimerRef) ->
|
|
||||||
{Waiting, Stopped, TimerRef};
|
|
||||||
ensure_reload_timer(
|
|
||||||
{Name, #{auto_reconnect := Intv}, Iter},
|
|
||||||
Waiting,
|
|
||||||
Stopped,
|
|
||||||
TimerRef
|
|
||||||
) when is_integer(Intv) ->
|
|
||||||
Next = maps:next(Iter),
|
|
||||||
case maps:is_key(Name, TimerRef) of
|
|
||||||
true ->
|
|
||||||
ensure_reload_timer(Next, Waiting, Stopped, TimerRef);
|
|
||||||
_ ->
|
|
||||||
Ref = erlang:start_timer(Intv, self(), {reload, Name}),
|
|
||||||
TimerRef2 = maps:put(Name, Ref, TimerRef),
|
|
||||||
ensure_reload_timer(Next, Waiting, Stopped, TimerRef2)
|
|
||||||
end;
|
|
||||||
ensure_reload_timer({Name, Opts, Iter}, Waiting, Stopped, TimerRef) ->
|
|
||||||
ensure_reload_timer(
|
|
||||||
maps:next(Iter),
|
|
||||||
maps:remove(Name, Waiting),
|
|
||||||
maps:put(Name, Opts, Stopped),
|
|
||||||
TimerRef
|
|
||||||
).
|
|
||||||
|
|
||||||
-spec clean_reload_timer(server_name(), state()) -> state().
|
|
||||||
clean_reload_timer(Name, State = #{trefs := TRefs}) ->
|
|
||||||
case maps:take(Name, TRefs) of
|
|
||||||
error ->
|
|
||||||
State;
|
State;
|
||||||
{TRef, NTRefs} ->
|
#{status := disable} ->
|
||||||
_ = erlang:cancel_timer(TRef),
|
State;
|
||||||
State#{trefs := NTRefs}
|
Server ->
|
||||||
|
clean_reload_timer(Server),
|
||||||
|
case server(Name) of
|
||||||
|
undefined ->
|
||||||
|
State;
|
||||||
|
Service ->
|
||||||
|
ok = unsave(Name),
|
||||||
|
ok = emqx_exhook_server:unload(Service),
|
||||||
|
Servers2 = Servers#{Name := set_disable(Server)},
|
||||||
|
State#{servers := Servers2}
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
ensure_reload_timer(#{timer := Timer} = Server) when is_reference(Timer) ->
|
||||||
|
Server;
|
||||||
|
ensure_reload_timer(#{name := Name, auto_reconnect := Intv} = Server) when is_integer(Intv) ->
|
||||||
|
Ref = erlang:start_timer(Intv, self(), {reload, Name}),
|
||||||
|
Server#{status := connecting, timer := Ref};
|
||||||
|
ensure_reload_timer(Server) ->
|
||||||
|
Server#{status := unconnected}.
|
||||||
|
|
||||||
|
-spec clean_reload_timer(server()) -> ok.
|
||||||
|
clean_reload_timer(#{timer := undefined}) ->
|
||||||
|
ok;
|
||||||
|
clean_reload_timer(#{timer := Timer}) ->
|
||||||
|
_ = erlang:cancel_timer(Timer),
|
||||||
|
ok.
|
||||||
|
|
||||||
-spec do_move(binary(), position(), list(server_options())) ->
|
-spec do_move(binary(), position(), list(server_options())) ->
|
||||||
not_found | list(server_options()).
|
not_found | list(server_options()).
|
||||||
do_move(Name, Position, ConfL) ->
|
do_move(Name, Position, ConfL) ->
|
||||||
|
|
@ -545,37 +421,32 @@ move_to([H | T], Position, Server, HeadL) ->
|
||||||
move_to([], _Position, _Server, _HeadL) ->
|
move_to([], _Position, _Server, _HeadL) ->
|
||||||
not_found.
|
not_found.
|
||||||
|
|
||||||
-spec reorder(list(server_options())) -> orders().
|
-spec reorder(list(server_options()), servers()) -> servers().
|
||||||
reorder(ServerL) ->
|
reorder(ServerL, Servers) ->
|
||||||
Orders = reorder(ServerL, 1, #{}),
|
Orders = reorder(ServerL, 1, Servers),
|
||||||
update_order(Orders),
|
update_order(Orders),
|
||||||
Orders.
|
Orders.
|
||||||
|
|
||||||
reorder([#{name := Name} | T], Order, Orders) ->
|
reorder([#{name := Name} | T], Order, Servers) ->
|
||||||
reorder(T, Order + 1, Orders#{Name => Order});
|
reorder(T, Order + 1, update_order(Name, Servers, Order));
|
||||||
reorder([], _Order, Orders) ->
|
reorder([], _Order, Servers) ->
|
||||||
Orders.
|
Servers.
|
||||||
|
|
||||||
get_servers_info(Status, Map) ->
|
update_order(Name, Servers, Order) ->
|
||||||
|
Server = maps:get(Name, Servers),
|
||||||
|
Servers#{Name := Server#{order := Order}}.
|
||||||
|
|
||||||
|
get_servers_info(Svrs) ->
|
||||||
Fold = fun(Name, Conf, Acc) ->
|
Fold = fun(Name, Conf, Acc) ->
|
||||||
[
|
[
|
||||||
maps:merge(Conf, #{
|
maps:merge(Conf, #{hooks => hooks(Name)})
|
||||||
status => Status,
|
|
||||||
hooks => hooks(Name)
|
|
||||||
})
|
|
||||||
| Acc
|
| Acc
|
||||||
]
|
]
|
||||||
end,
|
end,
|
||||||
maps:fold(Fold, [], Map).
|
maps:fold(Fold, [], Svrs).
|
||||||
|
|
||||||
where_is_server(Name, #{running := Running}) when is_map_key(Name, Running) ->
|
where_is_server(Name, #{servers := Servers}) ->
|
||||||
{running, Running};
|
maps:get(Name, Servers, not_found).
|
||||||
where_is_server(Name, #{waiting := Waiting}) when is_map_key(Name, Waiting) ->
|
|
||||||
{waiting, Waiting};
|
|
||||||
where_is_server(Name, #{stopped := Stopped}) when is_map_key(Name, Stopped) ->
|
|
||||||
{stopped, Stopped};
|
|
||||||
where_is_server(_, _) ->
|
|
||||||
not_found.
|
|
||||||
|
|
||||||
-type replace_fun() :: fun((server_options()) -> server_options()).
|
-type replace_fun() :: fun((server_options()) -> server_options()).
|
||||||
|
|
||||||
|
|
@ -604,15 +475,10 @@ restart_server(Name, ConfL, State) ->
|
||||||
case where_is_server(Name, State) of
|
case where_is_server(Name, State) of
|
||||||
not_found ->
|
not_found ->
|
||||||
{{error, not_found}, State};
|
{{error, not_found}, State};
|
||||||
{Where, Map} ->
|
Server ->
|
||||||
State2 = State#{Where := Map#{Name := Conf}},
|
Server2 = maps:merge(Server, Conf),
|
||||||
{ok, State3} = do_unload_server(Name, State2),
|
State2 = do_unload_server(Name, State),
|
||||||
case do_load_server(Name, State3) of
|
do_load_server(Server2, State2)
|
||||||
{ok, State4} ->
|
|
||||||
{ok, State4};
|
|
||||||
{Error, State4} ->
|
|
||||||
{Error, State4}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -620,9 +486,11 @@ sort_name_by_order(Names, Orders) ->
|
||||||
lists:sort(
|
lists:sort(
|
||||||
fun
|
fun
|
||||||
(A, B) when is_binary(A) ->
|
(A, B) when is_binary(A) ->
|
||||||
maps:get(A, Orders) < maps:get(B, Orders);
|
emqx_map_lib:deep_get([A, order], Orders) <
|
||||||
|
emqx_map_lib:deep_get([B, order], Orders);
|
||||||
(#{name := A}, #{name := B}) ->
|
(#{name := A}, #{name := B}) ->
|
||||||
maps:get(A, Orders) < maps:get(B, Orders)
|
emqx_map_lib:deep_get([A, order], Orders) <
|
||||||
|
emqx_map_lib:deep_get([B, order], Orders)
|
||||||
end,
|
end,
|
||||||
Names
|
Names
|
||||||
).
|
).
|
||||||
|
|
@ -630,6 +498,16 @@ sort_name_by_order(Names, Orders) ->
|
||||||
refresh_tick() ->
|
refresh_tick() ->
|
||||||
erlang:send_after(?REFRESH_INTERVAL, self(), ?FUNCTION_NAME).
|
erlang:send_after(?REFRESH_INTERVAL, self(), ?FUNCTION_NAME).
|
||||||
|
|
||||||
|
options_to_server(Options) ->
|
||||||
|
maps:merge(Options, #{status => unconnected, timer => undefined, order => 0}).
|
||||||
|
|
||||||
|
update_servers(Servers, State) ->
|
||||||
|
update_order(Servers),
|
||||||
|
State#{servers := Servers}.
|
||||||
|
|
||||||
|
set_disable(Server) ->
|
||||||
|
Server#{status := disable, timer := undefined}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Server state persistent
|
%% Server state persistent
|
||||||
save(Name, ServerState) ->
|
save(Name, ServerState) ->
|
||||||
|
|
@ -661,8 +539,17 @@ server(Name) ->
|
||||||
Service -> Service
|
Service -> Service
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update_order(Orders) ->
|
update_order(Servers) ->
|
||||||
Running = running(),
|
Running = running(),
|
||||||
|
Orders = maps:filter(
|
||||||
|
fun
|
||||||
|
(Name, #{status := connected}) ->
|
||||||
|
lists:member(Name, Running);
|
||||||
|
(_, _) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
Servers
|
||||||
|
),
|
||||||
Running2 = sort_name_by_order(Running, Orders),
|
Running2 = sort_name_by_order(Running, Orders),
|
||||||
persistent_term:put(?APP, Running2).
|
persistent_term:put(?APP, Running2).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ fields(server) ->
|
||||||
)},
|
)},
|
||||||
{pool_size,
|
{pool_size,
|
||||||
sc(
|
sc(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
default => 8,
|
default => 8,
|
||||||
example => 8,
|
example => 8,
|
||||||
|
|
|
||||||
|
|
@ -86,40 +86,44 @@
|
||||||
%% Load/Unload APIs
|
%% Load/Unload APIs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec load(binary(), map()) -> {ok, server()} | {error, term()} | disable.
|
-spec load(binary(), map()) -> {ok, server()} | {error, term()} | {load_error, term()} | disable.
|
||||||
load(_Name, #{enable := false}) ->
|
load(_Name, #{enable := false}) ->
|
||||||
disable;
|
disable;
|
||||||
load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts) ->
|
load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts) ->
|
||||||
ReqOpts = #{timeout => Timeout, failed_action => FailedAction},
|
ReqOpts = #{timeout => Timeout, failed_action => FailedAction},
|
||||||
{SvrAddr, ClientOpts} = channel_opts(Opts),
|
case channel_opts(Opts) of
|
||||||
case
|
{ok, {SvrAddr, ClientOpts}} ->
|
||||||
emqx_exhook_sup:start_grpc_client_channel(
|
case
|
||||||
Name,
|
emqx_exhook_sup:start_grpc_client_channel(
|
||||||
SvrAddr,
|
Name,
|
||||||
ClientOpts
|
SvrAddr,
|
||||||
)
|
ClientOpts
|
||||||
of
|
)
|
||||||
{ok, _ChannPoolPid} ->
|
of
|
||||||
case do_init(Name, ReqOpts) of
|
{ok, _ChannPoolPid} ->
|
||||||
{ok, HookSpecs} ->
|
case do_init(Name, ReqOpts) of
|
||||||
%% Register metrics
|
{ok, HookSpecs} ->
|
||||||
Prefix = lists:flatten(io_lib:format("exhook.~ts.", [Name])),
|
%% Register metrics
|
||||||
ensure_metrics(Prefix, HookSpecs),
|
Prefix = lists:flatten(io_lib:format("exhook.~ts.", [Name])),
|
||||||
%% Ensure hooks
|
ensure_metrics(Prefix, HookSpecs),
|
||||||
ensure_hooks(HookSpecs),
|
%% Ensure hooks
|
||||||
{ok, #{
|
ensure_hooks(HookSpecs),
|
||||||
name => Name,
|
{ok, #{
|
||||||
options => ReqOpts,
|
name => Name,
|
||||||
channel => _ChannPoolPid,
|
options => ReqOpts,
|
||||||
hookspec => HookSpecs,
|
channel => _ChannPoolPid,
|
||||||
prefix => Prefix
|
hookspec => HookSpecs,
|
||||||
}};
|
prefix => Prefix
|
||||||
|
}};
|
||||||
|
{error, Reason} ->
|
||||||
|
emqx_exhook_sup:stop_grpc_client_channel(Name),
|
||||||
|
{load_error, Reason}
|
||||||
|
end;
|
||||||
{error, _} = E ->
|
{error, _} = E ->
|
||||||
emqx_exhook_sup:stop_grpc_client_channel(Name),
|
|
||||||
E
|
E
|
||||||
end;
|
end;
|
||||||
{error, _} = E ->
|
Error ->
|
||||||
E
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
|
|
@ -130,7 +134,7 @@ channel_opts(Opts = #{url := URL}) ->
|
||||||
),
|
),
|
||||||
case uri_string:parse(URL) of
|
case uri_string:parse(URL) of
|
||||||
#{scheme := <<"http">>, host := Host, port := Port} ->
|
#{scheme := <<"http">>, host := Host, port := Port} ->
|
||||||
{format_http_uri("http", Host, Port), ClientOpts};
|
{ok, {format_http_uri("http", Host, Port), ClientOpts}};
|
||||||
#{scheme := <<"https">>, host := Host, port := Port} ->
|
#{scheme := <<"https">>, host := Host, port := Port} ->
|
||||||
SslOpts =
|
SslOpts =
|
||||||
case maps:get(ssl, Opts, undefined) of
|
case maps:get(ssl, Opts, undefined) of
|
||||||
|
|
@ -154,9 +158,9 @@ channel_opts(Opts = #{url := URL}) ->
|
||||||
transport_opts => SslOpts
|
transport_opts => SslOpts
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{format_http_uri("https", Host, Port), NClientOpts};
|
{ok, {format_http_uri("https", Host, Port), NClientOpts}};
|
||||||
Error ->
|
Error ->
|
||||||
error({bad_server_url, URL, Error})
|
{error, {bad_server_url, URL, Error}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format_http_uri(Scheme, Host, Port) ->
|
format_http_uri(Scheme, Host, Port) ->
|
||||||
|
|
|
||||||
|
|
@ -284,12 +284,12 @@ fields(gateway_overview) ->
|
||||||
)},
|
)},
|
||||||
{max_connections,
|
{max_connections,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{desc => <<"The Gateway allowed maximum connections/clients">>}
|
#{desc => <<"The Gateway allowed maximum connections/clients">>}
|
||||||
)},
|
)},
|
||||||
{current_connections,
|
{current_connections,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
non_neg_integer(),
|
||||||
#{desc => <<"The Gateway current connected connections/clients">>}
|
#{desc => <<"The Gateway current connected connections/clients">>}
|
||||||
)},
|
)},
|
||||||
{listeners,
|
{listeners,
|
||||||
|
|
@ -410,11 +410,11 @@ convert_listener_struct(Schema) ->
|
||||||
),
|
),
|
||||||
lists:keystore(listeners, 1, Schema1, {listeners, ListenerSchema}).
|
lists:keystore(listeners, 1, Schema1, {listeners, ListenerSchema}).
|
||||||
|
|
||||||
remove_listener_and_authn(Schmea) ->
|
remove_listener_and_authn(Schema) ->
|
||||||
lists:keydelete(
|
lists:keydelete(
|
||||||
authentication,
|
authentication,
|
||||||
1,
|
1,
|
||||||
lists:keydelete(listeners, 1, Schmea)
|
lists:keydelete(listeners, 1, Schema)
|
||||||
).
|
).
|
||||||
|
|
||||||
listeners_schema(?R_REF(_Mod, tcp_listeners)) ->
|
listeners_schema(?R_REF(_Mod, tcp_listeners)) ->
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ params_paging_in_qs() ->
|
||||||
[
|
[
|
||||||
{page,
|
{page,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
@ -394,7 +394,7 @@ params_paging_in_qs() ->
|
||||||
)},
|
)},
|
||||||
{limit,
|
{limit,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
|
||||||
|
|
@ -101,30 +101,39 @@ clients(get, #{
|
||||||
bindings := #{name := Name0},
|
bindings := #{name := Name0},
|
||||||
query_string := QString
|
query_string := QString
|
||||||
}) ->
|
}) ->
|
||||||
with_gateway(Name0, fun(GwName, _) ->
|
Fun = fun(GwName, _) ->
|
||||||
TabName = emqx_gateway_cm:tabname(info, GwName),
|
TabName = emqx_gateway_cm:tabname(info, GwName),
|
||||||
case maps:get(<<"node">>, QString, undefined) of
|
Result =
|
||||||
undefined ->
|
case maps:get(<<"node">>, QString, undefined) of
|
||||||
Response = emqx_mgmt_api:cluster_query(
|
undefined ->
|
||||||
QString,
|
emqx_mgmt_api:cluster_query(
|
||||||
TabName,
|
QString,
|
||||||
?CLIENT_QSCHEMA,
|
TabName,
|
||||||
?QUERY_FUN
|
?CLIENT_QSCHEMA,
|
||||||
),
|
?QUERY_FUN
|
||||||
emqx_mgmt_util:generate_response(Response);
|
);
|
||||||
Node1 ->
|
Node0 ->
|
||||||
Node = binary_to_atom(Node1, utf8),
|
Node1 = binary_to_atom(Node0, utf8),
|
||||||
QStringWithoutNode = maps:without([<<"node">>], QString),
|
QStringWithoutNode = maps:without([<<"node">>], QString),
|
||||||
Response = emqx_mgmt_api:node_query(
|
emqx_mgmt_api:node_query(
|
||||||
Node,
|
Node1,
|
||||||
QStringWithoutNode,
|
QStringWithoutNode,
|
||||||
TabName,
|
TabName,
|
||||||
?CLIENT_QSCHEMA,
|
?CLIENT_QSCHEMA,
|
||||||
?QUERY_FUN
|
?QUERY_FUN
|
||||||
),
|
)
|
||||||
emqx_mgmt_util:generate_response(Response)
|
end,
|
||||||
|
case Result of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Response ->
|
||||||
|
{200, Response}
|
||||||
end
|
end
|
||||||
end).
|
end,
|
||||||
|
with_gateway(Name0, Fun).
|
||||||
|
|
||||||
clients_insta(get, #{
|
clients_insta(get, #{
|
||||||
bindings := #{
|
bindings := #{
|
||||||
|
|
@ -392,21 +401,21 @@ format_channel_info({_, Infos, Stats} = R) ->
|
||||||
{heap_size, Stats, 0},
|
{heap_size, Stats, 0},
|
||||||
{reductions, Stats, 0}
|
{reductions, Stats, 0}
|
||||||
],
|
],
|
||||||
eval(FetchX ++ extra_feilds(R)).
|
eval(FetchX ++ extra_fields(R)).
|
||||||
|
|
||||||
extra_feilds({_, Infos, _Stats} = R) ->
|
extra_fields({_, Infos, _Stats} = R) ->
|
||||||
extra_feilds(
|
extra_fields(
|
||||||
maps:get(protocol, maps:get(clientinfo, Infos)),
|
maps:get(protocol, maps:get(clientinfo, Infos)),
|
||||||
R
|
R
|
||||||
).
|
).
|
||||||
|
|
||||||
extra_feilds(lwm2m, {_, Infos, _Stats}) ->
|
extra_fields(lwm2m, {_, Infos, _Stats}) ->
|
||||||
ClientInfo = maps:get(clientinfo, Infos, #{}),
|
ClientInfo = maps:get(clientinfo, Infos, #{}),
|
||||||
[
|
[
|
||||||
{endpoint_name, ClientInfo},
|
{endpoint_name, ClientInfo},
|
||||||
{lifetime, ClientInfo}
|
{lifetime, ClientInfo}
|
||||||
];
|
];
|
||||||
extra_feilds(_, _) ->
|
extra_fields(_, _) ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
eval(Ls) ->
|
eval(Ls) ->
|
||||||
|
|
@ -495,7 +504,7 @@ schema("/gateway/:name/clients/:clientid/subscriptions") ->
|
||||||
#{
|
#{
|
||||||
200 => emqx_dashboard_swagger:schema_with_examples(
|
200 => emqx_dashboard_swagger:schema_with_examples(
|
||||||
hoconsc:array(ref(subscription)),
|
hoconsc:array(ref(subscription)),
|
||||||
examples_subsctiption_list()
|
examples_subscription_list()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -506,14 +515,14 @@ schema("/gateway/:name/clients/:clientid/subscriptions") ->
|
||||||
parameters => params_client_insta(),
|
parameters => params_client_insta(),
|
||||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||||
ref(subscription),
|
ref(subscription),
|
||||||
examples_subsctiption()
|
examples_subscription()
|
||||||
),
|
),
|
||||||
responses =>
|
responses =>
|
||||||
?STANDARD_RESP(
|
?STANDARD_RESP(
|
||||||
#{
|
#{
|
||||||
201 => emqx_dashboard_swagger:schema_with_examples(
|
201 => emqx_dashboard_swagger:schema_with_examples(
|
||||||
ref(subscription),
|
ref(subscription),
|
||||||
examples_subsctiption()
|
examples_subscription()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -664,7 +673,7 @@ params_paging() ->
|
||||||
[
|
[
|
||||||
{page,
|
{page,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
@ -674,7 +683,7 @@ params_paging() ->
|
||||||
)},
|
)},
|
||||||
{limit,
|
{limit,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
desc => <<"Page Limit">>,
|
desc => <<"Page Limit">>,
|
||||||
|
|
@ -1089,7 +1098,7 @@ examples_client() ->
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
examples_subsctiption_list() ->
|
examples_subscription_list() ->
|
||||||
#{
|
#{
|
||||||
general_subscription_list =>
|
general_subscription_list =>
|
||||||
#{
|
#{
|
||||||
|
|
@ -1103,7 +1112,7 @@ examples_subsctiption_list() ->
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
examples_subsctiption() ->
|
examples_subscription() ->
|
||||||
#{
|
#{
|
||||||
general_subscription =>
|
general_subscription =>
|
||||||
#{
|
#{
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ users(get, #{bindings := #{name := Name0, id := Id}, query_string := Qs}) ->
|
||||||
Name0,
|
Name0,
|
||||||
Id,
|
Id,
|
||||||
fun(_GwName, #{id := AuthId, chain_name := ChainName}) ->
|
fun(_GwName, #{id := AuthId, chain_name := ChainName}) ->
|
||||||
emqx_authn_api:list_users(ChainName, AuthId, page_pramas(Qs))
|
emqx_authn_api:list_users(ChainName, AuthId, page_params(Qs))
|
||||||
end
|
end
|
||||||
);
|
);
|
||||||
users(post, #{
|
users(post, #{
|
||||||
|
|
@ -261,7 +261,7 @@ import_users(post, #{
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Utils
|
%% Utils
|
||||||
|
|
||||||
page_pramas(Qs) ->
|
page_params(Qs) ->
|
||||||
maps:with([<<"page">>, <<"limit">>], Qs).
|
maps:with([<<"page">>, <<"limit">>], Qs).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -555,7 +555,7 @@ params_paging_in_qs() ->
|
||||||
[
|
[
|
||||||
{page,
|
{page,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
@ -565,7 +565,7 @@ params_paging_in_qs() ->
|
||||||
)},
|
)},
|
||||||
{limit,
|
{limit,
|
||||||
mk(
|
mk(
|
||||||
integer(),
|
pos_integer(),
|
||||||
#{
|
#{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
@ -627,10 +627,12 @@ fields(tcp_listener_opts) ->
|
||||||
{high_watermark, mk(binary(), #{})},
|
{high_watermark, mk(binary(), #{})},
|
||||||
{nodelay, mk(boolean(), #{})},
|
{nodelay, mk(boolean(), #{})},
|
||||||
{reuseaddr, boolean()},
|
{reuseaddr, boolean()},
|
||||||
|
%% TODO: duri
|
||||||
{send_timeout, binary()},
|
{send_timeout, binary()},
|
||||||
{send_timeout_close, boolean()}
|
{send_timeout_close, boolean()}
|
||||||
];
|
];
|
||||||
fields(ssl_listener_opts) ->
|
fields(ssl_listener_opts) ->
|
||||||
|
%% TODO: maybe use better ssl options schema from emqx_ssl_lib or somewhere
|
||||||
[
|
[
|
||||||
{cacertfile, binary()},
|
{cacertfile, binary()},
|
||||||
{certfile, binary()},
|
{certfile, binary()},
|
||||||
|
|
@ -762,7 +764,7 @@ common_listener_opts() ->
|
||||||
required => false,
|
required => false,
|
||||||
desc =>
|
desc =>
|
||||||
<<
|
<<
|
||||||
"The Mounpoint for clients of the listener. "
|
"The Mountpoint for clients of the listener. "
|
||||||
"The gateway-level mountpoint configuration can be overloaded "
|
"The gateway-level mountpoint configuration can be overloaded "
|
||||||
"when it is not null or empty string"
|
"when it is not null or empty string"
|
||||||
>>
|
>>
|
||||||
|
|
@ -774,7 +776,7 @@ common_listener_opts() ->
|
||||||
emqx_authn_schema:authenticator_type(),
|
emqx_authn_schema:authenticator_type(),
|
||||||
#{
|
#{
|
||||||
required => {false, recursively},
|
required => {false, recursively},
|
||||||
desc => <<"The authenticatior for this listener">>
|
desc => <<"The authenticator for this listener">>
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
] ++ emqx_gateway_schema:proxy_protocol_opts().
|
] ++ emqx_gateway_schema:proxy_protocol_opts().
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-type ip_port() :: tuple().
|
-type ip_port() :: tuple().
|
||||||
-type duration() :: integer().
|
-type duration() :: non_neg_integer().
|
||||||
-type duration_s() :: integer().
|
-type duration_s() :: non_neg_integer().
|
||||||
-type bytesize() :: integer().
|
-type bytesize() :: pos_integer().
|
||||||
-type comma_separated_list() :: list().
|
-type comma_separated_list() :: list().
|
||||||
|
|
||||||
-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}).
|
-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}).
|
||||||
|
|
@ -117,7 +117,7 @@ fields(stomp_frame) ->
|
||||||
[
|
[
|
||||||
{max_headers,
|
{max_headers,
|
||||||
sc(
|
sc(
|
||||||
integer(),
|
non_neg_integer(),
|
||||||
#{
|
#{
|
||||||
default => 10,
|
default => 10,
|
||||||
desc => "The maximum number of Header"
|
desc => "The maximum number of Header"
|
||||||
|
|
@ -125,7 +125,7 @@ fields(stomp_frame) ->
|
||||||
)},
|
)},
|
||||||
{max_headers_length,
|
{max_headers_length,
|
||||||
sc(
|
sc(
|
||||||
integer(),
|
non_neg_integer(),
|
||||||
#{
|
#{
|
||||||
default => 1024,
|
default => 1024,
|
||||||
desc => "The maximum string length of the Header Value"
|
desc => "The maximum string length of the Header Value"
|
||||||
|
|
|
||||||
|
|
@ -307,12 +307,12 @@ t_case_stomp_subscribe(_) ->
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
timer:sleep(100),
|
timer:sleep(200),
|
||||||
Msg = emqx_message:make(Topic, Payload),
|
Msg = emqx_message:make(Topic, Payload),
|
||||||
emqx:publish(Msg),
|
emqx:publish(Msg),
|
||||||
|
|
||||||
timer:sleep(100),
|
timer:sleep(200),
|
||||||
{ok, Data1} = gen_tcp:recv(Sock, 0, 2000),
|
{ok, Data1} = gen_tcp:recv(Sock, 0, 10000),
|
||||||
{ok, Frame1, _, _} = Mod:parse(Data1),
|
{ok, Frame1, _, _} = Mod:parse(Data1),
|
||||||
Checker(Frame1)
|
Checker(Frame1)
|
||||||
end,
|
end,
|
||||||
|
|
@ -406,7 +406,11 @@ t_case_exproto_subscribe(_) ->
|
||||||
%% Helpers
|
%% Helpers
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
try_publish_recv(Topic, Publish, Checker) ->
|
try_publish_recv(Topic, Publish, Checker) ->
|
||||||
|
try_publish_recv(Topic, Publish, Checker, 500).
|
||||||
|
|
||||||
|
try_publish_recv(Topic, Publish, Checker, Timeout) ->
|
||||||
emqx:subscribe(Topic),
|
emqx:subscribe(Topic),
|
||||||
|
timer:sleep(200),
|
||||||
Clear = fun(Msg) ->
|
Clear = fun(Msg) ->
|
||||||
emqx:unsubscribe(Topic),
|
emqx:unsubscribe(Topic),
|
||||||
Checker(Msg)
|
Checker(Msg)
|
||||||
|
|
@ -416,7 +420,7 @@ try_publish_recv(Topic, Publish, Checker) ->
|
||||||
receive
|
receive
|
||||||
{deliver, Topic, Msg} ->
|
{deliver, Topic, Msg} ->
|
||||||
Clear(Msg)
|
Clear(Msg)
|
||||||
after 500 ->
|
after Timeout ->
|
||||||
Clear(timeout)
|
Clear(timeout)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
emqx_mgmt_api_alarms {
|
||||||
|
|
||||||
|
list_alarms_api {
|
||||||
|
desc {
|
||||||
|
en: """List alarms"""
|
||||||
|
zh: """列出告警,获取告警列表"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_alarms_api {
|
||||||
|
desc {
|
||||||
|
en: """Remove all deactivated alarms"""
|
||||||
|
zh: """删除所有历史告警(非活跃告警)"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_alarms_api_response204 {
|
||||||
|
desc {
|
||||||
|
en: """Remove all deactivated alarms ok"""
|
||||||
|
zh: """删除所有历史告警(非活跃告警)成功"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_alarms_qs_activated {
|
||||||
|
desc {
|
||||||
|
en: """Activate alarms, or deactivate alarms. Default is false"""
|
||||||
|
zh: """活跃中的告警,或历史告警(非活跃告警),默认为 false"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
desc {
|
||||||
|
en: """Alarm in node"""
|
||||||
|
zh: """告警节点名称"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name {
|
||||||
|
desc {
|
||||||
|
en: """Alarm name"""
|
||||||
|
zh: """告警名称"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message {
|
||||||
|
desc {
|
||||||
|
en: """Alarm readable information"""
|
||||||
|
zh: """告警信息"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
desc {
|
||||||
|
en: """Alarm details information"""
|
||||||
|
zh: """告警详细信息"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
duration {
|
||||||
|
desc {
|
||||||
|
en: """Alarms duration time; UNIX time stamp, millisecond"""
|
||||||
|
zh: """告警持续时间,单位:毫秒"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activate_at {
|
||||||
|
desc {
|
||||||
|
en: """Alarms activate time, RFC 3339"""
|
||||||
|
zh: """告警开始时间,使用 rfc3339 标准时间格式"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate_at {
|
||||||
|
desc {
|
||||||
|
en: """Alarms deactivate time, RFC 3339"""
|
||||||
|
zh: """告警结束时间,使用 rfc3339 标准时间格式"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
emqx_mgmt_api_banned {
|
||||||
|
|
||||||
|
list_banned_api {
|
||||||
|
desc {
|
||||||
|
en: """List banned."""
|
||||||
|
zh: """列出黑名单"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """List Banned"""
|
||||||
|
zh: """列出黑名单"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create_banned_api {
|
||||||
|
desc {
|
||||||
|
en: """Create banned."""
|
||||||
|
zh: """创建黑名单"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create_banned_api_response400 {
|
||||||
|
desc {
|
||||||
|
en: """Banned already existed, or bad args."""
|
||||||
|
zh: """黑名单已存在,或参数格式有错误"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_banned_api {
|
||||||
|
desc {
|
||||||
|
en: """Delete banned"""
|
||||||
|
zh: """删除黑名单"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_banned_api_response404 {
|
||||||
|
desc {
|
||||||
|
en: """Banned not found. May be the banned time has been exceeded"""
|
||||||
|
zh: """黑名单未找到,可能为已经超期失效"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create_banned {
|
||||||
|
desc {
|
||||||
|
en: """List banned."""
|
||||||
|
zh: """列出黑名单"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """List Banned"""
|
||||||
|
zh: """列出黑名单"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
as {
|
||||||
|
desc {
|
||||||
|
en: """Banned type clientid, username, peerhost"""
|
||||||
|
zh: """黑名单类型,可选 clientid、username、peerhost"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Banned Type"""
|
||||||
|
zh: """黑名单类型"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
who {
|
||||||
|
desc {
|
||||||
|
en: """Client info as banned type"""
|
||||||
|
zh: """设备信息"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Banned Info"""
|
||||||
|
zh: """黑名单信息"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
by {
|
||||||
|
desc {
|
||||||
|
en: """Commander"""
|
||||||
|
zh: """黑名单创建者"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Commander"""
|
||||||
|
zh: """黑名单创建者"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reason {
|
||||||
|
desc {
|
||||||
|
en: """Banned reason"""
|
||||||
|
zh: """黑名单创建原因"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Reason"""
|
||||||
|
zh: """原因"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
at {
|
||||||
|
desc {
|
||||||
|
en: """Create banned time, rfc3339, now if not specified"""
|
||||||
|
zh: """黑名单创建时间,默认为当前"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Create banned time"""
|
||||||
|
zh: """黑名单创建时间"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
until {
|
||||||
|
desc {
|
||||||
|
en: """Cancel banned time, rfc3339, now + 5 minute if not specified"""
|
||||||
|
zh: """黑名单结束时间,默认为创建时间 + 5 分钟"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Cancel banned time"""
|
||||||
|
zh: """黑名单结束时间"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
]}.
|
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
{erl_opts, [warn_unused_vars,
|
{erl_opts, [
|
||||||
warn_shadow_vars,
|
warn_unused_vars,
|
||||||
warn_unused_import,
|
warn_shadow_vars,
|
||||||
warn_obsolete_guard,
|
warn_unused_import,
|
||||||
warnings_as_errors,
|
warn_obsolete_guard,
|
||||||
debug_info,
|
warnings_as_errors,
|
||||||
{parse_transform}]}.
|
debug_info,
|
||||||
|
{parse_transform}
|
||||||
|
]}.
|
||||||
|
|
||||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
{xref_checks, [
|
||||||
locals_not_used, deprecated_function_calls,
|
undefined_function_calls,
|
||||||
warnings_as_errors, deprecated_functions]}.
|
undefined_functions,
|
||||||
|
locals_not_used,
|
||||||
|
deprecated_function_calls,
|
||||||
|
warnings_as_errors,
|
||||||
|
deprecated_functions
|
||||||
|
]}.
|
||||||
{cover_enabled, true}.
|
{cover_enabled, true}.
|
||||||
{cover_opts, [verbose]}.
|
{cover_opts, [verbose]}.
|
||||||
{cover_export_enabled, true}.
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_management,
|
{application, emqx_management, [
|
||||||
[{description, "EMQX Management API and CLI"},
|
{description, "EMQX Management API and CLI"},
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
% strict semver, bump manually!
|
||||||
{modules, []},
|
{vsn, "5.0.0"},
|
||||||
{registered, [emqx_management_sup]},
|
{modules, []},
|
||||||
{applications, [kernel,stdlib,emqx_plugins,minirest,emqx]},
|
{registered, [emqx_management_sup]},
|
||||||
{mod, {emqx_mgmt_app,[]}},
|
{applications, [kernel, stdlib, emqx_plugins, minirest, emqx]},
|
||||||
{env, []},
|
{mod, {emqx_mgmt_app, []}},
|
||||||
{licenses, ["Apache-2.0"]},
|
{env, []},
|
||||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||||
{"Github", "https://github.com/emqx/emqx-management"}
|
{links, [
|
||||||
]}
|
{"Homepage", "https://emqx.io/"},
|
||||||
]}.
|
{"Github", "https://github.com/emqx/emqx-management"}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1]).
|
roots/0,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
namespace() -> management.
|
namespace() -> management.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,76 +25,82 @@
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
%% Nodes and Brokers API
|
%% Nodes and Brokers API
|
||||||
-export([ list_nodes/0
|
-export([
|
||||||
, lookup_node/1
|
list_nodes/0,
|
||||||
, list_brokers/0
|
lookup_node/1,
|
||||||
, lookup_broker/1
|
list_brokers/0,
|
||||||
, node_info/0
|
lookup_broker/1,
|
||||||
, node_info/1
|
node_info/0,
|
||||||
, broker_info/0
|
node_info/1,
|
||||||
, broker_info/1
|
broker_info/0,
|
||||||
]).
|
broker_info/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Metrics and Stats
|
%% Metrics and Stats
|
||||||
-export([ get_metrics/0
|
-export([
|
||||||
, get_metrics/1
|
get_metrics/0,
|
||||||
, get_stats/0
|
get_metrics/1,
|
||||||
, get_stats/1
|
get_stats/0,
|
||||||
]).
|
get_stats/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Clients, Sessions
|
%% Clients, Sessions
|
||||||
-export([ lookup_client/2
|
-export([
|
||||||
, lookup_client/3
|
lookup_client/2,
|
||||||
, kickout_client/1
|
lookup_client/3,
|
||||||
, list_authz_cache/1
|
kickout_client/1,
|
||||||
, list_client_subscriptions/1
|
list_authz_cache/1,
|
||||||
, client_subscriptions/2
|
list_client_subscriptions/1,
|
||||||
, clean_authz_cache/1
|
client_subscriptions/2,
|
||||||
, clean_authz_cache/2
|
clean_authz_cache/1,
|
||||||
, clean_authz_cache_all/0
|
clean_authz_cache/2,
|
||||||
, clean_authz_cache_all/1
|
clean_authz_cache_all/0,
|
||||||
, set_ratelimit_policy/2
|
clean_authz_cache_all/1,
|
||||||
, set_quota_policy/2
|
set_ratelimit_policy/2,
|
||||||
, set_keepalive/2
|
set_quota_policy/2,
|
||||||
]).
|
set_keepalive/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Internal funcs
|
%% Internal funcs
|
||||||
-export([do_call_client/2]).
|
-export([do_call_client/2]).
|
||||||
|
|
||||||
%% Subscriptions
|
%% Subscriptions
|
||||||
-export([ list_subscriptions/1
|
-export([
|
||||||
, list_subscriptions_via_topic/2
|
list_subscriptions/1,
|
||||||
, list_subscriptions_via_topic/3
|
list_subscriptions_via_topic/2,
|
||||||
, lookup_subscriptions/1
|
list_subscriptions_via_topic/3,
|
||||||
, lookup_subscriptions/2
|
lookup_subscriptions/1,
|
||||||
|
lookup_subscriptions/2,
|
||||||
|
|
||||||
, do_list_subscriptions/0
|
do_list_subscriptions/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% PubSub
|
%% PubSub
|
||||||
-export([ subscribe/2
|
-export([
|
||||||
, do_subscribe/2
|
subscribe/2,
|
||||||
, publish/1
|
do_subscribe/2,
|
||||||
, unsubscribe/2
|
publish/1,
|
||||||
, do_unsubscribe/2
|
unsubscribe/2,
|
||||||
]).
|
do_unsubscribe/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Alarms
|
%% Alarms
|
||||||
-export([ get_alarms/1
|
-export([
|
||||||
, get_alarms/2
|
get_alarms/1,
|
||||||
, deactivate/2
|
get_alarms/2,
|
||||||
, delete_all_deactivated_alarms/0
|
deactivate/2,
|
||||||
, delete_all_deactivated_alarms/1
|
delete_all_deactivated_alarms/0,
|
||||||
]).
|
delete_all_deactivated_alarms/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Banned
|
%% Banned
|
||||||
-export([ create_banned/1
|
-export([
|
||||||
, delete_banned/1
|
create_banned/1,
|
||||||
]).
|
delete_banned/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Common Table API
|
%% Common Table API
|
||||||
-export([ max_row_limit/0
|
-export([max_row_limit/0]).
|
||||||
]).
|
|
||||||
|
|
||||||
-define(APP, emqx_management).
|
-define(APP, emqx_management).
|
||||||
|
|
||||||
|
|
@ -113,24 +119,26 @@ list_nodes() ->
|
||||||
lookup_node(Node) -> node_info(Node).
|
lookup_node(Node) -> node_info(Node).
|
||||||
|
|
||||||
node_info() ->
|
node_info() ->
|
||||||
Memory = emqx_vm:get_memory(),
|
Memory = emqx_vm:get_memory(),
|
||||||
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
|
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
|
||||||
BrokerInfo = emqx_sys:info(),
|
BrokerInfo = emqx_sys:info(),
|
||||||
Info#{node => node(),
|
Info#{
|
||||||
otp_release => otp_rel(),
|
node => node(),
|
||||||
memory_total => proplists:get_value(allocated, Memory),
|
otp_release => otp_rel(),
|
||||||
memory_used => proplists:get_value(used, Memory),
|
memory_total => proplists:get_value(allocated, Memory),
|
||||||
process_available => erlang:system_info(process_limit),
|
memory_used => proplists:get_value(used, Memory),
|
||||||
process_used => erlang:system_info(process_count),
|
process_available => erlang:system_info(process_limit),
|
||||||
|
process_used => erlang:system_info(process_count),
|
||||||
|
|
||||||
max_fds => proplists:get_value(
|
max_fds => proplists:get_value(
|
||||||
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
|
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))
|
||||||
connections => ets:info(emqx_channel, size),
|
),
|
||||||
node_status => 'Running',
|
connections => ets:info(emqx_channel, size),
|
||||||
uptime => proplists:get_value(uptime, BrokerInfo),
|
node_status => 'Running',
|
||||||
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
uptime => proplists:get_value(uptime, BrokerInfo),
|
||||||
role => mria_rlog:role()
|
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
||||||
}.
|
role => mria_rlog:role()
|
||||||
|
}.
|
||||||
|
|
||||||
node_info(Node) ->
|
node_info(Node) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
|
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
|
||||||
|
|
@ -167,18 +175,21 @@ get_metrics(Node) ->
|
||||||
|
|
||||||
get_stats() ->
|
get_stats() ->
|
||||||
GlobalStatsKeys =
|
GlobalStatsKeys =
|
||||||
[ 'retained.count'
|
[
|
||||||
, 'retained.max'
|
'retained.count',
|
||||||
, 'topics.count'
|
'retained.max',
|
||||||
, 'topics.max'
|
'topics.count',
|
||||||
, 'subscriptions.shared.count'
|
'topics.max',
|
||||||
, 'subscriptions.shared.max'
|
'subscriptions.shared.count',
|
||||||
|
'subscriptions.shared.max'
|
||||||
],
|
],
|
||||||
CountStats = nodes_info_count([
|
CountStats = nodes_info_count([
|
||||||
begin
|
begin
|
||||||
Stats = get_stats(Node),
|
Stats = get_stats(Node),
|
||||||
delete_keys(Stats, GlobalStatsKeys)
|
delete_keys(Stats, GlobalStatsKeys)
|
||||||
end || Node <- mria_mnesia:running_nodes()]),
|
end
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]),
|
||||||
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
|
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
|
||||||
maps:merge(CountStats, GlobalStats).
|
maps:merge(CountStats, GlobalStats).
|
||||||
|
|
||||||
|
|
@ -207,21 +218,28 @@ nodes_info_count(PropList) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
lookup_client({clientid, ClientId}, FormatFun) ->
|
lookup_client({clientid, ClientId}, FormatFun) ->
|
||||||
lists:append([lookup_client(Node, {clientid, ClientId}, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]);
|
lookup_client(Node, {clientid, ClientId}, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]);
|
||||||
lookup_client({username, Username}, FormatFun) ->
|
lookup_client({username, Username}, FormatFun) ->
|
||||||
lists:append([lookup_client(Node, {username, Username}, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]).
|
lookup_client(Node, {username, Username}, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]).
|
||||||
|
|
||||||
lookup_client(Node, Key, {M, F}) ->
|
lookup_client(Node, Key, {M, F}) ->
|
||||||
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
|
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
|
||||||
{error, Err} -> {error, Err};
|
{error, Err} ->
|
||||||
L -> lists:map(fun({Chan, Info0, Stats}) ->
|
{error, Err};
|
||||||
Info = Info0#{node => Node},
|
L ->
|
||||||
M:F({Chan, Info, Stats})
|
lists:map(
|
||||||
end,
|
fun({Chan, Info0, Stats}) ->
|
||||||
L)
|
Info = Info0#{node => Node},
|
||||||
|
M:F({Chan, Info, Stats})
|
||||||
|
end,
|
||||||
|
L
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
kickout_client({ClientID, FormatFun}) ->
|
kickout_client({ClientID, FormatFun}) ->
|
||||||
|
|
@ -266,7 +284,7 @@ clean_authz_cache(Node, ClientId) ->
|
||||||
clean_authz_cache_all() ->
|
clean_authz_cache_all() ->
|
||||||
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
|
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
|
||||||
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
|
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
BadNodes -> {error, BadNodes}
|
BadNodes -> {error, BadNodes}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -287,9 +305,13 @@ set_keepalive(_ClientId, _Interval) ->
|
||||||
%% @private
|
%% @private
|
||||||
call_client(ClientId, Req) ->
|
call_client(ClientId, Req) ->
|
||||||
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
|
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
|
||||||
Expected = lists:filter(fun({error, _}) -> false;
|
Expected = lists:filter(
|
||||||
(_) -> true
|
fun
|
||||||
end, Results),
|
({error, _}) -> false;
|
||||||
|
(_) -> true
|
||||||
|
end,
|
||||||
|
Results
|
||||||
|
),
|
||||||
case Expected of
|
case Expected of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Result | _] -> Result
|
[Result | _] -> Result
|
||||||
|
|
@ -299,13 +321,15 @@ call_client(ClientId, Req) ->
|
||||||
-spec do_call_client(emqx_types:clientid(), term()) -> term().
|
-spec do_call_client(emqx_types:clientid(), term()) -> term().
|
||||||
do_call_client(ClientId, Req) ->
|
do_call_client(ClientId, Req) ->
|
||||||
case emqx_cm:lookup_channels(ClientId) of
|
case emqx_cm:lookup_channels(ClientId) of
|
||||||
[] -> {error, not_found};
|
[] ->
|
||||||
|
{error, not_found};
|
||||||
Pids when is_list(Pids) ->
|
Pids when is_list(Pids) ->
|
||||||
Pid = lists:last(Pids),
|
Pid = lists:last(Pids),
|
||||||
case emqx_cm:get_chan_info(ClientId, Pid) of
|
case emqx_cm:get_chan_info(ClientId, Pid) of
|
||||||
#{conninfo := #{conn_mod := ConnMod}} ->
|
#{conninfo := #{conn_mod := ConnMod}} ->
|
||||||
erlang:apply(ConnMod, call, [Pid, Req]);
|
erlang:apply(ConnMod, call, [Pid, Req]);
|
||||||
undefined -> {error, not_found}
|
undefined ->
|
||||||
|
{error, not_found}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -320,22 +344,28 @@ call_client(Node, ClientId, Req) ->
|
||||||
-spec do_list_subscriptions() -> [map()].
|
-spec do_list_subscriptions() -> [map()].
|
||||||
do_list_subscriptions() ->
|
do_list_subscriptions() ->
|
||||||
case check_row_limit([mqtt_subproperty]) of
|
case check_row_limit([mqtt_subproperty]) of
|
||||||
false -> throw(max_row_limit);
|
false ->
|
||||||
ok -> [#{topic => Topic, clientid => ClientId, options => Options}
|
throw(max_row_limit);
|
||||||
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)]
|
ok ->
|
||||||
|
[
|
||||||
|
#{topic => Topic, clientid => ClientId, options => Options}
|
||||||
|
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)
|
||||||
|
]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_subscriptions(Node) ->
|
list_subscriptions(Node) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
|
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
|
||||||
|
|
||||||
list_subscriptions_via_topic(Topic, FormatFun) ->
|
list_subscriptions_via_topic(Topic, FormatFun) ->
|
||||||
lists:append([list_subscriptions_via_topic(Node, Topic, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]).
|
list_subscriptions_via_topic(Node, Topic, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]).
|
||||||
|
|
||||||
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
|
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
|
||||||
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
|
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
|
||||||
{error, Reason} -> {error, Reason};
|
{error, Reason} -> {error, Reason};
|
||||||
Result -> M:F(Result)
|
Result -> M:F(Result)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup_subscriptions(ClientId) ->
|
lookup_subscriptions(ClientId) ->
|
||||||
|
|
@ -354,20 +384,17 @@ subscribe(ClientId, TopicTables) ->
|
||||||
subscribe([Node | Nodes], ClientId, TopicTables) ->
|
subscribe([Node | Nodes], ClientId, TopicTables) ->
|
||||||
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
|
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
|
||||||
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
|
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
|
||||||
{subscribe, Res} ->
|
{subscribe, Res} -> {subscribe, Res, Node}
|
||||||
{subscribe, Res, Node}
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
subscribe([], _ClientId, _TopicTables) ->
|
subscribe([], _ClientId, _TopicTables) ->
|
||||||
{error, channel_not_found}.
|
{error, channel_not_found}.
|
||||||
|
|
||||||
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
|
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
|
||||||
{subscribe, _} | {error, atom()}.
|
{subscribe, _} | {error, atom()}.
|
||||||
do_subscribe(ClientId, TopicTables) ->
|
do_subscribe(ClientId, TopicTables) ->
|
||||||
case ets:lookup(emqx_channel, ClientId) of
|
case ets:lookup(emqx_channel, ClientId) of
|
||||||
[] -> {error, channel_not_found};
|
[] -> {error, channel_not_found};
|
||||||
[{_, Pid}] ->
|
[{_, Pid}] -> Pid ! {subscribe, TopicTables}
|
||||||
Pid ! {subscribe, TopicTables}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%TODO: ???
|
%%TODO: ???
|
||||||
|
|
@ -376,12 +403,12 @@ publish(Msg) ->
|
||||||
emqx:publish(Msg).
|
emqx:publish(Msg).
|
||||||
|
|
||||||
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
unsubscribe(ClientId, Topic) ->
|
unsubscribe(ClientId, Topic) ->
|
||||||
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
|
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
|
||||||
|
|
||||||
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
|
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
unsubscribe([Node | Nodes], ClientId, Topic) ->
|
unsubscribe([Node | Nodes], ClientId, Topic) ->
|
||||||
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
|
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
|
||||||
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
|
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
|
||||||
|
|
@ -391,12 +418,11 @@ unsubscribe([], _ClientId, _Topic) ->
|
||||||
{error, channel_not_found}.
|
{error, channel_not_found}.
|
||||||
|
|
||||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, _}.
|
{unsubscribe, _} | {error, _}.
|
||||||
do_unsubscribe(ClientId, Topic) ->
|
do_unsubscribe(ClientId, Topic) ->
|
||||||
case ets:lookup(emqx_channel, ClientId) of
|
case ets:lookup(emqx_channel, ClientId) of
|
||||||
[] -> {error, channel_not_found};
|
[] -> {error, channel_not_found};
|
||||||
[{_, Pid}] ->
|
[{_, Pid}] -> Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
||||||
Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -426,11 +452,18 @@ add_duration_field([], _Now, Acc) ->
|
||||||
Acc;
|
Acc;
|
||||||
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
|
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
|
||||||
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
|
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
|
||||||
|
add_duration_field(
|
||||||
add_duration_field( [Alarm = #{ activated := false
|
[
|
||||||
, activate_at := ActivateAt
|
Alarm = #{
|
||||||
, deactivate_at := DeactivateAt} | Rest]
|
activated := false,
|
||||||
, Now, Acc) ->
|
activate_at := ActivateAt,
|
||||||
|
deactivate_at := DeactivateAt
|
||||||
|
}
|
||||||
|
| Rest
|
||||||
|
],
|
||||||
|
Now,
|
||||||
|
Acc
|
||||||
|
) ->
|
||||||
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
|
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -462,13 +495,13 @@ check_row_limit([], _Limit) ->
|
||||||
ok;
|
ok;
|
||||||
check_row_limit([Tab | Tables], Limit) ->
|
check_row_limit([Tab | Tables], Limit) ->
|
||||||
case table_size(Tab) > Limit of
|
case table_size(Tab) > Limit of
|
||||||
true -> false;
|
true -> false;
|
||||||
false -> check_row_limit(Tables, Limit)
|
false -> check_row_limit(Tables, Limit)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_results(Results) ->
|
check_results(Results) ->
|
||||||
case lists:any(fun(Item) -> Item =:= ok end, Results) of
|
case lists:any(fun(Item) -> Item =:= ok end, Results) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> wrap_rpc(lists:last(Results))
|
false -> wrap_rpc(lists:last(Results))
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,18 @@
|
||||||
|
|
||||||
-define(FRESH_SELECT, fresh_select).
|
-define(FRESH_SELECT, fresh_select).
|
||||||
|
|
||||||
-export([ paginate/3
|
-export([
|
||||||
, paginate/4
|
paginate/3,
|
||||||
]).
|
paginate/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% first_next query APIs
|
%% first_next query APIs
|
||||||
-export([ node_query/5
|
-export([
|
||||||
, cluster_query/4
|
node_query/5,
|
||||||
, select_table_with_count/5
|
cluster_query/4,
|
||||||
, b2i/1
|
select_table_with_count/5,
|
||||||
]).
|
b2i/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([do_query/6]).
|
-export([do_query/6]).
|
||||||
|
|
||||||
|
|
@ -50,30 +52,30 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
||||||
Limit = b2i(limit(Params)),
|
Limit = b2i(limit(Params)),
|
||||||
Cursor = qlc:cursor(Qh),
|
Cursor = qlc:cursor(Qh),
|
||||||
case Page > 1 of
|
case Page > 1 of
|
||||||
true ->
|
true ->
|
||||||
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
||||||
ok;
|
ok;
|
||||||
false -> ok
|
false ->
|
||||||
|
ok
|
||||||
end,
|
end,
|
||||||
Rows = qlc:next_answers(Cursor, Limit),
|
Rows = qlc:next_answers(Cursor, Limit),
|
||||||
qlc:delete_cursor(Cursor),
|
qlc:delete_cursor(Cursor),
|
||||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
#{
|
||||||
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]}.
|
meta => #{page => Page, limit => Limit, count => Count},
|
||||||
|
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
|
||||||
|
}.
|
||||||
|
|
||||||
query_handle(Table) when is_atom(Table) ->
|
query_handle(Table) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table)]);
|
qlc:q([R || R <- ets:table(Table)]);
|
||||||
|
|
||||||
query_handle({Table, Opts}) when is_atom(Table) ->
|
query_handle({Table, Opts}) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||||
|
|
||||||
query_handle([Table]) when is_atom(Table) ->
|
query_handle([Table]) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table)]);
|
qlc:q([R || R <- ets:table(Table)]);
|
||||||
|
|
||||||
query_handle([{Table, Opts}]) when is_atom(Table) ->
|
query_handle([{Table, Opts}]) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||||
|
|
||||||
query_handle(Tables) ->
|
query_handle(Tables) ->
|
||||||
qlc:append([query_handle(T) || T <- Tables]). %
|
%
|
||||||
|
qlc:append([query_handle(T) || T <- Tables]).
|
||||||
|
|
||||||
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||||
Options = {traverse, {select, MatchSpec}},
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
|
@ -87,16 +89,12 @@ query_handle(Tables, MatchSpec) ->
|
||||||
|
|
||||||
count(Table) when is_atom(Table) ->
|
count(Table) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count({Table, _}) when is_atom(Table) ->
|
count({Table, _}) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count([Table]) when is_atom(Table) ->
|
count([Table]) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count([{Table, _}]) when is_atom(Table) ->
|
count([{Table, _}]) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count(Tables) ->
|
count(Tables) ->
|
||||||
lists:sum([count(T) || T <- Tables]).
|
lists:sum([count(T) || T <- Tables]).
|
||||||
|
|
||||||
|
|
@ -121,7 +119,7 @@ limit(Params) ->
|
||||||
|
|
||||||
init_meta(Params) ->
|
init_meta(Params) ->
|
||||||
Limit = b2i(limit(Params)),
|
Limit = b2i(limit(Params)),
|
||||||
Page = b2i(page(Params)),
|
Page = b2i(page(Params)),
|
||||||
#{
|
#{
|
||||||
page => Page,
|
page => Page,
|
||||||
limit => Limit,
|
limit => Limit,
|
||||||
|
|
@ -134,17 +132,24 @@ init_meta(Params) ->
|
||||||
|
|
||||||
node_query(Node, QString, Tab, QSchema, QueryFun) ->
|
node_query(Node, QString, Tab, QSchema, QueryFun) ->
|
||||||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||||
page_limit_check_query( init_meta(QString)
|
page_limit_check_query(
|
||||||
, { fun do_node_query/5
|
init_meta(QString),
|
||||||
, [Node, Tab, NQString, QueryFun, init_meta(QString)]}).
|
{fun do_node_query/5, [Node, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||||
|
).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
|
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
|
||||||
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
||||||
|
|
||||||
do_node_query( Node, Tab, QString, QueryFun, Continuation
|
do_node_query(
|
||||||
, Meta = #{limit := Limit}
|
Node,
|
||||||
, Results) ->
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
Continuation,
|
||||||
|
Meta = #{limit := Limit},
|
||||||
|
Results
|
||||||
|
) ->
|
||||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||||
{error, {badrpc, R}} ->
|
{error, {badrpc, R}} ->
|
||||||
{error, Node, {badrpc, R}};
|
{error, Node, {badrpc, R}};
|
||||||
|
|
@ -164,18 +169,33 @@ cluster_query(QString, Tab, QSchema, QueryFun) ->
|
||||||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
page_limit_check_query(
|
page_limit_check_query(
|
||||||
init_meta(QString)
|
init_meta(QString),
|
||||||
, {fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}).
|
{fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||||
|
).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
|
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
|
||||||
do_cluster_query( Nodes, Tab, QString, QueryFun
|
do_cluster_query(
|
||||||
, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
Nodes,
|
||||||
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
_Continuation = ?FRESH_SELECT,
|
||||||
|
Meta,
|
||||||
|
_Results = []
|
||||||
|
).
|
||||||
|
|
||||||
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
|
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
|
||||||
#{meta => Meta, data => Results};
|
#{meta => Meta, data => Results};
|
||||||
do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
do_cluster_query(
|
||||||
Meta = #{limit := Limit}, Results) ->
|
[Node | Tail] = Nodes,
|
||||||
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
Continuation,
|
||||||
|
Meta = #{limit := Limit},
|
||||||
|
Results
|
||||||
|
) ->
|
||||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||||
{error, {badrpc, R}} ->
|
{error, {badrpc, R}} ->
|
||||||
{error, Node, {bar_rpc, R}};
|
{error, Node, {bar_rpc, R}};
|
||||||
|
|
@ -192,11 +212,18 @@ do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% @private This function is exempt from BPAPI
|
%% @private This function is exempt from BPAPI
|
||||||
do_query(Node, Tab, QString, {M,F}, Continuation, Limit) when Node =:= node() ->
|
do_query(Node, Tab, QString, {M, F}, Continuation, Limit) when Node =:= node() ->
|
||||||
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
|
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
|
||||||
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
|
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
|
||||||
case rpc:call(Node, ?MODULE, do_query,
|
case
|
||||||
[Node, Tab, QString, QueryFun, Continuation, Limit], 50000) of
|
rpc:call(
|
||||||
|
Node,
|
||||||
|
?MODULE,
|
||||||
|
do_query,
|
||||||
|
[Node, Tab, QString, QueryFun, Continuation, Limit],
|
||||||
|
50000
|
||||||
|
)
|
||||||
|
of
|
||||||
{badrpc, _} = R -> {error, R};
|
{badrpc, _} = R -> {error, R};
|
||||||
Ret -> Ret
|
Ret -> Ret
|
||||||
end.
|
end.
|
||||||
|
|
@ -220,8 +247,9 @@ sub_query_result(Len, Rows, Limit, Results, Meta) ->
|
||||||
%% Table Select
|
%% Table Select
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun) when
|
||||||
when is_function(FuzzyFilterFun) andalso Limit > 0 ->
|
is_function(FuzzyFilterFun) andalso Limit > 0
|
||||||
|
->
|
||||||
case ets:select(Tab, Ms, Limit) of
|
case ets:select(Tab, Ms, Limit) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
|
@ -229,8 +257,9 @@ select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
||||||
Rows = FuzzyFilterFun(RawResult),
|
Rows = FuzzyFilterFun(RawResult),
|
||||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||||
end;
|
end;
|
||||||
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun)
|
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun) when
|
||||||
when is_function(FuzzyFilterFun) ->
|
is_function(FuzzyFilterFun)
|
||||||
|
->
|
||||||
case ets:select(ets:repair_continuation(Continuation, Ms)) of
|
case ets:select(ets:repair_continuation(Continuation, Ms)) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
|
@ -238,8 +267,9 @@ select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun
|
||||||
Rows = FuzzyFilterFun(RawResult),
|
Rows = FuzzyFilterFun(RawResult),
|
||||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||||
end;
|
end;
|
||||||
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun)
|
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun) when
|
||||||
when Limit > 0 ->
|
Limit > 0
|
||||||
|
->
|
||||||
case ets:select(Tab, Ms, Limit) of
|
case ets:select(Tab, Ms, Limit) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
|
@ -267,36 +297,53 @@ parse_qstring(QString, QSchema) ->
|
||||||
do_parse_qstring([], _, Acc1, Acc2) ->
|
do_parse_qstring([], _, Acc1, Acc2) ->
|
||||||
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
|
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
|
||||||
{lists:reverse(Acc1), lists:reverse(NAcc2)};
|
{lists:reverse(Acc1), lists:reverse(NAcc2)};
|
||||||
|
|
||||||
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
|
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
|
||||||
case proplists:get_value(Key, QSchema) of
|
case proplists:get_value(Key, QSchema) of
|
||||||
undefined -> do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
undefined ->
|
||||||
|
do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
||||||
Type ->
|
Type ->
|
||||||
case Key of
|
case Key of
|
||||||
<<Prefix:4/binary, NKey/binary>>
|
<<Prefix:4/binary, NKey/binary>> when
|
||||||
when Prefix =:= <<"gte_">>;
|
Prefix =:= <<"gte_">>;
|
||||||
Prefix =:= <<"lte_">> ->
|
Prefix =:= <<"lte_">>
|
||||||
OpposeKey = case Prefix of
|
->
|
||||||
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
OpposeKey =
|
||||||
<<"lte_">> -> <<"gte_", NKey/binary>>
|
case Prefix of
|
||||||
end,
|
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
||||||
|
<<"lte_">> -> <<"gte_", NKey/binary>>
|
||||||
|
end,
|
||||||
case lists:keytake(OpposeKey, 1, RestQString) of
|
case lists:keytake(OpposeKey, 1, RestQString) of
|
||||||
false ->
|
false ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, Type) | Acc1], Acc2);
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
);
|
||||||
{value, {K2, V2}, NParams} ->
|
{value, {K2, V2}, NParams} ->
|
||||||
do_parse_qstring( NParams, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, K2, V2, Type) | Acc1], Acc2)
|
NParams,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, K2, V2, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
case is_fuzzy_key(Key) of
|
case is_fuzzy_key(Key) of
|
||||||
true ->
|
true ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, Acc1, [qs(Key, Value, Type) | Acc2]);
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
Acc1,
|
||||||
|
[qs(Key, Value, Type) | Acc2]
|
||||||
|
);
|
||||||
_ ->
|
_ ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, Type) | Acc1], Acc2)
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
@ -310,7 +357,7 @@ qs(K, Value0, Type) ->
|
||||||
try
|
try
|
||||||
qs(K, to_type(Value0, Type))
|
qs(K, to_type(Value0, Type))
|
||||||
catch
|
catch
|
||||||
throw : bad_value_type ->
|
throw:bad_value_type ->
|
||||||
throw({bad_value_type, {K, Type, Value0}})
|
throw({bad_value_type, {K, Type, Value0}})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -333,12 +380,11 @@ is_fuzzy_key(_) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
page_start(1, _) -> 1;
|
page_start(1, _) -> 1;
|
||||||
page_start(Page, Limit) -> (Page-1) * Limit + 1.
|
page_start(Page, Limit) -> (Page - 1) * Limit + 1.
|
||||||
|
|
||||||
|
|
||||||
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
|
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
PageStart = page_start(Page, Limit),
|
PageStart = page_start(Page, Limit),
|
||||||
PageEnd = Page * Limit,
|
PageEnd = Page * Limit,
|
||||||
case Count + Len of
|
case Count + Len of
|
||||||
NCount when NCount < PageStart ->
|
NCount when NCount < PageStart ->
|
||||||
{more, Meta#{count => NCount}};
|
{more, Meta#{count => NCount}};
|
||||||
|
|
@ -353,7 +399,7 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
case (Count - Len) < PageStart of
|
case (Count - Len) < PageStart of
|
||||||
true ->
|
true ->
|
||||||
NeedNowNum = Count - PageStart + 1,
|
NeedNowNum = Count - PageStart + 1,
|
||||||
SubStart = Len - NeedNowNum + 1,
|
SubStart = Len - NeedNowNum + 1,
|
||||||
{SubStart, NeedNowNum};
|
{SubStart, NeedNowNum};
|
||||||
false ->
|
false ->
|
||||||
{_SubStart = 1, _NeedNowNum = Len}
|
{_SubStart = 1, _NeedNowNum = Len}
|
||||||
|
|
@ -361,8 +407,9 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
|
|
||||||
page_limit_check_query(Meta, {F, A}) ->
|
page_limit_check_query(Meta, {F, A}) ->
|
||||||
case Meta of
|
case Meta of
|
||||||
#{page := Page, limit := Limit}
|
#{page := Page, limit := Limit} when
|
||||||
when Page < 1; Limit < 1 ->
|
Page < 1; Limit < 1
|
||||||
|
->
|
||||||
{error, page_limit_invalid};
|
{error, page_limit_invalid};
|
||||||
_ ->
|
_ ->
|
||||||
erlang:apply(F, A)
|
erlang:apply(F, A)
|
||||||
|
|
@ -376,7 +423,7 @@ to_type(V, TargetType) ->
|
||||||
try
|
try
|
||||||
to_type_(V, TargetType)
|
to_type_(V, TargetType)
|
||||||
catch
|
catch
|
||||||
_ : _ ->
|
_:_ ->
|
||||||
throw(bad_value_type)
|
throw(bad_value_type)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -419,37 +466,43 @@ to_ip_port(IPAddress) ->
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
params2qs_test() ->
|
params2qs_test() ->
|
||||||
QSchema = [{<<"str">>, binary},
|
QSchema = [
|
||||||
{<<"int">>, integer},
|
{<<"str">>, binary},
|
||||||
{<<"atom">>, atom},
|
{<<"int">>, integer},
|
||||||
{<<"ts">>, timestamp},
|
{<<"atom">>, atom},
|
||||||
{<<"gte_range">>, integer},
|
{<<"ts">>, timestamp},
|
||||||
{<<"lte_range">>, integer},
|
{<<"gte_range">>, integer},
|
||||||
{<<"like_fuzzy">>, binary},
|
{<<"lte_range">>, integer},
|
||||||
{<<"match_topic">>, binary}],
|
{<<"like_fuzzy">>, binary},
|
||||||
QString = [{<<"str">>, <<"abc">>},
|
{<<"match_topic">>, binary}
|
||||||
{<<"int">>, <<"123">>},
|
],
|
||||||
{<<"atom">>, <<"connected">>},
|
QString = [
|
||||||
{<<"ts">>, <<"156000">>},
|
{<<"str">>, <<"abc">>},
|
||||||
{<<"gte_range">>, <<"1">>},
|
{<<"int">>, <<"123">>},
|
||||||
{<<"lte_range">>, <<"5">>},
|
{<<"atom">>, <<"connected">>},
|
||||||
{<<"like_fuzzy">>, <<"user">>},
|
{<<"ts">>, <<"156000">>},
|
||||||
{<<"match_topic">>, <<"t/#">>}],
|
{<<"gte_range">>, <<"1">>},
|
||||||
ExpectedQs = [{str, '=:=', <<"abc">>},
|
{<<"lte_range">>, <<"5">>},
|
||||||
{int, '=:=', 123},
|
{<<"like_fuzzy">>, <<"user">>},
|
||||||
{atom, '=:=', connected},
|
{<<"match_topic">>, <<"t/#">>}
|
||||||
{ts, '=:=', 156000},
|
],
|
||||||
{range, '>=', 1, '=<', 5}
|
ExpectedQs = [
|
||||||
],
|
{str, '=:=', <<"abc">>},
|
||||||
FuzzyNQString = [{fuzzy, like, <<"user">>},
|
{int, '=:=', 123},
|
||||||
{topic, match, <<"t/#">>}],
|
{atom, '=:=', connected},
|
||||||
|
{ts, '=:=', 156000},
|
||||||
|
{range, '>=', 1, '=<', 5}
|
||||||
|
],
|
||||||
|
FuzzyNQString = [
|
||||||
|
{fuzzy, like, <<"user">>},
|
||||||
|
{topic, match, <<"t/#">>}
|
||||||
|
],
|
||||||
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
|
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
|
||||||
|
|
||||||
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
|
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
|
|
||||||
b2i(Bin) when is_binary(Bin) ->
|
b2i(Bin) when is_binary(Bin) ->
|
||||||
binary_to_integer(Bin);
|
binary_to_integer(Bin);
|
||||||
b2i(Any) ->
|
b2i(Any) ->
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
|
@ -38,13 +39,16 @@ schema("/alarms") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => alarms,
|
'operationId' => alarms,
|
||||||
get => #{
|
get => #{
|
||||||
description => <<"EMQX alarms">>,
|
description => ?DESC(list_alarms_api),
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{activated, hoconsc:mk(boolean(), #{in => query,
|
{activated,
|
||||||
desc => <<"All alarms, if not specified">>,
|
hoconsc:mk(boolean(), #{
|
||||||
required => false})}
|
in => query,
|
||||||
|
desc => ?DESC(get_alarms_qs_activated),
|
||||||
|
required => false
|
||||||
|
})}
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [
|
200 => [
|
||||||
|
|
@ -53,34 +57,48 @@ schema("/alarms") ->
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
description => <<"Remove all deactivated alarms">>,
|
description => ?DESC(delete_alarms_api),
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Remove all deactivated alarms ok">>
|
204 => ?DESC(delete_alarms_api_response204)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(alarm) ->
|
fields(alarm) ->
|
||||||
[
|
[
|
||||||
{node, hoconsc:mk(binary(),
|
{node,
|
||||||
#{desc => <<"Alarm in node">>, example => atom_to_list(node())})},
|
hoconsc:mk(
|
||||||
{name, hoconsc:mk(binary(),
|
binary(),
|
||||||
#{desc => <<"Alarm name">>, example => <<"high_system_memory_usage">>})},
|
#{desc => ?DESC(node), example => atom_to_list(node())}
|
||||||
{message, hoconsc:mk(binary(), #{desc => <<"Alarm readable information">>,
|
)},
|
||||||
example => <<"System memory usage is higher than 70%">>})},
|
{name,
|
||||||
{details, hoconsc:mk(map(), #{desc => <<"Alarm details information">>,
|
hoconsc:mk(
|
||||||
example => #{<<"high_watermark">> => 70}})},
|
binary(),
|
||||||
{duration, hoconsc:mk(integer(),
|
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>}
|
||||||
#{desc => <<"Alarms duration time; UNIX time stamp, millisecond">>,
|
)},
|
||||||
example => 297056})},
|
{message,
|
||||||
{activate_at, hoconsc:mk(binary(), #{desc => <<"Alarms activate time, RFC 3339">>,
|
hoconsc:mk(binary(), #{
|
||||||
example => <<"2021-10-25T11:52:52.548+08:00">>})},
|
desc => ?DESC(message),
|
||||||
{deactivate_at, hoconsc:mk(binary(),
|
example => <<"System memory usage is higher than 70%">>
|
||||||
#{desc => <<"Nullable, alarms deactivate time, RFC 3339">>,
|
})},
|
||||||
example => <<"2021-10-31T10:52:52.548+08:00">>})}
|
{details,
|
||||||
|
hoconsc:mk(map(), #{
|
||||||
|
desc => ?DESC(details),
|
||||||
|
example => #{<<"high_watermark">> => 70}
|
||||||
|
})},
|
||||||
|
{duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
|
||||||
|
{activate_at,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => ?DESC(activate_at),
|
||||||
|
example => <<"2021-10-25T11:52:52.548+08:00">>
|
||||||
|
})},
|
||||||
|
{deactivate_at,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => ?DESC(deactivate_at),
|
||||||
|
example => <<"2021-10-31T10:52:52.548+08:00">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
emqx_dashboard_swagger:fields(limit) ++
|
emqx_dashboard_swagger:fields(limit) ++
|
||||||
|
|
@ -93,9 +111,15 @@ alarms(get, #{query_string := QString}) ->
|
||||||
true -> ?ACTIVATED_ALARM;
|
true -> ?ACTIVATED_ALARM;
|
||||||
false -> ?DEACTIVATED_ALARM
|
false -> ?DEACTIVATED_ALARM
|
||||||
end,
|
end,
|
||||||
Response = emqx_mgmt_api:cluster_query(QString, Table, [], {?MODULE, query}),
|
case emqx_mgmt_api:cluster_query(QString, Table, [], {?MODULE, query}) of
|
||||||
emqx_mgmt_util:generate_response(Response);
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Response ->
|
||||||
|
{200, Response}
|
||||||
|
end;
|
||||||
alarms(delete, _Params) ->
|
alarms(delete, _Params) ->
|
||||||
_ = emqx_mgmt:delete_all_deactivated_alarms(),
|
_ = emqx_mgmt:delete_all_deactivated_alarms(),
|
||||||
{204}.
|
{204}.
|
||||||
|
|
@ -104,11 +128,10 @@ alarms(delete, _Params) ->
|
||||||
%% internal
|
%% internal
|
||||||
|
|
||||||
query(Table, _QsSpec, Continuation, Limit) ->
|
query(Table, _QsSpec, Continuation, Limit) ->
|
||||||
Ms = [{'$1',[],['$1']}],
|
Ms = [{'$1', [], ['$1']}],
|
||||||
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
|
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
|
||||||
|
|
||||||
format_alarm(Alarms) when is_list(Alarms) ->
|
format_alarm(Alarms) when is_list(Alarms) ->
|
||||||
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
|
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
|
||||||
|
|
||||||
format_alarm(Alarm) ->
|
format_alarm(Alarm) ->
|
||||||
emqx_alarm:format(Alarm).
|
emqx_alarm:format(Alarm).
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ api_spec() ->
|
||||||
paths() ->
|
paths() ->
|
||||||
["/api_key", "/api_key/:name"].
|
["/api_key", "/api_key/:name"].
|
||||||
|
|
||||||
|
|
||||||
schema("/api_key") ->
|
schema("/api_key") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => api_key,
|
'operationId' => api_key,
|
||||||
|
|
@ -82,41 +81,80 @@ schema("/api_key/:name") ->
|
||||||
|
|
||||||
fields(app) ->
|
fields(app) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
hoconsc:mk(
|
||||||
validator => fun ?MODULE:validate_name/1,
|
binary(),
|
||||||
example => <<"EMQX-API-KEY-1">>})},
|
#{
|
||||||
{api_key, hoconsc:mk(binary(),
|
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||||
#{desc => """TODO:uses HMAC-SHA256 for signing.""",
|
validator => fun ?MODULE:validate_name/1,
|
||||||
example => <<"a4697a5c75a769f6">>})},
|
example => <<"EMQX-API-KEY-1">>
|
||||||
{api_secret, hoconsc:mk(binary(),
|
}
|
||||||
#{desc => """An API secret is a simple encrypted string that identifies"""
|
)},
|
||||||
"""an application without any principal."""
|
{api_key,
|
||||||
"""They are useful for accessing public data anonymously,"""
|
hoconsc:mk(
|
||||||
"""and are used to associate API requests.""",
|
binary(),
|
||||||
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>})},
|
#{
|
||||||
{expired_at, hoconsc:mk(hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
desc => "" "TODO:uses HMAC-SHA256 for signing." "",
|
||||||
#{desc => "No longer valid datetime",
|
example => <<"a4697a5c75a769f6">>
|
||||||
example => <<"2021-12-05T02:01:34.186Z">>,
|
}
|
||||||
required => false,
|
)},
|
||||||
default => undefined
|
{api_secret,
|
||||||
})},
|
hoconsc:mk(
|
||||||
{created_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
binary(),
|
||||||
#{desc => "ApiKey create datetime",
|
#{
|
||||||
example => <<"2021-12-01T00:00:00.000Z">>
|
desc =>
|
||||||
})},
|
""
|
||||||
{desc, hoconsc:mk(binary(),
|
"An API secret is a simple encrypted string that identifies"
|
||||||
#{example => <<"Note">>, required => false})},
|
""
|
||||||
|
""
|
||||||
|
"an application without any principal."
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"They are useful for accessing public data anonymously,"
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"and are used to associate API requests."
|
||||||
|
"",
|
||||||
|
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{expired_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
||||||
|
#{
|
||||||
|
desc => "No longer valid datetime",
|
||||||
|
example => <<"2021-12-05T02:01:34.186Z">>,
|
||||||
|
required => false,
|
||||||
|
default => undefined
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{created_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
emqx_datetime:epoch_second(),
|
||||||
|
#{
|
||||||
|
desc => "ApiKey create datetime",
|
||||||
|
example => <<"2021-12-01T00:00:00.000Z">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{desc,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{example => <<"Note">>, required => false}
|
||||||
|
)},
|
||||||
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
|
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
hoconsc:mk(
|
||||||
example => <<"EMQX-API-KEY-1">>,
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
||||||
})}
|
example => <<"EMQX-API-KEY-1">>,
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||||
|
|
@ -129,7 +167,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete(Keys, Fields) ->
|
delete(Keys, Fields) ->
|
||||||
|
|
@ -146,10 +185,13 @@ api_key(post, #{body := App}) ->
|
||||||
ExpiredAt = ensure_expired_at(App),
|
ExpiredAt = ensure_expired_at(App),
|
||||||
Desc = unicode:characters_to_binary(Desc0, unicode),
|
Desc = unicode:characters_to_binary(Desc0, unicode),
|
||||||
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
|
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
|
||||||
{ok, NewApp} -> {200, format(NewApp)};
|
{ok, NewApp} ->
|
||||||
|
{200, format(NewApp)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => 'BAD_REQUEST',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'BAD_REQUEST',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
|
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
|
||||||
|
|
@ -184,5 +226,5 @@ format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
|
||||||
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
|
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
|
||||||
}.
|
}.
|
||||||
|
|
||||||
ensure_expired_at(#{<<"expired_at">> := ExpiredAt})when is_integer(ExpiredAt) -> ExpiredAt;
|
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
|
||||||
ensure_expired_at(_) -> undefined.
|
ensure_expired_at(_) -> undefined.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
-module(emqx_mgmt_api_banned).
|
-module(emqx_mgmt_api_banned).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
|
@ -23,16 +24,19 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([format/1]).
|
-export([format/1]).
|
||||||
|
|
||||||
-export([ banned/2
|
-export([
|
||||||
, delete_banned/2
|
banned/2,
|
||||||
]).
|
delete_banned/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TAB, emqx_banned).
|
-define(TAB, emqx_banned).
|
||||||
|
|
||||||
|
|
@ -48,28 +52,29 @@ paths() ->
|
||||||
|
|
||||||
schema("/banned") ->
|
schema("/banned") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => banned,
|
'operationId' => banned,
|
||||||
get => #{
|
get => #{
|
||||||
description => <<"List banned">>,
|
description => ?DESC(list_banned_api),
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit)
|
hoconsc:ref(emqx_dashboard_swagger, limit)
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>[
|
200 => [
|
||||||
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
|
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
|
||||||
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
|
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
post => #{
|
post => #{
|
||||||
description => <<"Create banned">>,
|
description => ?DESC(create_banned_api),
|
||||||
'requestBody' => hoconsc:mk(hoconsc:ref(ban)),
|
'requestBody' => hoconsc:mk(hoconsc:ref(ban)),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
|
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
|
||||||
400 => emqx_dashboard_swagger:error_codes(
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
||||||
<<"Banned already existed, or bad args">>)
|
?DESC(create_banned_api_response400)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -77,53 +82,71 @@ schema("/banned/:as/:who") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => delete_banned,
|
'operationId' => delete_banned,
|
||||||
delete => #{
|
delete => #{
|
||||||
description => <<"Delete banned">>,
|
description => ?DESC(delete_banned_api),
|
||||||
parameters => [
|
parameters => [
|
||||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
{as,
|
||||||
desc => <<"Banned type">>,
|
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||||
in => path,
|
desc => ?DESC(as),
|
||||||
example => username})},
|
required => true,
|
||||||
{who, hoconsc:mk(binary(), #{
|
in => path,
|
||||||
desc => <<"Client info as banned type">>,
|
example => username
|
||||||
in => path,
|
})},
|
||||||
example => <<"Badass">>})}
|
{who,
|
||||||
],
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => ?DESC(who),
|
||||||
|
required => true,
|
||||||
|
in => path,
|
||||||
|
example => <<"Badass">>
|
||||||
|
})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Delete banned success">>,
|
204 => <<"Delete banned success">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['NOT_FOUND'],
|
['NOT_FOUND'],
|
||||||
<<"Banned not found. May be the banned time has been exceeded">>)
|
?DESC(delete_banned_api_response404)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(ban) ->
|
fields(ban) ->
|
||||||
[
|
[
|
||||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
{as,
|
||||||
desc => <<"Banned type clientid, username, peerhost">>,
|
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||||
required => true,
|
desc => ?DESC(as),
|
||||||
example => username})},
|
required => true,
|
||||||
{who, hoconsc:mk(binary(), #{
|
example => username
|
||||||
desc => <<"Client info as banned type">>,
|
})},
|
||||||
required => true,
|
{who,
|
||||||
example => <<"Banned name"/utf8>>})},
|
hoconsc:mk(binary(), #{
|
||||||
{by, hoconsc:mk(binary(), #{
|
desc => ?DESC(who),
|
||||||
desc => <<"Commander">>,
|
required => true,
|
||||||
required => false,
|
example => <<"Banned name"/utf8>>
|
||||||
example => <<"mgmt_api">>})},
|
})},
|
||||||
{reason, hoconsc:mk(binary(), #{
|
{by,
|
||||||
desc => <<"Banned reason">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
desc => ?DESC(by),
|
||||||
example => <<"Too many requests">>})},
|
required => false,
|
||||||
{at, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
example => <<"mgmt_api">>
|
||||||
desc => <<"Create banned time, rfc3339, now if not specified">>,
|
})},
|
||||||
required => false,
|
{reason,
|
||||||
example => <<"2021-10-25T21:48:47+08:00">>})},
|
hoconsc:mk(binary(), #{
|
||||||
{until, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
desc => ?DESC(reason),
|
||||||
desc => <<"Cancel banned time, rfc3339, now + 5 minute if not specified">>,
|
required => false,
|
||||||
required => false,
|
example => <<"Too many requests">>
|
||||||
example => <<"2021-10-25T21:53:47+08:00">>})
|
})},
|
||||||
}
|
{at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||||
|
desc => ?DESC(at),
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-10-25T21:48:47+08:00">>
|
||||||
|
})},
|
||||||
|
{until,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||||
|
desc => ?DESC(until),
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-10-25T21:53:47+08:00">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
|
|
@ -140,8 +163,7 @@ banned(post, #{body := Body}) ->
|
||||||
Ban ->
|
Ban ->
|
||||||
case emqx_banned:create(Ban) of
|
case emqx_banned:create(Ban) of
|
||||||
{ok, Banned} -> {200, format(Banned)};
|
{ok, Banned} -> {200, format(Banned)};
|
||||||
{error, {already_exist, Old}} ->
|
{error, {already_exist, Old}} -> {400, 'ALREADY_EXISTS', format(Old)}
|
||||||
{400, 'ALREADY_EXISTS', format(Old)}
|
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,63 +26,70 @@
|
||||||
-include("emqx_mgmt.hrl").
|
-include("emqx_mgmt.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ clients/2
|
-export([
|
||||||
, client/2
|
clients/2,
|
||||||
, subscriptions/2
|
client/2,
|
||||||
, authz_cache/2
|
subscriptions/2,
|
||||||
, subscribe/2
|
authz_cache/2,
|
||||||
, unsubscribe/2
|
subscribe/2,
|
||||||
, subscribe_batch/2
|
unsubscribe/2,
|
||||||
, set_keepalive/2
|
subscribe_batch/2,
|
||||||
]).
|
set_keepalive/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format_channel_info/1
|
query/4,
|
||||||
]).
|
format_channel_info/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% for batch operation
|
%% for batch operation
|
||||||
-export([do_subscribe/3]).
|
-export([do_subscribe/3]).
|
||||||
|
|
||||||
-define(CLIENT_QTAB, emqx_channel_info).
|
-define(CLIENT_QTAB, emqx_channel_info).
|
||||||
|
|
||||||
-define(CLIENT_QSCHEMA,
|
-define(CLIENT_QSCHEMA, [
|
||||||
[ {<<"node">>, atom}
|
{<<"node">>, atom},
|
||||||
, {<<"username">>, binary}
|
{<<"username">>, binary},
|
||||||
, {<<"zone">>, atom}
|
{<<"zone">>, atom},
|
||||||
, {<<"ip_address">>, ip}
|
{<<"ip_address">>, ip},
|
||||||
, {<<"conn_state">>, atom}
|
{<<"conn_state">>, atom},
|
||||||
, {<<"clean_start">>, atom}
|
{<<"clean_start">>, atom},
|
||||||
, {<<"proto_name">>, binary}
|
{<<"proto_name">>, binary},
|
||||||
, {<<"proto_ver">>, integer}
|
{<<"proto_ver">>, integer},
|
||||||
, {<<"like_clientid">>, binary}
|
{<<"like_clientid">>, binary},
|
||||||
, {<<"like_username">>, binary}
|
{<<"like_username">>, binary},
|
||||||
, {<<"gte_created_at">>, timestamp}
|
{<<"gte_created_at">>, timestamp},
|
||||||
, {<<"lte_created_at">>, timestamp}
|
{<<"lte_created_at">>, timestamp},
|
||||||
, {<<"gte_connected_at">>, timestamp}
|
{<<"gte_connected_at">>, timestamp},
|
||||||
, {<<"lte_connected_at">>, timestamp}]).
|
{<<"lte_connected_at">>, timestamp}
|
||||||
|
]).
|
||||||
|
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
|
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
|
||||||
|
|
||||||
-define(CLIENT_ID_NOT_FOUND,
|
-define(CLIENT_ID_NOT_FOUND,
|
||||||
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>).
|
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>
|
||||||
|
).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/clients"
|
[
|
||||||
, "/clients/:clientid"
|
"/clients",
|
||||||
, "/clients/:clientid/authz_cache"
|
"/clients/:clientid",
|
||||||
, "/clients/:clientid/subscriptions"
|
"/clients/:clientid/authorization/cache",
|
||||||
, "/clients/:clientid/subscribe"
|
"/clients/:clientid/subscriptions",
|
||||||
, "/clients/:clientid/unsubscribe"
|
"/clients/:clientid/subscribe",
|
||||||
, "/clients/:clientid/keepalive"
|
"/clients/:clientid/unsubscribe",
|
||||||
|
"/clients/:clientid/keepalive"
|
||||||
].
|
].
|
||||||
|
|
||||||
schema("/clients") ->
|
schema("/clients") ->
|
||||||
|
|
@ -93,69 +100,105 @@ schema("/clients") ->
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{node, hoconsc:mk(binary(), #{
|
{node,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Node name">>,
|
required => false,
|
||||||
example => atom_to_list(node())})},
|
desc => <<"Node name">>,
|
||||||
{username, hoconsc:mk(binary(), #{
|
example => atom_to_list(node())
|
||||||
in => query,
|
})},
|
||||||
required => false,
|
{username,
|
||||||
desc => <<"User name">>})},
|
hoconsc:mk(binary(), #{
|
||||||
{zone, hoconsc:mk(binary(), #{
|
in => query,
|
||||||
in => query,
|
required => false,
|
||||||
required => false})},
|
desc => <<"User name">>
|
||||||
{ip_address, hoconsc:mk(binary(), #{
|
})},
|
||||||
in => query,
|
{zone,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Client's IP address">>,
|
in => query,
|
||||||
example => <<"127.0.0.1">>})},
|
required => false
|
||||||
{conn_state, hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
})},
|
||||||
in => query,
|
{ip_address,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"The current connection status of the client, ",
|
in => query,
|
||||||
"the possible values are connected,idle,disconnected">>})},
|
required => false,
|
||||||
{clean_start, hoconsc:mk(boolean(), #{
|
desc => <<"Client's IP address">>,
|
||||||
in => query,
|
example => <<"127.0.0.1">>
|
||||||
required => false,
|
})},
|
||||||
description => <<"Whether the client uses a new session">>})},
|
{conn_state,
|
||||||
{proto_name, hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
description => <<"Client protocol name, ",
|
desc =>
|
||||||
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>})},
|
<<"The current connection status of the client, ",
|
||||||
{proto_ver, hoconsc:mk(binary(), #{
|
"the possible values are connected,idle,disconnected">>
|
||||||
in => query,
|
})},
|
||||||
required => false,
|
{clean_start,
|
||||||
desc => <<"Client protocol version">>})},
|
hoconsc:mk(boolean(), #{
|
||||||
{like_clientid, hoconsc:mk(binary(), #{
|
in => query,
|
||||||
in => query,
|
required => false,
|
||||||
required => false,
|
description => <<"Whether the client uses a new session">>
|
||||||
desc => <<"Fuzzy search `clientid` as substring">>})},
|
})},
|
||||||
{like_username, hoconsc:mk(binary(), #{
|
{proto_name,
|
||||||
in => query,
|
hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Fuzzy search `username` as substring">>})},
|
required => false,
|
||||||
{gte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
description =>
|
||||||
in => query,
|
<<"Client protocol name, ",
|
||||||
required => false,
|
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>
|
||||||
desc => <<"Search client session creation time by greater",
|
})},
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
{proto_ver,
|
||||||
{lte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
hoconsc:mk(binary(), #{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
desc => <<"Search client session creation time by less",
|
desc => <<"Client protocol version">>
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
})},
|
||||||
{gte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
{like_clientid,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Search client connection creation time by greater"
|
required => false,
|
||||||
" than or equal method, rfc3339 or timestamp(epoch millisecond)">>})},
|
desc => <<"Fuzzy search `clientid` as substring">>
|
||||||
{lte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
})},
|
||||||
in => query,
|
{like_username,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Search client connection creation time by less"
|
in => query,
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})}
|
required => false,
|
||||||
|
desc => <<"Fuzzy search `username` as substring">>
|
||||||
|
})},
|
||||||
|
{gte_created_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc =>
|
||||||
|
<<"Search client session creation time by greater",
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||||
|
})},
|
||||||
|
{lte_created_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc =>
|
||||||
|
<<"Search client session creation time by less",
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||||
|
})},
|
||||||
|
{gte_connected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc => <<
|
||||||
|
"Search client connection creation time by greater"
|
||||||
|
" than or equal method, rfc3339 or timestamp(epoch millisecond)"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{lte_connected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc => <<
|
||||||
|
"Search client connection creation time by less"
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)"
|
||||||
|
>>
|
||||||
|
})}
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [
|
200 => [
|
||||||
|
|
@ -164,10 +207,11 @@ schema("/clients") ->
|
||||||
],
|
],
|
||||||
400 =>
|
400 =>
|
||||||
emqx_dashboard_swagger:error_codes(
|
emqx_dashboard_swagger:error_codes(
|
||||||
['INVALID_PARAMETER'], <<"Invalid parameters">>)}
|
['INVALID_PARAMETER'], <<"Invalid parameters">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid") ->
|
schema("/clients/:clientid") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => client,
|
'operationId' => client,
|
||||||
|
|
@ -177,42 +221,47 @@ schema("/clients/:clientid") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)}},
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
description => <<"Kick out client by client ID">>,
|
description => <<"Kick out client by client ID">>,
|
||||||
parameters => [
|
parameters => [
|
||||||
{clientid, hoconsc:mk(binary(), #{in => path})}],
|
{clientid, hoconsc:mk(binary(), #{in => path})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Kick out client successfully">>,
|
204 => <<"Kick out client successfully">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
schema("/clients/:clientid/authorization/cache") ->
|
||||||
schema("/clients/:clientid/authz_cache") ->
|
|
||||||
#{
|
#{
|
||||||
'operationId' => authz_cache,
|
'operationId' => authz_cache,
|
||||||
get => #{
|
get => #{
|
||||||
description => <<"Get client authz cache">>,
|
description => <<"Get client authz cache in the cluster.">>,
|
||||||
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
description => <<"Clean client authz cache">>,
|
description => <<"Clean client authz cache in the cluster.">>,
|
||||||
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Kick out client successfully">>,
|
204 => <<"Kick out client successfully">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/subscriptions") ->
|
schema("/clients/:clientid/subscriptions") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => subscriptions,
|
'operationId' => subscriptions,
|
||||||
|
|
@ -220,13 +269,15 @@ schema("/clients/:clientid/subscriptions") ->
|
||||||
description => <<"Get client subscriptions">>,
|
description => <<"Get client subscriptions">>,
|
||||||
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}),
|
200 => hoconsc:mk(
|
||||||
|
hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}
|
||||||
|
),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/subscribe") ->
|
schema("/clients/:clientid/subscribe") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => subscribe,
|
'operationId' => subscribe,
|
||||||
|
|
@ -237,11 +288,11 @@ schema("/clients/:clientid/subscribe") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
|
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/unsubscribe") ->
|
schema("/clients/:clientid/unsubscribe") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => unsubscribe,
|
'operationId' => unsubscribe,
|
||||||
|
|
@ -252,11 +303,11 @@ schema("/clients/:clientid/unsubscribe") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Unsubscribe OK">>,
|
204 => <<"Unsubscribe OK">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/keepalive") ->
|
schema("/clients/:clientid/keepalive") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => set_keepalive,
|
'operationId' => set_keepalive,
|
||||||
|
|
@ -267,96 +318,187 @@ schema("/clients/:clientid/keepalive") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(client) ->
|
fields(client) ->
|
||||||
[
|
[
|
||||||
{awaiting_rel_cnt, hoconsc:mk(integer(), #{desc =>
|
{awaiting_rel_cnt,
|
||||||
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{awaiting_rel_max, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"v4 api name [max_awaiting_rel]. "
|
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>
|
||||||
"Maximum allowed number of awaiting PUBREC packet">>})},
|
})},
|
||||||
{clean_start, hoconsc:mk(boolean(), #{desc =>
|
{awaiting_rel_max,
|
||||||
<<"Indicate whether the client is using a brand new session">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<
|
||||||
|
"v4 api name [max_awaiting_rel]. "
|
||||||
|
"Maximum allowed number of awaiting PUBREC packet"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{clean_start,
|
||||||
|
hoconsc:mk(boolean(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Indicate whether the client is using a brand new session">>
|
||||||
|
})},
|
||||||
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
|
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
|
||||||
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
|
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
|
||||||
{connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
{connected_at,
|
||||||
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>})},
|
hoconsc:mk(
|
||||||
{created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
emqx_datetime:epoch_millisecond(),
|
||||||
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>})},
|
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>}
|
||||||
{disconnected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{desc =>
|
)},
|
||||||
<<"Client offline time."
|
{created_at,
|
||||||
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)">>})},
|
hoconsc:mk(
|
||||||
{expiry_interval, hoconsc:mk(integer(), #{desc =>
|
emqx_datetime:epoch_millisecond(),
|
||||||
<<"Session expiration interval, with the unit of second">>})},
|
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>}
|
||||||
{heap_size, hoconsc:mk(integer(), #{desc =>
|
)},
|
||||||
<<"Process heap size with the unit of byte">>})},
|
{disconnected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
desc =>
|
||||||
|
<<
|
||||||
|
"Client offline time."
|
||||||
|
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{expiry_interval,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Session expiration interval, with the unit of second">>
|
||||||
|
})},
|
||||||
|
{heap_size,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Process heap size with the unit of byte">>
|
||||||
|
})},
|
||||||
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
|
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
|
||||||
{inflight_max, hoconsc:mk(integer(), #{desc =>
|
{inflight_max,
|
||||||
<<"v4 api name [max_inflight]. Maximum length of inflight">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"v4 api name [max_inflight]. Maximum length of inflight">>
|
||||||
|
})},
|
||||||
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
|
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
|
||||||
{is_bridge, hoconsc:mk(boolean(), #{desc =>
|
{is_bridge,
|
||||||
<<"Indicates whether the client is connectedvia bridge">>})},
|
hoconsc:mk(boolean(), #{
|
||||||
{keepalive, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"keepalive time, with the unit of second">>})},
|
<<"Indicates whether the client is connectedvia bridge">>
|
||||||
|
})},
|
||||||
|
{keepalive,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"keepalive time, with the unit of second">>
|
||||||
|
})},
|
||||||
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
|
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
|
||||||
{mqueue_dropped, hoconsc:mk(integer(), #{desc =>
|
{mqueue_dropped,
|
||||||
<<"Number of messages dropped by the message queue due to exceeding the length">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of messages dropped by the message queue due to exceeding the length">>
|
||||||
|
})},
|
||||||
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
|
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
|
||||||
{mqueue_max, hoconsc:mk(integer(), #{desc =>
|
{mqueue_max,
|
||||||
<<"v4 api name [max_mqueue]. Maximum length of message queue">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{node, hoconsc:mk(binary(), #{desc =>
|
desc =>
|
||||||
<<"Name of the node to which the client is connected">>})},
|
<<"v4 api name [max_mqueue]. Maximum length of message queue">>
|
||||||
|
})},
|
||||||
|
{node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Name of the node to which the client is connected">>
|
||||||
|
})},
|
||||||
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
|
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
|
||||||
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
|
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
|
||||||
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
|
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
|
||||||
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
|
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
|
||||||
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
|
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
|
||||||
{'recv_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
{'recv_msg.dropped',
|
||||||
<<"Number of dropped PUBLISH packets">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'recv_msg.dropped.await_pubrel_timeout', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
<<"Number of dropped PUBLISH packets">>
|
||||||
{'recv_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
})},
|
||||||
<<"Number of PUBLISH QoS0 packets received">>})},
|
{'recv_msg.dropped.await_pubrel_timeout',
|
||||||
{'recv_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
hoconsc:mk(integer(), #{
|
||||||
<<"Number of PUBLISH QoS1 packets received">>})},
|
desc =>
|
||||||
{'recv_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
<<"Number of dropped PUBLISH packets due to expired">>
|
||||||
<<"Number of PUBLISH QoS2 packets received">>})},
|
})},
|
||||||
|
{'recv_msg.qos0',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS0 packets received">>
|
||||||
|
})},
|
||||||
|
{'recv_msg.qos1',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS1 packets received">>
|
||||||
|
})},
|
||||||
|
{'recv_msg.qos2',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS2 packets received">>
|
||||||
|
})},
|
||||||
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
|
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
|
||||||
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
|
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
|
||||||
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
|
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
|
||||||
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
|
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
|
||||||
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
|
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
|
||||||
{'send_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
{'send_msg.dropped',
|
||||||
<<"Number of dropped PUBLISH packets">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'send_msg.dropped.expired', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
<<"Number of dropped PUBLISH packets">>
|
||||||
{'send_msg.dropped.queue_full', hoconsc:mk(integer(), #{desc =>
|
})},
|
||||||
<<"Number of dropped PUBLISH packets due to queue full">>})},
|
{'send_msg.dropped.expired',
|
||||||
{'send_msg.dropped.too_large', hoconsc:mk(integer(), #{desc =>
|
hoconsc:mk(integer(), #{
|
||||||
<<"Number of dropped PUBLISH packets due to packet length too large">>})},
|
desc =>
|
||||||
{'send_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
<<"Number of dropped PUBLISH packets due to expired">>
|
||||||
<<"Number of PUBLISH QoS0 packets sent">>})},
|
})},
|
||||||
{'send_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
{'send_msg.dropped.queue_full',
|
||||||
<<"Number of PUBLISH QoS1 packets sent">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'send_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of PUBLISH QoS2 packets sent">>})},
|
<<"Number of dropped PUBLISH packets due to queue full">>
|
||||||
|
})},
|
||||||
|
{'send_msg.dropped.too_large',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of dropped PUBLISH packets due to packet length too large">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos0',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS0 packets sent">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos1',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS1 packets sent">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos2',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS2 packets sent">>
|
||||||
|
})},
|
||||||
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
|
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
|
||||||
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
|
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
|
||||||
{subscriptions_cnt, hoconsc:mk(integer(), #{desc =>
|
{subscriptions_cnt,
|
||||||
<<"Number of subscriptions established by this client.">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{subscriptions_max, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"v4 api name [max_subscriptions]",
|
<<"Number of subscriptions established by this client.">>
|
||||||
" Maximum number of subscriptions allowed by this client">>})},
|
})},
|
||||||
|
{subscriptions_max,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"v4 api name [max_subscriptions]",
|
||||||
|
" Maximum number of subscriptions allowed by this client">>
|
||||||
|
})},
|
||||||
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
|
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
|
||||||
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
|
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
|
||||||
{zone, hoconsc:mk(binary(), #{desc =>
|
{zone,
|
||||||
<<"Indicate the configuration group used by the client">>})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Indicate the configuration group used by the client">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(authz_cache) ->
|
fields(authz_cache) ->
|
||||||
[
|
[
|
||||||
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
|
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
|
||||||
|
|
@ -364,23 +506,19 @@ fields(authz_cache) ->
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
|
||||||
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
|
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(keepalive) ->
|
fields(keepalive) ->
|
||||||
[
|
[
|
||||||
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
|
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(subscribe) ->
|
fields(subscribe) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
|
||||||
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
|
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(unsubscribe) ->
|
fields(unsubscribe) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
emqx_dashboard_swagger:fields(limit) ++
|
emqx_dashboard_swagger:fields(limit) ++
|
||||||
|
|
@ -393,13 +531,11 @@ clients(get, #{query_string := QString}) ->
|
||||||
|
|
||||||
client(get, #{bindings := Bindings}) ->
|
client(get, #{bindings := Bindings}) ->
|
||||||
lookup(Bindings);
|
lookup(Bindings);
|
||||||
|
|
||||||
client(delete, #{bindings := Bindings}) ->
|
client(delete, #{bindings := Bindings}) ->
|
||||||
kickout(Bindings).
|
kickout(Bindings).
|
||||||
|
|
||||||
authz_cache(get, #{bindings := Bindings}) ->
|
authz_cache(get, #{bindings := Bindings}) ->
|
||||||
get_authz_cache(Bindings);
|
get_authz_cache(Bindings);
|
||||||
|
|
||||||
authz_cache(delete, #{bindings := Bindings}) ->
|
authz_cache(delete, #{bindings := Bindings}) ->
|
||||||
clean_authz_cache(Bindings).
|
clean_authz_cache(Bindings).
|
||||||
|
|
||||||
|
|
@ -415,11 +551,14 @@ unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) ->
|
||||||
%% TODO: batch
|
%% TODO: batch
|
||||||
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
|
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
|
||||||
Topics =
|
Topics =
|
||||||
[begin
|
[
|
||||||
Topic = maps:get(<<"topic">>, TopicInfo),
|
begin
|
||||||
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
Topic = maps:get(<<"topic">>, TopicInfo),
|
||||||
#{topic => Topic, qos => Qos}
|
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
||||||
end || TopicInfo <- TopicInfos],
|
#{topic => Topic, qos => Qos}
|
||||||
|
end
|
||||||
|
|| TopicInfo <- TopicInfos
|
||||||
|
],
|
||||||
subscribe_batch(#{clientid => ClientID, topics => Topics}).
|
subscribe_batch(#{clientid => ClientID, topics => Topics}).
|
||||||
|
|
||||||
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
||||||
|
|
@ -436,16 +575,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
||||||
qos => maps:get(qos, SubOpts)
|
qos => maps:get(qos, SubOpts)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
{200, lists:map(Formatter, Subs)}
|
{200, lists:map(Formatter, Subs)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
||||||
case maps:find(<<"interval">>, Body) of
|
case maps:find(<<"interval">>, Body) of
|
||||||
error -> {400, 'BAD_REQUEST',"Interval Not Found"};
|
error ->
|
||||||
|
{400, 'BAD_REQUEST', "Interval Not Found"};
|
||||||
{ok, Interval} ->
|
{ok, Interval} ->
|
||||||
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
|
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
|
||||||
ok -> lookup(#{clientid => ClientID});
|
ok -> lookup(#{clientid => ClientID});
|
||||||
{error, not_found} ->{404, ?CLIENT_ID_NOT_FOUND};
|
{error, not_found} -> {404, ?CLIENT_ID_NOT_FOUND};
|
||||||
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
|
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
@ -454,17 +594,34 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
||||||
%% api apply
|
%% api apply
|
||||||
|
|
||||||
list_clients(QString) ->
|
list_clients(QString) ->
|
||||||
case maps:get(<<"node">>, QString, undefined) of
|
Result =
|
||||||
undefined ->
|
case maps:get(<<"node">>, QString, undefined) of
|
||||||
Response = emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB,
|
undefined ->
|
||||||
?CLIENT_QSCHEMA, ?QUERY_FUN),
|
emqx_mgmt_api:cluster_query(
|
||||||
emqx_mgmt_util:generate_response(Response);
|
QString,
|
||||||
Node1 ->
|
?CLIENT_QTAB,
|
||||||
Node = binary_to_atom(Node1, utf8),
|
?CLIENT_QSCHEMA,
|
||||||
QStringWithoutNode = maps:without([<<"node">>], QString),
|
?QUERY_FUN
|
||||||
Response = emqx_mgmt_api:node_query(Node, QStringWithoutNode,
|
);
|
||||||
?CLIENT_QTAB, ?CLIENT_QSCHEMA, ?QUERY_FUN),
|
Node0 ->
|
||||||
emqx_mgmt_util:generate_response(Response)
|
Node1 = binary_to_atom(Node0, utf8),
|
||||||
|
QStringWithoutNode = maps:without([<<"node">>], QString),
|
||||||
|
emqx_mgmt_api:node_query(
|
||||||
|
Node1,
|
||||||
|
QStringWithoutNode,
|
||||||
|
?CLIENT_QTAB,
|
||||||
|
?CLIENT_QSCHEMA,
|
||||||
|
?QUERY_FUN
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
case Result of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Response ->
|
||||||
|
{200, Response}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup(#{clientid := ClientID}) ->
|
lookup(#{clientid := ClientID}) ->
|
||||||
|
|
@ -483,7 +640,7 @@ kickout(#{clientid := ClientID}) ->
|
||||||
{204}
|
{204}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_authz_cache(#{clientid := ClientID})->
|
get_authz_cache(#{clientid := ClientID}) ->
|
||||||
case emqx_mgmt:list_authz_cache(ClientID) of
|
case emqx_mgmt:list_authz_cache(ClientID) of
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, ?CLIENT_ID_NOT_FOUND};
|
{404, ?CLIENT_ID_NOT_FOUND};
|
||||||
|
|
@ -517,9 +674,9 @@ subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) ->
|
||||||
Response =
|
Response =
|
||||||
#{
|
#{
|
||||||
clientid => ClientID,
|
clientid => ClientID,
|
||||||
topic => Topic,
|
topic => Topic,
|
||||||
qos => Qos,
|
qos => Qos,
|
||||||
node => Node
|
node => Node
|
||||||
},
|
},
|
||||||
{200, Response}
|
{200, Response}
|
||||||
end.
|
end.
|
||||||
|
|
@ -555,7 +712,7 @@ do_subscribe(ClientID, Topic0, Qos) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
do_unsubscribe(ClientID, Topic) ->
|
do_unsubscribe(ClientID, Topic) ->
|
||||||
case emqx_mgmt:unsubscribe(ClientID, Topic) of
|
case emqx_mgmt:unsubscribe(ClientID, Topic) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
@ -569,14 +726,23 @@ do_unsubscribe(ClientID, Topic) ->
|
||||||
|
|
||||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(QString),
|
Ms = qs2ms(QString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_channel_info/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_channel_info/1
|
||||||
|
);
|
||||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(QString),
|
Ms = qs2ms(QString),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_channel_info/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_channel_info/1
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% QueryString to Match Spec
|
%% QueryString to Match Spec
|
||||||
|
|
@ -588,7 +754,6 @@ qs2ms(Qs) ->
|
||||||
|
|
||||||
qs2ms([], _, {MtchHead, Conds}) ->
|
qs2ms([], _, {MtchHead, Conds}) ->
|
||||||
{MtchHead, lists:reverse(Conds)};
|
{MtchHead, lists:reverse(Conds)};
|
||||||
|
|
||||||
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
|
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
|
||||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
|
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
|
||||||
qs2ms(Rest, N, {NMtchHead, Conds});
|
qs2ms(Rest, N, {NMtchHead, Conds});
|
||||||
|
|
@ -596,13 +761,16 @@ qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
|
||||||
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
|
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
|
||||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
|
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
|
||||||
NConds = put_conds(Qs, Holder, Conds),
|
NConds = put_conds(Qs, Holder, Conds),
|
||||||
qs2ms(Rest, N+1, {NMtchHead, NConds}).
|
qs2ms(Rest, N + 1, {NMtchHead, NConds}).
|
||||||
|
|
||||||
put_conds({_, Op, V}, Holder, Conds) ->
|
put_conds({_, Op, V}, Holder, Conds) ->
|
||||||
[{Op, Holder, V} | Conds];
|
[{Op, Holder, V} | Conds];
|
||||||
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
|
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
|
||||||
[{Op2, Holder, V2},
|
[
|
||||||
{Op1, Holder, V1} | Conds].
|
{Op2, Holder, V2},
|
||||||
|
{Op1, Holder, V1}
|
||||||
|
| Conds
|
||||||
|
].
|
||||||
|
|
||||||
ms(clientid, X) ->
|
ms(clientid, X) ->
|
||||||
#{clientinfo => #{clientid => X}};
|
#{clientinfo => #{clientid => X}};
|
||||||
|
|
@ -630,68 +798,78 @@ ms(created_at, X) ->
|
||||||
|
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
true;
|
true;
|
||||||
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
|
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
|
||||||
Val = case maps:get(Key, ClientInfo, <<>>) of
|
Val =
|
||||||
undefined -> <<>>;
|
case maps:get(Key, ClientInfo, <<>>) of
|
||||||
V -> V
|
undefined -> <<>>;
|
||||||
end,
|
V -> V
|
||||||
|
end,
|
||||||
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% format funcs
|
%% format funcs
|
||||||
|
|
||||||
format_channel_info({_, ClientInfo, ClientStats}) ->
|
format_channel_info({_, ClientInfo, ClientStats}) ->
|
||||||
Node = case ClientInfo of
|
Node =
|
||||||
#{node := N} -> N;
|
case ClientInfo of
|
||||||
_ -> node()
|
#{node := N} -> N;
|
||||||
end,
|
_ -> node()
|
||||||
StatsMap = maps:without([memory, next_pkt_id, total_heap_size],
|
end,
|
||||||
maps:from_list(ClientStats)),
|
StatsMap = maps:without(
|
||||||
|
[memory, next_pkt_id, total_heap_size],
|
||||||
|
maps:from_list(ClientStats)
|
||||||
|
),
|
||||||
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
|
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
|
||||||
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
|
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
|
||||||
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
||||||
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
|
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
|
||||||
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
|
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
|
||||||
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
|
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
|
||||||
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
|
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
|
||||||
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
||||||
RemoveList =
|
RemoveList =
|
||||||
[ auth_result
|
[
|
||||||
, peername
|
auth_result,
|
||||||
, sockname
|
peername,
|
||||||
, peerhost
|
sockname,
|
||||||
, conn_state
|
peerhost,
|
||||||
, send_pend
|
conn_state,
|
||||||
, conn_props
|
send_pend,
|
||||||
, peercert
|
conn_props,
|
||||||
, sockstate
|
peercert,
|
||||||
, subscriptions
|
sockstate,
|
||||||
, receive_maximum
|
subscriptions,
|
||||||
, protocol
|
receive_maximum,
|
||||||
, is_superuser
|
protocol,
|
||||||
, sockport
|
is_superuser,
|
||||||
, anonymous
|
sockport,
|
||||||
, mountpoint
|
anonymous,
|
||||||
, socktype
|
mountpoint,
|
||||||
, active_n
|
socktype,
|
||||||
, await_rel_timeout
|
active_n,
|
||||||
, conn_mod
|
await_rel_timeout,
|
||||||
, sockname
|
conn_mod,
|
||||||
, retry_interval
|
sockname,
|
||||||
, upgrade_qos
|
retry_interval,
|
||||||
, id %% sessionID, defined in emqx_session.erl
|
upgrade_qos,
|
||||||
],
|
%% sessionID, defined in emqx_session.erl
|
||||||
|
id
|
||||||
|
],
|
||||||
TimesKeys = [created_at, connected_at, disconnected_at],
|
TimesKeys = [created_at, connected_at, disconnected_at],
|
||||||
%% format timestamp to rfc3339
|
%% format timestamp to rfc3339
|
||||||
lists:foldl(fun result_format_time_fun/2
|
lists:foldl(
|
||||||
, maps:without(RemoveList, ClientInfoMap)
|
fun result_format_time_fun/2,
|
||||||
, TimesKeys).
|
maps:without(RemoveList, ClientInfoMap),
|
||||||
|
TimesKeys
|
||||||
|
).
|
||||||
|
|
||||||
%% format func helpers
|
%% format func helpers
|
||||||
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
|
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
|
||||||
|
|
@ -703,20 +881,22 @@ result_format_time_fun(Key, NClientInfoMap) ->
|
||||||
case NClientInfoMap of
|
case NClientInfoMap of
|
||||||
#{Key := TimeStamp} ->
|
#{Key := TimeStamp} ->
|
||||||
NClientInfoMap#{
|
NClientInfoMap#{
|
||||||
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)};
|
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)
|
||||||
|
};
|
||||||
#{} ->
|
#{} ->
|
||||||
NClientInfoMap
|
NClientInfoMap
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}).
|
-spec peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}.
|
||||||
peername_dispart({Addr, Port}) ->
|
peername_dispart({Addr, Port}) ->
|
||||||
AddrBinary = list_to_binary(inet:ntoa(Addr)),
|
AddrBinary = list_to_binary(inet:ntoa(Addr)),
|
||||||
%% PortBinary = integer_to_binary(Port),
|
%% PortBinary = integer_to_binary(Port),
|
||||||
{AddrBinary, Port}.
|
{AddrBinary, Port}.
|
||||||
|
|
||||||
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
|
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
|
||||||
#{ access => PubSub,
|
#{
|
||||||
topic => Topic,
|
access => PubSub,
|
||||||
result => AuthzResult,
|
topic => Topic,
|
||||||
updated_time => Timestamp
|
result => AuthzResult,
|
||||||
}.
|
updated_time => Timestamp
|
||||||
|
}.
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ force_leave(delete, #{bindings := #{node := Node0}}) ->
|
||||||
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
|
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(join(node()) -> ok | ignore | {error, term()}).
|
-spec join(node()) -> ok | ignore | {error, term()}.
|
||||||
join(Node) ->
|
join(Node) ->
|
||||||
ekka:join(Node).
|
ekka:join(Node).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,13 @@
|
||||||
-export([api_spec/0, namespace/0]).
|
-export([api_spec/0, namespace/0]).
|
||||||
-export([paths/0, schema/1, fields/1]).
|
-export([paths/0, schema/1, fields/1]).
|
||||||
|
|
||||||
-export([ config/3
|
-export([
|
||||||
, config_reset/3
|
config/3,
|
||||||
, configs/3
|
config_reset/3,
|
||||||
, get_full_config/0
|
configs/3,
|
||||||
, global_zone_configs/3]).
|
get_full_config/0,
|
||||||
|
global_zone_configs/3
|
||||||
|
]).
|
||||||
|
|
||||||
-export([gen_schema/1]).
|
-export([gen_schema/1]).
|
||||||
|
|
||||||
|
|
@ -36,29 +38,30 @@
|
||||||
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
|
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
|
||||||
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
|
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
|
||||||
|
|
||||||
-define(EXCLUDES, [
|
-define(EXCLUDES,
|
||||||
<<"exhook">>,
|
[
|
||||||
<<"gateway">>,
|
<<"exhook">>,
|
||||||
<<"plugins">>,
|
<<"gateway">>,
|
||||||
<<"bridges">>,
|
<<"plugins">>,
|
||||||
<<"rule_engine">>,
|
<<"bridges">>,
|
||||||
<<"authorization">>,
|
<<"rule_engine">>,
|
||||||
<<"authentication">>,
|
<<"authorization">>,
|
||||||
<<"rpc">>,
|
<<"authentication">>,
|
||||||
<<"db">>,
|
<<"rpc">>,
|
||||||
<<"connectors">>,
|
<<"db">>,
|
||||||
<<"slow_subs">>,
|
<<"connectors">>,
|
||||||
<<"psk_authentication">>,
|
<<"slow_subs">>,
|
||||||
<<"topic_metrics">>,
|
<<"psk_authentication">>,
|
||||||
<<"rewrite">>,
|
<<"topic_metrics">>,
|
||||||
<<"auto_subscribe">>,
|
<<"rewrite">>,
|
||||||
<<"retainer">>,
|
<<"auto_subscribe">>,
|
||||||
<<"statsd">>,
|
<<"retainer">>,
|
||||||
<<"delayed">>,
|
<<"statsd">>,
|
||||||
<<"event_message">>,
|
<<"delayed">>,
|
||||||
<<"prometheus">>,
|
<<"event_message">>,
|
||||||
<<"telemetry">>,
|
<<"prometheus">>,
|
||||||
<<"sys_topics">>
|
<<"telemetry">>,
|
||||||
|
<<"sys_topics">>
|
||||||
] ++ global_zone_roots()
|
] ++ global_zone_roots()
|
||||||
).
|
).
|
||||||
|
|
||||||
|
|
@ -69,7 +72,7 @@ namespace() -> "configuration".
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
|
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
|
||||||
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
||||||
|
|
||||||
schema("/configs") ->
|
schema("/configs") ->
|
||||||
#{
|
#{
|
||||||
|
|
@ -77,12 +80,20 @@ schema("/configs") ->
|
||||||
get => #{
|
get => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description =>
|
description =>
|
||||||
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
||||||
parameters => [
|
parameters => [
|
||||||
{node, hoconsc:mk(typerefl:atom(),
|
{node,
|
||||||
#{in => query, required => false, example => <<"emqx@127.0.0.1">>,
|
hoconsc:mk(
|
||||||
desc =>
|
typerefl:atom(),
|
||||||
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
|
#{
|
||||||
|
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 => #{
|
responses => #{
|
||||||
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
|
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
|
||||||
}
|
}
|
||||||
|
|
@ -95,18 +106,29 @@ schema("/configs_reset/:rootname") ->
|
||||||
post => #{
|
post => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description =>
|
description =>
|
||||||
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>
|
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>\n"
|
||||||
- For a config entry that has default value, this resets it to the default value;
|
"- 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">>,
|
"- 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
|
%% 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
|
%% the schema of the changed configs is depends on the request parameter
|
||||||
%% `conf_path`, it cannot be defined here.
|
%% `conf_path`, it cannot be defined here.
|
||||||
parameters => [
|
parameters => [
|
||||||
{rootname, hoconsc:mk( hoconsc:enum(Paths)
|
{rootname,
|
||||||
, #{in => path, example => <<"sysmon">>})},
|
hoconsc:mk(
|
||||||
{conf_path, hoconsc:mk(typerefl:binary(),
|
hoconsc:enum(Paths),
|
||||||
#{in => query, required => false, example => <<"os.sysmem_high_watermark">>,
|
#{in => path, example => <<"sysmon">>}
|
||||||
desc => <<"The config path separated by '.' character">>})}],
|
)},
|
||||||
|
{conf_path,
|
||||||
|
hoconsc:mk(
|
||||||
|
typerefl:binary(),
|
||||||
|
#{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
example => <<"os.sysmem_high_watermark">>,
|
||||||
|
desc => <<"The config path separated by '.' character">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"Rest config successfully">>,
|
200 => <<"Rest config successfully">>,
|
||||||
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
|
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
|
||||||
|
|
@ -138,9 +160,11 @@ schema(Path) ->
|
||||||
'operationId' => config,
|
'operationId' => config,
|
||||||
get => #{
|
get => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description => iolist_to_binary([ <<"Get the sub-configurations under *">>
|
description => iolist_to_binary([
|
||||||
, RootKey
|
<<"Get the sub-configurations under *">>,
|
||||||
, <<"*">>]),
|
RootKey,
|
||||||
|
<<"*">>
|
||||||
|
]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => Schema,
|
200 => Schema,
|
||||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
||||||
|
|
@ -148,9 +172,11 @@ schema(Path) ->
|
||||||
},
|
},
|
||||||
put => #{
|
put => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description => iolist_to_binary([ <<"Update the sub-configurations under *">>
|
description => iolist_to_binary([
|
||||||
, RootKey
|
<<"Update the sub-configurations under *">>,
|
||||||
, <<"*">>]),
|
RootKey,
|
||||||
|
<<"*">>
|
||||||
|
]),
|
||||||
'requestBody' => Schema,
|
'requestBody' => Schema,
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => Schema,
|
200 => Schema,
|
||||||
|
|
@ -177,7 +203,6 @@ config(get, _Params, Req) ->
|
||||||
Path = conf_path(Req),
|
Path = conf_path(Req),
|
||||||
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
|
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
|
||||||
{200, Conf};
|
{200, Conf};
|
||||||
|
|
||||||
config(put, #{body := Body}, Req) ->
|
config(put, #{body := Body}, Req) ->
|
||||||
Path = conf_path(Req),
|
Path = conf_path(Req),
|
||||||
case emqx_conf:update(Path, Body, ?OPTS) of
|
case emqx_conf:update(Path, Body, ?OPTS) of
|
||||||
|
|
@ -189,21 +214,32 @@ config(put, #{body := Body}, Req) ->
|
||||||
|
|
||||||
global_zone_configs(get, _Params, _Req) ->
|
global_zone_configs(get, _Params, _Req) ->
|
||||||
Paths = global_zone_roots(),
|
Paths = global_zone_roots(),
|
||||||
Zones = lists:foldl(fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
Zones = lists:foldl(
|
||||||
#{}, Paths),
|
fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
||||||
|
#{},
|
||||||
|
Paths
|
||||||
|
),
|
||||||
{200, Zones};
|
{200, Zones};
|
||||||
global_zone_configs(put, #{body := Body}, _Req) ->
|
global_zone_configs(put, #{body := Body}, _Req) ->
|
||||||
Res =
|
Res =
|
||||||
maps:fold(fun(Path, Value, Acc) ->
|
maps:fold(
|
||||||
case emqx_conf:update([Path], Value, ?OPTS) of
|
fun(Path, Value, Acc) ->
|
||||||
{ok, #{raw_config := RawConf}} ->
|
case emqx_conf:update([Path], Value, ?OPTS) of
|
||||||
Acc#{Path => RawConf};
|
{ok, #{raw_config := RawConf}} ->
|
||||||
{error, Reason} ->
|
Acc#{Path => RawConf};
|
||||||
?SLOG(error, #{msg => "update global zone failed", reason => Reason,
|
{error, Reason} ->
|
||||||
path => Path, value => Value}),
|
?SLOG(error, #{
|
||||||
Acc
|
msg => "update global zone failed",
|
||||||
end
|
reason => Reason,
|
||||||
end, #{}, Body),
|
path => Path,
|
||||||
|
value => Value
|
||||||
|
}),
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Body
|
||||||
|
),
|
||||||
case maps:size(Res) =:= maps:size(Body) of
|
case maps:size(Res) =:= maps:size(Body) of
|
||||||
true -> {200, Res};
|
true -> {200, Res};
|
||||||
false -> {400, #{code => 'UPDATE_FAILED'}}
|
false -> {400, #{code => 'UPDATE_FAILED'}}
|
||||||
|
|
@ -213,7 +249,8 @@ config_reset(post, _Params, Req) ->
|
||||||
%% reset the config specified by the query string param 'conf_path'
|
%% reset the config specified by the query string param 'conf_path'
|
||||||
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
|
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
|
||||||
case emqx:reset_config(Path, #{}) of
|
case emqx:reset_config(Path, #{}) of
|
||||||
{ok, _} -> {200};
|
{ok, _} ->
|
||||||
|
{200};
|
||||||
{error, no_default_value} ->
|
{error, no_default_value} ->
|
||||||
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
|
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
@ -223,9 +260,8 @@ config_reset(post, _Params, Req) ->
|
||||||
configs(get, Params, _Req) ->
|
configs(get, Params, _Req) ->
|
||||||
Node = maps:get(node, Params, node()),
|
Node = maps:get(node, Params, node()),
|
||||||
case
|
case
|
||||||
lists:member(Node, mria_mnesia:running_nodes())
|
lists:member(Node, mria_mnesia:running_nodes()) andalso
|
||||||
andalso
|
emqx_management_proto_v1:get_full_config(Node)
|
||||||
emqx_management_proto_v1:get_full_config(Node)
|
|
||||||
of
|
of
|
||||||
false ->
|
false ->
|
||||||
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
|
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
|
||||||
|
|
@ -243,8 +279,11 @@ conf_path_reset(Req) ->
|
||||||
|
|
||||||
get_full_config() ->
|
get_full_config() ->
|
||||||
emqx_config:fill_defaults(
|
emqx_config:fill_defaults(
|
||||||
maps:without(?EXCLUDES,
|
maps:without(
|
||||||
emqx:get_raw_config([]))).
|
?EXCLUDES,
|
||||||
|
emqx:get_raw_config([])
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
get_config_with_default(Path) ->
|
get_config_with_default(Path) ->
|
||||||
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
|
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
|
||||||
|
|
@ -279,8 +318,11 @@ gen_schema(Conf) when is_list(Conf) ->
|
||||||
#{type => array, items => gen_schema(hd(Conf))}
|
#{type => array, items => gen_schema(hd(Conf))}
|
||||||
end;
|
end;
|
||||||
gen_schema(Conf) when is_map(Conf) ->
|
gen_schema(Conf) when is_map(Conf) ->
|
||||||
#{type => object, properties =>
|
#{
|
||||||
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)};
|
type => object,
|
||||||
|
properties =>
|
||||||
|
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)
|
||||||
|
};
|
||||||
gen_schema(_Conf) ->
|
gen_schema(_Conf) ->
|
||||||
%% the conf is not of JSON supported type, it may have been converted
|
%% the conf is not of JSON supported type, it may have been converted
|
||||||
%% by the hocon schema
|
%% by the hocon schema
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,10 @@
|
||||||
-import(emqx_dashboard_swagger, [error_codes/2, error_codes/1]).
|
-import(emqx_dashboard_swagger, [error_codes/2, error_codes/1]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
|
listener_type_status/2,
|
||||||
list_listeners/2,
|
list_listeners/2,
|
||||||
crud_listeners_by_id/2,
|
crud_listeners_by_id/2,
|
||||||
list_listeners_on_node/2,
|
action_listeners_by_id/2
|
||||||
crud_listener_by_id_on_node/2,
|
|
||||||
action_listeners_by_id/2,
|
|
||||||
action_listeners_by_id_on_node/2
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% for rpc call
|
%% for rpc call
|
||||||
|
|
@ -55,21 +53,35 @@ api_spec() ->
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
"/listeners_status",
|
||||||
"/listeners",
|
"/listeners",
|
||||||
"/listeners/:id",
|
"/listeners/:id",
|
||||||
"/listeners/:id/:action",
|
"/listeners/:id/:action"
|
||||||
"/nodes/:node/listeners",
|
|
||||||
"/nodes/:node/listeners/:id",
|
|
||||||
"/nodes/:node/listeners/:id/:action"
|
|
||||||
].
|
].
|
||||||
|
|
||||||
|
schema("/listeners_status") ->
|
||||||
|
#{
|
||||||
|
'operationId' => listener_type_status,
|
||||||
|
get => #{
|
||||||
|
tags => [<<"listeners">>],
|
||||||
|
desc => <<"List all running node's listeners live status. group by listener type">>,
|
||||||
|
responses => #{200 => ?HOCON(?ARRAY(?R_REF(listener_type_status)))}
|
||||||
|
}
|
||||||
|
};
|
||||||
schema("/listeners") ->
|
schema("/listeners") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => list_listeners,
|
'operationId' => list_listeners,
|
||||||
get => #{
|
get => #{
|
||||||
tags => [<<"listeners">>],
|
tags => [<<"listeners">>],
|
||||||
desc => <<"List all running node's listeners.">>,
|
desc => <<"List all running node's listeners for the specified type.">>,
|
||||||
responses => #{200 => ?HOCON(?ARRAY(?R_REF(listeners)))}
|
parameters => [
|
||||||
|
{type,
|
||||||
|
?HOCON(
|
||||||
|
?ENUM(listeners_type()),
|
||||||
|
#{desc => "Listener type", in => query, required => false}
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
responses => #{200 => ?HOCON(?ARRAY(?R_REF(listener_id_status)))}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/listeners/:id") ->
|
schema("/listeners/:id") ->
|
||||||
|
|
@ -80,17 +92,29 @@ schema("/listeners/:id") ->
|
||||||
desc => <<"List all running node's listeners for the specified id.">>,
|
desc => <<"List all running node's listeners for the specified id.">>,
|
||||||
parameters => [?R_REF(listener_id)],
|
parameters => [?R_REF(listener_id)],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => ?HOCON(?ARRAY(?R_REF(listeners)))
|
200 => ?HOCON(listener_schema(#{bind => true})),
|
||||||
|
404 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
put => #{
|
put => #{
|
||||||
tags => [<<"listeners">>],
|
tags => [<<"listeners">>],
|
||||||
desc => <<"Create or update the specified listener on all nodes.">>,
|
desc => <<"Update the specified listener on all nodes.">>,
|
||||||
parameters => [?R_REF(listener_id)],
|
parameters => [?R_REF(listener_id)],
|
||||||
'requestBody' => ?HOCON(listener_schema(), #{}),
|
'requestBody' => ?HOCON(listener_schema(#{bind => false}), #{}),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => ?HOCON(listener_schema(), #{}),
|
200 => ?HOCON(listener_schema(#{bind => true}), #{}),
|
||||||
400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
|
400 => error_codes(['BAD_REQUEST']),
|
||||||
|
404 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post => #{
|
||||||
|
tags => [<<"listeners">>],
|
||||||
|
desc => <<"Create the specified listener on all nodes.">>,
|
||||||
|
parameters => [?R_REF(listener_id)],
|
||||||
|
'requestBody' => ?HOCON(listener_schema(#{bind => true}), #{}),
|
||||||
|
responses => #{
|
||||||
|
200 => ?HOCON(listener_schema(#{bind => true}), #{}),
|
||||||
|
400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
|
|
@ -118,90 +142,8 @@ schema("/listeners/:id/:action") ->
|
||||||
400 => error_codes(['BAD_REQUEST'])
|
400 => error_codes(['BAD_REQUEST'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
schema("/nodes/:node/listeners") ->
|
|
||||||
#{
|
|
||||||
'operationId' => list_listeners_on_node,
|
|
||||||
get => #{
|
|
||||||
tags => [<<"listeners">>],
|
|
||||||
desc => <<"List all listeners on the specified node.">>,
|
|
||||||
parameters => [?R_REF(node)],
|
|
||||||
responses => #{
|
|
||||||
200 => ?HOCON(?ARRAY(listener_schema())),
|
|
||||||
400 => error_codes(['BAD_NODE', 'BAD_REQUEST'], ?NODE_NOT_FOUND_OR_DOWN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/nodes/:node/listeners/:id") ->
|
|
||||||
#{
|
|
||||||
'operationId' => crud_listener_by_id_on_node,
|
|
||||||
get => #{
|
|
||||||
tags => [<<"listeners">>],
|
|
||||||
desc => <<"Get the specified listener on the specified node.">>,
|
|
||||||
parameters => [
|
|
||||||
?R_REF(listener_id),
|
|
||||||
?R_REF(node)
|
|
||||||
],
|
|
||||||
responses => #{
|
|
||||||
200 => ?HOCON(listener_schema()),
|
|
||||||
400 => error_codes(['BAD_REQUEST']),
|
|
||||||
404 => error_codes(['BAD_LISTEN_ID'], ?NODE_LISTENER_NOT_FOUND)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
put => #{
|
|
||||||
tags => [<<"listeners">>],
|
|
||||||
desc => <<"Create or update the specified listener on the specified node.">>,
|
|
||||||
parameters => [
|
|
||||||
?R_REF(listener_id),
|
|
||||||
?R_REF(node)
|
|
||||||
],
|
|
||||||
'requestBody' => ?HOCON(listener_schema()),
|
|
||||||
responses => #{
|
|
||||||
200 => ?HOCON(listener_schema()),
|
|
||||||
400 => error_codes(['BAD_REQUEST'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
delete => #{
|
|
||||||
tags => [<<"listeners">>],
|
|
||||||
desc => <<"Delete the specified listener on the specified node.">>,
|
|
||||||
parameters => [
|
|
||||||
?R_REF(listener_id),
|
|
||||||
?R_REF(node)
|
|
||||||
],
|
|
||||||
responses => #{
|
|
||||||
204 => <<"Listener deleted">>,
|
|
||||||
400 => error_codes(['BAD_REQUEST'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/nodes/:node/listeners/:id/:action") ->
|
|
||||||
#{
|
|
||||||
'operationId' => action_listeners_by_id_on_node,
|
|
||||||
post => #{
|
|
||||||
tags => [<<"listeners">>],
|
|
||||||
desc => <<"Start/stop/restart listeners on a specified node.">>,
|
|
||||||
parameters => [
|
|
||||||
?R_REF(node),
|
|
||||||
?R_REF(listener_id),
|
|
||||||
?R_REF(action)
|
|
||||||
],
|
|
||||||
responses => #{
|
|
||||||
200 => <<"Updated">>,
|
|
||||||
400 => error_codes(['BAD_REQUEST'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(listeners) ->
|
|
||||||
[
|
|
||||||
{"node",
|
|
||||||
?HOCON(atom(), #{
|
|
||||||
desc => "Node name",
|
|
||||||
example => "emqx@127.0.0.1",
|
|
||||||
required => true
|
|
||||||
})},
|
|
||||||
{"listeners", ?ARRAY(listener_schema())}
|
|
||||||
];
|
|
||||||
fields(listener_id) ->
|
fields(listener_id) ->
|
||||||
[
|
[
|
||||||
{id,
|
{id,
|
||||||
|
|
@ -230,23 +172,56 @@ fields(node) ->
|
||||||
in => path
|
in => path
|
||||||
})}
|
})}
|
||||||
];
|
];
|
||||||
|
fields(listener_type_status) ->
|
||||||
|
[
|
||||||
|
{type, ?HOCON(?ENUM(listeners_type()), #{desc => "Listener type", required => true})},
|
||||||
|
{enable, ?HOCON(boolean(), #{desc => "Listener enable", required => true})},
|
||||||
|
{ids, ?HOCON(?ARRAY(string()), #{desc => "Listener Ids", required => true})},
|
||||||
|
{status, ?HOCON(?R_REF(status))},
|
||||||
|
{node_status, ?HOCON(?ARRAY(?R_REF(node_status)))}
|
||||||
|
];
|
||||||
|
fields(listener_id_status) ->
|
||||||
|
fields(listener_id) ++
|
||||||
|
[
|
||||||
|
{enable, ?HOCON(boolean(), #{desc => "Listener enable", required => true})},
|
||||||
|
{number, ?HOCON(typerefl:pos_integer(), #{desc => "ListenerId number"})},
|
||||||
|
{status, ?HOCON(?R_REF(status))},
|
||||||
|
{node_status, ?HOCON(?ARRAY(?R_REF(node_status)))}
|
||||||
|
];
|
||||||
|
fields(status) ->
|
||||||
|
[
|
||||||
|
{max_connections,
|
||||||
|
?HOCON(hoconsc:union([infinity, integer()]), #{desc => "Max connections"})},
|
||||||
|
{current_connections, ?HOCON(non_neg_integer(), #{desc => "Current connections"})}
|
||||||
|
];
|
||||||
|
fields(node_status) ->
|
||||||
|
fields(node) ++ fields(status);
|
||||||
fields(Type) ->
|
fields(Type) ->
|
||||||
Listeners = listeners_info(),
|
Listeners = listeners_info(#{bind => true}) ++ listeners_info(#{bind => false}),
|
||||||
[Schema] = [S || #{ref := ?R_REF(_, T), schema := S} <- Listeners, T =:= Type],
|
[Schema] = [S || #{ref := ?R_REF(_, T), schema := S} <- Listeners, T =:= Type],
|
||||||
Schema.
|
Schema.
|
||||||
|
|
||||||
listener_schema() ->
|
listener_schema(Opts) ->
|
||||||
?UNION(lists:map(fun(#{ref := Ref}) -> Ref end, listeners_info())).
|
?UNION(lists:map(fun(#{ref := Ref}) -> Ref end, listeners_info(Opts))).
|
||||||
|
|
||||||
listeners_info() ->
|
listeners_type() ->
|
||||||
|
lists:map(
|
||||||
|
fun({Type, _}) -> list_to_existing_atom(Type) end,
|
||||||
|
hocon_schema:fields(emqx_schema, "listeners")
|
||||||
|
).
|
||||||
|
|
||||||
|
listeners_info(Opts) ->
|
||||||
Listeners = hocon_schema:fields(emqx_schema, "listeners"),
|
Listeners = hocon_schema:fields(emqx_schema, "listeners"),
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({Type, #{type := ?MAP(_Name, ?R_REF(Mod, Field))}}) ->
|
fun({Type, #{type := ?MAP(_Name, ?R_REF(Mod, Field))}}) ->
|
||||||
Fields0 = hocon_schema:fields(Mod, Field),
|
Fields0 = hocon_schema:fields(Mod, Field),
|
||||||
Fields1 = lists:keydelete("authentication", 1, Fields0),
|
Fields1 = lists:keydelete("authentication", 1, Fields0),
|
||||||
|
Fields2 = lists:keydelete("limiter", 1, Fields1),
|
||||||
|
Fields3 = required_bind(Fields2, Opts),
|
||||||
|
Ref = listeners_ref(Type, Opts),
|
||||||
TypeAtom = list_to_existing_atom(Type),
|
TypeAtom = list_to_existing_atom(Type),
|
||||||
#{
|
#{
|
||||||
ref => ?R_REF(TypeAtom),
|
ref => ?R_REF(Ref),
|
||||||
schema => [
|
schema => [
|
||||||
{type, ?HOCON(?ENUM([TypeAtom]), #{desc => "Listener type", required => true})},
|
{type, ?HOCON(?ENUM([TypeAtom]), #{desc => "Listener type", required => true})},
|
||||||
{running, ?HOCON(boolean(), #{desc => "Listener status", required => false})},
|
{running, ?HOCON(boolean(), #{desc => "Listener status", required => false})},
|
||||||
|
|
@ -255,14 +230,33 @@ listeners_info() ->
|
||||||
desc => "Listener id",
|
desc => "Listener id",
|
||||||
required => true,
|
required => true,
|
||||||
validator => fun validate_id/1
|
validator => fun validate_id/1
|
||||||
})}
|
})},
|
||||||
| Fields1
|
{current_connections,
|
||||||
|
?HOCON(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => "Current connections", required => false}
|
||||||
|
)}
|
||||||
|
| Fields3
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
Listeners
|
Listeners
|
||||||
).
|
).
|
||||||
|
|
||||||
|
required_bind(Fields, #{bind := true}) ->
|
||||||
|
Fields;
|
||||||
|
required_bind(Fields, #{bind := false}) ->
|
||||||
|
{value, {_, Hocon}, Fields1} = lists:keytake("bind", 1, Fields),
|
||||||
|
[{"bind", Hocon#{required => false}} | Fields1].
|
||||||
|
|
||||||
|
listeners_ref(Type, #{bind := Bind}) ->
|
||||||
|
Suffix =
|
||||||
|
case Bind of
|
||||||
|
true -> "_required_bind";
|
||||||
|
false -> "_not_required_bind"
|
||||||
|
end,
|
||||||
|
Type ++ Suffix.
|
||||||
|
|
||||||
validate_id(Id) ->
|
validate_id(Id) ->
|
||||||
case emqx_listeners:parse_listener_id(Id) of
|
case emqx_listeners:parse_listener_id(Id) of
|
||||||
{error, Reason} -> {error, Reason};
|
{error, Reason} -> {error, Reason};
|
||||||
|
|
@ -270,19 +264,65 @@ validate_id(Id) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% api
|
%% api
|
||||||
list_listeners(get, _Request) ->
|
listener_type_status(get, _Request) ->
|
||||||
{200, list_listeners()}.
|
Listeners = maps:to_list(listener_status_by_type(list_listeners(), #{})),
|
||||||
|
List = lists:map(fun({Type, L}) -> L#{type => Type} end, Listeners),
|
||||||
|
{200, List}.
|
||||||
|
|
||||||
crud_listeners_by_id(get, #{bindings := #{id := Id}}) ->
|
list_listeners(get, #{query_string := Query}) ->
|
||||||
{200, list_listeners_by_id(Id)};
|
Listeners = list_listeners(),
|
||||||
|
NodeL =
|
||||||
|
case maps:find(<<"type">>, Query) of
|
||||||
|
{ok, Type} -> listener_type_filter(atom_to_binary(Type), Listeners);
|
||||||
|
error -> Listeners
|
||||||
|
end,
|
||||||
|
{200, listener_status_by_id(NodeL)}.
|
||||||
|
|
||||||
|
crud_listeners_by_id(get, #{bindings := #{id := Id0}}) ->
|
||||||
|
Listeners = [
|
||||||
|
Conf#{<<"id">> => Id, <<"type">> => Type}
|
||||||
|
|| {Id, Type, Conf} <- emqx_listeners:list_raw(),
|
||||||
|
Id =:= Id0
|
||||||
|
],
|
||||||
|
case Listeners of
|
||||||
|
[] -> {404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
|
||||||
|
[L] -> {200, L}
|
||||||
|
end;
|
||||||
crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
|
crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
|
||||||
case parse_listener_conf(Body0) of
|
case parse_listener_conf(Body0) of
|
||||||
{Id, Type, Name, Conf} ->
|
{Id, Type, Name, Conf} ->
|
||||||
case emqx_conf:update([listeners, Type, Name], Conf, ?OPTS(cluster)) of
|
Key = [listeners, Type, Name],
|
||||||
{ok, #{raw_config := _RawConf}} ->
|
case emqx_conf:get_raw(Key, undefined) of
|
||||||
crud_listeners_by_id(get, #{bindings => #{id => Id}});
|
undefined ->
|
||||||
{error, Reason} ->
|
{404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
|
PrevConf ->
|
||||||
|
MergeConf = emqx_map_lib:deep_merge(PrevConf, Conf),
|
||||||
|
case emqx_conf:update(Key, MergeConf, ?OPTS(cluster)) of
|
||||||
|
{ok, #{raw_config := _RawConf}} ->
|
||||||
|
crud_listeners_by_id(get, #{bindings => #{id => Id}});
|
||||||
|
{error, Reason} ->
|
||||||
|
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
||||||
|
_ ->
|
||||||
|
{400, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_ID_INCONSISTENT}}
|
||||||
|
end;
|
||||||
|
crud_listeners_by_id(post, #{bindings := #{id := Id}, body := Body0}) ->
|
||||||
|
case parse_listener_conf(Body0) of
|
||||||
|
{Id, Type, Name, Conf} ->
|
||||||
|
Key = [listeners, Type, Name],
|
||||||
|
case emqx_conf:get(Key, undefined) of
|
||||||
|
undefined ->
|
||||||
|
case emqx_conf:update([listeners, Type, Name], Conf, ?OPTS(cluster)) of
|
||||||
|
{ok, #{raw_config := _RawConf}} ->
|
||||||
|
crud_listeners_by_id(get, #{bindings => #{id => Id}});
|
||||||
|
{error, Reason} ->
|
||||||
|
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{400, #{code => 'BAD_LISTENER_ID', message => <<"Already Exist">>}}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
||||||
|
|
@ -298,64 +338,16 @@ crud_listeners_by_id(delete, #{bindings := #{id := Id}}) ->
|
||||||
|
|
||||||
parse_listener_conf(Conf0) ->
|
parse_listener_conf(Conf0) ->
|
||||||
Conf1 = maps:remove(<<"running">>, Conf0),
|
Conf1 = maps:remove(<<"running">>, Conf0),
|
||||||
{IdBin, Conf2} = maps:take(<<"id">>, Conf1),
|
Conf2 = maps:remove(<<"current_connections">>, Conf1),
|
||||||
{TypeBin, Conf3} = maps:take(<<"type">>, Conf2),
|
{IdBin, Conf3} = maps:take(<<"id">>, Conf2),
|
||||||
|
{TypeBin, Conf4} = maps:take(<<"type">>, Conf3),
|
||||||
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin),
|
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin),
|
||||||
TypeAtom = binary_to_existing_atom(TypeBin),
|
TypeAtom = binary_to_existing_atom(TypeBin),
|
||||||
case Type =:= TypeAtom of
|
case Type =:= TypeAtom of
|
||||||
true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf3};
|
true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf4};
|
||||||
false -> {error, listener_type_inconsistent}
|
false -> {error, listener_type_inconsistent}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_listeners_on_node(get, #{bindings := #{node := Node}}) ->
|
|
||||||
case list_listeners(Node) of
|
|
||||||
{error, nodedown} ->
|
|
||||||
{400, #{code => 'BAD_NODE', message => ?NODE_NOT_FOUND_OR_DOWN}};
|
|
||||||
{error, Reason} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
|
||||||
#{<<"listeners">> := Listener} ->
|
|
||||||
{200, Listener}
|
|
||||||
end.
|
|
||||||
|
|
||||||
crud_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) ->
|
|
||||||
case get_listener(Node, Id) of
|
|
||||||
{error, not_found} ->
|
|
||||||
{404, #{code => 'BAD_LISTEN_ID', message => ?NODE_LISTENER_NOT_FOUND}};
|
|
||||||
{error, Reason} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
|
||||||
Listener ->
|
|
||||||
{200, Listener}
|
|
||||||
end;
|
|
||||||
crud_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node}, body := Body}) ->
|
|
||||||
case parse_listener_conf(Body) of
|
|
||||||
{error, Reason} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
|
||||||
{Id, Type, _Name, Conf} ->
|
|
||||||
case update_listener(Node, Id, Conf) of
|
|
||||||
{error, nodedown} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?NODE_NOT_FOUND_OR_DOWN}};
|
|
||||||
%% TODO
|
|
||||||
{error, {eaddrinuse, _}} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?ADDR_PORT_INUSE}};
|
|
||||||
{error, Reason} ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
|
||||||
{ok, Listener} ->
|
|
||||||
{200, Listener#{<<"id">> => Id, <<"type">> => Type, <<"running">> => true}}
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?LISTENER_ID_INCONSISTENT}}
|
|
||||||
end;
|
|
||||||
crud_listener_by_id_on_node(delete, #{bindings := #{id := Id, node := Node}}) ->
|
|
||||||
case remove_listener(Node, Id) of
|
|
||||||
ok -> {204};
|
|
||||||
{error, Reason} -> {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
action_listeners_by_id_on_node(post,
|
|
||||||
#{bindings := #{id := Id, action := Action, node := Node}}) ->
|
|
||||||
{_, Result} = action_listeners(Node, Id, Action),
|
|
||||||
Result.
|
|
||||||
|
|
||||||
action_listeners_by_id(post, #{bindings := #{id := Id, action := Action}}) ->
|
action_listeners_by_id(post, #{bindings := #{id := Id, action := Action}}) ->
|
||||||
Results = [action_listeners(Node, Id, Action) || Node <- mria_mnesia:running_nodes()],
|
Results = [action_listeners(Node, Id, Action) || Node <- mria_mnesia:running_nodes()],
|
||||||
case
|
case
|
||||||
|
|
@ -418,31 +410,49 @@ list_listeners() ->
|
||||||
list_listeners(Node) ->
|
list_listeners(Node) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:list_listeners(Node)).
|
wrap_rpc(emqx_management_proto_v1:list_listeners(Node)).
|
||||||
|
|
||||||
list_listeners_by_id(Id) ->
|
listener_status_by_id(NodeL) ->
|
||||||
listener_id_filter(Id, list_listeners()).
|
Listeners = maps:to_list(listener_status_by_id(NodeL, #{})),
|
||||||
|
|
||||||
get_listener(Node, Id) ->
|
|
||||||
case listener_id_filter(Id, [list_listeners(Node)]) of
|
|
||||||
[#{<<"listeners">> := []}] -> {error, not_found};
|
|
||||||
[#{<<"listeners">> := [Listener]}] -> Listener
|
|
||||||
end.
|
|
||||||
|
|
||||||
listener_id_filter(Id, Listeners) ->
|
|
||||||
lists:map(
|
lists:map(
|
||||||
fun(Conf = #{<<"listeners">> := Listeners0}) ->
|
fun({Id, L}) ->
|
||||||
Conf#{
|
L1 = maps:remove(ids, L),
|
||||||
<<"listeners">> =>
|
#{node_status := Nodes} = L1,
|
||||||
[C || C = #{<<"id">> := Id0} <- Listeners0, Id =:= Id0]
|
L1#{number => maps:size(Nodes), id => Id}
|
||||||
}
|
|
||||||
end,
|
end,
|
||||||
Listeners
|
Listeners
|
||||||
).
|
).
|
||||||
|
|
||||||
update_listener(Node, Id, Config) ->
|
listener_status_by_type([], Acc) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:update_listener(Node, Id, Config)).
|
Acc;
|
||||||
|
listener_status_by_type([NodeL | Rest], Acc) ->
|
||||||
|
#{<<"node">> := Node, <<"listeners">> := Listeners} = NodeL,
|
||||||
|
Acc1 = lists:foldl(
|
||||||
|
fun(L, Acc0) -> format_status(<<"type">>, Node, L, Acc0) end,
|
||||||
|
Acc,
|
||||||
|
Listeners
|
||||||
|
),
|
||||||
|
listener_status_by_type(Rest, Acc1).
|
||||||
|
|
||||||
remove_listener(Node, Id) ->
|
listener_status_by_id([], Acc) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:remove_listener(Node, Id)).
|
Acc;
|
||||||
|
listener_status_by_id([NodeL | Rest], Acc) ->
|
||||||
|
#{<<"node">> := Node, <<"listeners">> := Listeners} = NodeL,
|
||||||
|
Acc1 = lists:foldl(
|
||||||
|
fun(L, Acc0) -> format_status(<<"id">>, Node, L, Acc0) end,
|
||||||
|
Acc,
|
||||||
|
Listeners
|
||||||
|
),
|
||||||
|
listener_status_by_id(Rest, Acc1).
|
||||||
|
|
||||||
|
listener_type_filter(Type0, Listeners) ->
|
||||||
|
lists:map(
|
||||||
|
fun(Conf = #{<<"listeners">> := Listeners0}) ->
|
||||||
|
Conf#{
|
||||||
|
<<"listeners">> =>
|
||||||
|
[C || C = #{<<"type">> := Type} <- Listeners0, Type =:= Type0]
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Listeners
|
||||||
|
).
|
||||||
|
|
||||||
-spec do_list_listeners() -> map().
|
-spec do_list_listeners() -> map().
|
||||||
do_list_listeners() ->
|
do_list_listeners() ->
|
||||||
|
|
@ -476,3 +486,75 @@ wrap_rpc({badrpc, Reason}) ->
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
wrap_rpc(Res) ->
|
wrap_rpc(Res) ->
|
||||||
Res.
|
Res.
|
||||||
|
|
||||||
|
format_status(Key, Node, Listener, Acc) ->
|
||||||
|
#{
|
||||||
|
<<"id">> := Id,
|
||||||
|
<<"running">> := Running,
|
||||||
|
<<"max_connections">> := MaxConnections,
|
||||||
|
<<"current_connections">> := CurrentConnections
|
||||||
|
} = Listener,
|
||||||
|
GroupKey = maps:get(Key, Listener),
|
||||||
|
case maps:find(GroupKey, Acc) of
|
||||||
|
error ->
|
||||||
|
Acc#{
|
||||||
|
GroupKey => #{
|
||||||
|
enable => Running,
|
||||||
|
ids => [Id],
|
||||||
|
status => #{
|
||||||
|
max_connections => MaxConnections,
|
||||||
|
current_connections => CurrentConnections
|
||||||
|
},
|
||||||
|
node_status => #{
|
||||||
|
Node => #{
|
||||||
|
max_connections => MaxConnections,
|
||||||
|
current_connections => CurrentConnections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{ok, GroupValue} ->
|
||||||
|
#{
|
||||||
|
ids := Ids,
|
||||||
|
status := #{
|
||||||
|
max_connections := MaxConnections0,
|
||||||
|
current_connections := CurrentConnections0
|
||||||
|
},
|
||||||
|
node_status := NodeStatus0
|
||||||
|
} = GroupValue,
|
||||||
|
NodeStatus =
|
||||||
|
case maps:find(Node, NodeStatus0) of
|
||||||
|
error ->
|
||||||
|
#{
|
||||||
|
Node => #{
|
||||||
|
max_connections => MaxConnections,
|
||||||
|
current_connections => CurrentConnections
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{ok, #{
|
||||||
|
max_connections := PrevMax,
|
||||||
|
current_connections := PrevCurr
|
||||||
|
}} ->
|
||||||
|
NodeStatus0#{
|
||||||
|
Node => #{
|
||||||
|
max_connections => max_conn(MaxConnections, PrevMax),
|
||||||
|
current_connections => CurrentConnections + PrevCurr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Acc#{
|
||||||
|
GroupKey =>
|
||||||
|
GroupValue#{
|
||||||
|
ids => lists:usort([Id | Ids]),
|
||||||
|
status => #{
|
||||||
|
max_connections => max_conn(MaxConnections0, MaxConnections),
|
||||||
|
current_connections => CurrentConnections0 + CurrentConnections
|
||||||
|
},
|
||||||
|
node_status => NodeStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end.
|
||||||
|
|
||||||
|
max_conn(_Int1, infinity) -> infinity;
|
||||||
|
max_conn(infinity, _Int) -> infinity;
|
||||||
|
max_conn(Int1, Int2) -> Int1 + Int2.
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,16 @@
|
||||||
-import(hoconsc, [mk/2, ref/2]).
|
-import(hoconsc, [mk/2, ref/2]).
|
||||||
|
|
||||||
%% minirest/dashbaord_swagger behaviour callbacks
|
%% minirest/dashbaord_swagger behaviour callbacks
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% http handlers
|
%% http handlers
|
||||||
-export([metrics/2]).
|
-export([metrics/2]).
|
||||||
|
|
@ -53,9 +55,12 @@ metrics(get, #{query_string := Qs}) ->
|
||||||
true ->
|
true ->
|
||||||
{200, emqx_mgmt:get_metrics()};
|
{200, emqx_mgmt:get_metrics()};
|
||||||
false ->
|
false ->
|
||||||
Data = [maps:from_list(
|
Data = [
|
||||||
emqx_mgmt:get_metrics(Node) ++ [{node, Node}])
|
maps:from_list(
|
||||||
|| Node <- mria_mnesia:running_nodes()],
|
emqx_mgmt:get_metrics(Node) ++ [{node, Node}]
|
||||||
|
)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
],
|
||||||
{200, Data}
|
{200, Data}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -64,23 +69,34 @@ metrics(get, #{query_string := Qs}) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
schema("/metrics") ->
|
schema("/metrics") ->
|
||||||
#{ 'operationId' => metrics
|
#{
|
||||||
, get =>
|
'operationId' => metrics,
|
||||||
#{ description => <<"EMQX metrics">>
|
get =>
|
||||||
, parameters =>
|
#{
|
||||||
[{ aggregate
|
description => <<"EMQX metrics">>,
|
||||||
, mk( boolean()
|
parameters =>
|
||||||
, #{ in => query
|
[
|
||||||
, required => false
|
{aggregate,
|
||||||
, desc => <<"Whether to aggregate all nodes Metrics">>})
|
mk(
|
||||||
}]
|
boolean(),
|
||||||
, responses =>
|
#{
|
||||||
#{ 200 => hoconsc:union(
|
in => query,
|
||||||
[ref(?MODULE, aggregated_metrics),
|
required => false,
|
||||||
hoconsc:array(ref(?MODULE, node_metrics))])
|
desc => <<"Whether to aggregate all nodes Metrics">>
|
||||||
}
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => hoconsc:union(
|
||||||
|
[
|
||||||
|
ref(?MODULE, aggregated_metrics),
|
||||||
|
hoconsc:array(ref(?MODULE, node_metrics))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[].
|
[].
|
||||||
|
|
@ -91,177 +107,354 @@ fields(node_metrics) ->
|
||||||
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
|
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
|
||||||
|
|
||||||
properties() ->
|
properties() ->
|
||||||
[ m('actions.failure',
|
[
|
||||||
<<"Number of failure executions of the rule engine action">>)
|
m(
|
||||||
, m('actions.success',
|
'actions.failure',
|
||||||
<<"Number of successful executions of the rule engine action">>)
|
<<"Number of failure executions of the rule engine action">>
|
||||||
, m('bytes.received',
|
),
|
||||||
<<"Number of bytes received ">>)
|
m(
|
||||||
, m('bytes.sent',
|
'actions.success',
|
||||||
<<"Number of bytes sent on this connection">>)
|
<<"Number of successful executions of the rule engine action">>
|
||||||
, m('client.auth.anonymous',
|
),
|
||||||
<<"Number of clients who log in anonymously">>)
|
m(
|
||||||
, m('client.authenticate',
|
'bytes.received',
|
||||||
<<"Number of client authentications">>)
|
<<"Number of bytes received ">>
|
||||||
, m('client.check_authz',
|
),
|
||||||
<<"Number of Authorization rule checks">>)
|
m(
|
||||||
, m('client.connack',
|
'bytes.sent',
|
||||||
<<"Number of CONNACK packet sent">>)
|
<<"Number of bytes sent on this connection">>
|
||||||
, m('client.connect',
|
),
|
||||||
<<"Number of client connections">>)
|
m(
|
||||||
, m('client.connected',
|
'client.auth.anonymous',
|
||||||
<<"Number of successful client connections">>)
|
<<"Number of clients who log in anonymously">>
|
||||||
, m('client.disconnected',
|
),
|
||||||
<<"Number of client disconnects">>)
|
m(
|
||||||
, m('client.subscribe',
|
'client.authenticate',
|
||||||
<<"Number of client subscriptions">>)
|
<<"Number of client authentications">>
|
||||||
, m('client.unsubscribe',
|
),
|
||||||
<<"Number of client unsubscriptions">>)
|
m(
|
||||||
, m('delivery.dropped',
|
'client.check_authz',
|
||||||
<<"Total number of discarded messages when sending">>)
|
<<"Number of Authorization rule checks">>
|
||||||
, m('delivery.dropped.expired',
|
),
|
||||||
<<"Number of messages dropped due to message expiration on sending">>)
|
m(
|
||||||
, m('delivery.dropped.no_local',
|
'client.connack',
|
||||||
<<"Number of messages that were dropped due to the No Local subscription "
|
<<"Number of CONNACK packet sent">>
|
||||||
"option when sending">>)
|
),
|
||||||
, m('delivery.dropped.qos0_msg',
|
m(
|
||||||
<<"Number of messages with QoS 0 that were dropped because the message "
|
'client.connect',
|
||||||
"queue was full when sending">>)
|
<<"Number of client connections">>
|
||||||
, m('delivery.dropped.queue_full',
|
),
|
||||||
<<"Number of messages with a non-zero QoS that were dropped because the "
|
m(
|
||||||
"message queue was full when sending">>)
|
'client.connected',
|
||||||
, m('delivery.dropped.too_large',
|
<<"Number of successful client connections">>
|
||||||
<<"The number of messages that were dropped because the length exceeded "
|
),
|
||||||
"the limit when sending">>)
|
m(
|
||||||
, m('messages.acked',
|
'client.disconnected',
|
||||||
<<"Number of received PUBACK and PUBREC packet">>)
|
<<"Number of client disconnects">>
|
||||||
, m('messages.delayed',
|
),
|
||||||
<<"Number of delay-published messages">>)
|
m(
|
||||||
, m('messages.delivered',
|
'client.subscribe',
|
||||||
<<"Number of messages forwarded to the subscription process internally">>)
|
<<"Number of client subscriptions">>
|
||||||
, m('messages.dropped',
|
),
|
||||||
<<"Total number of messages dropped before forwarding to the subscription process">>)
|
m(
|
||||||
, m('messages.dropped.await_pubrel_timeout',
|
'client.unsubscribe',
|
||||||
<<"Number of messages dropped due to waiting PUBREL timeout">>)
|
<<"Number of client unsubscriptions">>
|
||||||
, m('messages.dropped.no_subscribers',
|
),
|
||||||
<<"Number of messages dropped due to no subscribers">>)
|
m(
|
||||||
, m('messages.forward',
|
'delivery.dropped',
|
||||||
<<"Number of messages forwarded to other nodes">>)
|
<<"Total number of discarded messages when sending">>
|
||||||
, m('messages.publish',
|
),
|
||||||
<<"Number of messages published in addition to system messages">>)
|
m(
|
||||||
, m('messages.qos0.received',
|
'delivery.dropped.expired',
|
||||||
<<"Number of QoS 0 messages received from clients">>)
|
<<"Number of messages dropped due to message expiration on sending">>
|
||||||
, m('messages.qos0.sent',
|
),
|
||||||
<<"Number of QoS 0 messages sent to clients">>)
|
m(
|
||||||
, m('messages.qos1.received',
|
'delivery.dropped.no_local',
|
||||||
<<"Number of QoS 1 messages received from clients">>)
|
<<
|
||||||
, m('messages.qos1.sent',
|
"Number of messages that were dropped due to the No Local subscription "
|
||||||
<<"Number of QoS 1 messages sent to clients">>)
|
"option when sending"
|
||||||
, m('messages.qos2.received',
|
>>
|
||||||
<<"Number of QoS 2 messages received from clients">>)
|
),
|
||||||
, m('messages.qos2.sent',
|
m(
|
||||||
<<"Number of QoS 2 messages sent to clients">>)
|
'delivery.dropped.qos0_msg',
|
||||||
, m('messages.received',
|
<<
|
||||||
<<"Number of messages received from the client, equal to the sum of "
|
"Number of messages with QoS 0 that were dropped because the message "
|
||||||
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received">>)
|
"queue was full when sending"
|
||||||
, m('messages.retained',
|
>>
|
||||||
<<"Number of retained messages">>)
|
),
|
||||||
, m('messages.sent',
|
m(
|
||||||
<<"Number of messages sent to the client, equal to the sum of "
|
'delivery.dropped.queue_full',
|
||||||
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent">>)
|
<<
|
||||||
, m('packets.auth.received',
|
"Number of messages with a non-zero QoS that were dropped because the "
|
||||||
<<"Number of received AUTH packet">>)
|
"message queue was full when sending"
|
||||||
, m('packets.auth.sent',
|
>>
|
||||||
<<"Number of sent AUTH packet">>)
|
),
|
||||||
, m('packets.connack.auth_error',
|
m(
|
||||||
<<"Number of received CONNECT packet with failed authentication">>)
|
'delivery.dropped.too_large',
|
||||||
, m('packets.connack.error',
|
<<
|
||||||
<<"Number of received CONNECT packet with unsuccessful connections">>)
|
"The number of messages that were dropped because the length exceeded "
|
||||||
, m('packets.connack.sent',
|
"the limit when sending"
|
||||||
<<"Number of sent CONNACK packet">>)
|
>>
|
||||||
, m('packets.connect.received',
|
),
|
||||||
<<"Number of received CONNECT packet">>)
|
m(
|
||||||
, m('packets.disconnect.received',
|
'messages.acked',
|
||||||
<<"Number of received DISCONNECT packet">>)
|
<<"Number of received PUBACK and PUBREC packet">>
|
||||||
, m('packets.disconnect.sent',
|
),
|
||||||
<<"Number of sent DISCONNECT packet">>)
|
m(
|
||||||
, m('packets.pingreq.received',
|
'messages.delayed',
|
||||||
<<"Number of received PINGREQ packet">>)
|
<<"Number of delay-published messages">>
|
||||||
, m('packets.pingresp.sent',
|
),
|
||||||
<<"Number of sent PUBRESP packet">>)
|
m(
|
||||||
, m('packets.puback.inuse',
|
'messages.delivered',
|
||||||
<<"Number of received PUBACK packet with occupied identifiers">>)
|
<<"Number of messages forwarded to the subscription process internally">>
|
||||||
, m('packets.puback.missed',
|
),
|
||||||
<<"Number of received packet with identifiers.">>)
|
m(
|
||||||
, m('packets.puback.received',
|
'messages.dropped',
|
||||||
<<"Number of received PUBACK packet">>)
|
<<"Total number of messages dropped before forwarding to the subscription process">>
|
||||||
, m('packets.puback.sent',
|
),
|
||||||
<<"Number of sent PUBACK packet">>)
|
m(
|
||||||
, m('packets.pubcomp.inuse',
|
'messages.dropped.await_pubrel_timeout',
|
||||||
<<"Number of received PUBCOMP packet with occupied identifiers">>)
|
<<"Number of messages dropped due to waiting PUBREL timeout">>
|
||||||
, m('packets.pubcomp.missed',
|
),
|
||||||
<<"Number of missed PUBCOMP packet">>)
|
m(
|
||||||
, m('packets.pubcomp.received',
|
'messages.dropped.no_subscribers',
|
||||||
<<"Number of received PUBCOMP packet">>)
|
<<"Number of messages dropped due to no subscribers">>
|
||||||
, m('packets.pubcomp.sent',
|
),
|
||||||
<<"Number of sent PUBCOMP packet">>)
|
m(
|
||||||
, m('packets.publish.auth_error',
|
'messages.forward',
|
||||||
<<"Number of received PUBLISH packets with failed the Authorization check">>)
|
<<"Number of messages forwarded to other nodes">>
|
||||||
, m('packets.publish.dropped',
|
),
|
||||||
<<"Number of messages discarded due to the receiving limit">>)
|
m(
|
||||||
, m('packets.publish.error',
|
'messages.publish',
|
||||||
<<"Number of received PUBLISH packet that cannot be published">>)
|
<<"Number of messages published in addition to system messages">>
|
||||||
, m('packets.publish.inuse',
|
),
|
||||||
<<"Number of received PUBLISH packet with occupied identifiers">>)
|
m(
|
||||||
, m('packets.publish.received',
|
'messages.qos0.received',
|
||||||
<<"Number of received PUBLISH packet">>)
|
<<"Number of QoS 0 messages received from clients">>
|
||||||
, m('packets.publish.sent',
|
),
|
||||||
<<"Number of sent PUBLISH packet">>)
|
m(
|
||||||
, m('packets.pubrec.inuse',
|
'messages.qos0.sent',
|
||||||
<<"Number of received PUBREC packet with occupied identifiers">>)
|
<<"Number of QoS 0 messages sent to clients">>
|
||||||
, m('packets.pubrec.missed',
|
),
|
||||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
m(
|
||||||
, m('packets.pubrec.received',
|
'messages.qos1.received',
|
||||||
<<"Number of received PUBREC packet">>)
|
<<"Number of QoS 1 messages received from clients">>
|
||||||
, m('packets.pubrec.sent',
|
),
|
||||||
<<"Number of sent PUBREC packet">>)
|
m(
|
||||||
, m('packets.pubrel.missed',
|
'messages.qos1.sent',
|
||||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
<<"Number of QoS 1 messages sent to clients">>
|
||||||
, m('packets.pubrel.received',
|
),
|
||||||
<<"Number of received PUBREL packet">>)
|
m(
|
||||||
, m('packets.pubrel.sent',
|
'messages.qos2.received',
|
||||||
<<"Number of sent PUBREL packet">>)
|
<<"Number of QoS 2 messages received from clients">>
|
||||||
, m('packets.received',
|
),
|
||||||
<<"Number of received packet">>)
|
m(
|
||||||
, m('packets.sent',
|
'messages.qos2.sent',
|
||||||
<<"Number of sent packet">>)
|
<<"Number of QoS 2 messages sent to clients">>
|
||||||
, m('packets.suback.sent',
|
),
|
||||||
<<"Number of sent SUBACK packet">>)
|
m(
|
||||||
, m('packets.subscribe.auth_error',
|
'messages.received',
|
||||||
<<"Number of received SUBACK packet with failed Authorization check">>)
|
<<
|
||||||
, m('packets.subscribe.error',
|
"Number of messages received from the client, equal to the sum of "
|
||||||
<<"Number of received SUBSCRIBE packet with failed subscriptions">>)
|
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received"
|
||||||
, m('packets.subscribe.received',
|
>>
|
||||||
<<"Number of received SUBSCRIBE packet">>)
|
),
|
||||||
, m('packets.unsuback.sent',
|
m(
|
||||||
<<"Number of sent UNSUBACK packet">>)
|
'messages.retained',
|
||||||
, m('packets.unsubscribe.error',
|
<<"Number of retained messages">>
|
||||||
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>)
|
),
|
||||||
, m('packets.unsubscribe.received',
|
m(
|
||||||
<<"Number of received UNSUBSCRIBE packet">>)
|
'messages.sent',
|
||||||
, m('rules.matched',
|
<<
|
||||||
<<"Number of rule matched">>)
|
"Number of messages sent to the client, equal to the sum of "
|
||||||
, m('session.created',
|
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent"
|
||||||
<<"Number of sessions created">>)
|
>>
|
||||||
, m('session.discarded',
|
),
|
||||||
<<"Number of sessions dropped because Clean Session or Clean Start is true">>)
|
m(
|
||||||
, m('session.resumed',
|
'packets.auth.received',
|
||||||
<<"Number of sessions resumed because Clean Session or Clean Start is false">>)
|
<<"Number of received AUTH packet">>
|
||||||
, m('session.takenover',
|
),
|
||||||
<<"Number of sessions takenover because Clean Session or Clean Start is false">>)
|
m(
|
||||||
, m('session.terminated',
|
'packets.auth.sent',
|
||||||
<<"Number of terminated sessions">>)
|
<<"Number of sent AUTH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.auth_error',
|
||||||
|
<<"Number of received CONNECT packet with failed authentication">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.error',
|
||||||
|
<<"Number of received CONNECT packet with unsuccessful connections">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.sent',
|
||||||
|
<<"Number of sent CONNACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connect.received',
|
||||||
|
<<"Number of received CONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.disconnect.received',
|
||||||
|
<<"Number of received DISCONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.disconnect.sent',
|
||||||
|
<<"Number of sent DISCONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pingreq.received',
|
||||||
|
<<"Number of received PINGREQ packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pingresp.sent',
|
||||||
|
<<"Number of sent PUBRESP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.inuse',
|
||||||
|
<<"Number of received PUBACK packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.missed',
|
||||||
|
<<"Number of received packet with identifiers.">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.received',
|
||||||
|
<<"Number of received PUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.sent',
|
||||||
|
<<"Number of sent PUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.inuse',
|
||||||
|
<<"Number of received PUBCOMP packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.missed',
|
||||||
|
<<"Number of missed PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.received',
|
||||||
|
<<"Number of received PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.sent',
|
||||||
|
<<"Number of sent PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.auth_error',
|
||||||
|
<<"Number of received PUBLISH packets with failed the Authorization check">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.dropped',
|
||||||
|
<<"Number of messages discarded due to the receiving limit">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.error',
|
||||||
|
<<"Number of received PUBLISH packet that cannot be published">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.inuse',
|
||||||
|
<<"Number of received PUBLISH packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.received',
|
||||||
|
<<"Number of received PUBLISH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.sent',
|
||||||
|
<<"Number of sent PUBLISH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.inuse',
|
||||||
|
<<"Number of received PUBREC packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.missed',
|
||||||
|
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.received',
|
||||||
|
<<"Number of received PUBREC packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.sent',
|
||||||
|
<<"Number of sent PUBREC packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.missed',
|
||||||
|
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.received',
|
||||||
|
<<"Number of received PUBREL packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.sent',
|
||||||
|
<<"Number of sent PUBREL packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.received',
|
||||||
|
<<"Number of received packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.sent',
|
||||||
|
<<"Number of sent packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.suback.sent',
|
||||||
|
<<"Number of sent SUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.auth_error',
|
||||||
|
<<"Number of received SUBACK packet with failed Authorization check">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.error',
|
||||||
|
<<"Number of received SUBSCRIBE packet with failed subscriptions">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.received',
|
||||||
|
<<"Number of received SUBSCRIBE packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsuback.sent',
|
||||||
|
<<"Number of sent UNSUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsubscribe.error',
|
||||||
|
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsubscribe.received',
|
||||||
|
<<"Number of received UNSUBSCRIBE packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'rules.matched',
|
||||||
|
<<"Number of rule matched">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.created',
|
||||||
|
<<"Number of sessions created">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.discarded',
|
||||||
|
<<"Number of sessions dropped because Clean Session or Clean Start is true">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.resumed',
|
||||||
|
<<"Number of sessions resumed because Clean Session or Clean Start is false">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.takenover',
|
||||||
|
<<"Number of sessions takenover because Clean Session or Clean Start is false">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.terminated',
|
||||||
|
<<"Number of terminated sessions">>
|
||||||
|
)
|
||||||
].
|
].
|
||||||
|
|
||||||
m(K, Desc) ->
|
m(K, Desc) ->
|
||||||
{K, mk(integer(), #{desc => Desc})}.
|
{K, mk(non_neg_integer(), #{desc => Desc})}.
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,20 @@
|
||||||
-define(SOURCE_ERROR, 'SOURCE_ERROR').
|
-define(SOURCE_ERROR, 'SOURCE_ERROR').
|
||||||
|
|
||||||
%% Swagger specs from hocon schema
|
%% Swagger specs from hocon schema
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, schema/1
|
api_spec/0,
|
||||||
, paths/0
|
schema/1,
|
||||||
, fields/1
|
paths/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% API callbacks
|
%% API callbacks
|
||||||
-export([ nodes/2
|
-export([
|
||||||
, node/2
|
nodes/2,
|
||||||
, node_metrics/2
|
node/2,
|
||||||
, node_stats/2
|
node_metrics/2,
|
||||||
]).
|
node_stats/2
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API spec funcs
|
%% API spec funcs
|
||||||
|
|
@ -49,123 +51,183 @@ api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/nodes"
|
[
|
||||||
, "/nodes/:node"
|
"/nodes",
|
||||||
, "/nodes/:node/metrics"
|
"/nodes/:node",
|
||||||
, "/nodes/:node/stats"
|
"/nodes/:node/metrics",
|
||||||
|
"/nodes/:node/stats"
|
||||||
].
|
].
|
||||||
|
|
||||||
schema("/nodes") ->
|
schema("/nodes") ->
|
||||||
#{ 'operationId' => nodes
|
#{
|
||||||
, get =>
|
'operationId' => nodes,
|
||||||
#{ description => <<"List EMQX nodes">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{200 => mk( array(ref(node_info))
|
description => <<"List EMQX nodes">>,
|
||||||
, #{desc => <<"List all EMQX nodes">>})}
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => mk(
|
||||||
|
array(ref(node_info)),
|
||||||
|
#{desc => <<"List all EMQX nodes">>}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node") ->
|
schema("/nodes/:node") ->
|
||||||
#{ 'operationId' => node
|
#{
|
||||||
, get =>
|
'operationId' => node,
|
||||||
#{ description => <<"Get node info">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node info">>,
|
||||||
#{ 200 => mk( ref(node_info)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node info successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(node_info),
|
||||||
|
#{desc => <<"Get node info successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node/metrics") ->
|
schema("/nodes/:node/metrics") ->
|
||||||
#{ 'operationId' => node_metrics
|
#{
|
||||||
, get =>
|
'operationId' => node_metrics,
|
||||||
#{ description => <<"Get node metrics">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node metrics">>,
|
||||||
#{ 200 => mk( ref(?NODE_METRICS_MODULE, node_metrics)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node metrics successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(?NODE_METRICS_MODULE, node_metrics),
|
||||||
|
#{desc => <<"Get node metrics successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node/stats") ->
|
schema("/nodes/:node/stats") ->
|
||||||
#{ 'operationId' => node_stats
|
#{
|
||||||
, get =>
|
'operationId' => node_stats,
|
||||||
#{ description => <<"Get node stats">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node stats">>,
|
||||||
#{ 200 => mk( ref(?NODE_STATS_MODULE, node_stats_data)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node stats successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(?NODE_STATS_MODULE, node_stats_data),
|
||||||
|
#{desc => <<"Get node stats successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Fields
|
%% Fields
|
||||||
|
|
||||||
fields(node_name) ->
|
fields(node_name) ->
|
||||||
[ { node
|
[
|
||||||
, mk(atom()
|
{node,
|
||||||
, #{ in => path
|
mk(
|
||||||
, description => <<"Node name">>
|
atom(),
|
||||||
, required => true
|
#{
|
||||||
, example => <<"emqx@127.0.0.1">>
|
in => path,
|
||||||
})
|
description => <<"Node name">>,
|
||||||
}
|
required => true,
|
||||||
|
example => <<"emqx@127.0.0.1">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node_info) ->
|
fields(node_info) ->
|
||||||
[ { node
|
[
|
||||||
, mk( atom()
|
{node,
|
||||||
, #{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>})}
|
mk(
|
||||||
, { connections
|
atom(),
|
||||||
, mk( non_neg_integer()
|
#{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>}
|
||||||
, #{desc => <<"Number of clients currently connected to this node">>, example => 0})}
|
)},
|
||||||
, { load1
|
{connections,
|
||||||
, mk( string()
|
mk(
|
||||||
, #{desc => <<"CPU average load in 1 minute">>, example => "2.66"})}
|
non_neg_integer(),
|
||||||
, { load5
|
#{desc => <<"Number of clients currently connected to this node">>, example => 0}
|
||||||
, mk( string()
|
)},
|
||||||
, #{desc => <<"CPU average load in 5 minute">>, example => "2.66"})}
|
{load1,
|
||||||
, { load15
|
mk(
|
||||||
, mk( string()
|
string(),
|
||||||
, #{desc => <<"CPU average load in 15 minute">>, example => "2.66"})}
|
#{desc => <<"CPU average load in 1 minute">>, example => "2.66"}
|
||||||
, { max_fds
|
)},
|
||||||
, mk( non_neg_integer()
|
{load5,
|
||||||
, #{desc => <<"File descriptors limit">>, example => 1024})}
|
mk(
|
||||||
, { memory_total
|
string(),
|
||||||
, mk( emqx_schema:bytesize()
|
#{desc => <<"CPU average load in 5 minute">>, example => "2.66"}
|
||||||
, #{desc => <<"Allocated memory">>, example => "512.00M"})}
|
)},
|
||||||
, { memory_used
|
{load15,
|
||||||
, mk( emqx_schema:bytesize()
|
mk(
|
||||||
, #{desc => <<"Used memory">>, example => "256.00M"})}
|
string(),
|
||||||
, { node_status
|
#{desc => <<"CPU average load in 15 minute">>, example => "2.66"}
|
||||||
, mk( enum(['Running', 'Stopped'])
|
)},
|
||||||
, #{desc => <<"Node status">>, example => "Running"})}
|
{max_fds,
|
||||||
, { otp_release
|
mk(
|
||||||
, mk( string()
|
non_neg_integer(),
|
||||||
, #{ desc => <<"Erlang/OTP version">>, example => "24.2/12.2"})}
|
#{desc => <<"File descriptors limit">>, example => 1024}
|
||||||
, { process_available
|
)},
|
||||||
, mk( non_neg_integer()
|
{memory_total,
|
||||||
, #{desc => <<"Erlang processes limit">>, example => 2097152})}
|
mk(
|
||||||
, { process_used
|
emqx_schema:bytesize(),
|
||||||
, mk( non_neg_integer()
|
#{desc => <<"Allocated memory">>, example => "512.00M"}
|
||||||
, #{desc => <<"Running Erlang processes">>, example => 1024})}
|
)},
|
||||||
, { uptime
|
{memory_used,
|
||||||
, mk( non_neg_integer()
|
mk(
|
||||||
, #{desc => <<"System uptime, milliseconds">>, example => 5120000})}
|
emqx_schema:bytesize(),
|
||||||
, { version
|
#{desc => <<"Used memory">>, example => "256.00M"}
|
||||||
, mk( string()
|
)},
|
||||||
, #{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"})}
|
{node_status,
|
||||||
, { sys_path
|
mk(
|
||||||
, mk( string()
|
enum(['Running', 'Stopped']),
|
||||||
, #{desc => <<"Path to system files">>, example => "path/to/emqx"})}
|
#{desc => <<"Node status">>, example => "Running"}
|
||||||
, { log_path
|
)},
|
||||||
, mk( string()
|
{otp_release,
|
||||||
, #{desc => <<"Path to log files">>, example => "path/to/log | not found"})}
|
mk(
|
||||||
, { role
|
string(),
|
||||||
, mk( enum([core, replicant])
|
#{desc => <<"Erlang/OTP version">>, example => "24.2/12.2"}
|
||||||
, #{desc => <<"Node role">>, example => "core"})}
|
)},
|
||||||
|
{process_available,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"Erlang processes limit">>, example => 2097152}
|
||||||
|
)},
|
||||||
|
{process_used,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"Running Erlang processes">>, example => 1024}
|
||||||
|
)},
|
||||||
|
{uptime,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"System uptime, milliseconds">>, example => 5120000}
|
||||||
|
)},
|
||||||
|
{version,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"}
|
||||||
|
)},
|
||||||
|
{sys_path,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Path to system files">>, example => "path/to/emqx"}
|
||||||
|
)},
|
||||||
|
{log_path,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Path to log files">>, example => "path/to/log | not found"}
|
||||||
|
)},
|
||||||
|
{role,
|
||||||
|
mk(
|
||||||
|
enum([core, replicant]),
|
||||||
|
#{desc => <<"Node role">>, example => "core"}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -221,17 +283,20 @@ get_stats(Node) ->
|
||||||
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
|
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
|
||||||
{ok, SysPathBinary} = file:get_cwd(),
|
{ok, SysPathBinary} = file:get_cwd(),
|
||||||
SysPath = list_to_binary(SysPathBinary),
|
SysPath = list_to_binary(SysPathBinary),
|
||||||
LogPath = case log_path() of
|
LogPath =
|
||||||
undefined ->
|
case log_path() of
|
||||||
<<"not found">>;
|
undefined ->
|
||||||
Path0 ->
|
<<"not found">>;
|
||||||
Path = list_to_binary(Path0),
|
Path0 ->
|
||||||
<<SysPath/binary, Path/binary>>
|
Path = list_to_binary(Path0),
|
||||||
end,
|
<<SysPath/binary, Path/binary>>
|
||||||
Info#{ memory_total := emqx_mgmt_util:kmg(Total)
|
end,
|
||||||
, memory_used := emqx_mgmt_util:kmg(Used)
|
Info#{
|
||||||
, sys_path => SysPath
|
memory_total := emqx_mgmt_util:kmg(Total),
|
||||||
, log_path => LogPath}.
|
memory_used := emqx_mgmt_util:kmg(Used),
|
||||||
|
sys_path => SysPath,
|
||||||
|
log_path => LogPath
|
||||||
|
}.
|
||||||
|
|
||||||
log_path() ->
|
log_path() ->
|
||||||
Configs = logger:get_handler_config(),
|
Configs = logger:get_handler_config(),
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,30 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
|
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, fields/1
|
api_spec/0,
|
||||||
, paths/0
|
fields/1,
|
||||||
, schema/1
|
paths/0,
|
||||||
, namespace/0
|
schema/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ list_plugins/2
|
-export([
|
||||||
, upload_install/2
|
list_plugins/2,
|
||||||
, plugin/2
|
upload_install/2,
|
||||||
, update_plugin/2
|
plugin/2,
|
||||||
, update_boot_order/2
|
update_plugin/2,
|
||||||
]).
|
update_boot_order/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ validate_name/1
|
-export([
|
||||||
, get_plugins/0
|
validate_name/1,
|
||||||
, install_package/2
|
get_plugins/0,
|
||||||
, delete_package/1
|
install_package/2,
|
||||||
, describe_package/1
|
delete_package/1,
|
||||||
, ensure_action/2
|
describe_package/1,
|
||||||
]).
|
ensure_action/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
|
||||||
|
|
||||||
|
|
@ -65,9 +68,10 @@ schema("/plugins") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => list_plugins,
|
'operationId' => list_plugins,
|
||||||
get => #{
|
get => #{
|
||||||
description => "List all install plugins.<br>"
|
description =>
|
||||||
"Plugins are launched in top-down order.<br>"
|
"List all install plugins.<br>"
|
||||||
"Using `POST /plugins/{name}/move` to change the boot order.",
|
"Plugins are launched in top-down order.<br>"
|
||||||
|
"Using `POST /plugins/{name}/move` to change the boot order.",
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:array(hoconsc:ref(plugin))
|
200 => hoconsc:array(hoconsc:ref(plugin))
|
||||||
}
|
}
|
||||||
|
|
@ -77,20 +81,26 @@ schema("/plugins/install") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => upload_install,
|
'operationId' => upload_install,
|
||||||
post => #{
|
post => #{
|
||||||
description => "Install a plugin(plugin-vsn.tar.gz)."
|
description =>
|
||||||
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
"Install a plugin(plugin-vsn.tar.gz)."
|
||||||
"to develop plugin.",
|
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
||||||
|
"to develop plugin.",
|
||||||
'requestBody' => #{
|
'requestBody' => #{
|
||||||
content => #{
|
content => #{
|
||||||
'multipart/form-data' => #{
|
'multipart/form-data' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => #{
|
properties => #{
|
||||||
plugin => #{type => string, format => binary}}},
|
plugin => #{type => string, format => binary}
|
||||||
encoding => #{plugin => #{'contentType' => 'application/gzip'}}}}},
|
}
|
||||||
|
},
|
||||||
|
encoding => #{plugin => #{'contentType' => 'application/gzip'}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"OK">>,
|
200 => <<"OK">>,
|
||||||
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR','ALREADY_INSTALLED'])
|
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR', 'ALREADY_INSTALLED'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -118,12 +128,14 @@ schema("/plugins/:name/:action") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => update_plugin,
|
'operationId' => update_plugin,
|
||||||
put => #{
|
put => #{
|
||||||
description => "start/stop a installed plugin.<br>"
|
description =>
|
||||||
"- **start**: start the plugin.<br>"
|
"start/stop a installed plugin.<br>"
|
||||||
"- **stop**: stop the plugin.<br>",
|
"- **start**: start the plugin.<br>"
|
||||||
|
"- **stop**: stop the plugin.<br>",
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(name),
|
hoconsc:ref(name),
|
||||||
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}],
|
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"OK">>,
|
200 => <<"OK">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
|
||||||
|
|
@ -143,57 +155,83 @@ schema("/plugins/:name/move") ->
|
||||||
|
|
||||||
fields(plugin) ->
|
fields(plugin) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{
|
hoconsc:mk(
|
||||||
desc => "Name-Vsn: without .tar.gz",
|
binary(),
|
||||||
validator => fun ?MODULE:validate_name/1,
|
#{
|
||||||
required => true,
|
desc => "Name-Vsn: without .tar.gz",
|
||||||
example => "emqx_plugin_template-5.0-rc.1"})
|
validator => fun ?MODULE:validate_name/1,
|
||||||
},
|
required => true,
|
||||||
|
example => "emqx_plugin_template-5.0-rc.1"
|
||||||
|
}
|
||||||
|
)},
|
||||||
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
|
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
|
||||||
{builder, hoconsc:ref(?MODULE, builder)},
|
{builder, hoconsc:ref(?MODULE, builder)},
|
||||||
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
|
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
|
||||||
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
|
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
|
||||||
{git_commit_or_build_date, hoconsc:mk(string(), #{
|
{git_commit_or_build_date,
|
||||||
example => "2021-12-25",
|
hoconsc:mk(string(), #{
|
||||||
desc => "Last git commit date by `git log -1 --pretty=format:'%cd' "
|
example => "2021-12-25",
|
||||||
"--date=format:'%Y-%m-%d`.\n"
|
desc =>
|
||||||
" If the last commit date is not available, the build date will be presented."
|
"Last git commit date by `git log -1 --pretty=format:'%cd' "
|
||||||
})},
|
"--date=format:'%Y-%m-%d`.\n"
|
||||||
|
" If the last commit date is not available, the build date will be presented."
|
||||||
|
})},
|
||||||
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
|
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
|
||||||
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
|
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
|
||||||
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
|
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
|
||||||
{rel_vsn, hoconsc:mk(binary(),
|
{rel_vsn,
|
||||||
#{desc => "Plugins release version",
|
hoconsc:mk(
|
||||||
required => true,
|
binary(),
|
||||||
example => <<"5.0-rc.1">>})
|
#{
|
||||||
},
|
desc => "Plugins release version",
|
||||||
{rel_apps, hoconsc:mk(hoconsc:array(binary()),
|
required => true,
|
||||||
#{desc => "Aplications in plugin.",
|
example => <<"5.0-rc.1">>
|
||||||
required => true,
|
}
|
||||||
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]})
|
)},
|
||||||
},
|
{rel_apps,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:array(binary()),
|
||||||
|
#{
|
||||||
|
desc => "Aplications in plugin.",
|
||||||
|
required => true,
|
||||||
|
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]
|
||||||
|
}
|
||||||
|
)},
|
||||||
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
|
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
|
||||||
{description, hoconsc:mk(binary(),
|
{description,
|
||||||
#{desc => "Plugin description.",
|
hoconsc:mk(
|
||||||
required => true,
|
binary(),
|
||||||
example => "This is an demo plugin description"})
|
#{
|
||||||
},
|
desc => "Plugin description.",
|
||||||
{running_status, hoconsc:mk(hoconsc:array(hoconsc:ref(running_status)),
|
required => true,
|
||||||
#{required => true})},
|
example => "This is an demo plugin description"
|
||||||
{readme, hoconsc:mk(binary(), #{
|
}
|
||||||
example => "This is an demo plugin.",
|
)},
|
||||||
desc => "only return when `GET /plugins/{name}`.",
|
{running_status,
|
||||||
required => false})}
|
hoconsc:mk(
|
||||||
|
hoconsc:array(hoconsc:ref(running_status)),
|
||||||
|
#{required => true}
|
||||||
|
)},
|
||||||
|
{readme,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
example => "This is an demo plugin.",
|
||||||
|
desc => "only return when `GET /plugins/{name}`.",
|
||||||
|
required => false
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => list_to_binary(?NAME_RE),
|
hoconsc:mk(
|
||||||
example => "emqx_plugin_template-5.0-rc.1",
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => list_to_binary(?NAME_RE),
|
||||||
})}
|
example => "emqx_plugin_template-5.0-rc.1",
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(builder) ->
|
fields(builder) ->
|
||||||
[
|
[
|
||||||
|
|
@ -202,27 +240,38 @@ fields(builder) ->
|
||||||
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
|
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
|
||||||
];
|
];
|
||||||
fields(position) ->
|
fields(position) ->
|
||||||
[{position, hoconsc:mk(hoconsc:union([front, rear, binary()]),
|
[
|
||||||
#{
|
{position,
|
||||||
desc => """
|
hoconsc:mk(
|
||||||
Enable auto-boot at position in the boot list, where Position could be
|
hoconsc:union([front, rear, binary()]),
|
||||||
'front', 'rear', or 'before:other-vsn', 'after:other-vsn'
|
#{
|
||||||
to specify a relative position.
|
desc =>
|
||||||
""",
|
""
|
||||||
required => false
|
"\n"
|
||||||
})}];
|
" Enable auto-boot at position in the boot list, where Position could be\n"
|
||||||
|
" 'front', 'rear', or 'before:other-vsn', 'after:other-vsn'\n"
|
||||||
|
" to specify a relative position.\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(running_status) ->
|
fields(running_status) ->
|
||||||
[
|
[
|
||||||
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
|
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
|
||||||
{status, hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
{status,
|
||||||
desc => "Install plugin status at runtime</br>"
|
hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
||||||
"1. running: plugin is running.<br>"
|
desc =>
|
||||||
"2. stopped: plugin is stopped.<br>"
|
"Install plugin status at runtime</br>"
|
||||||
})}
|
"1. running: plugin is running.<br>"
|
||||||
|
"2. stopped: plugin is stopped.<br>"
|
||||||
|
})}
|
||||||
].
|
].
|
||||||
|
|
||||||
move_request_body() ->
|
move_request_body() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(hoconsc:ref(?MODULE, position),
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
|
hoconsc:ref(?MODULE, position),
|
||||||
#{
|
#{
|
||||||
move_to_front => #{
|
move_to_front => #{
|
||||||
summary => <<"move plugin on the front">>,
|
summary => <<"move plugin on the front">>,
|
||||||
|
|
@ -240,7 +289,8 @@ move_request_body() ->
|
||||||
summary => <<"move plugin after other plugins">>,
|
summary => <<"move plugin after other plugins">>,
|
||||||
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
|
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
|
||||||
}
|
}
|
||||||
}).
|
}
|
||||||
|
).
|
||||||
|
|
||||||
validate_name(Name) ->
|
validate_name(Name) ->
|
||||||
NameLen = byte_size(Name),
|
NameLen = byte_size(Name),
|
||||||
|
|
@ -250,7 +300,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% API CallBack Begin
|
%% API CallBack Begin
|
||||||
|
|
@ -271,29 +322,42 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) -
|
||||||
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
|
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
|
||||||
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
|
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
|
||||||
case filelib:wildcard(AppDir ++ "*.tar.gz") of
|
case filelib:wildcard(AppDir ++ "*.tar.gz") of
|
||||||
[] -> do_install_package(FileName, Bin);
|
[] ->
|
||||||
|
do_install_package(FileName, Bin);
|
||||||
OtherVsn ->
|
OtherVsn ->
|
||||||
{400, #{code => 'ALREADY_INSTALLED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p already installed",
|
code => 'ALREADY_INSTALLED',
|
||||||
[OtherVsn]))}}
|
message => iolist_to_binary(
|
||||||
|
io_lib:format(
|
||||||
|
"~p already installed",
|
||||||
|
[OtherVsn]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{400, #{code => 'ALREADY_INSTALLED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))}}
|
code => 'ALREADY_INSTALLED',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
upload_install(post, #{}) ->
|
upload_install(post, #{}) ->
|
||||||
{400, #{code => 'BAD_FORM_DATA',
|
{400, #{
|
||||||
|
code => 'BAD_FORM_DATA',
|
||||||
message =>
|
message =>
|
||||||
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>}
|
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>
|
||||||
}.
|
}}.
|
||||||
|
|
||||||
do_install_package(FileName, Bin) ->
|
do_install_package(FileName, Bin) ->
|
||||||
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
|
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
|
||||||
case lists:filter(fun(R) -> R =/= ok end, Res) of
|
case lists:filter(fun(R) -> R =/= ok end, Res) of
|
||||||
[] -> {200};
|
[] ->
|
||||||
|
{200};
|
||||||
[{error, Reason} | _] ->
|
[{error, Reason} | _] ->
|
||||||
{400, #{code => 'UNEXPECTED_ERROR',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'UNEXPECTED_ERROR',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
plugin(get, #{bindings := #{name := Name}}) ->
|
plugin(get, #{bindings := #{name := Name}}) ->
|
||||||
|
|
@ -302,7 +366,6 @@ plugin(get, #{bindings := #{name := Name}}) ->
|
||||||
[Plugin] -> {200, Plugin};
|
[Plugin] -> {200, Plugin};
|
||||||
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
|
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
plugin(delete, #{bindings := #{name := Name}}) ->
|
plugin(delete, #{bindings := #{name := Name}}) ->
|
||||||
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
|
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
|
||||||
return(204, Res).
|
return(204, Res).
|
||||||
|
|
@ -313,13 +376,17 @@ update_plugin(put, #{bindings := #{name := Name, action := Action}}) ->
|
||||||
|
|
||||||
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
|
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
|
||||||
case parse_position(Body, Name) of
|
case parse_position(Body, Name) of
|
||||||
{error, Reason} -> {400, #{code => 'BAD_POSITION', message => Reason}};
|
{error, Reason} ->
|
||||||
|
{400, #{code => 'BAD_POSITION', message => Reason}};
|
||||||
Position ->
|
Position ->
|
||||||
case emqx_plugins:ensure_enabled(Name, Position) of
|
case emqx_plugins:ensure_enabled(Name, Position) of
|
||||||
ok -> {200};
|
ok ->
|
||||||
|
{200};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => 'MOVE_FAILED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'MOVE_FAILED',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -347,7 +414,8 @@ delete_package(Name) ->
|
||||||
_ = emqx_plugins:ensure_disabled(Name),
|
_ = emqx_plugins:ensure_disabled(Name),
|
||||||
_ = emqx_plugins:purge(Name),
|
_ = emqx_plugins:purge(Name),
|
||||||
_ = emqx_plugins:delete_package(Name);
|
_ = emqx_plugins:delete_package(Name);
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% for RPC plugin update
|
%% for RPC plugin update
|
||||||
|
|
@ -361,15 +429,19 @@ ensure_action(Name, restart) ->
|
||||||
_ = emqx_plugins:ensure_enabled(Name),
|
_ = emqx_plugins:ensure_enabled(Name),
|
||||||
_ = emqx_plugins:restart(Name).
|
_ = emqx_plugins:restart(Name).
|
||||||
|
|
||||||
return(Code, ok) -> {Code};
|
return(Code, ok) ->
|
||||||
return(Code, {ok, Result}) -> {Code, Result};
|
{Code};
|
||||||
|
return(Code, {ok, Result}) ->
|
||||||
|
{Code, Result};
|
||||||
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
|
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
|
||||||
{404, #{code => 'NOT_FOUND', message => Path}};
|
{404, #{code => 'NOT_FOUND', message => Path}};
|
||||||
return(_, {error, Reason}) ->
|
return(_, {error, Reason}) ->
|
||||||
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
|
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
|
||||||
|
|
||||||
parse_position(#{<<"position">> := <<"front">>}, _) -> front;
|
parse_position(#{<<"position">> := <<"front">>}, _) ->
|
||||||
parse_position(#{<<"position">> := <<"rear">>}, _) -> rear;
|
front;
|
||||||
|
parse_position(#{<<"position">> := <<"rear">>}, _) ->
|
||||||
|
rear;
|
||||||
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
|
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
|
||||||
{error, <<"Invalid parameter. Cannot be placed before itself">>};
|
{error, <<"Invalid parameter. Cannot be placed before itself">>};
|
||||||
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
|
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
|
||||||
|
|
@ -382,7 +454,8 @@ parse_position(#{<<"position">> := <<"before:", Before/binary>>}, _Name) ->
|
||||||
{before, binary_to_list(Before)};
|
{before, binary_to_list(Before)};
|
||||||
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
|
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
|
||||||
{behind, binary_to_list(After)};
|
{behind, binary_to_list(After)};
|
||||||
parse_position(Position, _) -> {error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
parse_position(Position, _) ->
|
||||||
|
{error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
||||||
|
|
||||||
format_plugins(List) ->
|
format_plugins(List) ->
|
||||||
StatusMap = aggregate_status(List),
|
StatusMap = aggregate_status(List),
|
||||||
|
|
@ -392,13 +465,18 @@ format_plugins(List) ->
|
||||||
|
|
||||||
pack_status_in_order(List, StatusMap) ->
|
pack_status_in_order(List, StatusMap) ->
|
||||||
{Plugins, _} =
|
{Plugins, _} =
|
||||||
lists:foldl(fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
lists:foldl(
|
||||||
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
||||||
end, {[], StatusMap}, List),
|
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
||||||
|
end,
|
||||||
|
{[], StatusMap},
|
||||||
|
List
|
||||||
|
),
|
||||||
lists:reverse(Plugins).
|
lists:reverse(Plugins).
|
||||||
|
|
||||||
pack_plugin_in_order([], Acc, StatusAcc) -> {Acc, StatusAcc};
|
pack_plugin_in_order([], Acc, StatusAcc) ->
|
||||||
pack_plugin_in_order(_, Acc, StatusAcc)when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
{Acc, StatusAcc};
|
||||||
|
pack_plugin_in_order(_, Acc, StatusAcc) when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
||||||
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
||||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
|
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
|
||||||
case maps:find({Name, Vsn}, StatusAcc) of
|
case maps:find({Name, Vsn}, StatusAcc) of
|
||||||
|
|
@ -413,15 +491,20 @@ pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
||||||
|
|
||||||
aggregate_status(List) -> aggregate_status(List, #{}).
|
aggregate_status(List) -> aggregate_status(List, #{}).
|
||||||
|
|
||||||
aggregate_status([], Acc) -> Acc;
|
aggregate_status([], Acc) ->
|
||||||
|
Acc;
|
||||||
aggregate_status([{Node, Plugins} | List], Acc) ->
|
aggregate_status([{Node, Plugins} | List], Acc) ->
|
||||||
NewAcc =
|
NewAcc =
|
||||||
lists:foldl(fun(Plugin, SubAcc) ->
|
lists:foldl(
|
||||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
fun(Plugin, SubAcc) ->
|
||||||
Key = {Name, Vsn},
|
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
||||||
Value = #{node => Node, status => plugin_status(Plugin)},
|
Key = {Name, Vsn},
|
||||||
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
Value = #{node => Node, status => plugin_status(Plugin)},
|
||||||
end, Acc, Plugins),
|
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
||||||
|
end,
|
||||||
|
Acc,
|
||||||
|
Plugins
|
||||||
|
),
|
||||||
aggregate_status(List, NewAcc).
|
aggregate_status(List, NewAcc).
|
||||||
|
|
||||||
% running_status: running loaded, stopped
|
% running_status: running loaded, stopped
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,17 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ publish/2
|
-export([
|
||||||
, publish_batch/2]).
|
publish/2,
|
||||||
|
publish_batch/2
|
||||||
|
]).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
@ -46,7 +49,6 @@ schema("/publish") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/publish/bulk") ->
|
schema("/publish/bulk") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => publish_batch,
|
'operationId' => publish_batch,
|
||||||
|
|
@ -61,32 +63,43 @@ schema("/publish/bulk") ->
|
||||||
|
|
||||||
fields(publish_message) ->
|
fields(publish_message) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{
|
{topic,
|
||||||
desc => <<"Topic Name">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true,
|
desc => <<"Topic Name">>,
|
||||||
example => <<"api/example/topic">>})},
|
required => true,
|
||||||
{qos, hoconsc:mk(emqx_schema:qos(), #{
|
example => <<"api/example/topic">>
|
||||||
desc => <<"MQTT QoS">>,
|
})},
|
||||||
required => false,
|
{qos,
|
||||||
default => 0})},
|
hoconsc:mk(emqx_schema:qos(), #{
|
||||||
{from, hoconsc:mk(binary(), #{
|
desc => <<"MQTT QoS">>,
|
||||||
desc => <<"From client ID">>,
|
required => false,
|
||||||
required => false,
|
default => 0
|
||||||
example => <<"api_example_client">>})},
|
})},
|
||||||
{payload, hoconsc:mk(binary(), #{
|
{from,
|
||||||
desc => <<"MQTT Payload">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true,
|
desc => <<"From client ID">>,
|
||||||
example => <<"hello emqx api">>})},
|
required => false,
|
||||||
{retain, hoconsc:mk(boolean(), #{
|
example => <<"api_example_client">>
|
||||||
desc => <<"MQTT Retain Message">>,
|
})},
|
||||||
required => false,
|
{payload,
|
||||||
default => false})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"MQTT Payload">>,
|
||||||
|
required => true,
|
||||||
|
example => <<"hello emqx api">>
|
||||||
|
})},
|
||||||
|
{retain,
|
||||||
|
hoconsc:mk(boolean(), #{
|
||||||
|
desc => <<"MQTT Retain Message">>,
|
||||||
|
required => false,
|
||||||
|
default => false
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(publish_message_info) ->
|
fields(publish_message_info) ->
|
||||||
[
|
[
|
||||||
{id, hoconsc:mk(binary(), #{
|
{id,
|
||||||
desc => <<"Internal Message ID">>})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"Internal Message ID">>
|
||||||
|
})}
|
||||||
] ++ fields(publish_message).
|
] ++ fields(publish_message).
|
||||||
|
|
||||||
publish(post, #{body := Body}) ->
|
publish(post, #{body := Body}) ->
|
||||||
|
|
@ -100,19 +113,21 @@ publish_batch(post, #{body := Body}) ->
|
||||||
{200, format_message(Messages)}.
|
{200, format_message(Messages)}.
|
||||||
|
|
||||||
message(Map) ->
|
message(Map) ->
|
||||||
From = maps:get(<<"from">>, Map, http_api),
|
From = maps:get(<<"from">>, Map, http_api),
|
||||||
QoS = maps:get(<<"qos">>, Map, 0),
|
QoS = maps:get(<<"qos">>, Map, 0),
|
||||||
Topic = maps:get(<<"topic">>, Map),
|
Topic = maps:get(<<"topic">>, Map),
|
||||||
Payload = maps:get(<<"payload">>, Map),
|
Payload = maps:get(<<"payload">>, Map),
|
||||||
Retain = maps:get(<<"retain">>, Map, false),
|
Retain = maps:get(<<"retain">>, Map, false),
|
||||||
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
|
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
|
||||||
|
|
||||||
messages(List) ->
|
messages(List) ->
|
||||||
[message(MessageMap) || MessageMap <- List].
|
[message(MessageMap) || MessageMap <- List].
|
||||||
|
|
||||||
format_message(Messages) when is_list(Messages)->
|
format_message(Messages) when is_list(Messages) ->
|
||||||
[format_message(Message) || Message <- Messages];
|
[format_message(Message) || Message <- Messages];
|
||||||
format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags}) ->
|
format_message(#message{
|
||||||
|
id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags
|
||||||
|
}) ->
|
||||||
#{
|
#{
|
||||||
id => emqx_guid:to_hexstr(ID),
|
id => emqx_guid:to_hexstr(ID),
|
||||||
qos => Qos,
|
qos => Qos,
|
||||||
|
|
@ -124,5 +139,5 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload
|
||||||
|
|
||||||
to_binary(Data) when is_binary(Data) ->
|
to_binary(Data) when is_binary(Data) ->
|
||||||
Data;
|
Data;
|
||||||
to_binary(Data) ->
|
to_binary(Data) ->
|
||||||
list_to_binary(io_lib:format("~p", [Data])).
|
list_to_binary(io_lib:format("~p", [Data])).
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,22 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-import( hoconsc
|
-import(
|
||||||
, [ mk/2
|
hoconsc,
|
||||||
, ref/1
|
[
|
||||||
, ref/2
|
mk/2,
|
||||||
, array/1]).
|
ref/1,
|
||||||
|
ref/2,
|
||||||
|
array/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([list/2]).
|
-export([list/2]).
|
||||||
|
|
||||||
|
|
@ -40,102 +45,80 @@ paths() ->
|
||||||
["/stats"].
|
["/stats"].
|
||||||
|
|
||||||
schema("/stats") ->
|
schema("/stats") ->
|
||||||
#{ 'operationId' => list
|
#{
|
||||||
, get =>
|
'operationId' => list,
|
||||||
#{ description => <<"EMQX stats">>
|
get =>
|
||||||
, tags => [<<"stats">>]
|
#{
|
||||||
, parameters => [ref(aggregate)]
|
description => <<"EMQX stats">>,
|
||||||
, responses =>
|
tags => [<<"stats">>],
|
||||||
#{ 200 => mk( hoconsc:union([ ref(?MODULE, node_stats_data)
|
parameters => [ref(aggregate)],
|
||||||
, array(ref(?MODULE, aggergate_data))
|
responses =>
|
||||||
])
|
#{
|
||||||
, #{ desc => <<"List stats ok">> })
|
200 => mk(
|
||||||
}
|
hoconsc:union([
|
||||||
|
ref(?MODULE, node_stats_data),
|
||||||
|
array(ref(?MODULE, aggergate_data))
|
||||||
|
]),
|
||||||
|
#{desc => <<"List stats ok">>}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(aggregate) ->
|
fields(aggregate) ->
|
||||||
[ { aggregate
|
[
|
||||||
, mk( boolean()
|
{aggregate,
|
||||||
, #{ desc => <<"Calculation aggregate for all nodes">>
|
mk(
|
||||||
, in => query
|
boolean(),
|
||||||
, required => false
|
#{
|
||||||
, example => false})}
|
desc => <<"Calculation aggregate for all nodes">>,
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
example => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node_stats_data) ->
|
fields(node_stats_data) ->
|
||||||
[ { 'channels.count'
|
[
|
||||||
, mk( integer(), #{ desc => <<"sessions.count">>
|
stats_schema('channels.count', <<"sessions.count">>),
|
||||||
, example => 0})}
|
stats_schema('channels.max', <<"session.max">>),
|
||||||
, { 'channels.max'
|
stats_schema('connections.count', <<"Number of current connections">>),
|
||||||
, mk( integer(), #{ desc => <<"session.max">>
|
stats_schema('connections.max', <<"Historical maximum number of connections">>),
|
||||||
, example => 0})}
|
stats_schema('delayed.count', <<"Number of delayed messages">>),
|
||||||
, { 'connections.count'
|
stats_schema('delayed.max', <<"Historical maximum number of delayed messages">>),
|
||||||
, mk( integer(), #{ desc => <<"Number of current connections">>
|
stats_schema('live_connections.count', <<"Number of current live connections">>),
|
||||||
, example => 0})}
|
stats_schema('live_connections.max', <<"Historical maximum number of live connections">>),
|
||||||
, { 'connections.max'
|
stats_schema('retained.count', <<"Number of currently retained messages">>),
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of connections">>
|
stats_schema('retained.max', <<"Historical maximum number of retained messages">>),
|
||||||
, example => 0})}
|
stats_schema('sessions.count', <<"Number of current sessions">>),
|
||||||
, { 'delayed.count'
|
stats_schema('sessions.max', <<"Historical maximum number of sessions">>),
|
||||||
, mk( integer(), #{ desc => <<"Number of delayed messages">>
|
stats_schema('suboptions.count', <<"subscriptions.count">>),
|
||||||
, example => 0})}
|
stats_schema('suboptions.max', <<"subscriptions.max">>),
|
||||||
, { 'delayed.max'
|
stats_schema('subscribers.count', <<"Number of current subscribers">>),
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of delayed messages">>
|
stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
|
||||||
, example => 0})}
|
stats_schema(
|
||||||
, { 'live_connections.count'
|
'subscriptions.count',
|
||||||
, mk( integer(), #{ desc => <<"Number of current live connections">>
|
<<"Number of current subscriptions, including shared subscriptions">>
|
||||||
, example => 0})}
|
),
|
||||||
, { 'live_connections.max'
|
stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of live connections">>
|
stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
|
||||||
, example => 0})}
|
stats_schema(
|
||||||
, { 'retained.count'
|
'subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>
|
||||||
, mk( integer(), #{ desc => <<"Number of currently retained messages">>
|
),
|
||||||
, example => 0})}
|
stats_schema('topics.count', <<"Number of current topics">>),
|
||||||
, { 'retained.max'
|
stats_schema('topics.max', <<"Historical maximum number of topics">>)
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of retained messages">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'sessions.count'
|
|
||||||
, mk( integer(), #{ desc => <<"Number of current sessions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'sessions.max'
|
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of sessions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'suboptions.count'
|
|
||||||
, mk( integer(), #{ desc => <<"subscriptions.count">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'suboptions.max'
|
|
||||||
, mk( integer(), #{ desc => <<"subscriptions.max">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscribers.count'
|
|
||||||
, mk( integer(), #{ desc => <<"Number of current subscribers">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscribers.max'
|
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of subscribers">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscriptions.count'
|
|
||||||
, mk( integer(), #{ desc => <<"Number of current subscriptions, including shared subscriptions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscriptions.max'
|
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of subscriptions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscriptions.shared.count'
|
|
||||||
, mk( integer(), #{ desc => <<"Number of current shared subscriptions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'subscriptions.shared.max'
|
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of shared subscriptions">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'topics.count'
|
|
||||||
, mk( integer(), #{ desc => <<"Number of current topics">>
|
|
||||||
, example => 0})}
|
|
||||||
, { 'topics.max'
|
|
||||||
, mk( integer(), #{ desc => <<"Historical maximum number of topics">>
|
|
||||||
, example => 0})}
|
|
||||||
];
|
];
|
||||||
fields(aggergate_data) ->
|
fields(aggergate_data) ->
|
||||||
[ { node
|
[
|
||||||
, mk( string(), #{ desc => <<"Node name">>
|
{node,
|
||||||
, example => <<"emqx@127.0.0.1">>})}
|
mk(string(), #{
|
||||||
|
desc => <<"Node name">>,
|
||||||
|
example => <<"emqx@127.0.0.1">>
|
||||||
|
})}
|
||||||
] ++ fields(node_stats_data).
|
] ++ fields(node_stats_data).
|
||||||
|
|
||||||
|
stats_schema(Name, Desc) ->
|
||||||
|
{Name, mk(non_neg_integer(), #{desc => Desc, example => 0})}.
|
||||||
|
|
||||||
%%%==============================================================================================
|
%%%==============================================================================================
|
||||||
%% api apply
|
%% api apply
|
||||||
|
|
@ -144,7 +127,9 @@ list(get, #{query_string := Qs}) ->
|
||||||
true ->
|
true ->
|
||||||
{200, emqx_mgmt:get_stats()};
|
{200, emqx_mgmt:get_stats()};
|
||||||
_ ->
|
_ ->
|
||||||
Data = [maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}]) ||
|
Data = [
|
||||||
Node <- mria_mnesia:running_nodes()],
|
maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}])
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
],
|
||||||
{200, Data}
|
{200, Data}
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@
|
||||||
%% API
|
%% API
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([running_status/2]).
|
-export([running_status/2]).
|
||||||
|
|
||||||
|
|
@ -31,22 +32,30 @@ paths() ->
|
||||||
["/status"].
|
["/status"].
|
||||||
|
|
||||||
schema("/status") ->
|
schema("/status") ->
|
||||||
#{ 'operationId' => running_status
|
#{
|
||||||
, get =>
|
'operationId' => running_status,
|
||||||
#{ description => <<"Node running status">>
|
get =>
|
||||||
, security => []
|
#{
|
||||||
, responses =>
|
description => <<"Node running status">>,
|
||||||
#{200 =>
|
security => [],
|
||||||
#{ description => <<"Node is running">>
|
responses =>
|
||||||
, content =>
|
#{
|
||||||
#{ 'text/plain' =>
|
200 =>
|
||||||
#{ schema => #{type => string}
|
#{
|
||||||
, example => <<"Node emqx@127.0.0.1 is started\nemqx is running">>}
|
description => <<"Node is running">>,
|
||||||
}
|
content =>
|
||||||
}
|
#{
|
||||||
}
|
'text/plain' =>
|
||||||
|
#{
|
||||||
|
schema => #{type => string},
|
||||||
|
example =>
|
||||||
|
<<"Node emqx@127.0.0.1 is started\nemqx is running">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API Handler funcs
|
%% API Handler funcs
|
||||||
|
|
@ -62,7 +71,7 @@ running_status(get, _Params) ->
|
||||||
end,
|
end,
|
||||||
AppStatus =
|
AppStatus =
|
||||||
case lists:keysearch(emqx, 1, application:which_applications()) of
|
case lists:keysearch(emqx, 1, application:which_applications()) of
|
||||||
false -> not_running;
|
false -> not_running;
|
||||||
{value, _Val} -> running
|
{value, _Val} -> running
|
||||||
end,
|
end,
|
||||||
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
|
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
|
||||||
|
|
|
||||||
|
|
@ -22,26 +22,30 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([subscriptions/2]).
|
-export([subscriptions/2]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format/1
|
query/4,
|
||||||
]).
|
format/1
|
||||||
|
]).
|
||||||
|
|
||||||
-define(SUBS_QTABLE, emqx_suboption).
|
-define(SUBS_QTABLE, emqx_suboption).
|
||||||
|
|
||||||
-define(SUBS_QSCHEMA,
|
-define(SUBS_QSCHEMA, [
|
||||||
[ {<<"clientid">>, binary}
|
{<<"clientid">>, binary},
|
||||||
, {<<"topic">>, binary}
|
{<<"topic">>, binary},
|
||||||
, {<<"share">>, binary}
|
{<<"share">>, binary},
|
||||||
, {<<"share_group">>, binary}
|
{<<"share_group">>, binary},
|
||||||
, {<<"qos">>, integer}
|
{<<"qos">>, integer},
|
||||||
, {<<"match_topic">>, binary}]).
|
{<<"match_topic">>, binary}
|
||||||
|
]).
|
||||||
|
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
|
|
||||||
|
|
@ -58,7 +62,9 @@ schema("/subscriptions") ->
|
||||||
description => <<"List subscriptions">>,
|
description => <<"List subscriptions">>,
|
||||||
parameters => parameters(),
|
parameters => parameters(),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})}}
|
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})
|
||||||
|
}
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(subscription) ->
|
fields(subscription) ->
|
||||||
|
|
@ -74,62 +80,89 @@ parameters() ->
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{
|
{
|
||||||
node, hoconsc:mk(binary(), #{
|
node,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Node name">>,
|
required => false,
|
||||||
example => atom_to_list(node())})
|
desc => <<"Node name">>,
|
||||||
|
example => atom_to_list(node())
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
clientid, hoconsc:mk(binary(), #{
|
clientid,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Client ID">>})
|
required => false,
|
||||||
|
desc => <<"Client ID">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
qos, hoconsc:mk(emqx_schema:qos(), #{
|
qos,
|
||||||
in => query,
|
hoconsc:mk(emqx_schema:qos(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"QoS">>})
|
required => false,
|
||||||
|
desc => <<"QoS">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic, hoconsc:mk(binary(), #{
|
topic,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Topic, url encoding">>})
|
required => false,
|
||||||
|
desc => <<"Topic, url encoding">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match_topic, hoconsc:mk(binary(), #{
|
match_topic,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Match topic string, url encoding">>})
|
required => false,
|
||||||
|
desc => <<"Match topic string, url encoding">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
share_group, hoconsc:mk(binary(), #{
|
share_group,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Shared subscription group name">>})
|
required => false,
|
||||||
|
desc => <<"Shared subscription group name">>
|
||||||
|
})
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
subscriptions(get, #{query_string := QString}) ->
|
subscriptions(get, #{query_string := QString}) ->
|
||||||
case maps:get(<<"node">>, QString, undefined) of
|
Response =
|
||||||
undefined ->
|
case maps:get(<<"node">>, QString, undefined) of
|
||||||
Response = emqx_mgmt_api:cluster_query(QString, ?SUBS_QTABLE,
|
undefined ->
|
||||||
?SUBS_QSCHEMA, ?QUERY_FUN),
|
emqx_mgmt_api:cluster_query(
|
||||||
emqx_mgmt_util:generate_response(Response);
|
QString,
|
||||||
Node ->
|
?SUBS_QTABLE,
|
||||||
Response = emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), QString,
|
?SUBS_QSCHEMA,
|
||||||
?SUBS_QTABLE, ?SUBS_QSCHEMA, ?QUERY_FUN),
|
?QUERY_FUN
|
||||||
emqx_mgmt_util:generate_response(Response)
|
);
|
||||||
|
Node0 ->
|
||||||
|
emqx_mgmt_api:node_query(
|
||||||
|
binary_to_atom(Node0, utf8),
|
||||||
|
QString,
|
||||||
|
?SUBS_QTABLE,
|
||||||
|
?SUBS_QSCHEMA,
|
||||||
|
?QUERY_FUN
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
case Response of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Result ->
|
||||||
|
{200, Result}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format(Items) when is_list(Items) ->
|
format(Items) when is_list(Items) ->
|
||||||
[format(Item) || Item <- Items];
|
[format(Item) || Item <- Items];
|
||||||
|
|
||||||
format({{Subscriber, Topic}, Options}) ->
|
format({{Subscriber, Topic}, Options}) ->
|
||||||
format({Subscriber, Topic, Options});
|
format({Subscriber, Topic, Options});
|
||||||
|
|
||||||
format({_Subscriber, Topic, Options = #{share := Group}}) ->
|
format({_Subscriber, Topic, Options = #{share := Group}}) ->
|
||||||
QoS = maps:get(qos, Options),
|
QoS = maps:get(qos, Options),
|
||||||
#{
|
#{
|
||||||
|
|
@ -153,19 +186,30 @@ format({_Subscriber, Topic, Options}) ->
|
||||||
|
|
||||||
query(Tab, {Qs, []}, Continuation, Limit) ->
|
query(Tab, {Qs, []}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs),
|
Ms = qs2ms(Qs),
|
||||||
emqx_mgmt_api:select_table_with_count( Tab, Ms
|
emqx_mgmt_api:select_table_with_count(
|
||||||
, Continuation, Limit, fun format/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format/1
|
||||||
|
);
|
||||||
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
|
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs),
|
Ms = qs2ms(Qs),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
|
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
|
||||||
emqx_mgmt_api:select_table_with_count( Tab, {Ms, FuzzyFilterFun}
|
emqx_mgmt_api:select_table_with_count(
|
||||||
, Continuation, Limit, fun format/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format/1
|
||||||
|
).
|
||||||
|
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
|
|
|
||||||
|
|
@ -22,23 +22,24 @@
|
||||||
%% API
|
%% API
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ topics/2
|
-export([
|
||||||
, topic/2
|
topics/2,
|
||||||
]).
|
topic/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4]).
|
-export([query/4]).
|
||||||
|
|
||||||
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
|
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
|
||||||
|
|
||||||
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
|
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
|
||||||
|
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
|
|
@ -73,19 +74,23 @@ schema("/topics/:topic") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
|
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
|
||||||
404 =>
|
404 =>
|
||||||
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'],<<"Topic not found">>)
|
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'], <<"Topic not found">>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(topic) ->
|
fields(topic) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{
|
{topic,
|
||||||
desc => <<"Topic Name">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true})},
|
desc => <<"Topic Name">>,
|
||||||
{node, hoconsc:mk(binary(), #{
|
required => true
|
||||||
desc => <<"Node">>,
|
})},
|
||||||
required => true})}
|
{node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"Node">>,
|
||||||
|
required => true
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
|
|
@ -103,9 +108,19 @@ topic(get, #{bindings := Bindings}) ->
|
||||||
%%%==============================================================================================
|
%%%==============================================================================================
|
||||||
%% api apply
|
%% api apply
|
||||||
do_list(Params) ->
|
do_list(Params) ->
|
||||||
Response = emqx_mgmt_api:node_query(
|
case
|
||||||
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}),
|
emqx_mgmt_api:node_query(
|
||||||
emqx_mgmt_util:generate_response(Response).
|
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, page_limit_invalid} ->
|
||||||
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
{error, Node, {badrpc, R}} ->
|
||||||
|
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
||||||
|
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
||||||
|
Response ->
|
||||||
|
{200, Response}
|
||||||
|
end.
|
||||||
|
|
||||||
lookup(#{topic := Topic}) ->
|
lookup(#{topic := Topic}) ->
|
||||||
case emqx_router:lookup_routes(Topic) of
|
case emqx_router:lookup_routes(Topic) of
|
||||||
|
|
@ -121,16 +136,18 @@ generate_topic(Params = #{<<"topic">> := Topic}) ->
|
||||||
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
|
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
|
||||||
generate_topic(Params = #{topic := Topic}) ->
|
generate_topic(Params = #{topic := Topic}) ->
|
||||||
Params#{topic => uri_string:percent_decode(Topic)};
|
Params#{topic => uri_string:percent_decode(Topic)};
|
||||||
generate_topic(Params) -> Params.
|
generate_topic(Params) ->
|
||||||
|
Params.
|
||||||
|
|
||||||
query(Tab, {Qs, _}, Continuation, Limit) ->
|
query(Tab, {Qs, _}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
|
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
|
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
|
||||||
|
|
||||||
qs2ms([], Res) -> Res;
|
qs2ms([], Res) ->
|
||||||
qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
Res;
|
||||||
|
qs2ms([{topic, '=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
||||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
|
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
|
||||||
qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
qs2ms([{node, '=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
||||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
|
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
|
||||||
|
|
||||||
format(#route{topic = Topic, dest = {_, Node}}) ->
|
format(#route{topic = Topic, dest = {_, Node}}) ->
|
||||||
|
|
@ -140,7 +157,8 @@ format(#route{topic = Topic, dest = Node}) ->
|
||||||
|
|
||||||
topic_param(In) ->
|
topic_param(In) ->
|
||||||
{
|
{
|
||||||
topic, hoconsc:mk(binary(), #{
|
topic,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Topic Name">>,
|
desc => <<"Topic Name">>,
|
||||||
in => In,
|
in => In,
|
||||||
required => (In == path),
|
required => (In == path),
|
||||||
|
|
@ -148,9 +166,10 @@ topic_param(In) ->
|
||||||
})
|
})
|
||||||
}.
|
}.
|
||||||
|
|
||||||
node_param()->
|
node_param() ->
|
||||||
{
|
{
|
||||||
node, hoconsc:mk(binary(), #{
|
node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Node Name">>,
|
desc => <<"Node Name">>,
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
|
||||||
|
|
@ -21,26 +21,29 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, fields/1
|
api_spec/0,
|
||||||
, paths/0
|
fields/1,
|
||||||
, schema/1
|
paths/0,
|
||||||
, namespace/0
|
schema/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ trace/2
|
-export([
|
||||||
, delete_trace/2
|
trace/2,
|
||||||
, update_trace/2
|
delete_trace/2,
|
||||||
, download_trace_log/2
|
update_trace/2,
|
||||||
, stream_log_file/2
|
download_trace_log/2,
|
||||||
]).
|
stream_log_file/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([validate_name/1]).
|
-export([validate_name/1]).
|
||||||
|
|
||||||
%% for rpc
|
%% for rpc
|
||||||
-export([ read_trace_file/3
|
-export([
|
||||||
, get_trace_size/0
|
read_trace_file/3,
|
||||||
]).
|
get_trace_size/0
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
|
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
|
||||||
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
|
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
|
||||||
|
|
@ -53,7 +56,6 @@ api_spec() ->
|
||||||
paths() ->
|
paths() ->
|
||||||
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
|
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
|
||||||
|
|
||||||
|
|
||||||
schema("/trace") ->
|
schema("/trace") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => trace,
|
'operationId' => trace,
|
||||||
|
|
@ -68,9 +70,14 @@ schema("/trace") ->
|
||||||
'requestBody' => delete([status, log_size], fields(trace)),
|
'requestBody' => delete([status, log_size], fields(trace)),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(trace),
|
200 => hoconsc:ref(trace),
|
||||||
400 => emqx_dashboard_swagger:error_codes(['ALREADY_EXISTS',
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
'DUPLICATE_CONDITION', 'INVALID_PARAMS'],
|
[
|
||||||
<<"trace name already exists">>)
|
'ALREADY_EXISTS',
|
||||||
|
'DUPLICATE_CONDITION',
|
||||||
|
'INVALID_PARAMS'
|
||||||
|
],
|
||||||
|
<<"trace name already exists">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
|
|
@ -112,12 +119,13 @@ schema("/trace/:name/download") ->
|
||||||
parameters => [hoconsc:ref(name)],
|
parameters => [hoconsc:ref(name)],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>
|
200 =>
|
||||||
#{description => "A trace zip file",
|
#{
|
||||||
content => #{
|
description => "A trace zip file",
|
||||||
'application/octet-stream' =>
|
content => #{
|
||||||
#{schema => #{type => "string", format => "binary"}}
|
'application/octet-stream' =>
|
||||||
|
#{schema => #{type => "string", format => "binary"}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -134,92 +142,151 @@ schema("/trace/:name/log") ->
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>
|
200 =>
|
||||||
[
|
[
|
||||||
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
||||||
| fields(bytes) ++ fields(position)
|
| fields(bytes) ++ fields(position)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(trace) ->
|
fields(trace) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
hoconsc:mk(
|
||||||
validator => fun ?MODULE:validate_name/1,
|
binary(),
|
||||||
required => true,
|
#{
|
||||||
example => <<"EMQX-TRACE-1">>})},
|
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||||
{type, hoconsc:mk(hoconsc:enum([clientid, topic, ip_address]),
|
validator => fun ?MODULE:validate_name/1,
|
||||||
#{desc => """Filter type""",
|
required => true,
|
||||||
required => true,
|
example => <<"EMQX-TRACE-1">>
|
||||||
example => <<"clientid">>})},
|
}
|
||||||
{topic, hoconsc:mk(binary(),
|
)},
|
||||||
#{desc => """support mqtt wildcard topic.""",
|
{type,
|
||||||
required => false,
|
hoconsc:mk(
|
||||||
example => <<"/dev/#">>})},
|
hoconsc:enum([clientid, topic, ip_address]),
|
||||||
{clientid, hoconsc:mk(binary(),
|
#{
|
||||||
#{desc => """mqtt clientid.""",
|
desc => "" "Filter type" "",
|
||||||
required => false,
|
required => true,
|
||||||
example => <<"dev-001">>})},
|
example => <<"clientid">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{topic,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
desc => "" "support mqtt wildcard topic." "",
|
||||||
|
required => false,
|
||||||
|
example => <<"/dev/#">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{clientid,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
desc => "" "mqtt clientid." "",
|
||||||
|
required => false,
|
||||||
|
example => <<"dev-001">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
%% TODO add ip_address type in emqx_schema.erl
|
%% TODO add ip_address type in emqx_schema.erl
|
||||||
{ip_address, hoconsc:mk(binary(),
|
{ip_address,
|
||||||
#{desc => "client ip address",
|
hoconsc:mk(
|
||||||
required => false,
|
binary(),
|
||||||
example => <<"127.0.0.1">>
|
#{
|
||||||
})},
|
desc => "client ip address",
|
||||||
{status, hoconsc:mk(hoconsc:enum([running, stopped, waiting]),
|
required => false,
|
||||||
#{desc => "trace status",
|
example => <<"127.0.0.1">>
|
||||||
required => false,
|
}
|
||||||
example => running
|
)},
|
||||||
})},
|
{status,
|
||||||
{start_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
hoconsc:mk(
|
||||||
#{desc => "rfc3339 timestamp or epoch second",
|
hoconsc:enum([running, stopped, waiting]),
|
||||||
required => false,
|
#{
|
||||||
example => <<"2021-11-04T18:17:38+08:00">>
|
desc => "trace status",
|
||||||
})},
|
required => false,
|
||||||
{end_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
example => running
|
||||||
#{desc => "rfc3339 timestamp or epoch second",
|
}
|
||||||
required => false,
|
)},
|
||||||
example => <<"2021-11-05T18:17:38+08:00">>
|
{start_at,
|
||||||
})},
|
hoconsc:mk(
|
||||||
{log_size, hoconsc:mk(hoconsc:array(map()),
|
emqx_datetime:epoch_second(),
|
||||||
#{desc => "trace log size",
|
#{
|
||||||
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
desc => "rfc3339 timestamp or epoch second",
|
||||||
required => false})}
|
required => false,
|
||||||
|
example => <<"2021-11-04T18:17:38+08:00">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{end_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
emqx_datetime:epoch_second(),
|
||||||
|
#{
|
||||||
|
desc => "rfc3339 timestamp or epoch second",
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-11-05T18:17:38+08:00">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{log_size,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:array(map()),
|
||||||
|
#{
|
||||||
|
desc => "trace log size",
|
||||||
|
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => <<"[a-zA-Z0-9-_]">>,
|
hoconsc:mk(
|
||||||
example => <<"EMQX-TRACE-1">>,
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => <<"[a-zA-Z0-9-_]">>,
|
||||||
})}
|
example => <<"EMQX-TRACE-1">>,
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node) ->
|
fields(node) ->
|
||||||
[{node, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{node,
|
||||||
desc => "Node name",
|
hoconsc:mk(
|
||||||
in => query,
|
binary(),
|
||||||
required => false
|
#{
|
||||||
})}];
|
desc => "Node name",
|
||||||
|
in => query,
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(bytes) ->
|
fields(bytes) ->
|
||||||
[{bytes, hoconsc:mk(integer(),
|
[
|
||||||
#{
|
{bytes,
|
||||||
desc => "Maximum number of bytes to store in request",
|
hoconsc:mk(
|
||||||
in => query,
|
integer(),
|
||||||
required => false,
|
#{
|
||||||
default => 1000
|
desc => "Maximum number of bytes to store in request",
|
||||||
})}];
|
in => query,
|
||||||
|
required => false,
|
||||||
|
default => 1000
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(position) ->
|
fields(position) ->
|
||||||
[{position, hoconsc:mk(integer(),
|
[
|
||||||
#{
|
{position,
|
||||||
desc => "Offset from the current trace position.",
|
hoconsc:mk(
|
||||||
in => query,
|
integer(),
|
||||||
required => false,
|
#{
|
||||||
default => 0
|
desc => "Offset from the current trace position.",
|
||||||
})}].
|
in => query,
|
||||||
|
required => false,
|
||||||
|
default => 0
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
].
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||||
|
|
||||||
|
|
@ -231,7 +298,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete(Keys, Fields) ->
|
delete(Keys, Fields) ->
|
||||||
|
|
@ -239,32 +307,48 @@ delete(Keys, Fields) ->
|
||||||
|
|
||||||
trace(get, _Params) ->
|
trace(get, _Params) ->
|
||||||
case emqx_trace:list() of
|
case emqx_trace:list() of
|
||||||
[] -> {200, []};
|
[] ->
|
||||||
|
{200, []};
|
||||||
List0 ->
|
List0 ->
|
||||||
List = lists:sort(fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
List = lists:sort(
|
||||||
emqx_trace:format(List0)),
|
fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
||||||
|
emqx_trace:format(List0)
|
||||||
|
),
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
|
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
|
||||||
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
|
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Traces =
|
Traces =
|
||||||
lists:map(fun(Trace = #{name := Name, start_at := Start,
|
lists:map(
|
||||||
end_at := End, enable := Enable, type := Type, filter := Filter}) ->
|
fun(
|
||||||
FileName = emqx_trace:filename(Name, Start),
|
Trace = #{
|
||||||
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
name := Name,
|
||||||
Trace0 = maps:without([enable, filter], Trace),
|
start_at := Start,
|
||||||
Trace0#{log_size => LogSize
|
end_at := End,
|
||||||
, Type => iolist_to_binary(Filter)
|
enable := Enable,
|
||||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
type := Type,
|
||||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
filter := Filter
|
||||||
, status => status(Enable, Start, End, Now)
|
}
|
||||||
}
|
) ->
|
||||||
end, List),
|
FileName = emqx_trace:filename(Name, Start),
|
||||||
|
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
||||||
|
Trace0 = maps:without([enable, filter], Trace),
|
||||||
|
Trace0#{
|
||||||
|
log_size => LogSize,
|
||||||
|
Type => iolist_to_binary(Filter),
|
||||||
|
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||||
|
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||||
|
status => status(Enable, Start, End, Now)
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
List
|
||||||
|
),
|
||||||
{200, Traces}
|
{200, Traces}
|
||||||
end;
|
end;
|
||||||
trace(post, #{body := Param}) ->
|
trace(post, #{body := Param}) ->
|
||||||
case emqx_trace:create(Param) of
|
case emqx_trace:create(Param) of
|
||||||
{ok, Trace0} -> {200, format_trace(Trace0)};
|
{ok, Trace0} ->
|
||||||
|
{200, format_trace(Trace0)};
|
||||||
{error, {already_existed, Name}} ->
|
{error, {already_existed, Name}} ->
|
||||||
{400, #{
|
{400, #{
|
||||||
code => 'ALREADY_EXISTS',
|
code => 'ALREADY_EXISTS',
|
||||||
|
|
@ -287,18 +371,27 @@ trace(delete, _Param) ->
|
||||||
|
|
||||||
format_trace(Trace0) ->
|
format_trace(Trace0) ->
|
||||||
[
|
[
|
||||||
#{start_at := Start, end_at := End,
|
#{
|
||||||
enable := Enable, type := Type, filter := Filter} = Trace1
|
start_at := Start,
|
||||||
|
end_at := End,
|
||||||
|
enable := Enable,
|
||||||
|
type := Type,
|
||||||
|
filter := Filter
|
||||||
|
} = Trace1
|
||||||
] = emqx_trace:format([Trace0]),
|
] = emqx_trace:format([Trace0]),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
LogSize = lists:foldl(fun(Node, Acc) -> Acc#{Node => 0} end, #{},
|
LogSize = lists:foldl(
|
||||||
mria_mnesia:running_nodes()),
|
fun(Node, Acc) -> Acc#{Node => 0} end,
|
||||||
|
#{},
|
||||||
|
mria_mnesia:running_nodes()
|
||||||
|
),
|
||||||
Trace2 = maps:without([enable, filter], Trace1),
|
Trace2 = maps:without([enable, filter], Trace1),
|
||||||
Trace2#{log_size => LogSize
|
Trace2#{
|
||||||
, Type => iolist_to_binary(Filter)
|
log_size => LogSize,
|
||||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
Type => iolist_to_binary(Filter),
|
||||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||||
, status => status(Enable, Start, End, Now)
|
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||||
|
status => status(Enable, Start, End, Now)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
delete_trace(delete, #{bindings := #{name := Name}}) ->
|
delete_trace(delete, #{bindings := #{name := Name}}) ->
|
||||||
|
|
@ -334,25 +427,34 @@ download_trace_log(get, #{bindings := #{name := Name}}) ->
|
||||||
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
|
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
|
||||||
},
|
},
|
||||||
{200, Headers, {file_binary, ZipName, Binary}};
|
{200, Headers, {file_binary, ZipName, Binary}};
|
||||||
{error, not_found} -> ?NOT_FOUND(Name)
|
{error, not_found} ->
|
||||||
|
?NOT_FOUND(Name)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
|
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
|
||||||
lists:foldl(fun(Res, Acc) ->
|
lists:foldl(
|
||||||
case Res of
|
fun(Res, Acc) ->
|
||||||
{ok, Node, Bin} ->
|
case Res of
|
||||||
FileName = Node ++ "-" ++ TraceLog,
|
{ok, Node, Bin} ->
|
||||||
ZipName = filename:join([ZipDir, FileName]),
|
FileName = Node ++ "-" ++ TraceLog,
|
||||||
case file:write_file(ZipName, Bin) of
|
ZipName = filename:join([ZipDir, FileName]),
|
||||||
ok -> [FileName | Acc];
|
case file:write_file(ZipName, Bin) of
|
||||||
_ -> Acc
|
ok -> [FileName | Acc];
|
||||||
end;
|
_ -> Acc
|
||||||
{error, Node, Reason} ->
|
end;
|
||||||
?SLOG(error, #{msg => "download_trace_log_error", node => Node,
|
{error, Node, Reason} ->
|
||||||
log => TraceLog, reason => Reason}),
|
?SLOG(error, #{
|
||||||
Acc
|
msg => "download_trace_log_error",
|
||||||
end
|
node => Node,
|
||||||
end, [], TraceFiles).
|
log => TraceLog,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
TraceFiles
|
||||||
|
).
|
||||||
|
|
||||||
collect_trace_file(TraceLog) ->
|
collect_trace_file(TraceLog) ->
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
|
|
@ -376,18 +478,25 @@ stream_log_file(get, #{bindings := #{name := Name}, query_string := Query}) ->
|
||||||
{eof, Size} ->
|
{eof, Size} ->
|
||||||
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
|
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
|
||||||
{200, #{meta => Meta, items => <<"">>}};
|
{200, #{meta => Meta, items => <<"">>}};
|
||||||
{error, enoent} -> %% the waiting trace should return "" not error.
|
%% the waiting trace should return "" not error.
|
||||||
|
{error, enoent} ->
|
||||||
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
|
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
|
||||||
{200, #{meta => Meta, items => <<"">>}};
|
{200, #{meta => Meta, items => <<"">>}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "read_file_failed",
|
?SLOG(error, #{
|
||||||
node => Node, name => Name, reason => Reason,
|
msg => "read_file_failed",
|
||||||
position => Position, bytes => Bytes}),
|
node => Node,
|
||||||
|
name => Name,
|
||||||
|
reason => Reason,
|
||||||
|
position => Position,
|
||||||
|
bytes => Bytes
|
||||||
|
}),
|
||||||
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
|
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
|
||||||
{badrpc, nodedown} ->
|
{badrpc, nodedown} ->
|
||||||
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
|
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
|
||||||
end;
|
end;
|
||||||
{error, not_found} -> {400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
{error, not_found} ->
|
||||||
|
{400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
|
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
|
||||||
|
|
@ -396,23 +505,31 @@ get_trace_size() ->
|
||||||
Node = node(),
|
Node = node(),
|
||||||
case file:list_dir(TraceDir) of
|
case file:list_dir(TraceDir) of
|
||||||
{ok, AllFiles} ->
|
{ok, AllFiles} ->
|
||||||
lists:foldl(fun(File, Acc) ->
|
lists:foldl(
|
||||||
FullFileName = filename:join(TraceDir, File),
|
fun(File, Acc) ->
|
||||||
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
FullFileName = filename:join(TraceDir, File),
|
||||||
end, #{}, lists:delete("zip", AllFiles));
|
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
||||||
_ -> #{}
|
end,
|
||||||
|
#{},
|
||||||
|
lists:delete("zip", AllFiles)
|
||||||
|
);
|
||||||
|
_ ->
|
||||||
|
#{}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% this is an rpc call for stream_log_file/2
|
%% this is an rpc call for stream_log_file/2
|
||||||
-spec read_trace_file( binary()
|
-spec read_trace_file(
|
||||||
, non_neg_integer()
|
binary(),
|
||||||
, non_neg_integer()
|
non_neg_integer(),
|
||||||
) -> {ok, binary()}
|
non_neg_integer()
|
||||||
| {error, _}
|
) ->
|
||||||
| {eof, non_neg_integer()}.
|
{ok, binary()}
|
||||||
|
| {error, _}
|
||||||
|
| {eof, non_neg_integer()}.
|
||||||
read_trace_file(Name, Position, Limit) ->
|
read_trace_file(Name, Position, Limit) ->
|
||||||
case emqx_trace:get_trace_filename(Name) of
|
case emqx_trace:get_trace_filename(Name) of
|
||||||
{error, _} = Error -> Error;
|
{error, _} = Error ->
|
||||||
|
Error;
|
||||||
{ok, TraceFile} ->
|
{ok, TraceFile} ->
|
||||||
TraceDir = emqx_trace:trace_dir(),
|
TraceDir = emqx_trace:trace_dir(),
|
||||||
TracePath = filename:join([TraceDir, TraceFile]),
|
TracePath = filename:join([TraceDir, TraceFile]),
|
||||||
|
|
@ -423,13 +540,16 @@ read_file(Path, Offset, Bytes) ->
|
||||||
case file:open(Path, [read, raw, binary]) of
|
case file:open(Path, [read, raw, binary]) of
|
||||||
{ok, IoDevice} ->
|
{ok, IoDevice} ->
|
||||||
try
|
try
|
||||||
_ = case Offset of
|
_ =
|
||||||
|
case Offset of
|
||||||
0 -> ok;
|
0 -> ok;
|
||||||
_ -> file:position(IoDevice, {bof, Offset})
|
_ -> file:position(IoDevice, {bof, Offset})
|
||||||
end,
|
end,
|
||||||
case file:read(IoDevice, Bytes) of
|
case file:read(IoDevice, Bytes) of
|
||||||
{ok, Bin} -> {ok, Bin};
|
{ok, Bin} ->
|
||||||
{error, Reason} -> {error, Reason};
|
{ok, Bin};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason};
|
||||||
eof ->
|
eof ->
|
||||||
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
|
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
|
||||||
{eof, Size}
|
{eof, Size}
|
||||||
|
|
@ -437,20 +557,27 @@ read_file(Path, Offset, Bytes) ->
|
||||||
after
|
after
|
||||||
file:close(IoDevice)
|
file:close(IoDevice)
|
||||||
end;
|
end;
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
to_node(Node) ->
|
to_node(Node) ->
|
||||||
try {ok, binary_to_existing_atom(Node)}
|
try
|
||||||
catch _:_ ->
|
{ok, binary_to_existing_atom(Node)}
|
||||||
{error, not_found}
|
catch
|
||||||
|
_:_ ->
|
||||||
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
collect_file_size(Nodes, FileName, AllFiles) ->
|
collect_file_size(Nodes, FileName, AllFiles) ->
|
||||||
lists:foldl(fun(Node, Acc) ->
|
lists:foldl(
|
||||||
Size = maps:get({Node, FileName}, AllFiles, 0),
|
fun(Node, Acc) ->
|
||||||
Acc#{Node => Size}
|
Size = maps:get({Node, FileName}, AllFiles, 0),
|
||||||
end, #{}, Nodes).
|
Acc#{Node => Size}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Nodes
|
||||||
|
).
|
||||||
|
|
||||||
status(false, _Start, _End, _Now) -> <<"stopped">>;
|
status(false, _Start, _End, _Now) -> <<"stopped">>;
|
||||||
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
|
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,10 @@
|
||||||
|
|
||||||
-define(APP, emqx_management).
|
-define(APP, emqx_management).
|
||||||
|
|
||||||
-export([ start/2
|
-export([
|
||||||
, stop/1
|
start/2,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
-include("emqx_mgmt.hrl").
|
-include("emqx_mgmt.hrl").
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,15 @@
|
||||||
-export([mnesia/1]).
|
-export([mnesia/1]).
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
||||||
-export([ create/4
|
-export([
|
||||||
, read/1
|
create/4,
|
||||||
, update/4
|
read/1,
|
||||||
, delete/1
|
update/4,
|
||||||
, list/0
|
delete/1,
|
||||||
]).
|
list/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ authorize/3 ]).
|
-export([authorize/3]).
|
||||||
|
|
||||||
-define(APP, emqx_app).
|
-define(APP, emqx_app).
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@
|
||||||
desc = <<>> :: binary() | '_',
|
desc = <<>> :: binary() | '_',
|
||||||
expired_at = 0 :: integer() | undefined | '_',
|
expired_at = 0 :: integer() | undefined | '_',
|
||||||
created_at = 0 :: integer() | '_'
|
created_at = 0 :: integer() | '_'
|
||||||
}).
|
}).
|
||||||
|
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?APP, [
|
ok = mria:create_table(?APP, [
|
||||||
|
|
@ -47,7 +48,8 @@ mnesia(boot) ->
|
||||||
{rlog_shard, ?COMMON_SHARD},
|
{rlog_shard, ?COMMON_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, ?APP},
|
{record_name, ?APP},
|
||||||
{attributes, record_info(fields, ?APP)}]).
|
{attributes, record_info(fields, ?APP)}
|
||||||
|
]).
|
||||||
|
|
||||||
create(Name, Enable, ExpiredAt, Desc) ->
|
create(Name, Enable, ExpiredAt, Desc) ->
|
||||||
case mnesia:table_info(?APP, size) < 30 of
|
case mnesia:table_info(?APP, size) < 30 of
|
||||||
|
|
@ -61,13 +63,14 @@ read(Name) ->
|
||||||
[] -> mnesia:abort(not_found);
|
[] -> mnesia:abort(not_found);
|
||||||
[App] -> to_map(App)
|
[App] -> to_map(App)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
update(Name, Enable, ExpiredAt, Desc) ->
|
update(Name, Enable, ExpiredAt, Desc) ->
|
||||||
Fun = fun() ->
|
Fun = fun() ->
|
||||||
case mnesia:read(?APP, Name, write) of
|
case mnesia:read(?APP, Name, write) of
|
||||||
[] -> mnesia:abort(not_found);
|
[] ->
|
||||||
|
mnesia:abort(not_found);
|
||||||
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
|
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
|
||||||
App =
|
App =
|
||||||
App0#?APP{
|
App0#?APP{
|
||||||
|
|
@ -78,22 +81,25 @@ update(Name, Enable, ExpiredAt, Desc) ->
|
||||||
ok = mnesia:write(App),
|
ok = mnesia:write(App),
|
||||||
to_map(App)
|
to_map(App)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
delete(Name) ->
|
delete(Name) ->
|
||||||
Fun = fun() ->
|
Fun = fun() ->
|
||||||
case mnesia:read(?APP, Name) of
|
case mnesia:read(?APP, Name) of
|
||||||
[] -> mnesia:abort(not_found);
|
[] -> mnesia:abort(not_found);
|
||||||
[_App] -> mnesia:delete({?APP, Name}) end
|
[_App] -> mnesia:delete({?APP, Name})
|
||||||
end,
|
end
|
||||||
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
list() ->
|
list() ->
|
||||||
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
||||||
|
|
||||||
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
{error, <<"not_allowed">>};
|
||||||
|
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||||
|
{error, <<"not_allowed">>};
|
||||||
authorize(_Path, ApiKey, ApiSecret) ->
|
authorize(_Path, ApiKey, ApiSecret) ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
case find_by_api_key(ApiKey) of
|
case find_by_api_key(ApiKey) of
|
||||||
|
|
@ -102,28 +108,35 @@ authorize(_Path, ApiKey, ApiSecret) ->
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
error -> {error, "secret_error"}
|
error -> {error, "secret_error"}
|
||||||
end;
|
end;
|
||||||
{ok, true, _ExpiredAt, _SecretHash} -> {error, "secret_expired"};
|
{ok, true, _ExpiredAt, _SecretHash} ->
|
||||||
{ok, false, _ExpiredAt, _SecretHash} -> {error, "secret_disable"};
|
{error, "secret_expired"};
|
||||||
{error, Reason} -> {error, Reason}
|
{ok, false, _ExpiredAt, _SecretHash} ->
|
||||||
|
{error, "secret_disable"};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_by_api_key(ApiKey) ->
|
find_by_api_key(ApiKey) ->
|
||||||
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
||||||
case trans(Fun) of
|
case trans(Fun) of
|
||||||
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
||||||
{ok, Enable, ExpiredAt, SecretHash};
|
{ok, Enable, ExpiredAt, SecretHash};
|
||||||
_ -> {error, "not_found"}
|
_ ->
|
||||||
|
{error, "not_found"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
ensure_not_undefined(undefined, Old) -> Old;
|
ensure_not_undefined(undefined, Old) -> Old;
|
||||||
ensure_not_undefined(New, _Old) -> New.
|
ensure_not_undefined(New, _Old) -> New.
|
||||||
|
|
||||||
to_map(Apps)when is_list(Apps) ->
|
to_map(Apps) when is_list(Apps) ->
|
||||||
Fields = record_info(fields, ?APP),
|
Fields = record_info(fields, ?APP),
|
||||||
lists:map(fun(Trace0 = #?APP{}) ->
|
lists:map(
|
||||||
[_ | Values] = tuple_to_list(Trace0),
|
fun(Trace0 = #?APP{}) ->
|
||||||
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
[_ | Values] = tuple_to_list(Trace0),
|
||||||
end, Apps);
|
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
||||||
|
end,
|
||||||
|
Apps
|
||||||
|
);
|
||||||
to_map(App0) ->
|
to_map(App0) ->
|
||||||
[App] = to_map([App0]),
|
[App] = to_map([App0]),
|
||||||
App.
|
App.
|
||||||
|
|
@ -149,16 +162,18 @@ create_app(Name, Enable, ExpiredAt, Desc) ->
|
||||||
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
||||||
trans(fun() ->
|
trans(fun() ->
|
||||||
case mnesia:read(?APP, Name) of
|
case mnesia:read(?APP, Name) of
|
||||||
[_] -> mnesia:abort(name_already_existed);
|
[_] ->
|
||||||
|
mnesia:abort(name_already_existed);
|
||||||
[] ->
|
[] ->
|
||||||
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
|
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
|
||||||
[] ->
|
[] ->
|
||||||
ok = mnesia:write(App),
|
ok = mnesia:write(App),
|
||||||
to_map(App);
|
to_map(App);
|
||||||
_ -> mnesia:abort(api_key_already_existed)
|
_ ->
|
||||||
|
mnesia:abort(api_key_already_existed)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
trans(Fun) ->
|
trans(Fun) ->
|
||||||
case mria:transaction(?COMMON_SHARD, Fun) of
|
case mria:transaction(?COMMON_SHARD, Fun) of
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok, {{one_for_one, 1, 5}, []}}.
|
{ok, {{one_for_one, 1, 5}, []}}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,41 +16,40 @@
|
||||||
|
|
||||||
-module(emqx_mgmt_util).
|
-module(emqx_mgmt_util).
|
||||||
|
|
||||||
-export([ strftime/1
|
-export([
|
||||||
, datetime/1
|
strftime/1,
|
||||||
, kmg/1
|
datetime/1,
|
||||||
, ntoa/1
|
kmg/1,
|
||||||
, merge_maps/2
|
ntoa/1,
|
||||||
, batch_operation/3
|
merge_maps/2,
|
||||||
]).
|
batch_operation/3
|
||||||
|
]).
|
||||||
-export([ bad_request/0
|
|
||||||
, bad_request/1
|
|
||||||
, properties/1
|
|
||||||
, page_params/0
|
|
||||||
, schema/1
|
|
||||||
, schema/2
|
|
||||||
, object_schema/1
|
|
||||||
, object_schema/2
|
|
||||||
, array_schema/1
|
|
||||||
, array_schema/2
|
|
||||||
, object_array_schema/1
|
|
||||||
, object_array_schema/2
|
|
||||||
, page_schema/1
|
|
||||||
, page_object_schema/1
|
|
||||||
, error_schema/1
|
|
||||||
, error_schema/2
|
|
||||||
, batch_schema/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([generate_response/1]).
|
|
||||||
|
|
||||||
|
-export([
|
||||||
|
bad_request/0,
|
||||||
|
bad_request/1,
|
||||||
|
properties/1,
|
||||||
|
page_params/0,
|
||||||
|
schema/1,
|
||||||
|
schema/2,
|
||||||
|
object_schema/1,
|
||||||
|
object_schema/2,
|
||||||
|
array_schema/1,
|
||||||
|
array_schema/2,
|
||||||
|
object_array_schema/1,
|
||||||
|
object_array_schema/2,
|
||||||
|
page_schema/1,
|
||||||
|
page_object_schema/1,
|
||||||
|
error_schema/1,
|
||||||
|
error_schema/2,
|
||||||
|
batch_schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([urldecode/1]).
|
-export([urldecode/1]).
|
||||||
|
|
||||||
-define(KB, 1024).
|
-define(KB, 1024).
|
||||||
-define(MB, (1024*1024)).
|
-define(MB, (1024 * 1024)).
|
||||||
-define(GB, (1024*1024*1024)).
|
-define(GB, (1024 * 1024 * 1024)).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Strftime
|
%% Strftime
|
||||||
|
|
@ -58,17 +57,17 @@
|
||||||
|
|
||||||
strftime({MegaSecs, Secs, _MicroSecs}) ->
|
strftime({MegaSecs, Secs, _MicroSecs}) ->
|
||||||
strftime(datetime(MegaSecs * 1000000 + Secs));
|
strftime(datetime(MegaSecs * 1000000 + Secs));
|
||||||
|
|
||||||
strftime(Secs) when is_integer(Secs) ->
|
strftime(Secs) when is_integer(Secs) ->
|
||||||
strftime(datetime(Secs));
|
strftime(datetime(Secs));
|
||||||
|
strftime({{Y, M, D}, {H, MM, S}}) ->
|
||||||
strftime({{Y,M,D}, {H,MM,S}}) ->
|
|
||||||
lists:flatten(
|
lists:flatten(
|
||||||
io_lib:format(
|
io_lib:format(
|
||||||
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
|
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
datetime(Timestamp) when is_integer(Timestamp) ->
|
datetime(Timestamp) when is_integer(Timestamp) ->
|
||||||
Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}),
|
Epoch = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
||||||
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
|
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
|
||||||
calendar:universal_time_to_local_time(Universal).
|
calendar:universal_time_to_local_time(Universal).
|
||||||
|
|
||||||
|
|
@ -83,19 +82,27 @@ kmg(Byte) ->
|
||||||
kmg(F, S) ->
|
kmg(F, S) ->
|
||||||
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
|
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
|
||||||
|
|
||||||
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
|
ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
|
||||||
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
||||||
ntoa(IP) ->
|
ntoa(IP) ->
|
||||||
inet_parse:ntoa(IP).
|
inet_parse:ntoa(IP).
|
||||||
|
|
||||||
merge_maps(Default, New) ->
|
merge_maps(Default, New) ->
|
||||||
maps:fold(fun(K, V, Acc) ->
|
maps:fold(
|
||||||
case maps:get(K, Acc, undefined) of
|
fun(K, V, Acc) ->
|
||||||
OldV when is_map(OldV),
|
case maps:get(K, Acc, undefined) of
|
||||||
is_map(V) -> Acc#{K => merge_maps(OldV, V)};
|
OldV when
|
||||||
_ -> Acc#{K => V}
|
is_map(OldV),
|
||||||
end
|
is_map(V)
|
||||||
end, Default, New).
|
->
|
||||||
|
Acc#{K => merge_maps(OldV, V)};
|
||||||
|
_ ->
|
||||||
|
Acc#{K => V}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Default,
|
||||||
|
New
|
||||||
|
).
|
||||||
|
|
||||||
urldecode(S) ->
|
urldecode(S) ->
|
||||||
emqx_http_lib:uri_decode(S).
|
emqx_http_lib:uri_decode(S).
|
||||||
|
|
@ -126,8 +133,13 @@ array_schema(Schema, Desc) ->
|
||||||
object_array_schema(Properties) when is_map(Properties) ->
|
object_array_schema(Properties) when is_map(Properties) ->
|
||||||
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
|
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
|
||||||
object_array_schema(Properties, Desc) ->
|
object_array_schema(Properties, Desc) ->
|
||||||
json_content_schema(#{type => array,
|
json_content_schema(
|
||||||
items => #{type => object, properties => Properties}}, Desc).
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => Properties}
|
||||||
|
},
|
||||||
|
Desc
|
||||||
|
).
|
||||||
|
|
||||||
page_schema(Ref) when is_atom(Ref) ->
|
page_schema(Ref) when is_atom(Ref) ->
|
||||||
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
|
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
|
||||||
|
|
@ -137,9 +149,11 @@ page_schema(Schema) ->
|
||||||
properties => #{
|
properties => #{
|
||||||
meta => #{
|
meta => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => properties([{page, integer},
|
properties => properties([
|
||||||
{limit, integer},
|
{page, integer},
|
||||||
{count, integer}])
|
{limit, integer},
|
||||||
|
{count, integer}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
data => #{
|
data => #{
|
||||||
type => array,
|
type => array,
|
||||||
|
|
@ -158,8 +172,10 @@ error_schema(Description) ->
|
||||||
error_schema(Description, Enum) ->
|
error_schema(Description, Enum) ->
|
||||||
Schema = #{
|
Schema = #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => properties([{code, string, <<>>, Enum},
|
properties => properties([
|
||||||
{message, string}])
|
{code, string, <<>>, Enum},
|
||||||
|
{message, string}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
json_content_schema(Schema, Description).
|
json_content_schema(Schema, Description).
|
||||||
|
|
||||||
|
|
@ -171,20 +187,28 @@ batch_schema(DefName) when is_binary(DefName) ->
|
||||||
properties => #{
|
properties => #{
|
||||||
success => #{
|
success => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
description => <<"Success count">>},
|
description => <<"Success count">>
|
||||||
|
},
|
||||||
failed => #{
|
failed => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
description => <<"Failed count">>},
|
description => <<"Failed count">>
|
||||||
|
},
|
||||||
detail => #{
|
detail => #{
|
||||||
type => array,
|
type => array,
|
||||||
description => <<"Failed object & reason">>,
|
description => <<"Failed object & reason">>,
|
||||||
items => #{
|
items => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties =>
|
properties =>
|
||||||
#{
|
#{
|
||||||
data => minirest:ref(DefName),
|
data => minirest:ref(DefName),
|
||||||
reason => #{
|
reason => #{
|
||||||
type => <<"string">>}}}}}},
|
type => <<"string">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
json_content_schema(Schema).
|
json_content_schema(Schema).
|
||||||
|
|
||||||
json_content_schema(Schema) when is_map(Schema) ->
|
json_content_schema(Schema) when is_map(Schema) ->
|
||||||
|
|
@ -214,7 +238,7 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) ->
|
||||||
case erlang:apply(Module, Function, Args) of
|
case erlang:apply(Module, Function, Args) of
|
||||||
ok ->
|
ok ->
|
||||||
batch_operation(Module, Function, ArgsList, Failed);
|
batch_operation(Module, Function, ArgsList, Failed);
|
||||||
{error ,Reason} ->
|
{error, Reason} ->
|
||||||
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
|
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -227,52 +251,77 @@ properties([Key | Props], Acc) when is_atom(Key) ->
|
||||||
properties([{Key, Type} | Props], Acc) ->
|
properties([{Key, Type} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type}, Acc));
|
properties(Props, maps:put(Key, #{type => Type}, Acc));
|
||||||
properties([{Key, object, Props1} | Props], Acc) ->
|
properties([{Key, object, Props1} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => object,
|
properties(
|
||||||
properties => properties(Props1)}, Acc));
|
Props,
|
||||||
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => properties(Props1)
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, {array, object}, Props1} | Props], Acc) ->
|
properties([{Key, {array, object}, Props1} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => array,
|
properties(
|
||||||
items => #{type => object,
|
Props,
|
||||||
properties => properties(Props1)
|
maps:put(
|
||||||
}}, Acc));
|
Key,
|
||||||
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => properties(Props1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
|
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => array,
|
properties(
|
||||||
items => #{type => Type},
|
Props,
|
||||||
description => Desc}, Acc));
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{type => Type},
|
||||||
|
description => Desc
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, Type, Desc} | Props], Acc) ->
|
properties([{Key, Type, Desc} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
|
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
|
||||||
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
|
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type,
|
properties(
|
||||||
description => Desc,
|
Props,
|
||||||
enum => Enum}, Acc)).
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => Type,
|
||||||
|
description => Desc,
|
||||||
|
enum => Enum
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
).
|
||||||
page_params() ->
|
page_params() ->
|
||||||
[#{
|
[
|
||||||
name => page,
|
#{
|
||||||
in => query,
|
name => page,
|
||||||
description => <<"Page">>,
|
in => query,
|
||||||
schema => #{type => integer, default => 1}
|
description => <<"Page">>,
|
||||||
},
|
schema => #{type => integer, default => 1}
|
||||||
#{
|
},
|
||||||
name => limit,
|
#{
|
||||||
in => query,
|
name => limit,
|
||||||
description => <<"Page size">>,
|
in => query,
|
||||||
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
description => <<"Page size">>,
|
||||||
}].
|
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
bad_request() ->
|
bad_request() ->
|
||||||
bad_request(<<"Bad Request">>).
|
bad_request(<<"Bad Request">>).
|
||||||
bad_request(Desc) ->
|
bad_request(Desc) ->
|
||||||
object_schema(properties([{message, string}, {code, string}]), Desc).
|
object_schema(properties([{message, string}, {code, string}]), Desc).
|
||||||
|
|
||||||
%%%==============================================================================================
|
|
||||||
%% Response util
|
|
||||||
|
|
||||||
generate_response(QueryResult) ->
|
|
||||||
case QueryResult of
|
|
||||||
{error, page_limit_invalid} ->
|
|
||||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
|
||||||
{error, Node, {badrpc, R}} ->
|
|
||||||
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
|
|
||||||
{500, #{code => <<"NODE_DOWN">>, message => Message}};
|
|
||||||
Response ->
|
|
||||||
{200, Response}
|
|
||||||
end.
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,13 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
, get_plugins/0
|
introduced_in/0,
|
||||||
, install_package/2
|
get_plugins/0,
|
||||||
, describe_package/1
|
install_package/2,
|
||||||
, delete_package/1
|
describe_package/1,
|
||||||
, ensure_action/2
|
delete_package/1,
|
||||||
|
ensure_action/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,13 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
|
introduced_in/0,
|
||||||
|
|
||||||
, trace_file/2
|
trace_file/2,
|
||||||
, get_trace_size/1
|
get_trace_size/1,
|
||||||
, read_trace_file/4
|
read_trace_file/4
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
|
@ -31,21 +32,22 @@ introduced_in() ->
|
||||||
"5.0.0".
|
"5.0.0".
|
||||||
|
|
||||||
-spec get_trace_size([node()]) ->
|
-spec get_trace_size([node()]) ->
|
||||||
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
||||||
get_trace_size(Nodes) ->
|
get_trace_size(Nodes) ->
|
||||||
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
|
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
|
||||||
|
|
||||||
-spec trace_file([node()], file:name_all()) ->
|
-spec trace_file([node()], file:name_all()) ->
|
||||||
emqx_rpc:multicall_result(
|
emqx_rpc:multicall_result(
|
||||||
{ok, Node :: list(), Binary :: binary()} |
|
{ok, Node :: list(), Binary :: binary()}
|
||||||
{error, Node :: list(), Reason :: term()}).
|
| {error, Node :: list(), Reason :: term()}
|
||||||
|
).
|
||||||
trace_file(Nodes, File) ->
|
trace_file(Nodes, File) ->
|
||||||
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
|
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
|
||||||
|
|
||||||
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
|
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
|
||||||
{ok, binary()}
|
{ok, binary()}
|
||||||
| {error, _}
|
| {error, _}
|
||||||
| {eof, non_neg_integer()}
|
| {eof, non_neg_integer()}
|
||||||
| {badrpc, _}.
|
| {badrpc, _}.
|
||||||
read_trace_file(Node, Name, Position, Limit) ->
|
read_trace_file(Node, Name, Position, Limit) ->
|
||||||
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
|
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_mgmt_api_alarms_SUITE).
|
-module(emqx_mgmt_api_alarms_SUITE).
|
||||||
|
|
||||||
|
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
|
@ -55,8 +54,8 @@ get_alarms(AssertCount, Activated) ->
|
||||||
Headers = emqx_mgmt_api_test_util:auth_header_(),
|
Headers = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
|
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
|
||||||
Data = emqx_json:decode(Response, [return_maps]),
|
Data = emqx_json:decode(Response, [return_maps]),
|
||||||
Meta = maps:get(<<"meta">>, Data),
|
Meta = maps:get(<<"meta">>, Data),
|
||||||
Page = maps:get(<<"page">>, Meta),
|
Page = maps:get(<<"page">>, Meta),
|
||||||
Limit = maps:get(<<"limit">>, Meta),
|
Limit = maps:get(<<"limit">>, Meta),
|
||||||
Count = maps:get(<<"count">>, Meta),
|
Count = maps:get(<<"count">>, Meta),
|
||||||
?assertEqual(Page, 1),
|
?assertEqual(Page, 1),
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@
|
||||||
|
|
||||||
all() -> [{group, parallel}, {group, sequence}].
|
all() -> [{group, parallel}, {group, sequence}].
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
suite() -> [{timetrap, {minutes, 1}}].
|
||||||
groups() -> [
|
groups() ->
|
||||||
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
[
|
||||||
{sequence, [], [t_create_failed]}
|
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
||||||
].
|
{sequence, [], [t_create_failed]}
|
||||||
|
].
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
emqx_mgmt_api_test_util:init_suite(),
|
emqx_mgmt_api_test_util:init_suite(),
|
||||||
|
|
@ -37,15 +38,20 @@ end_per_suite(_) ->
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
Name = <<"EMQX-API-KEY-1">>,
|
Name = <<"EMQX-API-KEY-1">>,
|
||||||
{ok, Create} = create_app(Name),
|
{ok, Create} = create_app(Name),
|
||||||
?assertMatch(#{<<"api_key">> := _,
|
?assertMatch(
|
||||||
<<"api_secret">> := _,
|
#{
|
||||||
<<"created_at">> := _,
|
<<"api_key">> := _,
|
||||||
<<"desc">> := _,
|
<<"api_secret">> := _,
|
||||||
<<"enable">> := true,
|
<<"created_at">> := _,
|
||||||
<<"expired_at">> := _,
|
<<"desc">> := _,
|
||||||
<<"name">> := Name}, Create),
|
<<"enable">> := true,
|
||||||
|
<<"expired_at">> := _,
|
||||||
|
<<"name">> := Name
|
||||||
|
},
|
||||||
|
Create
|
||||||
|
),
|
||||||
{ok, List} = list_app(),
|
{ok, List} = list_app(),
|
||||||
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
||||||
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
|
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
|
||||||
{ok, App1} = read_app(Name),
|
{ok, App1} = read_app(Name),
|
||||||
?assertEqual(Name, maps:get(<<"name">>, App1)),
|
?assertEqual(Name, maps:get(<<"name">>, App1)),
|
||||||
|
|
@ -64,9 +70,12 @@ t_create_failed(_Config) ->
|
||||||
|
|
||||||
{ok, List} = list_app(),
|
{ok, List} = list_app(),
|
||||||
CreateNum = 30 - erlang:length(List),
|
CreateNum = 30 - erlang:length(List),
|
||||||
Names = lists:map(fun(Seq) ->
|
Names = lists:map(
|
||||||
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
fun(Seq) ->
|
||||||
end, lists:seq(1, CreateNum)),
|
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
||||||
|
end,
|
||||||
|
lists:seq(1, CreateNum)
|
||||||
|
),
|
||||||
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
|
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
|
||||||
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
|
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
|
||||||
|
|
||||||
|
|
@ -93,7 +102,8 @@ t_update(_Config) ->
|
||||||
?assertEqual(Name, maps:get(<<"name">>, Update1)),
|
?assertEqual(Name, maps:get(<<"name">>, Update1)),
|
||||||
?assertEqual(false, maps:get(<<"enable">>, Update1)),
|
?assertEqual(false, maps:get(<<"enable">>, Update1)),
|
||||||
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
|
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
|
||||||
?assertEqual(calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
?assertEqual(
|
||||||
|
calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
||||||
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
|
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
|
||||||
),
|
),
|
||||||
Unexpired1 = maps:without([expired_at], Change),
|
Unexpired1 = maps:without([expired_at], Change),
|
||||||
|
|
@ -117,10 +127,14 @@ t_delete(_Config) ->
|
||||||
t_authorize(_Config) ->
|
t_authorize(_Config) ->
|
||||||
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
|
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
|
||||||
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
|
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
|
||||||
BasicHeader = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
BasicHeader = emqx_common_test_http:auth_header(
|
||||||
binary_to_list(ApiSecret)),
|
binary_to_list(ApiKey),
|
||||||
SecretError = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
binary_to_list(ApiSecret)
|
||||||
binary_to_list(ApiKey)),
|
),
|
||||||
|
SecretError = emqx_common_test_http:auth_header(
|
||||||
|
binary_to_list(ApiKey),
|
||||||
|
binary_to_list(ApiKey)
|
||||||
|
),
|
||||||
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
|
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
|
||||||
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
|
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
|
||||||
|
|
||||||
|
|
@ -134,8 +148,10 @@ t_authorize(_Config) ->
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
|
||||||
|
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
?assertMatch(
|
||||||
update_app(Name, #{enable => false})),
|
{ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
||||||
|
update_app(Name, #{enable => false})
|
||||||
|
),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||||
|
|
||||||
Expired = #{
|
Expired = #{
|
||||||
|
|
@ -145,8 +161,10 @@ t_authorize(_Config) ->
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
|
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||||
UnExpired = #{expired_at => undefined},
|
UnExpired = #{expired_at => undefined},
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
?assertMatch(
|
||||||
update_app(Name, UnExpired)),
|
{ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
||||||
|
update_app(Name, UnExpired)
|
||||||
|
),
|
||||||
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
|
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -159,7 +177,6 @@ t_create_unexpired_app(_Config) ->
|
||||||
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
|
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
list_app() ->
|
list_app() ->
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
|
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
|
||||||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,17 @@ t_create(_Config) ->
|
||||||
until => Until
|
until => Until
|
||||||
},
|
},
|
||||||
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
|
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
|
||||||
?assertEqual(#{<<"as">> => As,
|
?assertEqual(
|
||||||
<<"at">> => At,
|
#{
|
||||||
<<"by">> => By,
|
<<"as">> => As,
|
||||||
<<"reason">> => Reason,
|
<<"at">> => At,
|
||||||
<<"until">> => Until,
|
<<"by">> => By,
|
||||||
<<"who">> => ClientId
|
<<"reason">> => Reason,
|
||||||
}, ClientIdBannedRes),
|
<<"until">> => Until,
|
||||||
|
<<"who">> => ClientId
|
||||||
|
},
|
||||||
|
ClientIdBannedRes
|
||||||
|
),
|
||||||
PeerHost = <<"192.168.2.13">>,
|
PeerHost = <<"192.168.2.13">>,
|
||||||
PeerHostBanned = #{
|
PeerHostBanned = #{
|
||||||
as => <<"peerhost">>,
|
as => <<"peerhost">>,
|
||||||
|
|
@ -64,15 +68,19 @@ t_create(_Config) ->
|
||||||
until => Until
|
until => Until
|
||||||
},
|
},
|
||||||
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
|
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
|
||||||
?assertEqual(#{<<"as">> => <<"peerhost">>,
|
?assertEqual(
|
||||||
<<"at">> => At,
|
#{
|
||||||
<<"by">> => By,
|
<<"as">> => <<"peerhost">>,
|
||||||
<<"reason">> => Reason,
|
<<"at">> => At,
|
||||||
<<"until">> => Until,
|
<<"by">> => By,
|
||||||
<<"who">> => PeerHost
|
<<"reason">> => Reason,
|
||||||
}, PeerHostBannedRes),
|
<<"until">> => Until,
|
||||||
|
<<"who">> => PeerHost
|
||||||
|
},
|
||||||
|
PeerHostBannedRes
|
||||||
|
),
|
||||||
{ok, #{<<"data">> := List}} = list_banned(),
|
{ok, #{<<"data">> := List}} = list_banned(),
|
||||||
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
||||||
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -94,8 +102,10 @@ t_create_failed(_Config) ->
|
||||||
},
|
},
|
||||||
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
|
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
|
||||||
?assertEqual(BadRequest, create_banned(BadPeerHost)),
|
?assertEqual(BadRequest, create_banned(BadPeerHost)),
|
||||||
Expired = BadPeerHost#{until => emqx_banned:to_rfc3339(Now - 1),
|
Expired = BadPeerHost#{
|
||||||
who => <<"127.0.0.1">>},
|
until => emqx_banned:to_rfc3339(Now - 1),
|
||||||
|
who => <<"127.0.0.1">>
|
||||||
|
},
|
||||||
?assertEqual(BadRequest, create_banned(Expired)),
|
?assertEqual(BadRequest, create_banned(Expired)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -117,8 +127,10 @@ t_delete(_Config) ->
|
||||||
},
|
},
|
||||||
{ok, _} = create_banned(Banned),
|
{ok, _} = create_banned(Banned),
|
||||||
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
|
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
|
||||||
?assertMatch({error,{"HTTP/1.1",404,"Not Found"}},
|
?assertMatch(
|
||||||
delete_banned(binary_to_list(As), binary_to_list(Who))),
|
{error, {"HTTP/1.1", 404, "Not Found"}},
|
||||||
|
delete_banned(binary_to_list(As), binary_to_list(Who))
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
list_banned() ->
|
list_banned() ->
|
||||||
|
|
|
||||||
|
|
@ -44,20 +44,20 @@ t_clients(_) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
|
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||||
{ok, _} = emqtt:connect(C2),
|
{ok, _} = emqtt:connect(C2),
|
||||||
|
|
||||||
timer:sleep(300),
|
timer:sleep(300),
|
||||||
|
|
||||||
%% get /clients
|
%% get /clients
|
||||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||||
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
||||||
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
|
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
|
||||||
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
||||||
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
||||||
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
||||||
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
||||||
?assertEqual(ClientsPage, 1),
|
?assertEqual(ClientsPage, 1),
|
||||||
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
|
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
|
||||||
?assertEqual(ClientsCount, 2),
|
?assertEqual(ClientsCount, 2),
|
||||||
|
|
@ -76,29 +76,49 @@ t_clients(_) ->
|
||||||
AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path),
|
AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path),
|
||||||
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
|
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
|
||||||
|
|
||||||
%% get /clients/:clientid/authz_cache should has no authz cache
|
%% get /clients/:clientid/authorization/cache should has no authz cache
|
||||||
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path(["clients",
|
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "authz_cache"]),
|
"clients",
|
||||||
|
binary_to_list(ClientId1),
|
||||||
|
"authorization",
|
||||||
|
"cache"
|
||||||
|
]),
|
||||||
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
|
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
|
||||||
?assertEqual("[]", Client1AuthzCache),
|
?assertEqual("[]", Client1AuthzCache),
|
||||||
|
|
||||||
%% post /clients/:clientid/subscribe
|
%% post /clients/:clientid/subscribe
|
||||||
SubscribeBody = #{topic => Topic, qos => Qos},
|
SubscribeBody = #{topic => Topic, qos => Qos},
|
||||||
SubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
SubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "subscribe"]),
|
"clients",
|
||||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, SubscribePath,
|
binary_to_list(ClientId1),
|
||||||
"", AuthHeader, SubscribeBody),
|
"subscribe"
|
||||||
|
]),
|
||||||
|
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||||
|
post,
|
||||||
|
SubscribePath,
|
||||||
|
"",
|
||||||
|
AuthHeader,
|
||||||
|
SubscribeBody
|
||||||
|
),
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
|
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
|
||||||
?assertEqual(AfterSubTopic, Topic),
|
?assertEqual(AfterSubTopic, Topic),
|
||||||
?assertEqual(AfterSubQos, Qos),
|
?assertEqual(AfterSubQos, Qos),
|
||||||
|
|
||||||
%% post /clients/:clientid/unsubscribe
|
%% post /clients/:clientid/unsubscribe
|
||||||
UnSubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
UnSubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "unsubscribe"]),
|
"clients",
|
||||||
|
binary_to_list(ClientId1),
|
||||||
|
"unsubscribe"
|
||||||
|
]),
|
||||||
UnSubscribeBody = #{topic => Topic},
|
UnSubscribeBody = #{topic => Topic},
|
||||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, UnSubscribePath,
|
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||||
"", AuthHeader, UnSubscribeBody),
|
post,
|
||||||
|
UnSubscribePath,
|
||||||
|
"",
|
||||||
|
AuthHeader,
|
||||||
|
UnSubscribeBody
|
||||||
|
),
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
|
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
|
||||||
|
|
||||||
|
|
@ -118,44 +138,58 @@ t_query_clients_with_time(_) ->
|
||||||
ClientId2 = <<"client2">>,
|
ClientId2 = <<"client2">>,
|
||||||
|
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||||
{ok, _} = emqtt:connect(C2),
|
{ok, _} = emqtt:connect(C2),
|
||||||
|
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
|
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||||
%% get /clients with time(rfc3339)
|
%% get /clients with time(rfc3339)
|
||||||
NowTimeStampInt = erlang:system_time(millisecond),
|
NowTimeStampInt = erlang:system_time(millisecond),
|
||||||
%% Do not uri_encode `=` to `%3D`
|
%% Do not uri_encode `=` to `%3D`
|
||||||
Rfc3339String = emqx_http_lib:uri_encode(binary:bin_to_list(
|
Rfc3339String = emqx_http_lib:uri_encode(
|
||||||
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt))),
|
binary:bin_to_list(
|
||||||
|
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt)
|
||||||
|
)
|
||||||
|
),
|
||||||
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
|
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
|
||||||
|
|
||||||
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
||||||
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
||||||
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
||||||
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
||||||
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
||||||
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
||||||
|
|
||||||
RequestResults =
|
RequestResults =
|
||||||
[emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
[
|
||||||
|| Param <- LteParamRfc3339 ++ LteParamStamp
|
emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
||||||
++ GteParamRfc3339 ++ GteParamStamp],
|
|| Param <-
|
||||||
DecodedResults = [emqx_json:decode(Response, [return_maps])
|
LteParamRfc3339 ++ LteParamStamp ++
|
||||||
|| {ok, Response} <- RequestResults],
|
GteParamRfc3339 ++ GteParamStamp
|
||||||
|
],
|
||||||
|
DecodedResults = [
|
||||||
|
emqx_json:decode(Response, [return_maps])
|
||||||
|
|| {ok, Response} <- RequestResults
|
||||||
|
],
|
||||||
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
|
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
|
||||||
%% EachData :: list()
|
%% EachData :: list()
|
||||||
[?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
[
|
||||||
|
?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
||||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||||
#{<<"created_at">> := CreatedAt} <- EachData],
|
#{<<"created_at">> := CreatedAt} <- EachData
|
||||||
[?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
],
|
||||||
|
[
|
||||||
|
?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
||||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||||
#{<<"connected_at">> := ConnectedAt} <- EachData],
|
#{<<"connected_at">> := ConnectedAt} <- EachData
|
||||||
[?assertEqual(EachData, [])
|
],
|
||||||
|| #{<<"data">> := EachData} <- GteResponseDecodeds],
|
[
|
||||||
|
?assertEqual(EachData, [])
|
||||||
|
|| #{<<"data">> := EachData} <- GteResponseDecodeds
|
||||||
|
],
|
||||||
|
|
||||||
%% testcase cleanup, kickout client1 and client2
|
%% testcase cleanup, kickout client1 and client2
|
||||||
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
|
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
|
||||||
|
|
@ -169,7 +203,7 @@ t_keepalive(_Config) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
|
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
|
||||||
Body = #{interval => 11},
|
Body = #{interval => 11},
|
||||||
{error,{"HTTP/1.1",404,"Not Found"}} =
|
{error, {"HTTP/1.1", 404, "Not Found"}} =
|
||||||
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
|
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
|
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
|
|
@ -190,5 +224,6 @@ time_string_to_epoch(DateTime, Unit) when is_binary(DateTime) ->
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
calendar:rfc3339_to_system_time(
|
calendar:rfc3339_to_system_time(
|
||||||
binary_to_list(DateTime), [{unit, Unit}])
|
binary_to_list(DateTime), [{unit, Unit}]
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,13 @@ end_per_suite(_) ->
|
||||||
|
|
||||||
t_get(_Config) ->
|
t_get(_Config) ->
|
||||||
{ok, Configs} = get_configs(),
|
{ok, Configs} = get_configs(),
|
||||||
maps:map(fun(Name, Value) ->
|
maps:map(
|
||||||
{ok, Config} = get_config(Name),
|
fun(Name, Value) ->
|
||||||
?assertEqual(Value, Config)
|
{ok, Config} = get_config(Name),
|
||||||
end, maps:remove(<<"license">>, Configs)),
|
?assertEqual(Value, Config)
|
||||||
|
end,
|
||||||
|
maps:remove(<<"license">>, Configs)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
|
|
@ -50,8 +53,10 @@ t_update(_Config) ->
|
||||||
|
|
||||||
%% update failed
|
%% update failed
|
||||||
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
|
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}},
|
?assertMatch(
|
||||||
update_config(<<"sysmon">>, ErrorSysMon)),
|
{error, {"HTTP/1.1", 400, _}},
|
||||||
|
update_config(<<"sysmon">>, ErrorSysMon)
|
||||||
|
),
|
||||||
{ok, SysMon2} = get_config(<<"sysmon">>),
|
{ok, SysMon2} = get_config(<<"sysmon">>),
|
||||||
?assertEqual(SysMon1, SysMon2),
|
?assertEqual(SysMon1, SysMon2),
|
||||||
|
|
||||||
|
|
@ -101,8 +106,10 @@ t_global_zone(_Config) ->
|
||||||
{ok, Zones} = get_global_zone(),
|
{ok, Zones} = get_global_zone(),
|
||||||
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
|
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
|
||||||
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
|
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
|
||||||
?assertEqual(emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
?assertEqual(
|
||||||
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)),
|
emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
||||||
|
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)
|
||||||
|
),
|
||||||
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
|
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
|
||||||
{ok, #{}} = update_global_zone(NewZones),
|
{ok, #{}} = update_global_zone(NewZones),
|
||||||
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
|
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
|
||||||
|
|
@ -133,7 +140,8 @@ get_config(Name) ->
|
||||||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||||
{ok, Res} ->
|
{ok, Res} ->
|
||||||
{ok, emqx_json:decode(Res, [return_maps])};
|
{ok, emqx_json:decode(Res, [return_maps])};
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_configs() ->
|
get_configs() ->
|
||||||
|
|
@ -153,8 +161,11 @@ update_config(Name, Change) ->
|
||||||
|
|
||||||
reset_config(Name, Key) ->
|
reset_config(Name, Key) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Path = binary_to_list(iolist_to_binary(
|
Path = binary_to_list(
|
||||||
emqx_mgmt_api_test_util:api_path(["configs_reset", Name]))),
|
iolist_to_binary(
|
||||||
|
emqx_mgmt_api_test_util:api_path(["configs_reset", Name])
|
||||||
|
)
|
||||||
|
),
|
||||||
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
|
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
|
||||||
{ok, []} -> ok;
|
{ok, []} -> ok;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ end_per_suite(_) ->
|
||||||
t_list_listeners(_) ->
|
t_list_listeners(_) ->
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["listeners"]),
|
Path = emqx_mgmt_api_test_util:api_path(["listeners"]),
|
||||||
Res = request(get, Path, [], []),
|
Res = request(get, Path, [], []),
|
||||||
Expect = emqx_mgmt_api_listeners:do_list_listeners(),
|
#{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(),
|
||||||
?assertEqual(emqx_json:encode([Expect]), emqx_json:encode(Res)),
|
?assertEqual(length(Expect), length(Res)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_crud_listeners_by_id(_) ->
|
t_crud_listeners_by_id(_) ->
|
||||||
|
|
@ -44,19 +44,18 @@ t_crud_listeners_by_id(_) ->
|
||||||
NewListenerId = <<"tcp:new">>,
|
NewListenerId = <<"tcp:new">>,
|
||||||
TcpPath = emqx_mgmt_api_test_util:api_path(["listeners", TcpListenerId]),
|
TcpPath = emqx_mgmt_api_test_util:api_path(["listeners", TcpListenerId]),
|
||||||
NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]),
|
NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]),
|
||||||
[#{<<"listeners">> := [TcpListener], <<"node">> := Node}] = request(get, TcpPath, [], []),
|
TcpListener = request(get, TcpPath, [], []),
|
||||||
?assertEqual(atom_to_binary(node()), Node),
|
|
||||||
|
|
||||||
%% create
|
%% create
|
||||||
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
||||||
?assertMatch([#{<<"listeners">> := []}], request(get, NewPath, [], [])),
|
?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])),
|
||||||
NewConf = TcpListener#{
|
NewConf = TcpListener#{
|
||||||
<<"id">> => NewListenerId,
|
<<"id">> => NewListenerId,
|
||||||
<<"bind">> => <<"0.0.0.0:2883">>
|
<<"bind">> => <<"0.0.0.0:2883">>
|
||||||
},
|
},
|
||||||
[#{<<"listeners">> := [Create]}] = request(put, NewPath, [], NewConf),
|
Create = request(post, NewPath, [], NewConf),
|
||||||
?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))),
|
?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))),
|
||||||
[#{<<"listeners">> := [Get1]}] = request(get, NewPath, [], []),
|
Get1 = request(get, NewPath, [], []),
|
||||||
?assertMatch(Create, Get1),
|
?assertMatch(Create, Get1),
|
||||||
?assert(is_running(NewListenerId)),
|
?assert(is_running(NewListenerId)),
|
||||||
|
|
||||||
|
|
@ -67,64 +66,21 @@ t_crud_listeners_by_id(_) ->
|
||||||
<<"id">> => BadId,
|
<<"id">> => BadId,
|
||||||
<<"bind">> => <<"0.0.0.0:2883">>
|
<<"bind">> => <<"0.0.0.0:2883">>
|
||||||
},
|
},
|
||||||
?assertEqual({error, {"HTTP/1.1", 400, "Bad Request"}}, request(put, BadPath, [], BadConf)),
|
?assertMatch({error, {"HTTP/1.1", 400, _}}, request(post, BadPath, [], BadConf)),
|
||||||
|
|
||||||
%% update
|
%% update
|
||||||
#{<<"acceptors">> := Acceptors} = Create,
|
#{<<"acceptors">> := Acceptors} = Create,
|
||||||
Acceptors1 = Acceptors + 10,
|
Acceptors1 = Acceptors + 10,
|
||||||
[#{<<"listeners">> := [Update]}] =
|
Update =
|
||||||
request(put, NewPath, [], Create#{<<"acceptors">> => Acceptors1}),
|
request(put, NewPath, [], Create#{<<"acceptors">> => Acceptors1}),
|
||||||
?assertMatch(#{<<"acceptors">> := Acceptors1}, Update),
|
?assertMatch(#{<<"acceptors">> := Acceptors1}, Update),
|
||||||
[#{<<"listeners">> := [Get2]}] = request(get, NewPath, [], []),
|
|
||||||
?assertMatch(#{<<"acceptors">> := Acceptors1}, Get2),
|
|
||||||
|
|
||||||
%% delete
|
|
||||||
?assertEqual([], delete(NewPath)),
|
|
||||||
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
|
||||||
?assertMatch([#{<<"listeners">> := []}], request(get, NewPath, [], [])),
|
|
||||||
?assertEqual([], delete(NewPath)),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
t_list_listeners_on_node(_) ->
|
|
||||||
Node = atom_to_list(node()),
|
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners"]),
|
|
||||||
Listeners = request(get, Path, [], []),
|
|
||||||
#{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(),
|
|
||||||
?assertEqual(emqx_json:encode(Expect), emqx_json:encode(Listeners)),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
t_crud_listener_by_id_on_node(_) ->
|
|
||||||
TcpListenerId = <<"tcp:default">>,
|
|
||||||
NewListenerId = <<"tcp:new1">>,
|
|
||||||
Node = atom_to_list(node()),
|
|
||||||
TcpPath = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners", TcpListenerId]),
|
|
||||||
NewPath = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners", NewListenerId]),
|
|
||||||
TcpListener = request(get, TcpPath, [], []),
|
|
||||||
|
|
||||||
%% create
|
|
||||||
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
|
||||||
?assertMatch({error,{"HTTP/1.1", 404, "Not Found"}}, request(get, NewPath, [], [])),
|
|
||||||
Create = request(put, NewPath, [], TcpListener#{
|
|
||||||
<<"id">> => NewListenerId,
|
|
||||||
<<"bind">> => <<"0.0.0.0:3883">>
|
|
||||||
}),
|
|
||||||
?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))),
|
|
||||||
Get1 = request(get, NewPath, [], []),
|
|
||||||
?assertMatch(Create, Get1),
|
|
||||||
?assert(is_running(NewListenerId)),
|
|
||||||
|
|
||||||
%% update
|
|
||||||
#{<<"acceptors">> := Acceptors} = Create,
|
|
||||||
Acceptors1 = Acceptors + 10,
|
|
||||||
Update = request(put, NewPath, [], Create#{<<"acceptors">> => Acceptors1}),
|
|
||||||
?assertMatch(#{<<"acceptors">> := Acceptors1}, Update),
|
|
||||||
Get2 = request(get, NewPath, [], []),
|
Get2 = request(get, NewPath, [], []),
|
||||||
?assertMatch(#{<<"acceptors">> := Acceptors1}, Get2),
|
?assertMatch(#{<<"acceptors">> := Acceptors1}, Get2),
|
||||||
|
|
||||||
%% delete
|
%% delete
|
||||||
?assertEqual([], delete(NewPath)),
|
?assertEqual([], delete(NewPath)),
|
||||||
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
?assertEqual({error, not_found}, is_running(NewListenerId)),
|
||||||
?assertMatch({error, {"HTTP/1.1", 404, "Not Found"}}, request(get, NewPath, [], [])),
|
?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])),
|
||||||
?assertEqual([], delete(NewPath)),
|
?assertEqual([], delete(NewPath)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -139,8 +95,8 @@ action_listener(ID, Action, Running) ->
|
||||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, Path),
|
{ok, _} = emqx_mgmt_api_test_util:request_api(post, Path),
|
||||||
timer:sleep(500),
|
timer:sleep(500),
|
||||||
GetPath = emqx_mgmt_api_test_util:api_path(["listeners", ID]),
|
GetPath = emqx_mgmt_api_test_util:api_path(["listeners", ID]),
|
||||||
[#{<<"listeners">> := Listeners}] = request(get, GetPath, [], []),
|
Listener = request(get, GetPath, [], []),
|
||||||
[listener_stats(Listener, Running) || Listener <- Listeners].
|
listener_stats(Listener, Running).
|
||||||
|
|
||||||
request(Method, Url, QueryParams, Body) ->
|
request(Method, Url, QueryParams, Body) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
|
|
|
||||||
|
|
@ -40,16 +40,17 @@ t_single_node_metrics_api(_) ->
|
||||||
{ok, MetricsResponse} = request_helper("metrics"),
|
{ok, MetricsResponse} = request_helper("metrics"),
|
||||||
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
|
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||||
LocalNodeMetrics = maps:from_list(
|
LocalNodeMetrics = maps:from_list(
|
||||||
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]),
|
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]
|
||||||
|
),
|
||||||
match_helper(LocalNodeMetrics, MetricsFromAPI).
|
match_helper(LocalNodeMetrics, MetricsFromAPI).
|
||||||
|
|
||||||
match_helper(SystemMetrics, MetricsFromAPI) ->
|
match_helper(SystemMetrics, MetricsFromAPI) ->
|
||||||
length_equal(SystemMetrics, MetricsFromAPI),
|
length_equal(SystemMetrics, MetricsFromAPI),
|
||||||
Fun =
|
Fun =
|
||||||
fun (Key, {SysMetrics, APIMetrics}) ->
|
fun(Key, {SysMetrics, APIMetrics}) ->
|
||||||
Value = maps:get(Key, SysMetrics),
|
Value = maps:get(Key, SysMetrics),
|
||||||
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
||||||
{Value, {SysMetrics, APIMetrics}}
|
{Value, {SysMetrics, APIMetrics}}
|
||||||
end,
|
end,
|
||||||
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
|
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,19 +67,21 @@ t_nodes_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
||||||
t_log_path(_) ->
|
t_log_path(_) ->
|
||||||
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
|
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
|
||||||
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
|
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
|
||||||
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
|
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
<<"emqx-test.log">>,
|
<<"emqx-test.log">>,
|
||||||
filename:basename(Path)).
|
filename:basename(Path)
|
||||||
|
).
|
||||||
|
|
||||||
t_node_stats_api(_) ->
|
t_node_stats_api(_) ->
|
||||||
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
|
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
|
||||||
SystemStats= emqx_mgmt:get_stats(),
|
SystemStats = emqx_mgmt:get_stats(),
|
||||||
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
|
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
|
||||||
Stats = emqx_json:decode(StatsResponse, [return_maps]),
|
Stats = emqx_json:decode(StatsResponse, [return_maps]),
|
||||||
Fun =
|
Fun =
|
||||||
|
|
@ -91,12 +93,13 @@ t_node_stats_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
||||||
t_node_metrics_api(_) ->
|
t_node_metrics_api(_) ->
|
||||||
MetricsPath =
|
MetricsPath =
|
||||||
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
|
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
|
||||||
SystemMetrics= emqx_mgmt:get_metrics(),
|
SystemMetrics = emqx_mgmt:get_metrics(),
|
||||||
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
|
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
|
||||||
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
|
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||||
Fun =
|
Fun =
|
||||||
|
|
@ -108,4 +111,5 @@ t_node_metrics_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -56,17 +56,35 @@ todo_t_plugins(Config) ->
|
||||||
ok = emqx_plugins:delete_package(NameVsn),
|
ok = emqx_plugins:delete_package(NameVsn),
|
||||||
ok = install_plugin(PackagePath),
|
ok = install_plugin(PackagePath),
|
||||||
{ok, StopRes} = describe_plugins(NameVsn),
|
{ok, StopRes} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StopRes
|
||||||
|
),
|
||||||
{ok, StopRes1} = update_plugin(NameVsn, "start"),
|
{ok, StopRes1} = update_plugin(NameVsn, "start"),
|
||||||
?assertEqual([], StopRes1),
|
?assertEqual([], StopRes1),
|
||||||
{ok, StartRes} = describe_plugins(NameVsn),
|
{ok, StartRes} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}]}, StartRes),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StartRes
|
||||||
|
),
|
||||||
{ok, []} = update_plugin(NameVsn, "stop"),
|
{ok, []} = update_plugin(NameVsn, "stop"),
|
||||||
{ok, StopRes2} = describe_plugins(NameVsn),
|
{ok, StopRes2} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes2),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StopRes2
|
||||||
|
),
|
||||||
{ok, []} = uninstall_plugin(NameVsn),
|
{ok, []} = uninstall_plugin(NameVsn),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -87,8 +105,16 @@ describe_plugins(Name) ->
|
||||||
install_plugin(FilePath) ->
|
install_plugin(FilePath) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
||||||
case emqx_mgmt_api_test_util:upload_request(Path, FilePath, "plugin",
|
case
|
||||||
<<"application/gzip">>, [], Token) of
|
emqx_mgmt_api_test_util:upload_request(
|
||||||
|
Path,
|
||||||
|
FilePath,
|
||||||
|
"plugin",
|
||||||
|
<<"application/gzip">>,
|
||||||
|
[],
|
||||||
|
Token
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
|
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
@ -109,7 +135,6 @@ uninstall_plugin(Name) ->
|
||||||
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
|
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
|
||||||
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
|
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
|
||||||
|
|
||||||
|
|
||||||
build_demo_plugin_package(Dir) ->
|
build_demo_plugin_package(Dir) ->
|
||||||
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
|
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
|
||||||
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
|
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ end_per_suite(_) ->
|
||||||
emqx_mgmt_api_test_util:end_suite().
|
emqx_mgmt_api_test_util:end_suite().
|
||||||
|
|
||||||
t_publish_api(_) ->
|
t_publish_api(_) ->
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||||
|
|
@ -50,14 +52,16 @@ t_publish_api(_) ->
|
||||||
emqtt:disconnect(Client).
|
emqtt:disconnect(Client).
|
||||||
|
|
||||||
t_publish_bulk_api(_) ->
|
t_publish_bulk_api(_) ->
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||||
Payload = <<"hello">>,
|
Payload = <<"hello">>,
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
|
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
|
||||||
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Body =[#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
Body = [#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
||||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
|
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
|
||||||
ResponseMap = emqx_json:decode(Response, [return_maps]),
|
ResponseMap = emqx_json:decode(Response, [return_maps]),
|
||||||
?assertEqual(2, erlang:length(ResponseMap)),
|
?assertEqual(2, erlang:length(ResponseMap)),
|
||||||
|
|
@ -68,12 +72,12 @@ t_publish_bulk_api(_) ->
|
||||||
receive_assert(Topic, Qos, Payload) ->
|
receive_assert(Topic, Qos, Payload) ->
|
||||||
receive
|
receive
|
||||||
{publish, Message} ->
|
{publish, Message} ->
|
||||||
ReceiveTopic = maps:get(topic, Message),
|
ReceiveTopic = maps:get(topic, Message),
|
||||||
ReceiveQos = maps:get(qos, Message),
|
ReceiveQos = maps:get(qos, Message),
|
||||||
ReceivePayload = maps:get(payload, Message),
|
ReceivePayload = maps:get(payload, Message),
|
||||||
?assertEqual(ReceiveTopic , Topic),
|
?assertEqual(ReceiveTopic, Topic),
|
||||||
?assertEqual(ReceiveQos , Qos),
|
?assertEqual(ReceiveQos, Qos),
|
||||||
?assertEqual(ReceivePayload , Payload),
|
?assertEqual(ReceivePayload, Payload),
|
||||||
ok
|
ok
|
||||||
after 5000 ->
|
after 5000 ->
|
||||||
timeout
|
timeout
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ t_stats_api(_) ->
|
||||||
SystemStats1 = emqx_mgmt:get_stats(),
|
SystemStats1 = emqx_mgmt:get_stats(),
|
||||||
Fun1 =
|
Fun1 =
|
||||||
fun(Key) ->
|
fun(Key) ->
|
||||||
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
||||||
end,
|
end,
|
||||||
lists:foreach(Fun1, maps:keys(SystemStats1)),
|
lists:foreach(Fun1, maps:keys(SystemStats1)),
|
||||||
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
|
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,13 @@ t_subscription_api(_) ->
|
||||||
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
|
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
|
||||||
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
|
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
|
||||||
end,
|
end,
|
||||||
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
||||||
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
|
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
|
||||||
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
|
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
|
||||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
|
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
|
||||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
|
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
|
||||||
|
|
||||||
QS = uri_string:compose_query([
|
QS = uri_string:compose_query([
|
||||||
{"clientid", ?CLIENTID},
|
{"clientid", ?CLIENTID},
|
||||||
{"topic", ?TOPIC2_TOPIC_ONLY},
|
{"topic", ?TOPIC2_TOPIC_ONLY},
|
||||||
{"node", atom_to_list(node())},
|
{"node", atom_to_list(node())},
|
||||||
|
|
@ -83,11 +83,11 @@ t_subscription_api(_) ->
|
||||||
?assertEqual(length(SubscriptionsList2), 1),
|
?assertEqual(length(SubscriptionsList2), 1),
|
||||||
|
|
||||||
MatchQs = uri_string:compose_query([
|
MatchQs = uri_string:compose_query([
|
||||||
{"clientid", ?CLIENTID},
|
{"clientid", ?CLIENTID},
|
||||||
{"node", atom_to_list(node())},
|
{"node", atom_to_list(node())},
|
||||||
{"qos", "0"},
|
{"qos", "0"},
|
||||||
{"match_topic", "t/#"}
|
{"match_topic", "t/#"}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
|
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
|
||||||
MatchData = emqx_json:decode(MatchRes, [return_maps]),
|
MatchData = emqx_json:decode(MatchRes, [return_maps]),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ init_suite(Apps) ->
|
||||||
application:load(emqx_management),
|
application:load(emqx_management),
|
||||||
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
|
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
|
||||||
|
|
||||||
|
|
||||||
end_suite() ->
|
end_suite() ->
|
||||||
end_suite([]).
|
end_suite([]).
|
||||||
|
|
||||||
|
|
@ -39,15 +38,7 @@ end_suite(Apps) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_App) ->
|
set_special_configs(_App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -61,36 +52,44 @@ request_api(Method, Url, AuthOrHeaders) ->
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders) ->
|
request_api(Method, Url, QueryParams, AuthOrHeaders) ->
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, []).
|
request_api(Method, Url, QueryParams, AuthOrHeaders, []).
|
||||||
|
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, [])
|
request_api(Method, Url, QueryParams, AuthOrHeaders, []) when
|
||||||
when (Method =:= options) orelse
|
(Method =:= options) orelse
|
||||||
(Method =:= get) orelse
|
(Method =:= get) orelse
|
||||||
(Method =:= put) orelse
|
(Method =:= put) orelse
|
||||||
(Method =:= head) orelse
|
(Method =:= head) orelse
|
||||||
(Method =:= delete) orelse
|
(Method =:= delete) orelse
|
||||||
(Method =:= trace) ->
|
(Method =:= trace)
|
||||||
NewUrl = case QueryParams of
|
->
|
||||||
"" -> Url;
|
NewUrl =
|
||||||
_ -> Url ++ "?" ++ QueryParams
|
case QueryParams of
|
||||||
end,
|
"" -> Url;
|
||||||
|
_ -> Url ++ "?" ++ QueryParams
|
||||||
|
end,
|
||||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
|
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, Body)
|
request_api(Method, Url, QueryParams, AuthOrHeaders, Body) when
|
||||||
when (Method =:= post) orelse
|
(Method =:= post) orelse
|
||||||
(Method =:= patch) orelse
|
(Method =:= patch) orelse
|
||||||
(Method =:= put) orelse
|
(Method =:= put) orelse
|
||||||
(Method =:= delete) ->
|
(Method =:= delete)
|
||||||
NewUrl = case QueryParams of
|
->
|
||||||
"" -> Url;
|
NewUrl =
|
||||||
_ -> Url ++ "?" ++ QueryParams
|
case QueryParams of
|
||||||
end,
|
"" -> Url;
|
||||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}).
|
_ -> Url ++ "?" ++ QueryParams
|
||||||
|
end,
|
||||||
|
do_request_api(
|
||||||
|
Method,
|
||||||
|
{NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}
|
||||||
|
).
|
||||||
|
|
||||||
do_request_api(Method, Request)->
|
do_request_api(Method, Request) ->
|
||||||
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
||||||
case httpc:request(Method, Request, [], []) of
|
case httpc:request(Method, Request, [], []) of
|
||||||
{error, socket_closed_remotely} ->
|
{error, socket_closed_remotely} ->
|
||||||
{error, socket_closed_remotely};
|
{error, socket_closed_remotely};
|
||||||
{ok, {{"HTTP/1.1", Code, _}, _, Return} }
|
{ok, {{"HTTP/1.1", Code, _}, _, Return}} when
|
||||||
when Code >= 200 andalso Code =< 299 ->
|
Code >= 200 andalso Code =< 299
|
||||||
|
->
|
||||||
{ok, Return};
|
{ok, Return};
|
||||||
{ok, {Reason, _, _} = Error} ->
|
{ok, {Reason, _, _} = Error} ->
|
||||||
ct:pal("error: ~p~n", [Error]),
|
ct:pal("error: ~p~n", [Error]),
|
||||||
|
|
@ -105,11 +104,10 @@ auth_header_() ->
|
||||||
|
|
||||||
build_http_header(X) when is_list(X) ->
|
build_http_header(X) when is_list(X) ->
|
||||||
X;
|
X;
|
||||||
|
|
||||||
build_http_header(X) ->
|
build_http_header(X) ->
|
||||||
[X].
|
[X].
|
||||||
|
|
||||||
api_path(Parts)->
|
api_path(Parts) ->
|
||||||
?SERVER ++ filename:join([?BASE_PATH | Parts]).
|
?SERVER ++ filename:join([?BASE_PATH | Parts]).
|
||||||
|
|
||||||
%% Usage:
|
%% Usage:
|
||||||
|
|
@ -125,20 +123,27 @@ api_path(Parts)->
|
||||||
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
|
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
|
||||||
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
|
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
|
||||||
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||||
{ok, binary()} | {error, list()} when
|
{ok, binary()} | {error, list()}
|
||||||
URL:: binary(),
|
when
|
||||||
FilePath:: binary(),
|
URL :: binary(),
|
||||||
Name:: binary(),
|
FilePath :: binary(),
|
||||||
MimeType:: binary(),
|
Name :: binary(),
|
||||||
RequestData:: list(),
|
MimeType :: binary(),
|
||||||
AuthorizationToken:: binary().
|
RequestData :: list(),
|
||||||
|
AuthorizationToken :: binary().
|
||||||
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||||
Method = post,
|
Method = post,
|
||||||
Filename = filename:basename(FilePath),
|
Filename = filename:basename(FilePath),
|
||||||
{ok, Data} = file:read_file(FilePath),
|
{ok, Data} = file:read_file(FilePath),
|
||||||
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
|
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
|
||||||
RequestBody = format_multipart_formdata(Data, RequestData, Name,
|
RequestBody = format_multipart_formdata(
|
||||||
[Filename], MimeType, Boundary),
|
Data,
|
||||||
|
RequestData,
|
||||||
|
Name,
|
||||||
|
[Filename],
|
||||||
|
MimeType,
|
||||||
|
Boundary
|
||||||
|
),
|
||||||
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
|
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
|
||||||
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
|
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
|
||||||
Headers = [
|
Headers = [
|
||||||
|
|
@ -154,34 +159,56 @@ upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) -
|
||||||
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
|
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
|
||||||
|
|
||||||
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||||
binary() when
|
binary()
|
||||||
Data:: binary(),
|
when
|
||||||
Params:: list(),
|
Data :: binary(),
|
||||||
Name:: binary(),
|
Params :: list(),
|
||||||
FileNames:: list(),
|
Name :: binary(),
|
||||||
MimeType:: binary(),
|
FileNames :: list(),
|
||||||
Boundary:: binary().
|
MimeType :: binary(),
|
||||||
|
Boundary :: binary().
|
||||||
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||||
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
|
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
|
||||||
LineSeparator = <<"\r\n">>,
|
LineSeparator = <<"\r\n">>,
|
||||||
WithParams = lists:foldl(fun({Key, Value}, Acc) ->
|
WithParams = lists:foldl(
|
||||||
erlang:iolist_to_binary([
|
fun({Key, Value}, Acc) ->
|
||||||
Acc,
|
erlang:iolist_to_binary([
|
||||||
StartBoundary, LineSeparator,
|
Acc,
|
||||||
<<"Content-Disposition: form-data; name=\"">>, Key, <<"\"">>,
|
StartBoundary,
|
||||||
LineSeparator, LineSeparator,
|
LineSeparator,
|
||||||
Value, LineSeparator
|
<<"Content-Disposition: form-data; name=\"">>,
|
||||||
])
|
Key,
|
||||||
end, <<"">>, Params),
|
<<"\"">>,
|
||||||
WithPaths = lists:foldl(fun(FileName, Acc) ->
|
LineSeparator,
|
||||||
erlang:iolist_to_binary([
|
LineSeparator,
|
||||||
Acc,
|
Value,
|
||||||
StartBoundary, LineSeparator,
|
LineSeparator
|
||||||
<<"Content-Disposition: form-data; name=\"">>, Name, <<"\"; filename=\"">>,
|
])
|
||||||
FileName, <<"\"">>, LineSeparator,
|
end,
|
||||||
<<"Content-Type: ">>, MimeType, LineSeparator, LineSeparator,
|
<<"">>,
|
||||||
Data,
|
Params
|
||||||
LineSeparator
|
),
|
||||||
])
|
WithPaths = lists:foldl(
|
||||||
end, WithParams, FileNames),
|
fun(FileName, Acc) ->
|
||||||
|
erlang:iolist_to_binary([
|
||||||
|
Acc,
|
||||||
|
StartBoundary,
|
||||||
|
LineSeparator,
|
||||||
|
<<"Content-Disposition: form-data; name=\"">>,
|
||||||
|
Name,
|
||||||
|
<<"\"; filename=\"">>,
|
||||||
|
FileName,
|
||||||
|
<<"\"">>,
|
||||||
|
LineSeparator,
|
||||||
|
<<"Content-Type: ">>,
|
||||||
|
MimeType,
|
||||||
|
LineSeparator,
|
||||||
|
LineSeparator,
|
||||||
|
Data,
|
||||||
|
LineSeparator
|
||||||
|
])
|
||||||
|
end,
|
||||||
|
WithParams,
|
||||||
|
FileNames
|
||||||
|
),
|
||||||
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
|
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ end_per_suite(_) ->
|
||||||
|
|
||||||
t_nodes_api(_) ->
|
t_nodes_api(_) ->
|
||||||
Topic = <<"test_topic">>,
|
Topic = <<"test_topic">>,
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"routes_username">>, clientid => <<"routes_cid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"routes_username">>, clientid => <<"routes_cid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, _} = emqtt:subscribe(Client, Topic),
|
{ok, _, _} = emqtt:subscribe(Client, Topic),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ t_http_test(_Config) ->
|
||||||
#{
|
#{
|
||||||
<<"code">> => <<"BAD_REQUEST">>,
|
<<"code">> => <<"BAD_REQUEST">>,
|
||||||
<<"message">> => <<"name : mandatory_required_field">>
|
<<"message">> => <<"name : mandatory_required_field">>
|
||||||
}, json(Body)),
|
},
|
||||||
|
json(Body)
|
||||||
|
),
|
||||||
|
|
||||||
Name = <<"test-name">>,
|
Name = <<"test-name">>,
|
||||||
Trace = [
|
Trace = [
|
||||||
|
|
@ -77,32 +79,47 @@ t_http_test(_Config) ->
|
||||||
|
|
||||||
%% update
|
%% update
|
||||||
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
|
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
|
||||||
?assertEqual(#{<<"enable">> => false,
|
?assertEqual(
|
||||||
<<"name">> => <<"test-name">>}, json(Update)),
|
#{
|
||||||
|
<<"enable">> => false,
|
||||||
|
<<"name">> => <<"test-name">>
|
||||||
|
},
|
||||||
|
json(Update)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch({error, {"HTTP/1.1", 404, _}, _},
|
?assertMatch(
|
||||||
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})),
|
{error, {"HTTP/1.1", 404, _}, _},
|
||||||
|
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})
|
||||||
|
),
|
||||||
{ok, List1} = request_api(get, api_path("trace"), Header),
|
{ok, List1} = request_api(get, api_path("trace"), Header),
|
||||||
[Data1] = json(List1),
|
[Data1] = json(List1),
|
||||||
Node = atom_to_binary(node()),
|
Node = atom_to_binary(node()),
|
||||||
?assertMatch(#{
|
?assertMatch(
|
||||||
<<"status">> := <<"stopped">>,
|
#{
|
||||||
<<"name">> := <<"test-name">>,
|
<<"status">> := <<"stopped">>,
|
||||||
<<"log_size">> := #{Node := _},
|
<<"name">> := <<"test-name">>,
|
||||||
<<"start_at">> := _,
|
<<"log_size">> := #{Node := _},
|
||||||
<<"end_at">> := _,
|
<<"start_at">> := _,
|
||||||
<<"type">> := <<"topic">>,
|
<<"end_at">> := _,
|
||||||
<<"topic">> := <<"/x/y/z">>
|
<<"type">> := <<"topic">>,
|
||||||
}, Data1),
|
<<"topic">> := <<"/x/y/z">>
|
||||||
|
},
|
||||||
|
Data1
|
||||||
|
),
|
||||||
|
|
||||||
%% delete
|
%% delete
|
||||||
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
|
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
|
||||||
?assertEqual(<<>>, Delete),
|
?assertEqual(<<>>, Delete),
|
||||||
|
|
||||||
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound}
|
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound} =
|
||||||
= request_api(delete, api_path("trace/test-name"), Header),
|
request_api(delete, api_path("trace/test-name"), Header),
|
||||||
?assertEqual(#{<<"code">> => <<"NOT_FOUND">>,
|
?assertEqual(
|
||||||
<<"message">> => <<"test-name NOT FOUND">>}, json(DeleteNotFound)),
|
#{
|
||||||
|
<<"code">> => <<"NOT_FOUND">>,
|
||||||
|
<<"message">> => <<"test-name NOT FOUND">>
|
||||||
|
},
|
||||||
|
json(DeleteNotFound)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, List2} = request_api(get, api_path("trace"), Header),
|
{ok, List2} = request_api(get, api_path("trace"), Header),
|
||||||
?assertEqual([], json(List2)),
|
?assertEqual([], json(List2)),
|
||||||
|
|
@ -123,29 +140,43 @@ t_create_failed(_Config) ->
|
||||||
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
|
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
|
||||||
|
|
||||||
BadName1 = {<<"name">>, <<"test/bad">>},
|
BadName1 = {<<"name">>, <<"test/bad">>},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [BadName1 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [BadName1 | Trace])
|
||||||
|
),
|
||||||
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
|
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [BadName2 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [BadName2 | Trace])
|
||||||
|
),
|
||||||
|
|
||||||
%% already_exist
|
%% already_exist
|
||||||
GoodName = {<<"name">>, <<"test-name-0">>},
|
GoodName = {<<"name">>, <<"test-name-0">>},
|
||||||
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
|
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
|
||||||
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
|
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [GoodName | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [GoodName | Trace])
|
||||||
|
),
|
||||||
|
|
||||||
%% MAX Limited
|
%% MAX Limited
|
||||||
lists:map(fun(Seq) ->
|
lists:map(
|
||||||
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
fun(Seq) ->
|
||||||
Trace0 = [{name, Name0}, {type, topic},
|
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
||||||
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}],
|
Trace0 = [
|
||||||
{ok, _} = emqx_trace:create(Trace0)
|
{name, Name0},
|
||||||
end, lists:seq(1, 30 - ets:info(emqx_trace, size))),
|
{type, topic},
|
||||||
|
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}
|
||||||
|
],
|
||||||
|
{ok, _} = emqx_trace:create(Trace0)
|
||||||
|
end,
|
||||||
|
lists:seq(1, 30 - ets:info(emqx_trace, size))
|
||||||
|
),
|
||||||
GoodName1 = {<<"name">>, <<"test-name-1">>},
|
GoodName1 = {<<"name">>, <<"test-name-1">>},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])
|
||||||
|
),
|
||||||
unload(),
|
unload(),
|
||||||
emqx_trace:clear(),
|
emqx_trace:clear(),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -158,14 +189,23 @@ t_download_log(_Config) ->
|
||||||
create_trace(Name, ClientId, Now),
|
create_trace(Name, ClientId, Now),
|
||||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
[begin _ = emqtt:ping(Client) end ||_ <- lists:seq(1, 5)],
|
[
|
||||||
|
begin
|
||||||
|
_ = emqtt:ping(Client)
|
||||||
|
end
|
||||||
|
|| _ <- lists:seq(1, 5)
|
||||||
|
],
|
||||||
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
|
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
|
||||||
Header = auth_header_(),
|
Header = auth_header_(),
|
||||||
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
|
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
|
||||||
{ok, [_Comment,
|
{ok, [
|
||||||
#zip_file{name = ZipName,
|
_Comment,
|
||||||
info = #file_info{size = Size, type = regular, access = read_write}}]}
|
#zip_file{
|
||||||
= zip:table(Binary),
|
name = ZipName,
|
||||||
|
info = #file_info{size = Size, type = regular, access = read_write}
|
||||||
|
}
|
||||||
|
]} =
|
||||||
|
zip:table(Binary),
|
||||||
?assert(Size > 0),
|
?assert(Size > 0),
|
||||||
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
|
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
|
||||||
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
|
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
|
||||||
|
|
@ -176,13 +216,18 @@ create_trace(Name, ClientId, Start) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
#{timetrap => 900},
|
#{timetrap => 900},
|
||||||
begin
|
begin
|
||||||
{ok, _} = emqx_trace:create([{<<"name">>, Name},
|
{ok, _} = emqx_trace:create([
|
||||||
{<<"type">>, clientid}, {<<"clientid">>, ClientId}, {<<"start_at">>, Start}]),
|
{<<"name">>, Name},
|
||||||
|
{<<"type">>, clientid},
|
||||||
|
{<<"clientid">>, ClientId},
|
||||||
|
{<<"start_at">>, Start}
|
||||||
|
]),
|
||||||
?block_until(#{?snk_kind := update_trace_done})
|
?block_until(#{?snk_kind := update_trace_done})
|
||||||
end,
|
end,
|
||||||
fun(Trace) ->
|
fun(Trace) ->
|
||||||
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
|
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
t_stream_log(_Config) ->
|
t_stream_log(_Config) ->
|
||||||
application:set_env(emqx, allow_anonymous, true),
|
application:set_env(emqx, allow_anonymous, true),
|
||||||
|
|
@ -194,7 +239,12 @@ t_stream_log(_Config) ->
|
||||||
create_trace(Name, ClientId, Now - 10),
|
create_trace(Name, ClientId, Now - 10),
|
||||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
[begin _ = emqtt:ping(Client) end || _ <- lists:seq(1, 5)],
|
[
|
||||||
|
begin
|
||||||
|
_ = emqtt:ping(Client)
|
||||||
|
end
|
||||||
|
|| _ <- lists:seq(1, 5)
|
||||||
|
],
|
||||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
|
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
|
||||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
|
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
|
||||||
ok = emqtt:disconnect(Client),
|
ok = emqtt:disconnect(Client),
|
||||||
|
|
@ -239,8 +289,9 @@ do_request_api(Method, Request) ->
|
||||||
{error, socket_closed_remotely};
|
{error, socket_closed_remotely};
|
||||||
{error, {shutdown, server_closed}} ->
|
{error, {shutdown, server_closed}} ->
|
||||||
{error, server_closed};
|
{error, server_closed};
|
||||||
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}}
|
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when
|
||||||
when Code =:= 200 orelse Code =:= 201 orelse Code =:= 204 ->
|
Code =:= 200 orelse Code =:= 201 orelse Code =:= 204
|
||||||
|
->
|
||||||
{ok, Return};
|
{ok, Return};
|
||||||
{ok, {Reason, _Header, Body}} ->
|
{ok, {Reason, _Header, Body}} ->
|
||||||
{error, Reason, Body}
|
{error, Reason, Body}
|
||||||
|
|
@ -250,7 +301,8 @@ api_path(Path) ->
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
|
||||||
|
|
||||||
json(Data) ->
|
json(Data) ->
|
||||||
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.
|
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]),
|
||||||
|
Jsx.
|
||||||
|
|
||||||
load() ->
|
load() ->
|
||||||
emqx_trace:start_link().
|
emqx_trace:start_link().
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
emqx_modules_schema {
|
||||||
|
|
||||||
|
rewrite {
|
||||||
|
desc {
|
||||||
|
en: """The topic rewriting function of EMQX supports rewriting topic A to topic B when the client subscribes to topics, publishes messages, and cancels subscriptions according to user-configured rules.
|
||||||
|
Each rewrite rule consists of three parts: subject filter, regular expression, and target expression.
|
||||||
|
Under the premise that the subject rewriting function is enabled, when EMQX receives a subject-based MQTT message such as a PUBLISH message,
|
||||||
|
it will use the subject of the message to sequentially match the subject filter part of the rule in the configuration file. If the match is successful,
|
||||||
|
the regular expression is used to extract the information in the subject, and then replaced with the target expression to form a new subject.
|
||||||
|
Variables in the format of $N can be used in the target expression to match the elements extracted from the regular expression.
|
||||||
|
The value of $N is the Nth element extracted from the regular expression. For example, $1 is the regular expression. The first element extracted by the expression.
|
||||||
|
It should be noted that EMQX uses reverse order to read the rewrite rules in the configuration file.
|
||||||
|
When a topic can match the topic filter of multiple topic rewrite rules at the same time, EMQX will only use the first rule it matches. Rewrite.
|
||||||
|
If the regular expression in this rule does not match the subject of the MQTT message, the rewriting will fail, and no other rules will be attempted for rewriting.
|
||||||
|
Therefore, users need to carefully design MQTT message topics and topic rewriting rules when using them."""
|
||||||
|
zh: """EMQX 的主题重写功能支持根据用户配置的规则在客户端订阅主题、发布消息、取消订阅的时候将 A 主题重写为 B 主题。
|
||||||
|
重写规则分为 Pub 规则和 Sub 规则,Pub 规则匹配 PUSHLISH 报文携带的主题,Sub 规则匹配 SUBSCRIBE、UNSUBSCRIBE 报文携带的主题。
|
||||||
|
每条重写规则都由主题过滤器、正则表达式、目标表达式三部分组成。
|
||||||
|
在主题重写功能开启的前提下,EMQX 在收到诸如 PUBLISH 报文等带有主题的 MQTT 报文时,将使用报文中的主题去依次匹配配置文件中规则的主题过滤器部分,一旦成功匹配,则使用正则表达式提取主题中的信息,然后替换至目标表达式以构成新的主题。
|
||||||
|
目标表达式中可以使用 $N 这种格式的变量匹配正则表达中提取出来的元素,$N 的值为正则表达式中提取出来的第 N 个元素,比如 $1 即为正则表达式提取的第一个元素。
|
||||||
|
需要注意的是,EMQX 使用倒序读取配置文件中的重写规则,当一条主题可以同时匹配多条主题重写规则的主题过滤器时,EMQX 仅会使用它匹配到的第一条规则进行重写,如果该条规则中的正则表达式与 MQTT 报文主题不匹配,则重写失败,不会再尝试使用其他的规则进行重写。
|
||||||
|
因此用户在使用时需要谨慎的设计 MQTT 报文主题以及主题重写规则。"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Topic Rewrite"""
|
||||||
|
zh: """主题重写"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_source_topic {
|
||||||
|
desc {
|
||||||
|
en: """Source topic, specified by the client."""
|
||||||
|
zh: """源主题,客户端业务指定的主题"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Source Topic"""
|
||||||
|
zh: """源主题"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_action {
|
||||||
|
desc {
|
||||||
|
en: """subscribe: Rewrite topic when client do subscribe.
|
||||||
|
publish: Rewrite topic when client do publish.
|
||||||
|
all: Both"""
|
||||||
|
zh: """subscribe:订阅时重写主题;
|
||||||
|
publish:发布时重写主题;
|
||||||
|
all:全部重写主题"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Action"""
|
||||||
|
zh: """Action"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_re {
|
||||||
|
desc {
|
||||||
|
en: """Regular expressions"""
|
||||||
|
zh: """正则表达式"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_dest_topic {
|
||||||
|
desc {
|
||||||
|
en: """Destination topic."""
|
||||||
|
zh: """目标主题。"""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """Destination Topic"""
|
||||||
|
zh: """目标主题"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
emqx_rewrite_api {
|
||||||
|
|
||||||
|
list_topic_rewrite_api {
|
||||||
|
desc {
|
||||||
|
en: """List all rewrite rules"""
|
||||||
|
zh: """列出全部主题重写规则"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_topic_rewrite_api {
|
||||||
|
desc {
|
||||||
|
en: """Update all rewrite rules"""
|
||||||
|
zh: """更新全部主题重写规则"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
update_topic_rewrite_api_response413 {
|
||||||
|
desc {
|
||||||
|
en: """Rules count exceed max limit"""
|
||||||
|
zh: """超出主题重写规则数量上限"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -149,9 +149,9 @@ schema("/mqtt/delayed/messages") ->
|
||||||
[
|
[
|
||||||
{data, mk(hoconsc:array(ref("message")), #{})},
|
{data, mk(hoconsc:array(ref("message")), #{})},
|
||||||
{meta, [
|
{meta, [
|
||||||
{page, mk(integer(), #{})},
|
{page, mk(pos_integer(), #{})},
|
||||||
{limit, mk(integer(), #{})},
|
{limit, mk(pos_integer(), #{})},
|
||||||
{count, mk(integer(), #{})}
|
{count, mk(non_neg_integer(), #{})}
|
||||||
]}
|
]}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +163,11 @@ fields("message_without_payload") ->
|
||||||
{msgid, mk(integer(), #{desc => <<"Message Id (MQTT message id hash)">>})},
|
{msgid, mk(integer(), #{desc => <<"Message Id (MQTT message id hash)">>})},
|
||||||
{node, mk(binary(), #{desc => <<"The node where message from">>})},
|
{node, mk(binary(), #{desc => <<"The node where message from">>})},
|
||||||
{publish_at, mk(binary(), #{desc => <<"Client publish message time, rfc 3339">>})},
|
{publish_at, mk(binary(), #{desc => <<"Client publish message time, rfc 3339">>})},
|
||||||
{delayed_interval, mk(integer(), #{desc => <<"Delayed interval, second">>})},
|
{delayed_interval, mk(pos_integer(), #{desc => <<"Delayed interval, second">>})},
|
||||||
{delayed_remaining, mk(integer(), #{desc => <<"Delayed remaining, second">>})},
|
{delayed_remaining, mk(non_neg_integer(), #{desc => <<"Delayed remaining, second">>})},
|
||||||
{expected_at, mk(binary(), #{desc => <<"Expect publish time, rfc 3339">>})},
|
{expected_at, mk(binary(), #{desc => <<"Expect publish time, rfc 3339">>})},
|
||||||
{topic, mk(binary(), #{desc => <<"Topic">>, example => <<"/sys/#">>})},
|
{topic, mk(binary(), #{desc => <<"Topic">>, example => <<"/sys/#">>})},
|
||||||
{qos, mk(binary(), #{desc => <<"QoS">>})},
|
{qos, mk(emqx_schema:qos(), #{desc => <<"QoS">>})},
|
||||||
{from_clientid, mk(binary(), #{desc => <<"From ClientId">>})},
|
{from_clientid, mk(binary(), #{desc => <<"From ClientId">>})},
|
||||||
{from_username, mk(binary(), #{desc => <<"From Username">>})}
|
{from_username, mk(binary(), #{desc => <<"From Username">>})}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
-module(emqx_modules_schema).
|
-module(emqx_modules_schema).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
@ -50,17 +51,17 @@ fields("rewrite") ->
|
||||||
{action,
|
{action,
|
||||||
sc(
|
sc(
|
||||||
hoconsc:enum([subscribe, publish, all]),
|
hoconsc:enum([subscribe, publish, all]),
|
||||||
#{desc => <<"Action">>, example => publish}
|
#{required => true, desc => ?DESC(tr_action), example => publish}
|
||||||
)},
|
)},
|
||||||
{source_topic,
|
{source_topic,
|
||||||
sc(
|
sc(
|
||||||
binary(),
|
binary(),
|
||||||
#{desc => <<"Origin Topic">>, example => "x/#"}
|
#{required => true, desc => ?DESC(tr_source_topic), example => "x/#"}
|
||||||
)},
|
)},
|
||||||
{dest_topic,
|
{dest_topic,
|
||||||
sc(
|
sc(
|
||||||
binary(),
|
binary(),
|
||||||
#{desc => <<"Destination Topic">>, example => "z/y/$1"}
|
#{required => true, desc => ?DESC(tr_dest_topic), example => "z/y/$1"}
|
||||||
)},
|
)},
|
||||||
{re, fun regular_expression/1}
|
{re, fun regular_expression/1}
|
||||||
];
|
];
|
||||||
|
|
@ -72,14 +73,15 @@ desc("telemetry") ->
|
||||||
desc("delayed") ->
|
desc("delayed") ->
|
||||||
"Settings for the delayed module.";
|
"Settings for the delayed module.";
|
||||||
desc("rewrite") ->
|
desc("rewrite") ->
|
||||||
"Rewrite rule.";
|
?DESC(rewrite);
|
||||||
desc("topic_metrics") ->
|
desc("topic_metrics") ->
|
||||||
"";
|
"";
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
regular_expression(type) -> binary();
|
regular_expression(type) -> binary();
|
||||||
regular_expression(desc) -> "Regular expressions";
|
regular_expression(required) -> true;
|
||||||
|
regular_expression(desc) -> ?DESC(tr_re);
|
||||||
regular_expression(example) -> "^x/y/(.+)$";
|
regular_expression(example) -> "^x/y/(.+)$";
|
||||||
regular_expression(validator) -> fun is_re/1;
|
regular_expression(validator) -> fun is_re/1;
|
||||||
regular_expression(_) -> undefined.
|
regular_expression(_) -> undefined.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
-module(emqx_rewrite_api).
|
-module(emqx_rewrite_api).
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include("emqx_modules.hrl").
|
-include("emqx_modules.hrl").
|
||||||
|
|
||||||
|
|
@ -38,16 +40,16 @@ schema("/mqtt/topic_rewrite") ->
|
||||||
'operationId' => topic_rewrite,
|
'operationId' => topic_rewrite,
|
||||||
get => #{
|
get => #{
|
||||||
tags => ?API_TAG_MQTT,
|
tags => ?API_TAG_MQTT,
|
||||||
description => <<"List rewrite topic.">>,
|
description => ?DESC(list_topic_rewrite_api),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(
|
200 => hoconsc:mk(
|
||||||
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
|
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
|
||||||
#{desc => <<"List all rewrite rules">>}
|
#{desc => ?DESC(list_topic_rewrite_api)}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
put => #{
|
put => #{
|
||||||
description => <<"Update rewrite topic">>,
|
description => ?DESC(update_topic_rewrite_api),
|
||||||
tags => ?API_TAG_MQTT,
|
tags => ?API_TAG_MQTT,
|
||||||
'requestBody' => hoconsc:mk(
|
'requestBody' => hoconsc:mk(
|
||||||
hoconsc:array(
|
hoconsc:array(
|
||||||
|
|
@ -58,11 +60,11 @@ schema("/mqtt/topic_rewrite") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(
|
200 => hoconsc:mk(
|
||||||
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
|
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
|
||||||
#{desc => <<"Update rewrite topic success.">>}
|
#{desc => ?DESC(update_topic_rewrite_api)}
|
||||||
),
|
),
|
||||||
413 => emqx_dashboard_swagger:error_codes(
|
413 => emqx_dashboard_swagger:error_codes(
|
||||||
[?EXCEED_LIMIT],
|
[?EXCEED_LIMIT],
|
||||||
<<"Rules count exceed max limit">>
|
?DESC(update_topic_rewrite_api_response413)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -307,17 +307,19 @@ calculate_rate(CurrVal, #rate{max = MaxRate0, last_v = LastVal,
|
||||||
|
|
||||||
%% calculate the max rate since the emqx startup
|
%% calculate the max rate since the emqx startup
|
||||||
MaxRate =
|
MaxRate =
|
||||||
if MaxRate0 >= CurrRate -> MaxRate0;
|
case MaxRate0 >= CurrRate of
|
||||||
true -> CurrRate
|
true -> MaxRate0;
|
||||||
|
false -> CurrRate
|
||||||
end,
|
end,
|
||||||
|
|
||||||
%% calculate the average rate in last 5 mins
|
%% calculate the average rate in last 5 mins
|
||||||
{Last5MinSamples, Acc5Min, Last5Min} =
|
{Last5MinSamples, Acc5Min, Last5Min} =
|
||||||
if Tick =< ?SAMPCOUNT_5M ->
|
case Tick =< ?SAMPCOUNT_5M of
|
||||||
|
true ->
|
||||||
Acc = AccRate5Min0 + CurrRate,
|
Acc = AccRate5Min0 + CurrRate,
|
||||||
{lists:reverse([CurrRate | lists:reverse(Last5MinSamples0)]),
|
{lists:reverse([CurrRate | lists:reverse(Last5MinSamples0)]),
|
||||||
Acc, Acc / Tick};
|
Acc, Acc / Tick};
|
||||||
true ->
|
false ->
|
||||||
[FirstRate | Rates] = Last5MinSamples0,
|
[FirstRate | Rates] = Last5MinSamples0,
|
||||||
Acc = AccRate5Min0 + CurrRate - FirstRate,
|
Acc = AccRate5Min0 + CurrRate - FirstRate,
|
||||||
{lists:reverse([CurrRate | lists:reverse(Rates)]),
|
{lists:reverse([CurrRate | lists:reverse(Rates)]),
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue