chore: reformat mgmt code.

This commit is contained in:
Zhongwen Deng 2022-04-19 14:02:39 +08:00
parent 5661948a86
commit aa7807baeb
40 changed files with 3082 additions and 1843 deletions

View File

@ -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]}.

View File

@ -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"}
]}
]}.

View File

@ -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.

View File

@ -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.

View File

@ -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) ->

View File

@ -43,9 +43,12 @@ schema("/alarms") ->
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 => ?DESC(get_alarms_qs_activated), hoconsc:mk(boolean(), #{
required => false})} in => query,
desc => ?DESC(get_alarms_qs_activated),
required => false
})}
], ],
responses => #{ responses => #{
200 => [ 200 => [
@ -54,7 +57,7 @@ schema("/alarms") ->
] ]
} }
}, },
delete => #{ delete => #{
description => ?DESC(delete_alarms_api), description => ?DESC(delete_alarms_api),
responses => #{ responses => #{
204 => ?DESC(delete_alarms_api_response204) 204 => ?DESC(delete_alarms_api_response204)
@ -64,21 +67,38 @@ schema("/alarms") ->
fields(alarm) -> fields(alarm) ->
[ [
{node, hoconsc:mk(binary(), {node,
#{desc => ?DESC(node), example => atom_to_list(node())})}, hoconsc:mk(
{name, hoconsc:mk(binary(), binary(),
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>})}, #{desc => ?DESC(node), example => atom_to_list(node())}
{message, hoconsc:mk(binary(), #{desc => ?DESC(message), )},
example => <<"System memory usage is higher than 70%">>})}, {name,
{details, hoconsc:mk(map(), #{desc => ?DESC(details), hoconsc:mk(
example => #{<<"high_watermark">> => 70}})}, binary(),
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>}
)},
{message,
hoconsc:mk(binary(), #{
desc => ?DESC(message),
example => <<"System memory usage is higher than 70%">>
})},
{details,
hoconsc:mk(map(), #{
desc => ?DESC(details),
example => #{<<"high_watermark">> => 70}
})},
{duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})}, {duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
{activate_at, hoconsc:mk(binary(), #{desc => ?DESC(activate_at), {activate_at,
example => <<"2021-10-25T11:52:52.548+08:00">>})}, hoconsc:mk(binary(), #{
{deactivate_at, hoconsc:mk(binary(), #{desc => ?DESC(deactivate_at), desc => ?DESC(activate_at),
example => <<"2021-10-31T10:52:52.548+08:00">>})} 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) ++
@ -100,7 +120,6 @@ alarms(get, #{query_string := QString}) ->
Response -> Response ->
{200, Response} {200, Response}
end; end;
alarms(delete, _Params) -> alarms(delete, _Params) ->
_ = emqx_mgmt:delete_all_deactivated_alarms(), _ = emqx_mgmt:delete_all_deactivated_alarms(),
{204}. {204}.
@ -109,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).

View File

@ -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.

View File

@ -24,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).
@ -49,7 +52,7 @@ paths() ->
schema("/banned") -> schema("/banned") ->
#{ #{
'operationId' => banned, 'operationId' => banned,
get => #{ get => #{
description => ?DESC(list_banned_api), description => ?DESC(list_banned_api),
parameters => [ parameters => [
@ -57,7 +60,7 @@ schema("/banned") ->
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), #{})}
] ]
@ -69,8 +72,9 @@ schema("/banned") ->
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'],
?DESC(create_banned_api_response400)) ?DESC(create_banned_api_response400)
)
} }
} }
}; };
@ -80,51 +84,67 @@ schema("/banned/:as/:who") ->
delete => #{ delete => #{
description => ?DESC(delete_banned_api), description => ?DESC(delete_banned_api),
parameters => [ parameters => [
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{ {as,
desc => ?DESC(as), hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
in => path, desc => ?DESC(as),
example => username})}, in => path,
{who, hoconsc:mk(binary(), #{ example => username
desc => ?DESC(who), })},
in => path, {who,
example => <<"Badass">>})} hoconsc:mk(binary(), #{
], desc => ?DESC(who),
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'],
?DESC(delete_banned_api_response404)) ?DESC(delete_banned_api_response404)
)
} }
} }
}. }.
fields(ban) -> fields(ban) ->
[ [
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{ {as,
desc => ?DESC(as), hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
required => true, desc => ?DESC(as),
example => username})}, required => true,
{who, hoconsc:mk(binary(), #{ example => username
desc => ?DESC(who), })},
required => true, {who,
example => <<"Banned name"/utf8>>})}, hoconsc:mk(binary(), #{
{by, hoconsc:mk(binary(), #{ desc => ?DESC(who),
desc => ?DESC(by), required => true,
required => false, example => <<"Banned name"/utf8>>
example => <<"mgmt_api">>})}, })},
{reason, hoconsc:mk(binary(), #{ {by,
desc => ?DESC(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 => ?DESC(at), })},
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 => ?DESC(until), 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) ++
@ -141,8 +161,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.

View File

@ -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/authorization/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,19 +221,23 @@ 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/authorization/cache") ->
#{ #{
'operationId' => authz_cache, 'operationId' => authz_cache,
@ -199,7 +247,8 @@ schema("/clients/:clientid/authorization/cache") ->
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 => #{
@ -208,11 +257,11 @@ schema("/clients/:clientid/authorization/cache") ->
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,16 +594,26 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
%% api apply %% api apply
list_clients(QString) -> list_clients(QString) ->
Result = case maps:get(<<"node">>, QString, undefined) of Result =
undefined -> case maps:get(<<"node">>, QString, undefined) of
emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB, undefined ->
?CLIENT_QSCHEMA, ?QUERY_FUN); emqx_mgmt_api:cluster_query(
Node0 -> QString,
Node1 = binary_to_atom(Node0, utf8), ?CLIENT_QTAB,
QStringWithoutNode = maps:without([<<"node">>], QString), ?CLIENT_QSCHEMA,
emqx_mgmt_api:node_query(Node1, QStringWithoutNode, ?QUERY_FUN
?CLIENT_QTAB, ?CLIENT_QSCHEMA, ?QUERY_FUN) );
end, Node0 ->
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 case Result of
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
@ -490,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};
@ -524,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.
@ -562,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} ->
@ -576,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
@ -595,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});
@ -603,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}};
@ -637,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) ->
@ -710,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
}.

View File

@ -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).

View File

@ -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,28 +38,29 @@
-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">>,
<<"telemetry">>
] ++ global_zone_roots() ] ++ global_zone_roots()
). ).
@ -68,7 +71,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") ->
#{ #{
@ -76,12 +79,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())
} }
@ -94,18 +105,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'])
@ -137,9 +159,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">>)
@ -147,9 +171,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,
@ -176,7 +202,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
@ -188,21 +213,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'}}
@ -212,7 +248,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} ->
@ -222,9 +259,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])),
@ -242,8 +278,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)).
@ -278,8 +317,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

View File

@ -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,176 +107,353 @@ 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) ->

View File

@ -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(),

View File

@ -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

View File

@ -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])).

View File

@ -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,27 +45,38 @@ 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) ->
[ [
@ -80,17 +96,25 @@ fields(node_stats_data) ->
stats_schema('suboptions.max', <<"subscriptions.max">>), stats_schema('suboptions.max', <<"subscriptions.max">>),
stats_schema('subscribers.count', <<"Number of current subscribers">>), stats_schema('subscribers.count', <<"Number of current subscribers">>),
stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>), stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
stats_schema('subscriptions.count', <<"Number of current subscriptions, including shared subscriptions">>), stats_schema(
'subscriptions.count',
<<"Number of current subscriptions, including shared subscriptions">>
),
stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>), stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>), stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
stats_schema('subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>), stats_schema(
'subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>
),
stats_schema('topics.count', <<"Number of current topics">>), stats_schema('topics.count', <<"Number of current topics">>),
stats_schema('topics.max', <<"Historical maximum number of topics">>) stats_schema('topics.max', <<"Historical maximum number of topics">>)
]; ];
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) -> stats_schema(Name, Desc) ->
@ -103,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.

View File

@ -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]),

View File

@ -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,41 +80,53 @@ 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">>
})
} }
]. ].
@ -116,11 +134,20 @@ subscriptions(get, #{query_string := QString}) ->
Response = Response =
case maps:get(<<"node">>, QString, undefined) of case maps:get(<<"node">>, QString, undefined) of
undefined -> undefined ->
emqx_mgmt_api:cluster_query(QString, ?SUBS_QTABLE, emqx_mgmt_api:cluster_query(
?SUBS_QSCHEMA, ?QUERY_FUN); QString,
?SUBS_QTABLE,
?SUBS_QSCHEMA,
?QUERY_FUN
);
Node0 -> Node0 ->
emqx_mgmt_api:node_query(binary_to_atom(Node0, utf8), QString, emqx_mgmt_api:node_query(
?SUBS_QTABLE, ?SUBS_QSCHEMA, ?QUERY_FUN) binary_to_atom(Node0, utf8),
QString,
?SUBS_QTABLE,
?SUBS_QSCHEMA,
?QUERY_FUN
)
end, end,
case Response of case Response of
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
@ -134,10 +161,8 @@ subscriptions(get, #{query_string := QString}) ->
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),
#{ #{
@ -161,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(_, []) ->

View File

@ -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,8 +108,11 @@ topic(get, #{bindings := Bindings}) ->
%%%============================================================================================== %%%==============================================================================================
%% api apply %% api apply
do_list(Params) -> do_list(Params) ->
case emqx_mgmt_api:node_query( case
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}) of emqx_mgmt_api:node_query(
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}
)
of
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
{error, Node, {badrpc, R}} -> {error, Node, {badrpc, R}} ->
@ -128,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}}) ->
@ -147,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),
@ -155,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,

View File

@ -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">>;

View File

@ -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").

View File

@ -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

View File

@ -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}, []}}.

View File

@ -16,38 +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 -export([
, bad_request/1 bad_request/0,
, properties/1 bad_request/1,
, page_params/0 properties/1,
, schema/1 page_params/0,
, schema/2 schema/1,
, object_schema/1 schema/2,
, object_schema/2 object_schema/1,
, array_schema/1 object_schema/2,
, array_schema/2 array_schema/1,
, object_array_schema/1 array_schema/2,
, object_array_schema/2 object_array_schema/1,
, page_schema/1 object_array_schema/2,
, page_object_schema/1 page_schema/1,
, error_schema/1 page_object_schema/1,
, error_schema/2 error_schema/1,
, batch_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
@ -55,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).
@ -80,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).
@ -123,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)));
@ -134,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,
@ -155,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).
@ -168,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) ->
@ -211,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.
@ -224,36 +251,75 @@ 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">>).

View File

@ -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").

View File

@ -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]).

View File

@ -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),

View File

@ -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

View File

@ -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() ->

View File

@ -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),
@ -77,28 +77,48 @@ t_clients(_) ->
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2), ?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
%% get /clients/:clientid/authorization/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), "authorization", "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.

View File

@ -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

View File

@ -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)).

View File

@ -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)
).

View File

@ -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,

View File

@ -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

View File

@ -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"]),

View File

@ -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]),

View File

@ -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([]).
@ -53,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]),
@ -97,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:
@ -117,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 = [
@ -146,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]).

View File

@ -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),

View File

@ -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().