refactor(emqx_management): Move it under the umbrella

This commit is contained in:
Zaiming Shi 2020-10-29 22:32:47 +01:00
parent bcd98bda1c
commit 4d8fd6b427
38 changed files with 6255 additions and 1 deletions

View File

@ -0,0 +1,37 @@
## shallow clone for speed
REBAR_GIT_CLONE_OPTIONS += --depth 1
export REBAR_GIT_CLONE_OPTIONS
REBAR = rebar3
all: compile
compile:
$(REBAR) compile
clean: distclean
ct: compile
$(REBAR) as test ct -v
eunit: compile
$(REBAR) as test eunit
xref:
$(REBAR) xref
cover:
$(REBAR) cover
distclean:
@rm -rf _build
@rm -f data/app.*.config data/vm.*.args rebar.lock
CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish
$(CUTTLEFISH_SCRIPT):
@${REBAR} get-deps
@if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi
app.config: $(CUTTLEFISH_SCRIPT)
$(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/emqx_management.conf -i priv/emqx_management.schema -d data

View File

@ -0,0 +1,9 @@
# emqx-management
EMQ X Management API
## How to Design RESTful API?
http://restful-api-design.readthedocs.io/en/latest/scope.html

View File

@ -0,0 +1,52 @@
##--------------------------------------------------------------------
## EMQ X Management Plugin
##--------------------------------------------------------------------
## Max Row Limit
management.max_row_limit = 10000
## Application default secret
##
## Value: String
## management.application.default_secret = public
## Default Application ID
##
## Value: String
management.default_application.id = admin
## Default Application Secret
##
## Value: String
management.default_application.secret = public
##--------------------------------------------------------------------
## HTTP Listener
management.listener.http = 8081
management.listener.http.acceptors = 2
management.listener.http.max_clients = 512
management.listener.http.backlog = 512
management.listener.http.send_timeout = 15s
management.listener.http.send_timeout_close = on
management.listener.http.inet6 = false
management.listener.http.ipv6_v6only = false
##--------------------------------------------------------------------
## HTTPS Listener
## management.listener.https = 8081
## management.listener.https.acceptors = 2
## management.listener.https.max_clients = 512
## management.listener.https.backlog = 512
## management.listener.https.send_timeout = 15s
## management.listener.https.send_timeout_close = on
## management.listener.https.certfile = etc/certs/cert.pem
## management.listener.https.keyfile = etc/certs/key.pem
## management.listener.https.cacertfile = etc/certs/cacert.pem
## management.listener.https.verify = verify_peer
## management.listener.https.tls_versions = tlsv1.2,tlsv1.1,tlsv1
## management.listener.https.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA
## management.listener.https.fail_if_no_peer_cert = true
## management.listener.https.inet6 = false
## management.listener.https.ipv6_v6only = false

View File

@ -0,0 +1,35 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% Return Codes
-define(SUCCESS, 0). %% Success
-define(ERROR1, 101). %% badrpc
-define(ERROR2, 102). %% Unknown error
-define(ERROR3, 103). %% Username or password error
-define(ERROR4, 104). %% Empty username or password
-define(ERROR5, 105). %% User does not exist
-define(ERROR6, 106). %% Admin can not be deleted
-define(ERROR7, 107). %% Missing request parameter
-define(ERROR8, 108). %% Request parameter type error
-define(ERROR9, 109). %% Request parameter is not a json
-define(ERROR10, 110). %% Plugin has been loaded
-define(ERROR11, 111). %% Plugin has been unloaded
-define(ERROR12, 112). %% Client not online
-define(ERROR13, 113). %% User already exist
-define(ERROR14, 114). %% OldPassword error
-define(ERROR15, 115). %% bad topic
-define(VERSIONS, ["1", "3.2", "3.4", "4.0", "4.1", "4.2"]).

View File

@ -0,0 +1,239 @@
%%-*- mode: erlang -*-
%% emqx_management config mapping
{mapping, "management.max_row_limit", "emqx_management.max_row_limit", [
{default, 10000},
{datatype, integer}
]}.
{mapping, "management.default_application.id", "emqx_management.default_application_id", [
{default, undefined},
{datatype, string}
]}.
{mapping, "management.default_application.secret", "emqx_management.default_application_secret", [
{default, undefined},
{datatype, string}
]}.
{mapping, "management.application.default_secret", "emqx_management.application", [
{default, undefined},
{datatype, string}
]}.
{mapping, "management.listener.http", "emqx_management.listeners", [
{datatype, [integer, ip]}
]}.
{mapping, "management.listener.http.acceptors", "emqx_management.listeners", [
{default, 4},
{datatype, integer}
]}.
{mapping, "management.listener.http.max_clients", "emqx_management.listeners", [
{default, 512},
{datatype, integer}
]}.
{mapping, "management.listener.http.backlog", "emqx_management.listeners", [
{default, 1024},
{datatype, integer}
]}.
{mapping, "management.listener.http.send_timeout", "emqx_management.listeners", [
{datatype, {duration, ms}},
{default, "15s"}
]}.
{mapping, "management.listener.http.send_timeout_close", "emqx_management.listeners", [
{datatype, flag},
{default, on}
]}.
{mapping, "management.listener.http.recbuf", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.http.sndbuf", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.http.buffer", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.http.tune_buffer", "emqx_management.listeners", [
{datatype, flag},
hidden
]}.
{mapping, "management.listener.http.nodelay", "emqx_management.listeners", [
{datatype, {enum, [true, false]}},
hidden
]}.
{mapping, "management.listener.http.inet6", "emqx_management.listeners", [
{default, false},
{datatype, {enum, [true, false]}}
]}.
{mapping, "management.listener.http.ipv6_v6only", "emqx_management.listeners", [
{default, false},
{datatype, {enum, [true, false]}}
]}.
{mapping, "management.listener.https", "emqx_management.listeners", [
{datatype, [integer, ip]}
]}.
{mapping, "management.listener.https.acceptors", "emqx_management.listeners", [
{default, 8},
{datatype, integer}
]}.
{mapping, "management.listener.https.max_clients", "emqx_management.listeners", [
{default, 64},
{datatype, integer}
]}.
{mapping, "management.listener.https.backlog", "emqx_management.listeners", [
{default, 1024},
{datatype, integer}
]}.
{mapping, "management.listener.https.send_timeout", "emqx_management.listeners", [
{datatype, {duration, ms}},
{default, "15s"}
]}.
{mapping, "management.listener.https.send_timeout_close", "emqx_management.listeners", [
{datatype, flag},
{default, on}
]}.
{mapping, "management.listener.https.recbuf", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.https.sndbuf", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.https.buffer", "emqx_management.listeners", [
{datatype, bytesize},
hidden
]}.
{mapping, "management.listener.https.tune_buffer", "emqx_management.listeners", [
{datatype, flag},
hidden
]}.
{mapping, "management.listener.https.nodelay", "emqx_management.listeners", [
{datatype, {enum, [true, false]}},
hidden
]}.
{mapping, "management.listener.https.keyfile", "emqx_management.listeners", [
{datatype, string}
]}.
{mapping, "management.listener.https.certfile", "emqx_management.listeners", [
{datatype, string}
]}.
{mapping, "management.listener.https.cacertfile", "emqx_management.listeners", [
{datatype, string}
]}.
{mapping, "management.listener.https.verify", "emqx_management.listeners", [
{datatype, atom}
]}.
{mapping, "management.listener.https.ciphers", "emqx_management.listeners", [
{datatype, string}
]}.
{mapping, "management.listener.https.tls_versions", "emqx_management.listeners", [
{datatype, string}
]}.
{mapping, "management.listener.https.fail_if_no_peer_cert", "emqx_management.listeners", [
{datatype, {enum, [true, false]}}
]}.
{mapping, "management.listener.https.inet6", "emqx_management.listeners", [
{default, false},
{datatype, {enum, [true, false]}}
]}.
{mapping, "management.listener.https.ipv6_v6only", "emqx_management.listeners", [
{default, false},
{datatype, {enum, [true, false]}}
]}.
{translation, "emqx_management.application", fun(Conf) ->
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
Opts = fun(Prefix) ->
Filter([{default_secret, cuttlefish:conf_get(Prefix ++ ".default_secret", Conf)}])
end,
Prefix = "management.application",
Transfer = fun(default_secret, V) -> list_to_binary(V);
(_, V) -> V
end,
[{K, Transfer(K, V)}|| {K, V} <- Opts(Prefix)]
end}.
{translation, "emqx_management.listeners", fun(Conf) ->
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
Opts = fun(Prefix) ->
Filter([{num_acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)},
{max_connections, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}])
end,
TcpOpts = fun(Prefix) ->
Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)},
{send_timeout, cuttlefish:conf_get(Prefix ++ ".send_timeout", Conf, undefined)},
{send_timeout_close, cuttlefish:conf_get(Prefix ++ ".send_timeout_close", Conf, undefined)},
{recbuf, cuttlefish:conf_get(Prefix ++ ".recbuf", Conf, undefined)},
{sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)},
{buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)},
{nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)},
{inet6, cuttlefish:conf_get(Prefix ++ ".inet6", Conf)},
{ipv6_v6only, cuttlefish:conf_get(Prefix ++ ".ipv6_v6only", Conf)}])
end,
SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end,
SslOpts = fun(Prefix) ->
Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of
undefined -> undefined;
L -> [list_to_atom(V) || V <- L]
end,
Filter([{versions, Versions},
{ciphers, SplitFun(cuttlefish:conf_get(Prefix ++ ".ciphers", Conf, undefined))},
{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)},
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)},
{verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)},
{fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}])
end,
lists:foldl(
fun(Proto, Acc) ->
Prefix = "management.listener." ++ atom_to_list(Proto),
case cuttlefish:conf_get(Prefix, Conf, undefined) of
undefined -> Acc;
Port ->
[{Proto, Port, TcpOpts(Prefix) ++ Opts(Prefix)
++ case Proto of
http -> [];
https -> SslOpts(Prefix)
end} | Acc]
end
end, [], [http, https])
end}.

View File

@ -0,0 +1,30 @@
{deps,
[{minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.1"}}},
{cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}}
]}.
{profiles,
[{test,
[{deps,
[{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {branch, "develop"}}},
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.1.1"}}},
meck
]}
]}
]}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
warnings_as_errors,
debug_info,
{parse_transform}]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.

View File

@ -0,0 +1,14 @@
{application, emqx_management,
[{description, "EMQ X Management API and CLI"},
{vsn, "git"},
{modules, []},
{registered, [emqx_management_sup]},
{applications, [kernel,stdlib,minirest]},
{mod, {emqx_mgmt_app,[]}},
{env, []},
{licenses, ["Apache-2.0"]},
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"},
{"Github", "https://github.com/emqx/emqx-management"}
]}
]}.

View File

@ -0,0 +1,24 @@
%%-*- mode: erlang -*-
%% .app.src.script
RemoveLeadingV =
fun(Tag) ->
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
nomatch ->
re:replace(Tag, "/", "-", [{return ,list}]);
_ ->
%% if it is a version number prefixed by 'v' or 'e', then remove it
re:replace(Tag, "[v|e]", "", [{return ,list}])
end
end,
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
false -> CONFIG; % env var not defined
[] -> CONFIG; % env var set to empty string
Tag ->
[begin
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
{application, App, AppConf0}
end || Conf = {application, App, AppConf} <- CONFIG]
end.

View File

@ -0,0 +1,932 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt).
-include("emqx_mgmt.hrl").
-include_lib("stdlib/include/qlc.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-import(proplists, [get_value/2]).
%% Nodes and Brokers API
-export([ list_nodes/0
, lookup_node/1
, list_brokers/0
, lookup_broker/1
, node_info/1
, broker_info/1
]).
%% Metrics and Stats
-export([ get_metrics/0
, get_metrics/1
, get_all_topic_metrics/0
, get_topic_metrics/1
, get_topic_metrics/2
, register_topic_metrics/1
, register_topic_metrics/2
, unregister_topic_metrics/1
, unregister_topic_metrics/2
, unregister_all_topic_metrics/0
, unregister_all_topic_metrics/1
, get_stats/0
, get_stats/1
]).
%% Clients, Sessions
-export([ lookup_client/2
, lookup_client/3
, kickout_client/1
, list_acl_cache/1
, clean_acl_cache/1
, clean_acl_cache/2
, set_ratelimit_policy/2
, set_quota_policy/2
]).
%% Internal funcs
-export([call_client/3]).
%% Subscriptions
-export([ list_subscriptions/1
, list_subscriptions_via_topic/2
, list_subscriptions_via_topic/3
, lookup_subscriptions/1
, lookup_subscriptions/2
]).
%% Routes
-export([ list_routes/0
, lookup_routes/1
]).
%% PubSub
-export([ subscribe/2
, do_subscribe/2
, publish/1
, unsubscribe/2
, do_unsubscribe/2
]).
%% Plugins
-export([ list_plugins/0
, list_plugins/1
, load_plugin/2
, unload_plugin/2
, reload_plugin/2
]).
%% Modules
-export([ list_modules/0
, list_modules/1
, load_module/2
, unload_module/2
, reload_module/2
]).
%% Listeners
-export([ list_listeners/0
, list_listeners/1
]).
%% Alarms
-export([ get_alarms/1
, get_alarms/2
, deactivate/2
, delete_all_deactivated_alarms/0
, delete_all_deactivated_alarms/1
]).
%% Banned
-export([ create_banned/1
, delete_banned/1
]).
%% Export/Import
-export([ export_rules/0
, export_resources/0
, export_blacklist/0
, export_applications/0
, export_users/0
, export_auth_clientid/0
, export_auth_username/0
, export_auth_mnesia/0
, export_acl_mnesia/0
, export_schemas/0
, import_rules/1
, import_resources/1
, import_blacklist/1
, import_applications/1
, import_users/1
, import_auth_clientid/1
, import_auth_username/1
, import_auth_mnesia/1
, import_acl_mnesia/1
, import_schemas/1
, to_version/1
]).
-export([ enable_telemetry/0
, disable_telemetry/0
, get_telemetry_status/0
, get_telemetry_data/0
]).
%% Common Table API
-export([ item/2
, max_row_limit/0
]).
-define(MAX_ROW_LIMIT, 10000).
-define(APP, emqx_management).
%%--------------------------------------------------------------------
%% Node Info
%%--------------------------------------------------------------------
list_nodes() ->
Running = mnesia:system_info(running_db_nodes),
Stopped = mnesia:system_info(db_nodes) -- Running,
DownNodes = lists:map(fun stopped_node_info/1, Stopped),
[{Node, node_info(Node)} || Node <- Running] ++ DownNodes.
lookup_node(Node) -> node_info(Node).
node_info(Node) when Node =:= node() ->
Memory = emqx_vm:get_memory(),
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
BrokerInfo = emqx_sys:info(),
Info#{node => node(),
otp_release => iolist_to_binary(otp_rel()),
memory_total => get_value(allocated, Memory),
memory_used => get_value(used, Memory),
process_available => erlang:system_info(process_limit),
process_used => erlang:system_info(process_count),
max_fds => get_value(max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
connections => ets:info(emqx_channel, size),
node_status => 'Running',
uptime => iolist_to_binary(proplists:get_value(uptime, BrokerInfo)),
version => iolist_to_binary(proplists:get_value(version, BrokerInfo))
};
node_info(Node) ->
rpc_call(Node, node_info, [Node]).
stopped_node_info(Node) ->
#{name => Node, node_status => 'Stopped'}.
%%--------------------------------------------------------------------
%% Brokers
%%--------------------------------------------------------------------
list_brokers() ->
[{Node, broker_info(Node)} || Node <- ekka_mnesia:running_nodes()].
lookup_broker(Node) ->
broker_info(Node).
broker_info(Node) when Node =:= node() ->
Info = maps:from_list([{K, iolist_to_binary(V)} || {K, V} <- emqx_sys:info()]),
Info#{node => Node, otp_release => iolist_to_binary(otp_rel()), node_status => 'Running'};
broker_info(Node) ->
rpc_call(Node, broker_info, [Node]).
%%--------------------------------------------------------------------
%% Metrics and Stats
%%--------------------------------------------------------------------
get_metrics() ->
[{Node, get_metrics(Node)} || Node <- ekka_mnesia:running_nodes()].
get_metrics(Node) when Node =:= node() ->
emqx_metrics:all();
get_metrics(Node) ->
rpc_call(Node, get_metrics, [Node]).
get_all_topic_metrics() ->
lists:foldl(fun(Topic, Acc) ->
case get_topic_metrics(Topic) of
{error, _Reason} ->
Acc;
Metrics ->
[#{topic => Topic, metrics => Metrics} | Acc]
end
end, [], emqx_mod_topic_metrics:all_registered_topics()).
get_topic_metrics(Topic) ->
lists:foldl(fun(Node, Acc) ->
case get_topic_metrics(Node, Topic) of
{error, _Reason} ->
Acc;
Metrics ->
case Acc of
[] -> Metrics;
_ ->
lists:foldl(fun({K, V}, Acc0) ->
[{K, V + proplists:get_value(K, Metrics, 0)} | Acc0]
end, [], Acc)
end
end
end, [], ekka_mnesia:running_nodes()).
get_topic_metrics(Node, Topic) when Node =:= node() ->
emqx_mod_topic_metrics:metrics(Topic);
get_topic_metrics(Node, Topic) ->
rpc_call(Node, get_topic_metrics, [Node, Topic]).
register_topic_metrics(Topic) ->
Results = [register_topic_metrics(Node, Topic) || Node <- ekka_mnesia:running_nodes()],
case lists:any(fun(Item) -> Item =:= ok end, Results) of
true -> ok;
false -> lists:last(Results)
end.
register_topic_metrics(Node, Topic) when Node =:= node() ->
emqx_mod_topic_metrics:register(Topic);
register_topic_metrics(Node, Topic) ->
rpc_call(Node, register_topic_metrics, [Node, Topic]).
unregister_topic_metrics(Topic) ->
Results = [unregister_topic_metrics(Node, Topic) || Node <- ekka_mnesia:running_nodes()],
case lists:any(fun(Item) -> Item =:= ok end, Results) of
true -> ok;
false -> lists:last(Results)
end.
unregister_topic_metrics(Node, Topic) when Node =:= node() ->
emqx_mod_topic_metrics:unregister(Topic);
unregister_topic_metrics(Node, Topic) ->
rpc_call(Node, unregister_topic_metrics, [Node, Topic]).
unregister_all_topic_metrics() ->
Results = [unregister_all_topic_metrics(Node) || Node <- ekka_mnesia:running_nodes()],
case lists:any(fun(Item) -> Item =:= ok end, Results) of
true -> ok;
false -> lists:last(Results)
end.
unregister_all_topic_metrics(Node) when Node =:= node() ->
emqx_mod_topic_metrics:unregister_all();
unregister_all_topic_metrics(Node) ->
rpc_call(Node, unregister_topic_metrics, [Node]).
get_stats() ->
[{Node, get_stats(Node)} || Node <- ekka_mnesia:running_nodes()].
get_stats(Node) when Node =:= node() ->
emqx_stats:getstats();
get_stats(Node) ->
rpc_call(Node, get_stats, [Node]).
%%--------------------------------------------------------------------
%% Clients
%%--------------------------------------------------------------------
lookup_client({clientid, ClientId}, FormatFun) ->
lists:append([lookup_client(Node, {clientid, ClientId}, FormatFun) || Node <- ekka_mnesia:running_nodes()]);
lookup_client({username, Username}, FormatFun) ->
lists:append([lookup_client(Node, {username, Username}, FormatFun) || Node <- ekka_mnesia:running_nodes()]).
lookup_client(Node, {clientid, ClientId}, {M,F}) when Node =:= node() ->
M:F(ets:lookup(emqx_channel, ClientId));
lookup_client(Node, {clientid, ClientId}, FormatFun) ->
rpc_call(Node, lookup_client, [Node, {clientid, ClientId}, FormatFun]);
lookup_client(Node, {username, Username}, {M,F}) when Node =:= node() ->
MatchSpec = [{{'$1', #{clientinfo => #{username => '$2'}}, '_'}, [{'=:=','$2', Username}], ['$1']}],
M:F(ets:select(emqx_channel_info, MatchSpec));
lookup_client(Node, {username, Username}, FormatFun) ->
rpc_call(Node, lookup_client, [Node, {username, Username}, FormatFun]).
kickout_client(ClientId) ->
Results = [kickout_client(Node, ClientId) || Node <- ekka_mnesia:running_nodes()],
case lists:any(fun(Item) -> Item =:= ok end, Results) of
true -> ok;
false -> lists:last(Results)
end.
kickout_client(Node, ClientId) when Node =:= node() ->
emqx_cm:kick_session(ClientId);
kickout_client(Node, ClientId) ->
rpc_call(Node, kickout_client, [Node, ClientId]).
list_acl_cache(ClientId) ->
call_client(ClientId, list_acl_cache).
clean_acl_cache(ClientId) ->
Results = [clean_acl_cache(Node, ClientId) || Node <- ekka_mnesia:running_nodes()],
case lists:any(fun(Item) -> Item =:= ok end, Results) of
true -> ok;
false -> lists:last(Results)
end.
clean_acl_cache(Node, ClientId) when Node =:= node() ->
case emqx_cm:lookup_channels(ClientId) of
[] ->
{error, not_found};
Pids when is_list(Pids) ->
erlang:send(lists:last(Pids), clean_acl_cache),
ok
end;
clean_acl_cache(Node, ClientId) ->
rpc_call(Node, clean_acl_cache, [Node, ClientId]).
set_ratelimit_policy(ClientId, Policy) ->
call_client(ClientId, {ratelimit, Policy}).
set_quota_policy(ClientId, Policy) ->
call_client(ClientId, {quota, Policy}).
%% @private
call_client(ClientId, Req) ->
Results = [call_client(Node, ClientId, Req) || Node <- ekka_mnesia:running_nodes()],
Expected = lists:filter(fun({error, _}) -> false;
(_) -> true
end, Results),
case Expected of
[] -> {error, not_found};
[Result|_] -> Result
end.
%% @private
call_client(Node, ClientId, Req) when Node =:= node() ->
case emqx_cm:lookup_channels(ClientId) of
[] -> {error, not_found};
Pids when is_list(Pids) ->
Pid = lists:last(Pids),
case emqx_cm:get_chan_info(ClientId, Pid) of
#{conninfo := #{conn_mod := ConnMod}} ->
ConnMod:call(Pid, Req);
undefined -> {error, not_found}
end
end;
call_client(Node, ClientId, Req) ->
rpc_call(Node, call_client, [Node, ClientId, Req]).
%%--------------------------------------------------------------------
%% Subscriptions
%%--------------------------------------------------------------------
list_subscriptions(Node) when Node =:= node() ->
case check_row_limit([mqtt_subproperty]) of
false -> throw(max_row_limit);
ok -> [item(subscription, Sub) || Sub <- ets:tab2list(mqtt_subproperty)]
end;
list_subscriptions(Node) ->
rpc_call(Node, list_subscriptions, [Node]).
list_subscriptions_via_topic(Topic, FormatFun) ->
lists:append([list_subscriptions_via_topic(Node, Topic, FormatFun) || Node <- ekka_mnesia:running_nodes()]).
list_subscriptions_via_topic(Node, Topic, {M,F}) when Node =:= node() ->
MatchSpec = [{{{'_', '$1'}, '_'}, [{'=:=','$1', Topic}], ['$_']}],
M:F(ets:select(emqx_suboption, MatchSpec));
list_subscriptions_via_topic(Node, {topic, Topic}, FormatFun) ->
rpc_call(Node, list_subscriptions_via_topic, [Node, {topic, Topic}, FormatFun]).
lookup_subscriptions(ClientId) ->
lists:append([lookup_subscriptions(Node, ClientId) || Node <- ekka_mnesia:running_nodes()]).
lookup_subscriptions(Node, ClientId) when Node =:= node() ->
case ets:lookup(emqx_subid, ClientId) of
[] -> [];
[{_, Pid}] ->
ets:match_object(emqx_suboption, {{Pid, '_'}, '_'})
end;
lookup_subscriptions(Node, ClientId) ->
rpc_call(Node, lookup_subscriptions, [Node, ClientId]).
%%--------------------------------------------------------------------
%% Routes
%%--------------------------------------------------------------------
list_routes() ->
case check_row_limit(emqx_route) of
false -> throw(max_row_limit);
ok -> lists:append([ets:tab2list(Tab) || Tab <- emqx_route])
end.
lookup_routes(Topic) ->
emqx_router:lookup_routes(Topic).
%%--------------------------------------------------------------------
%% PubSub
%%--------------------------------------------------------------------
subscribe(ClientId, TopicTables) ->
subscribe(ekka_mnesia:running_nodes(), ClientId, TopicTables).
subscribe([Node | Nodes], ClientId, TopicTables) ->
case rpc_call(Node, do_subscribe, [ClientId, TopicTables]) of
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
Re -> Re
end;
subscribe([], _ClientId, _TopicTables) ->
{error, channel_not_found}.
do_subscribe(ClientId, TopicTables) ->
case ets:lookup(emqx_channel, ClientId) of
[] -> {error, channel_not_found};
[{_, Pid}] ->
Pid ! {subscribe, TopicTables}
end.
%%TODO: ???
publish(Msg) -> emqx:publish(Msg).
unsubscribe(ClientId, Topic) ->
unsubscribe(ekka_mnesia:running_nodes(), ClientId, Topic).
unsubscribe([Node | Nodes], ClientId, Topic) ->
case rpc_call(Node, do_unsubscribe, [ClientId, Topic]) of
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
Re -> Re
end;
unsubscribe([], _ClientId, _Topic) ->
{error, channel_not_found}.
do_unsubscribe(ClientId, Topic) ->
case ets:lookup(emqx_channel, ClientId) of
[] -> {error, channel_not_found};
[{_, Pid}] ->
Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
end.
%%--------------------------------------------------------------------
%% Plugins
%%--------------------------------------------------------------------
list_plugins() ->
[{Node, list_plugins(Node)} || Node <- ekka_mnesia:running_nodes()].
list_plugins(Node) when Node =:= node() ->
emqx_plugins:list();
list_plugins(Node) ->
rpc_call(Node, list_plugins, [Node]).
load_plugin(Node, Plugin) when Node =:= node() ->
emqx_plugins:load(Plugin);
load_plugin(Node, Plugin) ->
rpc_call(Node, load_plugin, [Node, Plugin]).
unload_plugin(Node, Plugin) when Node =:= node() ->
emqx_plugins:unload(Plugin);
unload_plugin(Node, Plugin) ->
rpc_call(Node, unload_plugin, [Node, Plugin]).
reload_plugin(Node, Plugin) when Node =:= node() ->
emqx_plugins:reload(Plugin);
reload_plugin(Node, Plugin) ->
rpc_call(Node, reload_plugin, [Node, Plugin]).
%%--------------------------------------------------------------------
%% Modules
%%--------------------------------------------------------------------
list_modules() ->
[{Node, list_modules(Node)} || Node <- ekka_mnesia:running_nodes()].
list_modules(Node) when Node =:= node() ->
emqx_modules:list();
list_modules(Node) ->
rpc_call(Node, list_modules, [Node]).
load_module(Node, Module) when Node =:= node() ->
emqx_modules:load(Module);
load_module(Node, Module) ->
rpc_call(Node, load_module, [Node, Module]).
unload_module(Node, Module) when Node =:= node() ->
emqx_modules:unload(Module);
unload_module(Node, Module) ->
rpc_call(Node, unload_module, [Node, Module]).
reload_module(Node, Module) when Node =:= node() ->
emqx_modules:reload(Module);
reload_module(Node, Module) ->
rpc_call(Node, reload_module, [Node, Module]).
%%--------------------------------------------------------------------
%% Listeners
%%--------------------------------------------------------------------
list_listeners() ->
[{Node, list_listeners(Node)} || Node <- ekka_mnesia:running_nodes()].
list_listeners(Node) when Node =:= node() ->
Tcp = lists:map(fun({{Protocol, ListenOn}, _Pid}) ->
#{protocol => Protocol,
listen_on => ListenOn,
acceptors => esockd:get_acceptors({Protocol, ListenOn}),
max_conns => esockd:get_max_connections({Protocol, ListenOn}),
current_conns => esockd:get_current_connections({Protocol, ListenOn}),
shutdown_count => esockd:get_shutdown_count({Protocol, ListenOn})}
end, esockd:listeners()),
Http = lists:map(fun({Protocol, Opts}) ->
#{protocol => Protocol,
listen_on => proplists:get_value(port, Opts),
acceptors => maps:get(num_acceptors, proplists:get_value(transport_options, Opts, #{}), 0),
max_conns => proplists:get_value(max_connections, Opts),
current_conns => proplists:get_value(all_connections, Opts),
shutdown_count => []}
end, ranch:info()),
Tcp ++ Http;
list_listeners(Node) ->
rpc_call(Node, list_listeners, [Node]).
%%--------------------------------------------------------------------
%% Get Alarms
%%--------------------------------------------------------------------
get_alarms(Type) ->
[{Node, get_alarms(Node, Type)} || Node <- ekka_mnesia:running_nodes()].
get_alarms(Node, Type) when Node =:= node() ->
emqx_alarm:get_alarms(Type);
get_alarms(Node, Type) ->
rpc_call(Node, get_alarms, [Node, Type]).
deactivate(Node, Name) when Node =:= node() ->
emqx_alarm:deactivate(Name);
deactivate(Node, Name) ->
rpc_call(Node, deactivate, [Node, Name]).
delete_all_deactivated_alarms() ->
[delete_all_deactivated_alarms(Node) || Node <- ekka_mnesia:running_nodes()].
delete_all_deactivated_alarms(Node) when Node =:= node() ->
emqx_alarm:delete_all_deactivated_alarms();
delete_all_deactivated_alarms(Node) ->
rpc_call(Node, delete_deactivated_alarms, [Node]).
%%--------------------------------------------------------------------
%% Banned API
%%--------------------------------------------------------------------
create_banned(Banned) ->
emqx_banned:create(Banned).
delete_banned(Who) ->
emqx_banned:delete(Who).
%%--------------------------------------------------------------------
%% Data Export and Import
%%--------------------------------------------------------------------
export_rules() ->
lists:map(fun({_, RuleId, _, RawSQL, _, _, _, _, _, _, Actions, Enabled, Desc}) ->
[{id, RuleId},
{rawsql, RawSQL},
{actions, actions_to_prop_list(Actions)},
{enabled, Enabled},
{description, Desc}]
end, emqx_rule_registry:get_rules()).
export_resources() ->
lists:foldl(fun({_, Id, Type, Config, CreatedAt, Desc}, Acc) ->
NCreatedAt = case CreatedAt of
undefined -> null;
_ -> CreatedAt
end,
[[{id, Id},
{type, Type},
{config, maps:to_list(Config)},
{created_at, NCreatedAt},
{description, Desc}] | Acc]
end, [], emqx_rule_registry:get_resources()).
export_blacklist() ->
lists:foldl(fun(#banned{who = Who, by = By, reason = Reason, at = At, until = Until}, Acc) ->
NWho = case Who of
{peerhost, Peerhost} -> {peerhost, inet:ntoa(Peerhost)};
_ -> Who
end,
[[{who, [NWho]}, {by, By}, {reason, Reason}, {at, At}, {until, Until}] | Acc]
end, [], ets:tab2list(emqx_banned)).
export_applications() ->
lists:foldl(fun({_, AppID, AppSecret, Name, Desc, Status, Expired}, Acc) ->
[[{id, AppID}, {secret, AppSecret}, {name, Name}, {desc, Desc}, {status, Status}, {expired, Expired}] | Acc]
end, [], ets:tab2list(mqtt_app)).
export_users() ->
lists:foldl(fun({_, Username, Password, Tags}, Acc) ->
[[{username, Username}, {password, base64:encode(Password)}, {tags, Tags}] | Acc]
end, [], ets:tab2list(mqtt_admin)).
export_auth_clientid() ->
case ets:info(emqx_auth_clientid) of
undefined -> [];
_ ->
lists:foldl(fun({_, ClientId, Password}, Acc) ->
[[{clientid, ClientId}, {password, Password}] | Acc]
end, [], ets:tab2list(emqx_auth_clientid))
end.
export_auth_username() ->
case ets:info(emqx_auth_username) of
undefined -> [];
_ ->
lists:foldl(fun({_, Username, Password}, Acc) ->
[[{username, Username}, {password, Password}] | Acc]
end, [], ets:tab2list(emqx_auth_username))
end.
export_auth_mnesia() ->
case ets:info(emqx_user) of
undefined -> [];
_ ->
lists:foldl(fun({_, Login, Password, IsSuperuser}, Acc) ->
[[{login, Login}, {password, Password}, {is_superuser, IsSuperuser}] | Acc]
end, [], ets:tab2list(emqx_user))
end.
export_acl_mnesia() ->
case ets:info(emqx_acl) of
undefined -> [];
_ ->
lists:foldl(fun({_, Login, Topic, Action, Allow}, Acc) ->
[[{login, Login}, {topic, Topic}, {action, Action}, {allow, Allow}] | Acc]
end, [], ets:tab2list(emqx_acl))
end.
export_schemas() ->
case ets:info(emqx_schema) of
undefined -> [];
_ ->
[emqx_schema_api:format_schema(Schema) || Schema <- emqx_schema_registry:get_all_schemas()]
end.
import_rules(Rules) ->
lists:foreach(fun(#{<<"id">> := RuleId,
<<"rawsql">> := RawSQL,
<<"actions">> := Actions,
<<"enabled">> := Enabled,
<<"description">> := Desc}) ->
Rule = #{
id => RuleId,
rawsql => RawSQL,
actions => map_to_actions(Actions),
enabled => Enabled,
description => Desc
},
try emqx_rule_engine:create_rule(Rule)
catch throw:{resource_not_initialized, _ResId} ->
emqx_rule_engine:create_rule(Rule#{enabled => false})
end
end, Rules).
import_resources(Reources) ->
lists:foreach(fun(#{<<"id">> := Id,
<<"type">> := Type,
<<"config">> := Config,
<<"created_at">> := CreatedAt,
<<"description">> := Desc}) ->
NCreatedAt = case CreatedAt of
null -> undefined;
_ -> CreatedAt
end,
emqx_rule_engine:create_resource(#{id => Id,
type => any_to_atom(Type),
config => Config,
created_at => NCreatedAt,
description => Desc})
end, Reources).
import_blacklist(Blacklist) ->
lists:foreach(fun(#{<<"who">> := Who,
<<"by">> := By,
<<"reason">> := Reason,
<<"at">> := At,
<<"until">> := Until}) ->
NWho = case Who of
#{<<"peerhost">> := Peerhost} ->
{ok, NPeerhost} = inet:parse_address(Peerhost),
{peerhost, NPeerhost};
#{<<"clientid">> := ClientId} -> {clientid, ClientId};
#{<<"username">> := Username} -> {username, Username}
end,
emqx_banned:create(#banned{who = NWho, by = By, reason = Reason, at = At, until = Until})
end, Blacklist).
import_applications(Apps) ->
lists:foreach(fun(#{<<"id">> := AppID,
<<"secret">> := AppSecret,
<<"name">> := Name,
<<"desc">> := Desc,
<<"status">> := Status,
<<"expired">> := Expired}) ->
NExpired = case is_integer(Expired) of
true -> Expired;
false -> undefined
end,
emqx_mgmt_auth:force_add_app(AppID, Name, AppSecret, Desc, Status, NExpired)
end, Apps).
import_users(Users) ->
lists:foreach(fun(#{<<"username">> := Username,
<<"password">> := Password,
<<"tags">> := Tags}) ->
NPassword = base64:decode(Password),
emqx_dashboard_admin:force_add_user(Username, NPassword, Tags)
end, Users).
import_auth_clientid(Lists) ->
case ets:info(emqx_auth_clientid) of
undefined -> ok;
_ ->
[ mnesia:dirty_write({emqx_auth_clientid, ClientId, Password}) || #{<<"clientid">> := ClientId,
<<"password">> := Password} <- Lists ]
end.
import_auth_username(Lists) ->
case ets:info(emqx_auth_username) of
undefined -> ok;
_ ->
[ mnesia:dirty_write({emqx_auth_username, Username, Password}) || #{<<"username">> := Username,
<<"password">> := Password} <- Lists ]
end.
import_auth_mnesia(Auths) ->
case ets:info(emqx_user) of
undefined -> ok;
_ ->
[ mnesia:dirty_write({emqx_user, Login, Password, IsSuperuser}) || #{<<"login">> := Login,
<<"password">> := Password,
<<"is_superuser">> := IsSuperuser} <- Auths ]
end.
import_acl_mnesia(Acls) ->
case ets:info(emqx_acl) of
undefined -> ok;
_ ->
[ mnesia:dirty_write({emqx_acl ,Login, Topic, Action, Allow}) || #{<<"login">> := Login,
<<"topic">> := Topic,
<<"action">> := Action,
<<"allow">> := Allow} <- Acls ]
end.
import_schemas(Schemas) ->
case ets:info(emqx_schema) of
undefined -> ok;
_ -> [emqx_schema_registry:add_schema(emqx_schema_api:make_schema_params(Schema)) || Schema <- Schemas]
end.
any_to_atom(L) when is_list(L) -> list_to_atom(L);
any_to_atom(B) when is_binary(B) -> binary_to_atom(B, utf8);
any_to_atom(A) when is_atom(A) -> A.
to_version(Version) when is_integer(Version) ->
integer_to_list(Version);
to_version(Version) when is_binary(Version) ->
binary_to_list(Version);
to_version(Version) when is_list(Version) ->
Version.
%%--------------------------------------------------------------------
%% Telemtry API
%%--------------------------------------------------------------------
enable_telemetry() ->
[enable_telemetry(Node) || Node <- ekka_mnesia:running_nodes()], ok.
enable_telemetry(Node) when Node =:= node() ->
emqx_telemetry:enable();
enable_telemetry(Node) ->
rpc_call(Node, enable_telemetry, [Node]).
disable_telemetry() ->
[disable_telemetry(Node) || Node <- ekka_mnesia:running_nodes()], ok.
disable_telemetry(Node) when Node =:= node() ->
emqx_telemetry:disable();
disable_telemetry(Node) ->
rpc_call(Node, disable_telemetry, [Node]).
get_telemetry_status() ->
[{enabled, emqx_telemetry:is_enabled()}].
get_telemetry_data() ->
emqx_telemetry:get_telemetry().
%%--------------------------------------------------------------------
%% Common Table API
%%--------------------------------------------------------------------
item(client, {ClientId, ChanPid}) ->
Attrs = case emqx_cm:get_chan_info(ClientId, ChanPid) of
undefined -> #{};
Attrs0 -> Attrs0
end,
Stats = case emqx_cm:get_chan_stats(ClientId, ChanPid) of
undefined -> #{};
Stats0 -> maps:from_list(Stats0)
end,
ClientInfo = maps:get(clientinfo, Attrs, #{}),
ConnInfo = maps:get(conninfo, Attrs, #{}),
Session = maps:get(session, Attrs, #{}),
Connected = case maps:get(conn_state, Attrs) of
connected -> true;
_ -> false
end,
NStats = Stats#{max_subscriptions => maps:get(subscriptions_max, Stats, 0),
max_inflight => maps:get(inflight_max, Stats, 0),
max_awaiting_rel => maps:get(awaiting_rel_max, Stats, 0),
max_mqueue => maps:get(mqueue_max, Stats, 0),
inflight => maps:get(inflight_cnt, Stats, 0),
awaiting_rel => maps:get(awaiting_rel_cnt, Stats, 0)},
lists:foldl(fun(Items, Acc) ->
maps:merge(Items, Acc)
end, #{connected => Connected},
[maps:with([ subscriptions_cnt, max_subscriptions,
inflight, max_inflight, awaiting_rel,
max_awaiting_rel, mqueue_len, mqueue_dropped,
max_mqueue, heap_size, reductions, mailbox_len,
recv_cnt, recv_msg, recv_oct, recv_pkt, send_cnt,
send_msg, send_oct, send_pkt], NStats),
maps:with([clientid, username, mountpoint, is_bridge, zone], ClientInfo),
maps:with([clean_start, keepalive, expiry_interval, proto_name,
proto_ver, peername, connected_at, disconnected_at], ConnInfo),
maps:with([created_at], Session)]);
item(subscription, {{Topic, ClientId}, Options}) ->
#{topic => Topic, clientid => ClientId, options => Options};
item(route, #route{topic = Topic, dest = Node}) ->
#{topic => Topic, node => Node};
item(route, {Topic, Node}) ->
#{topic => Topic, node => Node}.
%%--------------------------------------------------------------------
%% Internel Functions.
%%--------------------------------------------------------------------
rpc_call(Node, Fun, Args) ->
case rpc:call(Node, ?MODULE, Fun, Args) of
{badrpc, Reason} -> {error, Reason};
Res -> Res
end.
otp_rel() ->
lists:concat(["R", erlang:system_info(otp_release), "/", erlang:system_info(version)]).
check_row_limit(Tables) ->
check_row_limit(Tables, max_row_limit()).
check_row_limit([], _Limit) ->
ok;
check_row_limit([Tab|Tables], Limit) ->
case table_size(Tab) > Limit of
true -> false;
false -> check_row_limit(Tables, Limit)
end.
max_row_limit() ->
application:get_env(?APP, max_row_limit, ?MAX_ROW_LIMIT).
table_size(Tab) -> ets:info(Tab, size).
map_to_actions(Maps) ->
[map_to_action(M) || M <- Maps].
map_to_action(Map = #{<<"id">> := ActionInstId, <<"name">> := Name, <<"args">> := Args}) ->
#{id => ActionInstId,
name => any_to_atom(Name),
args => Args,
fallbacks => map_to_actions(maps:get(<<"fallbacks">>, Map, []))}.
actions_to_prop_list(Actions) ->
[action_to_prop_list(Act) || Act <- Actions].
action_to_prop_list({action_instance, ActionInstId, Name, FallbackActions, Args}) ->
[{id, ActionInstId},
{name, Name},
{fallbacks, actions_to_prop_list(FallbackActions)},
{args, Args}].

View File

@ -0,0 +1,326 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api).
-include_lib("stdlib/include/qlc.hrl").
-export([paginate/3]).
%% first_next query APIs
-export([ params2qs/2
, node_query/4
, cluster_query/3
, traverse_table/5
, select_table/5
]).
-export([do_query/5]).
paginate(Tables, Params, RowFun) ->
Qh = query_handle(Tables),
Count = count(Tables),
Page = page(Params),
Limit = limit(Params),
Cursor = qlc:cursor(Qh),
case Page > 1 of
true -> qlc:next_answers(Cursor, (Page - 1) * Limit);
false -> ok
end,
Rows = qlc:next_answers(Cursor, Limit),
qlc:delete_cursor(Cursor),
#{meta => #{page => Page, limit => Limit, count => Count},
data => [RowFun(Row) || Row <- Rows]}.
query_handle(Table) when is_atom(Table) ->
qlc:q([R|| R <- ets:table(Table)]);
query_handle([Table]) when is_atom(Table) ->
qlc:q([R|| R <- ets:table(Table)]);
query_handle(Tables) ->
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]).
count(Table) when is_atom(Table) ->
ets:info(Table, size);
count([Table]) when is_atom(Table) ->
ets:info(Table, size);
count(Tables) ->
lists:sum([count(T) || T <- Tables]).
count(Table, Nodes) ->
lists:sum([rpc_call(Node, ets, info, [Table, size], 5000) || Node <- Nodes]).
page(Params) ->
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
limit(Params) ->
case proplists:get_value(<<"_limit">>, Params) of
undefined -> emqx_mgmt:max_row_limit();
Size -> binary_to_integer(Size)
end.
%%--------------------------------------------------------------------
%% Node Query
%%--------------------------------------------------------------------
node_query(Node, Params, {Tab, QsSchema}, QueryFun) ->
{CodCnt, Qs} = params2qs(Params, QsSchema),
Limit = limit(Params),
Page = page(Params),
Start = if Page > 1 -> (Page-1) * Limit;
true -> 0
end,
{_, Rows} = do_query(Node, Qs, QueryFun, Start, Limit+1),
Meta = #{page => Page, limit => Limit},
NMeta = case CodCnt =:= 0 of
true -> Meta#{count => count(Tab), hasnext => length(Rows) > Limit};
_ -> Meta#{count => -1, hasnext => length(Rows) > Limit}
end,
#{meta => NMeta, data => lists:sublist(Rows, Limit)}.
%% @private
do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() ->
M:F(Qs, Start, Limit);
do_query(Node, Qs, QueryFun, Start, Limit) ->
rpc_call(Node, ?MODULE, do_query, [Node, Qs, QueryFun, Start, Limit], 50000).
%% @private
rpc_call(Node, M, F, A, T) ->
case rpc:call(Node, M, F, A, T) of
{badrpc, _} = R -> {error, R};
Res -> Res
end.
%%--------------------------------------------------------------------
%% Cluster Query
%%--------------------------------------------------------------------
cluster_query(Params, {Tab, QsSchema}, QueryFun) ->
{CodCnt, Qs} = params2qs(Params, QsSchema),
Limit = limit(Params),
Page = page(Params),
Start = if Page > 1 -> (Page-1) * Limit;
true -> 0
end,
Nodes = ekka_mnesia:running_nodes(),
Rows = do_cluster_query(Nodes, Qs, QueryFun, Start, Limit+1, []),
Meta = #{page => Page, limit => Limit},
NMeta = case CodCnt =:= 0 of
true -> Meta#{count => count(Tab, Nodes), hasnext => length(Rows) > Limit};
_ -> Meta#{count => -1, hasnext => length(Rows) > Limit}
end,
#{meta => NMeta, data => lists:sublist(Rows, Limit)}.
%% @private
do_cluster_query([], _, _, _, _, Acc) ->
lists:append(lists:reverse(Acc));
do_cluster_query([Node|Nodes], Qs, QueryFun, Start, Limit, Acc) ->
{NStart, Rows} = do_query(Node, Qs, QueryFun, Start, Limit),
case Limit - length(Rows) of
Rest when Rest > 0 ->
do_cluster_query(Nodes, Qs, QueryFun, NStart, Limit, [Rows|Acc]);
0 ->
lists:append(lists:reverse([Rows|Acc]))
end.
traverse_table(Tab, MatchFun, Start, Limit, FmtFun) ->
ets:safe_fixtable(Tab, true),
{NStart, Rows} = traverse_n_by_one(Tab, ets:first(Tab), MatchFun, Start, Limit, []),
ets:safe_fixtable(Tab, false),
{NStart, lists:map(FmtFun, Rows)}.
%% @private
traverse_n_by_one(_, '$end_of_table', _, Start, _, Acc) ->
{Start, lists:flatten(lists:reverse(Acc))};
traverse_n_by_one(_, _, _, Start, _Limit=0, Acc) ->
{Start, lists:flatten(lists:reverse(Acc))};
traverse_n_by_one(Tab, K, MatchFun, Start, Limit, Acc) ->
GetRows = fun _GetRows('$end_of_table', _, Ks) ->
{'$end_of_table', Ks};
_GetRows(Kn, 1, Ks) ->
{ets:next(Tab, Kn), [ets:lookup(Tab, Kn) | Ks]};
_GetRows(Kn, N, Ks) ->
_GetRows(ets:next(Tab, Kn), N-1, [ets:lookup(Tab, Kn) | Ks])
end,
{K2, Rows} = GetRows(K, 100, []),
case MatchFun(lists:flatten(lists:reverse(Rows))) of
[] ->
traverse_n_by_one(Tab, K2, MatchFun, Start, Limit, Acc);
Ls ->
case Start - length(Ls) of
N when N > 0 -> %% Skip
traverse_n_by_one(Tab, K2, MatchFun, N, Limit, Acc);
_ ->
Got = lists:sublist(Ls, Start+1, Limit),
NLimit = Limit - length(Got),
traverse_n_by_one(Tab, K2, MatchFun, 0, NLimit, [Got|Acc])
end
end.
select_table(Tab, Ms, 0, Limit, FmtFun) ->
case ets:select(Tab, Ms, Limit) of
'$end_of_table' ->
{0, []};
{Rows, _} ->
{0, lists:map(FmtFun, lists:reverse(Rows))}
end;
select_table(Tab, Ms, Start, Limit, FmtFun) ->
{NStart, Rows} = select_n_by_one(ets:select(Tab, Ms, Limit), Start, Limit, []),
{NStart, lists:map(FmtFun, Rows)}.
select_n_by_one('$end_of_table', Start, _Limit, Acc) ->
{Start, lists:flatten(lists:reverse(Acc))};
select_n_by_one(_, Start, _Limit = 0, Acc) ->
{Start, lists:flatten(lists:reverse(Acc))};
select_n_by_one({Rows0, Cons}, Start, Limit, Acc) ->
Rows = lists:reverse(Rows0),
case Start - length(Rows) of
N when N > 0 -> %% Skip
select_n_by_one(ets:select(Cons), N, Limit, Acc);
_ ->
Got = lists:sublist(Rows, Start+1, Limit),
NLimit = Limit - length(Got),
select_n_by_one(ets:select(Cons), 0, NLimit, [Got|Acc])
end.
params2qs(Params, QsSchema) ->
{Qs, Fuzzy} = pick_params_to_qs(Params, QsSchema, [], []),
{length(Qs) + length(Fuzzy), {Qs, Fuzzy}}.
%%--------------------------------------------------------------------
%% Intenal funcs
pick_params_to_qs([], _, Acc1, Acc2) ->
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
{lists:reverse(Acc1), lists:reverse(NAcc2)};
pick_params_to_qs([{Key, Value}|Params], QsKits, Acc1, Acc2) ->
case proplists:get_value(Key, QsKits) of
undefined -> pick_params_to_qs(Params, QsKits, Acc1, Acc2);
Type ->
case Key of
<<Prefix:5/binary, NKey/binary>>
when Prefix =:= <<"_gte_">>;
Prefix =:= <<"_lte_">> ->
OpposeKey = case Prefix of
<<"_gte_">> -> <<"_lte_", NKey/binary>>;
<<"_lte_">> -> <<"_gte_", NKey/binary>>
end,
case lists:keytake(OpposeKey, 1, Params) of
false ->
pick_params_to_qs(Params, QsKits, [qs(Key, Value, Type) | Acc1], Acc2);
{value, {K2, V2}, NParams} ->
pick_params_to_qs(NParams, QsKits, [qs(Key, Value, K2, V2, Type) | Acc1], Acc2)
end;
_ ->
case is_fuzzy_key(Key) of
true ->
pick_params_to_qs(Params, QsKits, Acc1, [qs(Key, Value, Type) | Acc2]);
_ ->
pick_params_to_qs(Params, QsKits, [qs(Key, Value, Type) | Acc1], Acc2)
end
end
end.
qs(<<"_gte_", Key/binary>>, Value, Type) ->
{binary_to_existing_atom(Key, utf8), '>=', to_type(Value, Type)};
qs(<<"_lte_", Key/binary>>, Value, Type) ->
{binary_to_existing_atom(Key, utf8), '=<', to_type(Value, Type)};
qs(<<"_like_", Key/binary>>, Value, Type) ->
{binary_to_existing_atom(Key, utf8), like, to_type(Value, Type)};
qs(<<"_match_", Key/binary>>, Value, Type) ->
{binary_to_existing_atom(Key, utf8), match, to_type(Value, Type)};
qs(Key, Value, Type) ->
{binary_to_existing_atom(Key, utf8), '=:=', to_type(Value, Type)}.
qs(K1, V1, K2, V2, Type) ->
{Key, Op1, NV1} = qs(K1, V1, Type),
{Key, Op2, NV2} = qs(K2, V2, Type),
{Key, Op1, NV1, Op2, NV2}.
is_fuzzy_key(<<"_like_", _/binary>>) ->
true;
is_fuzzy_key(<<"_match_", _/binary>>) ->
true;
is_fuzzy_key(_) ->
false.
%%--------------------------------------------------------------------
%% Types
to_type(V, atom) -> to_atom(V);
to_type(V, integer) -> to_integer(V);
to_type(V, timestamp) -> to_timestamp(V);
to_type(V, ip) -> aton(V);
to_type(V, _) -> V.
to_atom(A) when is_atom(A) ->
A;
to_atom(B) when is_binary(B) ->
binary_to_atom(B, utf8).
to_integer(I) when is_integer(I) ->
I;
to_integer(B) when is_binary(B) ->
binary_to_integer(B).
to_timestamp(I) when is_integer(I) ->
I;
to_timestamp(B) when is_binary(B) ->
binary_to_integer(B).
aton(B) when is_binary(B) ->
list_to_tuple([binary_to_integer(T) || T <- re:split(B, "[.]")]).
%%--------------------------------------------------------------------
%% EUnits
%%--------------------------------------------------------------------
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
params2qs_test() ->
Schema = [{<<"str">>, binary},
{<<"int">>, integer},
{<<"atom">>, atom},
{<<"ts">>, timestamp},
{<<"_gte_range">>, integer},
{<<"_lte_range">>, integer},
{<<"_like_fuzzy">>, binary},
{<<"_match_topic">>, binary}],
Params = [{<<"str">>, <<"abc">>},
{<<"int">>, <<"123">>},
{<<"atom">>, <<"connected">>},
{<<"ts">>, <<"156000">>},
{<<"_gte_range">>, <<"1">>},
{<<"_lte_range">>, <<"5">>},
{<<"_like_fuzzy">>, <<"user">>},
{<<"_match_topic">>, <<"t/#">>}],
ExpectedQs = [{str, '=:=', <<"abc">>},
{int, '=:=', 123},
{atom, '=:=', connected},
{ts, '=:=', 156000},
{range, '>=', 1, '=<', 5}
],
FuzzyQs = [{fuzzy, like, <<"user">>},
{topic, match, <<"t/#">>}],
?assertEqual({7, {ExpectedQs, FuzzyQs}}, params2qs(Params, Schema)),
{0, {[], []}} = params2qs([{not_a_predefined_params, val}], Schema).
-endif.

View File

@ -0,0 +1,137 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_alarms).
-include("emqx_mgmt.hrl").
-include_lib("emqx/include/emqx.hrl").
-rest_api(#{name => list_all_alarms,
method => 'GET',
path => "/alarms",
func => list,
descr => "List all alarms in the cluster"}).
-rest_api(#{name => list_node_alarms,
method => 'GET',
path => "nodes/:atom:node/alarms",
func => list,
descr => "List all alarms on a node"}).
-rest_api(#{name => list_all_activated_alarms,
method => 'GET',
path => "/alarms/activated",
func => list_activated,
descr => "List all activated alarm in the cluster"}).
-rest_api(#{name => list_node_activated_alarms,
method => 'GET',
path => "nodes/:atom:node/alarms/activated",
func => list_activated,
descr => "List all activated alarm on a node"}).
-rest_api(#{name => list_all_deactivated_alarms,
method => 'GET',
path => "/alarms/deactivated",
func => list_deactivated,
descr => "List all deactivated alarm in the cluster"}).
-rest_api(#{name => list_node_deactivated_alarms,
method => 'GET',
path => "nodes/:atom:node/alarms/deactivated",
func => list_deactivated,
descr => "List all deactivated alarm on a node"}).
-rest_api(#{name => deactivate_alarm,
method => 'POST',
path => "/alarms/deactivated",
func => deactivate,
descr => "Delete the special alarm on a node"}).
-rest_api(#{name => delete_all_deactivated_alarms,
method => 'DELETE',
path => "/alarms/deactivated",
func => delete_deactivated,
descr => "Delete all deactivated alarm in the cluster"}).
-rest_api(#{name => delete_node_deactivated_alarms,
method => 'DELETE',
path => "nodes/:atom:node/alarms/deactivated",
func => delete_deactivated,
descr => "Delete all deactivated alarm on a node"}).
-export([ list/2
, deactivate/2
, list_activated/2
, list_deactivated/2
, delete_deactivated/2
]).
list(Bindings, _Params) when map_size(Bindings) == 0 ->
{ok, #{code => ?SUCCESS,
data => [#{node => Node, alarms => Alarms} || {Node, Alarms} <- emqx_mgmt:get_alarms(all)]}};
list(#{node := Node}, _Params) ->
{ok, #{code => ?SUCCESS,
data => emqx_mgmt:get_alarms(Node, all)}}.
list_activated(Bindings, _Params) when map_size(Bindings) == 0 ->
{ok, #{code => ?SUCCESS,
data => [#{node => Node, alarms => Alarms} || {Node, Alarms} <- emqx_mgmt:get_alarms(activated)]}};
list_activated(#{node := Node}, _Params) ->
{ok, #{code => ?SUCCESS,
data => emqx_mgmt:get_alarms(Node, activated)}}.
list_deactivated(Bindings, _Params) when map_size(Bindings) == 0 ->
{ok, #{code => ?SUCCESS,
data => [#{node => Node, alarms => Alarms} || {Node, Alarms} <- emqx_mgmt:get_alarms(deactivated)]}};
list_deactivated(#{node := Node}, _Params) ->
{ok, #{code => ?SUCCESS,
data => emqx_mgmt:get_alarms(Node, deactivated)}}.
deactivate(_Bindings, Params) ->
Node = get_node(Params),
Name = get_name(Params),
do_deactivate(Node, Name).
delete_deactivated(Bindings, _Params) when map_size(Bindings) == 0 ->
emqx_mgmt:delete_all_deactivated_alarms(),
{ok, #{code => ?SUCCESS}};
delete_deactivated(#{node := Node}, _Params) ->
emqx_mgmt:delete_all_deactivated_alarms(Node),
{ok, #{code => ?SUCCESS}}.
get_node(Params) ->
binary_to_atom(proplists:get_value(<<"node">>, Params, undefined), utf8).
get_name(Params) ->
binary_to_atom(proplists:get_value(<<"name">>, Params, undefined), utf8).
do_deactivate(undefined, _) ->
minirest:return({error, missing_param});
do_deactivate(_, undefined) ->
minirest:return({error, missing_param});
do_deactivate(Node, Name) ->
case emqx_mgmt:deactivate(Node, Name) of
ok ->
minirest:return();
{error, Reason} ->
minirest:return({error, Reason})
end.

View File

@ -0,0 +1,109 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_apps).
-include("emqx_mgmt.hrl").
-import(proplists, [get_value/2]).
-import(minirest, [ return/0
, return/1
]).
-rest_api(#{name => add_app,
method => 'POST',
path => "/apps/",
func => add_app,
descr => "Add Application"}).
-rest_api(#{name => del_app,
method => 'DELETE',
path => "/apps/:bin:appid",
func => del_app,
descr => "Delete Application"}).
-rest_api(#{name => list_apps,
method => 'GET',
path => "/apps/",
func => list_apps,
descr => "List Applications"}).
-rest_api(#{name => lookup_app,
method => 'GET',
path => "/apps/:bin:appid",
func => lookup_app,
descr => "Lookup Application"}).
-rest_api(#{name => update_app,
method => 'PUT',
path => "/apps/:bin:appid",
func => update_app,
descr => "Update Application"}).
-export([ add_app/2
, del_app/2
, list_apps/2
, lookup_app/2
, update_app/2
]).
add_app(_Bindings, Params) ->
AppId = get_value(<<"app_id">>, Params),
Name = get_value(<<"name">>, Params),
Secret = get_value(<<"secret">>, Params),
Desc = get_value(<<"desc">>, Params),
Status = get_value(<<"status">>, Params),
Expired = get_value(<<"expired">>, Params),
case emqx_mgmt_auth:add_app(AppId, Name, Secret, Desc, Status, Expired) of
{ok, AppSecret} -> return({ok, #{secret => AppSecret}});
{error, Reason} -> return({error, ?ERROR2, Reason})
end.
del_app(#{appid := AppId}, _Params) ->
case emqx_mgmt_auth:del_app(AppId) of
ok -> return();
{error, Reason} -> return({error, ?ERROR2, Reason})
end.
list_apps(_Bindings, _Params) ->
return({ok, [format(Apps)|| Apps <- emqx_mgmt_auth:list_apps()]}).
lookup_app(#{appid := AppId}, _Params) ->
case emqx_mgmt_auth:lookup_app(AppId) of
{AppId, AppSecret, Name, Desc, Status, Expired} ->
return({ok, #{app_id => AppId,
secret => AppSecret,
name => Name,
desc => Desc,
status => Status,
expired => Expired}});
undefined ->
return({ok, #{}})
end.
update_app(#{appid := AppId}, Params) ->
Name = get_value(<<"name">>, Params),
Desc = get_value(<<"desc">>, Params),
Status = get_value(<<"status">>, Params),
Expired = get_value(<<"expired">>, Params),
case emqx_mgmt_auth:update_app(AppId, Name, Desc, Status, Expired) of
ok -> return();
{error, Reason} -> return({error, ?ERROR2, Reason})
end.
format({AppId, _AppSecret, Name, Desc, Status, Expired}) ->
[{app_id, AppId}, {name, Name}, {desc, Desc}, {status, Status}, {expired, Expired}].

View File

@ -0,0 +1,174 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_banned).
-include_lib("emqx/include/emqx.hrl").
-include("emqx_mgmt.hrl").
-import(proplists, [get_value/2]).
-import(minirest, [ return/0
, return/1
]).
-rest_api(#{name => list_banned,
method => 'GET',
path => "/banned/",
func => list,
descr => "List banned"}).
-rest_api(#{name => create_banned,
method => 'POST',
path => "/banned/",
func => create,
descr => "Create banned"}).
-rest_api(#{name => delete_banned,
method => 'DELETE',
path => "/banned/:as/:who",
func => delete,
descr => "Delete banned"}).
-export([ list/2
, create/2
, delete/2
]).
list(_Bindings, Params) ->
return({ok, emqx_mgmt_api:paginate(emqx_banned, Params, fun format/1)}).
create(_Bindings, Params) ->
case pipeline([fun ensure_required/1,
fun validate_params/1], Params) of
{ok, NParams} ->
{ok, Banned} = pack_banned(NParams),
ok = emqx_mgmt:create_banned(Banned),
return({ok, maps:from_list(Params)});
{error, Code, Message} ->
return({error, Code, Message})
end.
delete(#{as := As, who := Who}, _) ->
Params = [{<<"who">>, bin(emqx_mgmt_util:urldecode(Who))},
{<<"as">>, bin(emqx_mgmt_util:urldecode(As))}],
case pipeline([fun ensure_required/1,
fun validate_params/1], Params) of
{ok, NParams} ->
do_delete(get_value(<<"as">>, NParams), get_value(<<"who">>, NParams)),
return();
{error, Code, Message} ->
return({error, Code, Message})
end.
pipeline([], Params) ->
{ok, Params};
pipeline([Fun|More], Params) ->
case Fun(Params) of
{ok, NParams} ->
pipeline(More, NParams);
{error, Code, Message} ->
{error, Code, Message}
end.
%% Plugs
ensure_required(Params) when is_list(Params) ->
#{required_params := RequiredParams, message := Msg} = required_params(),
AllIncluded = lists:all(fun(Key) ->
lists:keymember(Key, 1, Params)
end, RequiredParams),
case AllIncluded of
true -> {ok, Params};
false ->
{error, ?ERROR7, Msg}
end.
validate_params(Params) ->
#{enum_values := AsEnums, message := Msg} = enum_values(as),
case lists:member(get_value(<<"as">>, Params), AsEnums) of
true -> {ok, Params};
false ->
{error, ?ERROR8, Msg}
end.
pack_banned(Params) ->
Now = erlang:system_time(second),
do_pack_banned(Params, #banned{by = <<"user">>,
at = Now,
until = Now + 300}).
do_pack_banned([], Banned) ->
{ok, Banned};
do_pack_banned([{<<"who">>, Who} | Params], Banned) ->
case lists:keytake(<<"as">>, 1, Params) of
{value, {<<"as">>, <<"peerhost">>}, Params2} ->
{ok, IPAddress} = inet:parse_address(str(Who)),
do_pack_banned(Params2, Banned#banned{who = {peerhost, IPAddress}});
{value, {<<"as">>, <<"clientid">>}, Params2} ->
do_pack_banned(Params2, Banned#banned{who = {clientid, Who}});
{value, {<<"as">>, <<"username">>}, Params2} ->
do_pack_banned(Params2, Banned#banned{who = {username, Who}})
end;
do_pack_banned([P1 = {<<"as">>, _}, P2 | Params], Banned) ->
do_pack_banned([P2, P1 | Params], Banned);
do_pack_banned([{<<"by">>, By} | Params], Banned) ->
do_pack_banned(Params, Banned#banned{by = By});
do_pack_banned([{<<"reason">>, Reason} | Params], Banned) ->
do_pack_banned(Params, Banned#banned{reason = Reason});
do_pack_banned([{<<"at">>, At} | Params], Banned) ->
do_pack_banned(Params, Banned#banned{at = At});
do_pack_banned([{<<"until">>, Until} | Params], Banned) ->
do_pack_banned(Params, Banned#banned{until = Until});
do_pack_banned([_P | Params], Banned) -> %% ingore other params
do_pack_banned(Params, Banned).
do_delete(<<"peerhost">>, Who) ->
{ok, IPAddress} = inet:parse_address(str(Who)),
emqx_mgmt:delete_banned({peerhost, IPAddress});
do_delete(<<"username">>, Who) ->
emqx_mgmt:delete_banned({username, bin(Who)});
do_delete(<<"clientid">>, Who) ->
emqx_mgmt:delete_banned({clientid, bin(Who)}).
required_params() ->
#{required_params => [<<"who">>, <<"as">>],
message => <<"missing mandatory params: ['who', 'as']">> }.
enum_values(as) ->
#{enum_values => [<<"clientid">>, <<"username">>, <<"peerhost">>],
message => <<"value of 'as' must be one of: ['clientid', 'username', 'peerhost']">> }.
%% Internal Functions
format(BannedList) when is_list(BannedList) ->
[format(Ban) || Ban <- BannedList];
format(#banned{who = {As, Who}, by = By, reason = Reason, at = At, until = Until}) ->
#{who => case As of
peerhost -> bin(inet:ntoa(Who));
_ -> Who
end,
as => As, by => By, reason => Reason, at => At, until => Until}.
bin(L) when is_list(L) ->
list_to_binary(L);
bin(B) when is_binary(B) ->
B.
str(B) when is_binary(B) ->
binary_to_list(B);
str(L) when is_list(L) ->
L.

View File

@ -0,0 +1,49 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_brokers).
-include("emqx_mgmt.hrl").
-import(minirest, [return/1]).
-rest_api(#{name => list_brokers,
method => 'GET',
path => "/brokers/",
func => list,
descr => "A list of brokers in the cluster"}).
-rest_api(#{name => get_broker,
method => 'GET',
path => "/brokers/:atom:node",
func => get,
descr => "Get broker info of a node"}).
-export([ list/2
, get/2
]).
list(_Bindings, _Params) ->
return({ok, [Info || {_Node, Info} <- emqx_mgmt:list_brokers()]}).
get(#{node := Node}, _Params) ->
case emqx_mgmt:lookup_broker(Node) of
{error, Reason} ->
return({error, ?ERROR2, Reason});
Info ->
return({ok, Info})
end.

View File

@ -0,0 +1,419 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_clients).
-include("emqx_mgmt.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx.hrl").
-import(minirest, [ return/0
, return/1
]).
-import(proplists, [get_value/2]).
-define(CLIENT_QS_SCHEMA, {emqx_channel_info,
[{<<"clientid">>, binary},
{<<"username">>, binary},
{<<"zone">>, atom},
{<<"ip_address">>, ip},
{<<"conn_state">>, atom},
{<<"clean_start">>, atom},
{<<"proto_name">>, binary},
{<<"proto_ver">>, integer},
{<<"_like_clientid">>, binary},
{<<"_like_username">>, binary},
{<<"_gte_created_at">>, timestamp},
{<<"_lte_created_at">>, timestamp},
{<<"_gte_connected_at">>, timestamp},
{<<"_lte_connected_at">>, timestamp}]}).
-rest_api(#{name => list_clients,
method => 'GET',
path => "/clients/",
func => list,
descr => "A list of clients on current node"}).
-rest_api(#{name => list_node_clients,
method => 'GET',
path => "nodes/:atom:node/clients/",
func => list,
descr => "A list of clients on specified node"}).
-rest_api(#{name => lookup_client,
method => 'GET',
path => "/clients/:bin:clientid",
func => lookup,
descr => "Lookup a client in the cluster"}).
-rest_api(#{name => lookup_node_client,
method => 'GET',
path => "nodes/:atom:node/clients/:bin:clientid",
func => lookup,
descr => "Lookup a client on the node"}).
-rest_api(#{name => lookup_client_via_username,
method => 'GET',
path => "/clients/username/:bin:username",
func => lookup,
descr => "Lookup a client via username in the cluster"
}).
-rest_api(#{name => lookup_node_client_via_username,
method => 'GET',
path => "/nodes/:atom:node/clients/username/:bin:username",
func => lookup,
descr => "Lookup a client via username on the node "
}).
-rest_api(#{name => kickout_client,
method => 'DELETE',
path => "/clients/:bin:clientid",
func => kickout,
descr => "Kick out the client in the cluster"}).
-rest_api(#{name => clean_acl_cache,
method => 'DELETE',
path => "/clients/:bin:clientid/acl_cache",
func => clean_acl_cache,
descr => "Clear the ACL cache of a specified client in the cluster"}).
-rest_api(#{name => list_acl_cache,
method => 'GET',
path => "/clients/:bin:clientid/acl_cache",
func => list_acl_cache,
descr => "List the ACL cache of a specified client in the cluster"}).
-rest_api(#{name => set_ratelimit_policy,
method => 'POST',
path => "/clients/:bin:clientid/ratelimit",
func => set_ratelimit_policy,
descr => "Set the client ratelimit policy"}).
-rest_api(#{name => clean_ratelimit,
method => 'DELETE',
path => "/clients/:bin:clientid/ratelimit",
func => clean_ratelimit,
descr => "Clear the ratelimit policy"}).
-rest_api(#{name => set_quota_policy,
method => 'POST',
path => "/clients/:bin:clientid/quota",
func => set_quota_policy,
descr => "Set the client quota policy"}).
-rest_api(#{name => clean_quota,
method => 'DELETE',
path => "/clients/:bin:clientid/quota",
func => clean_quota,
descr => "Clear the quota policy"}).
-import(emqx_mgmt_util, [ ntoa/1
, strftime/1
]).
-export([ list/2
, lookup/2
, kickout/2
, clean_acl_cache/2
, list_acl_cache/2
, set_ratelimit_policy/2
, set_quota_policy/2
, clean_ratelimit/2
, clean_quota/2
]).
-export([ query/3
, format/1
]).
-define(query_fun, {?MODULE, query}).
-define(format_fun, {?MODULE, format}).
list(Bindings, Params) when map_size(Bindings) == 0 ->
return({ok, emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun)});
list(#{node := Node}, Params) when Node =:= node() ->
return({ok, emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun)});
list(Bindings = #{node := Node}, Params) ->
case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of
{badrpc, Reason} -> return({error, ?ERROR1, Reason});
Res -> Res
end.
lookup(#{node := Node, clientid := ClientId}, _Params) ->
return({ok, emqx_mgmt:lookup_client(Node, {clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)});
lookup(#{clientid := ClientId}, _Params) ->
return({ok, emqx_mgmt:lookup_client({clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)});
lookup(#{node := Node, username := Username}, _Params) ->
return({ok, emqx_mgmt:lookup_client(Node, {username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)});
lookup(#{username := Username}, _Params) ->
return({ok, emqx_mgmt:lookup_client({username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}).
kickout(#{clientid := ClientId}, _Params) ->
case emqx_mgmt:kickout_client(emqx_mgmt_util:urldecode(ClientId)) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end.
clean_acl_cache(#{clientid := ClientId}, _Params) ->
case emqx_mgmt:clean_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end.
list_acl_cache(#{clientid := ClientId}, _Params) ->
case emqx_mgmt:list_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason});
Caches -> return({ok, [format_acl_cache(Cache) || Cache <- Caches]})
end.
set_ratelimit_policy(#{clientid := ClientId}, Params) ->
P = [{conn_bytes_in, get_value(<<"conn_bytes_in">>, Params)},
{conn_messages_in, get_value(<<"conn_messages_in">>, Params)}],
case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of
[] -> return();
Policy ->
case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end
end.
clean_ratelimit(#{clientid := ClientId}, _Params) ->
case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), []) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end.
set_quota_policy(#{clientid := ClientId}, Params) ->
P = [{conn_messages_routing, get_value(<<"conn_messages_routing">>, Params)}],
case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of
[] -> return();
Policy ->
case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end
end.
clean_quota(#{clientid := ClientId}, _Params) ->
case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), []) of
ok -> return();
{error, not_found} -> return({error, ?ERROR12, not_found});
{error, Reason} -> return({error, ?ERROR1, Reason})
end.
%% @private
%% S = 100,1s
%% | 100KB, 1m
parse_ratelimit_str(S) when is_binary(S) ->
parse_ratelimit_str(binary_to_list(S));
parse_ratelimit_str(S) ->
[L, D] = string:tokens(S, ", "),
Limit = case cuttlefish_bytesize:parse(L) of
Sz when is_integer(Sz) -> Sz;
{error, Reason1} -> error(Reason1)
end,
Duration = case cuttlefish_duration:parse(D, s) of
Secs when is_integer(Secs) -> Secs;
{error, Reason} -> error(Reason)
end,
{Limit, Duration}.
%%--------------------------------------------------------------------
%% Format
format(Items) when is_list(Items) ->
[format(Item) || Item <- Items];
format(Key) when is_tuple(Key) ->
format(emqx_mgmt:item(client, Key));
format(Data) when is_map(Data)->
{IpAddr, Port} = maps:get(peername, Data),
ConnectedAt = maps:get(connected_at, Data),
CreatedAt = maps:get(created_at, Data),
Data1 = maps:without([peername], Data),
maps:merge(Data1#{node => node(),
ip_address => iolist_to_binary(ntoa(IpAddr)),
port => Port,
connected_at => iolist_to_binary(strftime(ConnectedAt div 1000)),
created_at => iolist_to_binary(strftime(CreatedAt div 1000))},
case maps:get(disconnected_at, Data, undefined) of
undefined -> #{};
DisconnectedAt -> #{disconnected_at => iolist_to_binary(strftime(DisconnectedAt div 1000))}
end).
format_acl_cache({{PubSub, Topic}, {AclResult, Timestamp}}) ->
#{access => PubSub,
topic => Topic,
result => AclResult,
updated_time => Timestamp}.
%%--------------------------------------------------------------------
%% Query Functions
%%--------------------------------------------------------------------
query({Qs, []}, Start, Limit) ->
Ms = qs2ms_k(Qs),
emqx_mgmt_api:select_table(emqx_channel_info, Ms, Start, Limit, fun format/1);
query({Qs, Fuzzy}, Start, Limit) ->
Ms = qs2ms(Qs),
MatchFun = match_fun(Ms, Fuzzy),
emqx_mgmt_api:traverse_table(emqx_channel_info, MatchFun, Start, Limit, fun format/1).
%%--------------------------------------------------------------------
%% Match funcs
match_fun(Ms, Fuzzy) ->
MsC = ets:match_spec_compile(Ms),
REFuzzy = lists:map(fun({K, like, S}) ->
{ok, RE} = re:compile(S),
{K, like, RE}
end, Fuzzy),
fun(Rows) ->
case ets:match_spec_run(Rows, MsC) of
[] -> [];
Ls ->
lists:filtermap(fun(E) ->
case run_fuzzy_match(E, REFuzzy) of
false -> false;
true -> {true, element(1, E)}
end end, Ls)
end
end.
run_fuzzy_match(_, []) ->
true;
run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) ->
Val = case maps:get(Key, ClientInfo, "") of
undefined -> "";
V -> V
end,
re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy).
%%--------------------------------------------------------------------
%% QueryString to Match Spec
-spec qs2ms(list()) -> ets:match_spec().
qs2ms(Qs) ->
{MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}),
[{{'$1', MtchHead, '_'}, Conds, ['$_']}].
qs2ms_k(Qs) ->
{MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}),
[{{'$1', MtchHead, '_'}, Conds, ['$1']}].
qs2ms([], _, {MtchHead, Conds}) ->
{MtchHead, lists:reverse(Conds)};
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
qs2ms(Rest, N, {NMtchHead, Conds});
qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
NConds = put_conds(Qs, Holder, Conds),
qs2ms(Rest, N+1, {NMtchHead, NConds}).
put_conds({_, Op, V}, Holder, Conds) ->
[{Op, Holder, V} | Conds];
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
[{Op2, Holder, V2},
{Op1, Holder, V1} | Conds].
ms(clientid, X) ->
#{clientinfo => #{clientid => X}};
ms(username, X) ->
#{clientinfo => #{username => X}};
ms(zone, X) ->
#{clientinfo => #{zone => X}};
ms(ip_address, X) ->
#{clientinfo => #{peerhost => X}};
ms(conn_state, X) ->
#{conn_state => X};
ms(clean_start, X) ->
#{conninfo => #{clean_start => X}};
ms(proto_name, X) ->
#{conninfo => #{proto_name => X}};
ms(proto_ver, X) ->
#{conninfo => #{proto_ver => X}};
ms(connected_at, X) ->
#{conninfo => #{connected_at => X}};
ms(created_at, X) ->
#{session => #{created_at => X}}.
%%--------------------------------------------------------------------
%% EUnits
%%--------------------------------------------------------------------
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
params2qs_test() ->
QsSchema = element(2, ?CLIENT_QS_SCHEMA),
Params = [{<<"clientid">>, <<"abc">>},
{<<"username">>, <<"def">>},
{<<"zone">>, <<"external">>},
{<<"ip_address">>, <<"127.0.0.1">>},
{<<"conn_state">>, <<"connected">>},
{<<"clean_start">>, true},
{<<"proto_name">>, <<"MQTT">>},
{<<"proto_ver">>, 4},
{<<"_gte_created_at">>, 1},
{<<"_lte_created_at">>, 5},
{<<"_gte_connected_at">>, 1},
{<<"_lte_connected_at">>, 5},
{<<"_like_clientid">>, <<"a">>},
{<<"_like_username">>, <<"e">>}
],
ExpectedMtchHead =
#{clientinfo => #{clientid => <<"abc">>,
username => <<"def">>,
zone => external,
peerhost => {127,0,0,1}
},
conn_state => connected,
conninfo => #{clean_start => true,
proto_name => <<"MQTT">>,
proto_ver => 4,
connected_at => '$3'},
session => #{created_at => '$2'}},
ExpectedCondi = [{'>=','$2', 1},
{'=<','$2', 5},
{'>=','$3', 1},
{'=<','$3', 5}],
{10, {Qs1, []}} = emqx_mgmt_api:params2qs(Params, QsSchema),
[{{'$1', MtchHead, _}, Condi, _}] = qs2ms(Qs1),
?assertEqual(ExpectedMtchHead, MtchHead),
?assertEqual(ExpectedCondi, Condi),
[{{'$1', #{}, '_'}, [], ['$_']}] = qs2ms([]),
[{{'$1', #{}, '_'}, [], ['$1']}] = qs2ms_k([]).
-endif.

View File

@ -0,0 +1,217 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_data).
-include_lib("emqx/include/emqx.hrl").
-include_lib("kernel/include/file.hrl").
-include("emqx_mgmt.hrl").
-import(minirest, [ return/0
, return/1
]).
-rest_api(#{name => export,
method => 'POST',
path => "/data/export",
func => export,
descr => "Export data"}).
-rest_api(#{name => list_exported,
method => 'GET',
path => "/data/export",
func => list_exported,
descr => "List exported file"}).
-rest_api(#{name => import,
method => 'POST',
path => "/data/import",
func => import,
descr => "Import data"}).
-rest_api(#{name => download,
method => 'GET',
path => "/data/file/:filename",
func => download,
descr => "Download data file to local"}).
-rest_api(#{name => upload,
method => 'POST',
path => "/data/file",
func => upload,
descr => "Upload data file from local"}).
-rest_api(#{name => delete,
method => 'DELETE',
path => "/data/file/:filename",
func => delete,
descr => "Delete data file"}).
-export([ export/2
, list_exported/2
, import/2
, download/2
, upload/2
, delete/2
]).
export(_Bindings, _Params) ->
Rules = emqx_mgmt:export_rules(),
Resources = emqx_mgmt:export_resources(),
Blacklist = emqx_mgmt:export_blacklist(),
Apps = emqx_mgmt:export_applications(),
Users = emqx_mgmt:export_users(),
AuthClientid = emqx_mgmt:export_auth_clientid(),
AuthUsername = emqx_mgmt:export_auth_username(),
AuthMnesia = emqx_mgmt:export_auth_mnesia(),
AclMnesia = emqx_mgmt:export_acl_mnesia(),
Schemas = emqx_mgmt:export_schemas(),
Seconds = erlang:system_time(second),
{{Y, M, D}, {H, MM, S}} = emqx_mgmt_util:datetime(Seconds),
Filename = io_lib:format("emqx-export-~p-~p-~p-~p-~p-~p.json", [Y, M, D, H, MM, S]),
NFilename = filename:join([emqx:get_env(data_dir), Filename]),
Version = string:sub_string(emqx_sys:version(), 1, 3),
Data = [{version, erlang:list_to_binary(Version)},
{date, erlang:list_to_binary(emqx_mgmt_util:strftime(Seconds))},
{rules, Rules},
{resources, Resources},
{blacklist, Blacklist},
{apps, Apps},
{users, Users},
{auth_clientid, AuthClientid},
{auth_username, AuthUsername},
{auth_mnesia, AuthMnesia},
{acl_mnesia, AclMnesia},
{schemas, Schemas}],
Bin = emqx_json:encode(Data),
case file:write_file(NFilename, Bin) of
ok ->
case file:read_file_info(NFilename) of
{ok, #file_info{size = Size, ctime = {{Y, M, D}, {H, MM, S}}}} ->
CreatedAt = io_lib:format("~p-~p-~p ~p:~p:~p", [Y, M, D, H, MM, S]),
return({ok, [{filename, list_to_binary(Filename)},
{size, Size},
{created_at, list_to_binary(CreatedAt)}]});
{error, Reason} ->
return({error, Reason})
end;
{error, Reason} ->
return({error, Reason})
end.
list_exported(_Bindings, _Params) ->
Dir = emqx:get_env(data_dir),
{ok, Files} = file:list_dir_all(Dir),
List = lists:foldl(fun(File, Acc) ->
case filename:extension(File) =:= ".json" of
true ->
FullFile = filename:join([Dir, File]),
case file:read_file_info(FullFile) of
{ok, #file_info{size = Size, ctime = CTime = {{Y, M, D}, {H, MM, S}}}} ->
CreatedAt = io_lib:format("~p-~p-~p ~p:~p:~p", [Y, M, D, H, MM, S]),
Seconds = calendar:datetime_to_gregorian_seconds(CTime),
[{Seconds, [{filename, list_to_binary(File)},
{size, Size},
{created_at, list_to_binary(CreatedAt)}]} | Acc];
{error, Reason} ->
logger:error("Read file info of ~s failed with: ~p", [File, Reason]),
Acc
end;
false ->
Acc
end
end, [], Files),
NList = lists:map(fun({_, FileInfo}) -> FileInfo end, lists:keysort(1, List)),
return({ok, NList}).
import(_Bindings, Params) ->
case proplists:get_value(<<"filename">>, Params) of
undefined ->
return({error, missing_required_params});
Filename ->
FullFilename = filename:join([emqx:get_env(data_dir), Filename]),
case file:read_file(FullFilename) of
{ok, Json} ->
Data = emqx_json:decode(Json, [return_maps]),
Version = emqx_mgmt:to_version(maps:get(<<"version">>, Data)),
case lists:member(Version, ?VERSIONS) of
true ->
try
emqx_mgmt:import_resources(maps:get(<<"resources">>, Data, [])),
emqx_mgmt:import_rules(maps:get(<<"rules">>, Data, [])),
emqx_mgmt:import_blacklist(maps:get(<<"blacklist">>, Data, [])),
emqx_mgmt:import_applications(maps:get(<<"apps">>, Data, [])),
emqx_mgmt:import_users(maps:get(<<"users">>, Data, [])),
emqx_mgmt:import_auth_clientid(maps:get(<<"auth_clientid">>, Data, [])),
emqx_mgmt:import_auth_username(maps:get(<<"auth_username">>, Data, [])),
emqx_mgmt:import_auth_mnesia(maps:get(<<"auth_mnesia">>, Data, [])),
emqx_mgmt:import_acl_mnesia(maps:get(<<"acl_mnesia">>, Data, [])),
emqx_mgmt:import_schemas(maps:get(<<"schemas">>, Data, [])),
logger:debug("The emqx data has been imported successfully"),
return()
catch Class:Reason:Stack ->
logger:error("The emqx data import failed: ~0p", [{Class,Reason,Stack}]),
return({error, import_failed})
end;
false ->
logger:error("Unsupported version: ~p", [Version]),
return({error, unsupported_version})
end;
{error, Reason} ->
return({error, Reason})
end
end.
download(#{filename := Filename}, _Params) ->
FullFilename = filename:join([emqx:get_env(data_dir), Filename]),
case file:read_file(FullFilename) of
{ok, Bin} ->
{ok, #{filename => list_to_binary(Filename),
file => Bin}};
{error, Reason} ->
return({error, Reason})
end.
upload(Bindings, Params) ->
do_upload(Bindings, maps:from_list(Params)).
do_upload(_Bindings, #{<<"filename">> := Filename,
<<"file">> := Bin}) ->
FullFilename = filename:join([emqx:get_env(data_dir), Filename]),
case file:write_file(FullFilename, Bin) of
ok ->
return();
{error, Reason} ->
return({error, Reason})
end;
do_upload(Bindings, Params = #{<<"file">> := _}) ->
Seconds = erlang:system_time(second),
{{Y, M, D}, {H, MM, S}} = emqx_mgmt_util:datetime(Seconds),
Filename = io_lib:format("emqx-export-~p-~p-~p-~p-~p-~p.json", [Y, M, D, H, MM, S]),
do_upload(Bindings, Params#{<<"filename">> => Filename});
do_upload(_Bindings, _Params) ->
return({error, missing_required_params}).
delete(#{filename := Filename}, _Params) ->
FullFilename = filename:join([emqx:get_env(data_dir), Filename]),
case file:delete(FullFilename) of
ok ->
return();
{error, Reason} ->
return({error, Reason})
end.

View File

@ -0,0 +1,49 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_listeners).
-import(minirest, [return/1]).
-rest_api(#{name => list_listeners,
method => 'GET',
path => "/listeners/",
func => list,
descr => "A list of listeners in the cluster"}).
-rest_api(#{name => list_node_listeners,
method => 'GET',
path => "/nodes/:atom:node/listeners",
func => list,
descr => "A list of listeners on the node"}).
-export([list/2]).
%% List listeners on a node.
list(#{node := Node}, _Params) ->
return({ok, format(emqx_mgmt:list_listeners(Node))});
%% List listeners in the cluster.
list(_Binding, _Params) ->
return({ok, [#{node => Node, listeners => format(Listeners)}
|| {Node, Listeners} <- emqx_mgmt:list_listeners()]}).
format(Listeners) when is_list(Listeners) ->
[ Info#{listen_on => list_to_binary(esockd:to_string(ListenOn))}
|| Info = #{listen_on := ListenOn} <- Listeners ];
format({error, Reason}) -> [{error, Reason}].

View File

@ -0,0 +1,44 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_metrics).
-import(minirest, [return/1]).
-rest_api(#{name => list_all_metrics,
method => 'GET',
path => "/metrics",
func => list,
descr => "A list of metrics of all nodes in the cluster"}).
-rest_api(#{name => list_node_metrics,
method => 'GET',
path => "/nodes/:atom:node/metrics",
func => list,
descr => "A list of metrics of a node"}).
-export([list/2]).
list(Bindings, _Params) when map_size(Bindings) == 0 ->
return({ok, [#{node => Node, metrics => maps:from_list(Metrics)}
|| {Node, Metrics} <- emqx_mgmt:get_metrics()]});
list(#{node := Node}, _Params) ->
case emqx_mgmt:get_metrics(Node) of
{error, Reason} -> return({error, Reason});
Metrics -> return({ok, maps:from_list(Metrics)})
end.

View File

@ -0,0 +1,129 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_modules).
-include("emqx_mgmt.hrl").
-import(minirest, [return/1]).
-rest_api(#{name => list_all_modules,
method => 'GET',
path => "/modules/",
func => list,
descr => "List all modules in the cluster"}).
-rest_api(#{name => list_node_modules,
method => 'GET',
path => "/nodes/:atom:node/modules/",
func => list,
descr => "List all modules on a node"}).
-rest_api(#{name => load_node_module,
method => 'PUT',
path => "/nodes/:atom:node/modules/:atom:module/load",
func => load,
descr => "Load a module"}).
-rest_api(#{name => unload_node_module,
method => 'PUT',
path => "/nodes/:atom:node/modules/:atom:module/unload",
func => unload,
descr => "Unload a module"}).
-rest_api(#{name => reload_node_module,
method => 'PUT',
path => "/nodes/:atom:node/modules/:atom:module/reload",
func => reload,
descr => "Reload a module"}).
-rest_api(#{name => load_module,
method => 'PUT',
path => "/modules/:atom:module/load",
func => load,
descr => "load a module in the cluster"}).
-rest_api(#{name => unload_module,
method => 'PUT',
path => "/modules/:atom:module/unload",
func => unload,
descr => "Unload a module in the cluster"}).
-rest_api(#{name => reload_module,
method => 'PUT',
path => "/modules/:atom:module/reload",
func => reload,
descr => "Reload a module in the cluster"}).
-export([ list/2
, load/2
, unload/2
, reload/2
]).
list(#{node := Node}, _Params) ->
return({ok, [format(Module) || Module <- emqx_mgmt:list_modules(Node)]});
list(_Bindings, _Params) ->
return({ok, [format(Node, Modules) || {Node, Modules} <- emqx_mgmt:list_modules()]}).
load(#{node := Node, module := Module}, _Params) ->
return(emqx_mgmt:load_module(Node, Module));
load(#{module := Module}, _Params) ->
Results = [emqx_mgmt:load_module(Node, Module) || {Node, _Info} <- emqx_mgmt:list_nodes()],
case lists:filter(fun(Item) -> Item =/= ok end, Results) of
[] ->
return(ok);
Errors ->
return(lists:last(Errors))
end.
unload(#{node := Node, module := Module}, _Params) ->
return(emqx_mgmt:unload_module(Node, Module));
unload(#{module := Module}, _Params) ->
Results = [emqx_mgmt:unload_module(Node, Module) || {Node, _Info} <- emqx_mgmt:list_nodes()],
case lists:filter(fun(Item) -> Item =/= ok end, Results) of
[] ->
return(ok);
Errors ->
return(lists:last(Errors))
end.
reload(#{node := Node, module := Module}, _Params) ->
case emqx_mgmt:reload_module(Node, Module) of
ignore -> return(ok);
Result -> return(Result)
end;
reload(#{module := Module}, _Params) ->
Results = [emqx_mgmt:reload_module(Node, Module) || {Node, _Info} <- emqx_mgmt:list_nodes()],
case lists:filter(fun(Item) -> Item =/= ok end, Results) of
[] ->
return(ok);
Errors ->
return(lists:last(Errors))
end.
format(Node, Modules) ->
#{node => Node, modules => [format(Module) || Module <- Modules]}.
format({Name, Active}) ->
#{name => Name,
description => iolist_to_binary(Name:description()),
active => Active}.

View File

@ -0,0 +1,48 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_nodes).
-import(minirest, [return/1]).
-rest_api(#{name => list_nodes,
method => 'GET',
path => "/nodes/",
func => list,
descr => "A list of nodes in the cluster"}).
-rest_api(#{name => get_node,
method => 'GET',
path => "/nodes/:atom:node",
func => get,
descr => "Lookup a node in the cluster"}).
-export([ list/2
, get/2
]).
list(_Bindings, _Params) ->
return({ok, [format(Node, Info) || {Node, Info} <- emqx_mgmt:list_nodes()]}).
get(#{node := Node}, _Params) ->
return({ok, emqx_mgmt:lookup_node(Node)}).
format(Node, {error, Reason}) -> #{node => Node, error => Reason};
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
Info#{memory_total := emqx_mgmt_util:kmg(Total),
memory_used := emqx_mgmt_util:kmg(Used)}.

View File

@ -0,0 +1,117 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_plugins).
-include("emqx_mgmt.hrl").
-include_lib("emqx/include/emqx.hrl").
-import(minirest, [return/1]).
-rest_api(#{name => list_all_plugins,
method => 'GET',
path => "/plugins/",
func => list,
descr => "List all plugins in the cluster"}).
-rest_api(#{name => list_node_plugins,
method => 'GET',
path => "/nodes/:atom:node/plugins/",
func => list,
descr => "List all plugins on a node"}).
-rest_api(#{name => load_node_plugin,
method => 'PUT',
path => "/nodes/:atom:node/plugins/:atom:plugin/load",
func => load,
descr => "Load a plugin"}).
-rest_api(#{name => unload_node_plugin,
method => 'PUT',
path => "/nodes/:atom:node/plugins/:atom:plugin/unload",
func => unload,
descr => "Unload a plugin"}).
-rest_api(#{name => reload_node_plugin,
method => 'PUT',
path => "/nodes/:atom:node/plugins/:atom:plugin/reload",
func => reload,
descr => "Reload a plugin"}).
-rest_api(#{name => unload_plugin,
method => 'PUT',
path => "/plugins/:atom:plugin/unload",
func => unload,
descr => "Unload a plugin in the cluster"}).
-rest_api(#{name => reload_plugin,
method => 'PUT',
path => "/plugins/:atom:plugin/reload",
func => reload,
descr => "Reload a plugin in the cluster"}).
-export([ list/2
, load/2
, unload/2
, reload/2
]).
list(#{node := Node}, _Params) ->
return({ok, [format(Plugin) || Plugin <- emqx_mgmt:list_plugins(Node)]});
list(_Bindings, _Params) ->
return({ok, [format({Node, Plugins}) || {Node, Plugins} <- emqx_mgmt:list_plugins()]}).
load(#{node := Node, plugin := Plugin}, _Params) ->
return(emqx_mgmt:load_plugin(Node, Plugin)).
unload(#{node := Node, plugin := Plugin}, _Params) ->
return(emqx_mgmt:unload_plugin(Node, Plugin));
unload(#{plugin := Plugin}, _Params) ->
Results = [emqx_mgmt:unload_plugin(Node, Plugin) || {Node, _Info} <- emqx_mgmt:list_nodes()],
case lists:filter(fun(Item) -> Item =/= ok end, Results) of
[] ->
return(ok);
Errors ->
return(lists:last(Errors))
end.
reload(#{node := Node, plugin := Plugin}, _Params) ->
return(emqx_mgmt:reload_plugin(Node, Plugin));
reload(#{plugin := Plugin}, _Params) ->
Results = [emqx_mgmt:reload_plugin(Node, Plugin) || {Node, _Info} <- emqx_mgmt:list_nodes()],
case lists:filter(fun(Item) -> Item =/= ok end, Results) of
[] ->
return(ok);
Errors ->
return(lists:last(Errors))
end.
format({Node, Plugins}) ->
#{node => Node, plugins => [format(Plugin) || Plugin <- Plugins]};
format(#plugin{name = Name,
descr = Descr,
active = Active,
type = Type}) ->
#{name => Name,
description => iolist_to_binary(Descr),
active => Active,
type => Type}.

View File

@ -0,0 +1,240 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_pubsub).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include("emqx_mgmt.hrl").
-import(proplists, [ get_value/2
, get_value/3
]).
-import(minirest, [return/1]).
-rest_api(#{name => mqtt_subscribe,
method => 'POST',
path => "/mqtt/subscribe",
func => subscribe,
descr => "Subscribe a topic"}).
-rest_api(#{name => mqtt_publish,
method => 'POST',
path => "/mqtt/publish",
func => publish,
descr => "Publish a MQTT message"}).
-rest_api(#{name => mqtt_unsubscribe,
method => 'POST',
path => "/mqtt/unsubscribe",
func => unsubscribe,
descr => "Unsubscribe a topic"}).
-rest_api(#{name => mqtt_subscribe_batch,
method => 'POST',
path => "/mqtt/subscribe_batch",
func => subscribe_batch,
descr => "Batch subscribes topics"}).
-rest_api(#{name => mqtt_publish_batch,
method => 'POST',
path => "/mqtt/publish_batch",
func => publish_batch,
descr => "Batch publish MQTT messages"}).
-rest_api(#{name => mqtt_unsubscribe_batch,
method => 'POST',
path => "/mqtt/unsubscribe_batch",
func => unsubscribe_batch,
descr => "Batch unsubscribes topics"}).
-export([ subscribe/2
, publish/2
, unsubscribe/2
, subscribe_batch/2
, publish_batch/2
, unsubscribe_batch/2
]).
subscribe(_Bindings, Params) ->
logger:debug("API subscribe Params:~p", [Params]),
{ClientId, Topic, QoS} = parse_subscribe_params(Params),
return(do_subscribe(ClientId, Topic, QoS)).
publish(_Bindings, Params) ->
logger:debug("API publish Params:~p", [Params]),
{ClientId, Topic, Qos, Retain, Payload} = parse_publish_params(Params),
return(do_publish(ClientId, Topic, Qos, Retain, Payload)).
unsubscribe(_Bindings, Params) ->
logger:debug("API unsubscribe Params:~p", [Params]),
{ClientId, Topic} = parse_unsubscribe_params(Params),
return(do_unsubscribe(ClientId, Topic)).
subscribe_batch(_Bindings, Params) ->
logger:debug("API subscribe batch Params:~p", [Params]),
return({ok, loop_subscribe(Params)}).
publish_batch(_Bindings, Params) ->
logger:debug("API publish batch Params:~p", [Params]),
return({ok, loop_publish(Params)}).
unsubscribe_batch(_Bindings, Params) ->
logger:debug("API unsubscribe batch Params:~p", [Params]),
return({ok, loop_unsubscribe(Params)}).
loop_subscribe(Params) ->
loop_subscribe(Params, []).
loop_subscribe([], Result) ->
lists:reverse(Result);
loop_subscribe([Params | ParamsN], Acc) ->
{ClientId, Topic, QoS} = parse_subscribe_params(Params),
Code = case do_subscribe(ClientId, Topic, QoS) of
ok -> 0;
{_, Code0, _Reason} -> Code0
end,
Result = #{clientid => ClientId,
topic => resp_topic(get_value(<<"topic">>, Params), get_value(<<"topics">>, Params, <<"">>)),
code => Code},
loop_subscribe(ParamsN, [Result | Acc]).
loop_publish(Params) ->
loop_publish(Params, []).
loop_publish([], Result) ->
lists:reverse(Result);
loop_publish([Params | ParamsN], Acc) ->
{ClientId, Topic, Qos, Retain, Payload} = parse_publish_params(Params),
Code = case do_publish(ClientId, Topic, Qos, Retain, Payload) of
ok -> 0;
{_, Code0, _} -> Code0
end,
Result = #{topic => resp_topic(get_value(<<"topic">>, Params), get_value(<<"topics">>, Params, <<"">>)),
code => Code},
loop_publish(ParamsN, [Result | Acc]).
loop_unsubscribe(Params) ->
loop_unsubscribe(Params, []).
loop_unsubscribe([], Result) ->
lists:reverse(Result);
loop_unsubscribe([Params | ParamsN], Acc) ->
{ClientId, Topic} = parse_unsubscribe_params(Params),
Code = case do_unsubscribe(ClientId, Topic) of
ok -> 0;
{_, Code0, _} -> Code0
end,
Result = #{clientid => ClientId,
topic => resp_topic(get_value(<<"topic">>, Params), get_value(<<"topics">>, Params, <<"">>)),
code => Code},
loop_unsubscribe(ParamsN, [Result | Acc]).
do_subscribe(_ClientId, [], _QoS) ->
{ok, ?ERROR15, bad_topic};
do_subscribe(ClientId, Topics, QoS) ->
TopicTable = parse_topic_filters(Topics, QoS),
case emqx_mgmt:subscribe(ClientId, TopicTable) of
{error, Reason} -> {ok, ?ERROR12, Reason};
_ -> ok
end.
do_publish(_ClientId, [], _Qos, _Retain, _Payload) ->
{ok, ?ERROR15, bad_topic};
do_publish(ClientId, Topics, Qos, Retain, Payload) ->
lists:foreach(fun(Topic) ->
Msg = emqx_message:make(ClientId, Qos, Topic, Payload),
emqx_mgmt:publish(Msg#message{flags = #{retain => Retain}})
end, Topics),
ok.
do_unsubscribe(ClientId, Topic) ->
case validate_by_filter(Topic) of
true ->
case emqx_mgmt:unsubscribe(ClientId, Topic) of
{error, Reason} -> {ok, ?ERROR12, Reason};
_ -> ok
end;
false ->
{ok, ?ERROR15, bad_topic}
end.
parse_subscribe_params(Params) ->
ClientId = get_value(<<"clientid">>, Params),
Topics = topics(filter, get_value(<<"topic">>, Params), get_value(<<"topics">>, Params, <<"">>)),
QoS = get_value(<<"qos">>, Params, 0),
{ClientId, Topics, QoS}.
parse_publish_params(Params) ->
Topics = topics(name, get_value(<<"topic">>, Params), get_value(<<"topics">>, Params, <<"">>)),
ClientId = get_value(<<"clientid">>, Params),
Payload = decode_payload(get_value(<<"payload">>, Params, <<>>),
get_value(<<"encoding">>, Params, <<"plain">>)),
Qos = get_value(<<"qos">>, Params, 0),
Retain = get_value(<<"retain">>, Params, false),
Payload1 = maybe_maps_to_binary(Payload),
{ClientId, Topics, Qos, Retain, Payload1}.
parse_unsubscribe_params(Params) ->
ClientId = get_value(<<"clientid">>, Params),
Topic = get_value(<<"topic">>, Params),
{ClientId, Topic}.
topics(Type, undefined, Topics0) ->
Topics = binary:split(Topics0, <<",">>, [global]),
case Type of
name -> lists:filter(fun(T) -> validate_by_name(T) end, Topics);
filter -> lists:filter(fun(T) -> validate_by_filter(T) end, Topics)
end;
topics(Type, Topic, _) ->
topics(Type, undefined, Topic).
%%TODO:
% validate(qos, Qos) ->
% (Qos >= ?QOS_0) and (Qos =< ?QOS_2).
validate_by_filter(Topic) ->
validate_topic({filter, Topic}).
validate_by_name(Topic) ->
validate_topic({name, Topic}).
validate_topic({Type, Topic}) ->
try emqx_topic:validate({Type, Topic}) of
true -> true
catch
error:_Reason -> false
end.
parse_topic_filters(Topics, Qos) ->
[begin
{Topic, Opts} = emqx_topic:parse(Topic0),
{Topic, Opts#{qos => Qos}}
end || Topic0 <- Topics].
resp_topic(undefined, Topics) -> Topics;
resp_topic(Topic, _) -> Topic.
decode_payload(Payload, <<"base64">>) -> base64:decode(Payload);
decode_payload(Payload, _) -> Payload.
maybe_maps_to_binary(Payload) when is_binary(Payload) -> Payload;
maybe_maps_to_binary(Payload) ->
try
emqx_json:encode(Payload)
catch
_C : _E : S ->
error({encode_payload_fail, S})
end.

View File

@ -0,0 +1,49 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_routes).
-include_lib("emqx/include/emqx.hrl").
-import(minirest, [return/1]).
-rest_api(#{name => list_routes,
method => 'GET',
path => "/routes/",
func => list,
descr => "List routes"}).
-rest_api(#{name => lookup_routes,
method => 'GET',
path => "/routes/:bin:topic",
func => lookup,
descr => "Lookup routes to a topic"}).
-export([ list/2
, lookup/2
]).
list(Bindings, Params) when map_size(Bindings) == 0 ->
return({ok, emqx_mgmt_api:paginate(emqx_route, Params, fun format/1)}).
lookup(#{topic := Topic}, _Params) ->
Topic1 = emqx_mgmt_util:urldecode(Topic),
return({ok, [format(R) || R <- emqx_mgmt:lookup_routes(Topic1)]}).
format(#route{topic = Topic, dest = {_, Node}}) ->
#{topic => Topic, node => Node};
format(#route{topic = Topic, dest = Node}) ->
#{topic => Topic, node => Node}.

View File

@ -0,0 +1,47 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_stats).
-import(minirest, [return/1]).
-rest_api(#{name => list_stats,
method => 'GET',
path => "/stats/",
func => list,
descr => "A list of stats of all nodes in the cluster"}).
-rest_api(#{name => lookup_node_stats,
method => 'GET',
path => "/nodes/:atom:node/stats/",
func => lookup,
descr => "A list of stats of a node"}).
-export([ list/2
, lookup/2
]).
%% List stats of all nodes
list(Bindings, _Params) when map_size(Bindings) == 0 ->
return({ok, [#{node => Node, stats => maps:from_list(Stats)}
|| {Node, Stats} <- emqx_mgmt:get_stats()]}).
%% List stats of a node
lookup(#{node := Node}, _Params) ->
case emqx_mgmt:get_stats(Node) of
{error, Reason} -> return({error, Reason});
Stats -> return({ok, maps:from_list(Stats)})
end.

View File

@ -0,0 +1,154 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_subscriptions).
-include_lib("emqx/include/emqx.hrl").
-import(minirest, [return/1]).
-define(SUBS_QS_SCHEMA, {emqx_suboption,
[{<<"clientid">>, binary},
{<<"topic">>, binary},
{<<"share">>, binary},
{<<"qos">>, integer},
{<<"_match_topic">>, binary}]}).
-rest_api(#{name => list_subscriptions,
method => 'GET',
path => "/subscriptions/",
func => list,
descr => "A list of subscriptions in the cluster"}).
-rest_api(#{name => list_node_subscriptions,
method => 'GET',
path => "/nodes/:atom:node/subscriptions/",
func => list,
descr => "A list of subscriptions on a node"}).
-rest_api(#{name => lookup_client_subscriptions,
method => 'GET',
path => "/subscriptions/:bin:clientid",
func => lookup,
descr => "A list of subscriptions of a client"}).
-rest_api(#{name => lookup_client_subscriptions_with_node,
method => 'GET',
path => "/nodes/:atom:node/subscriptions/:bin:clientid",
func => lookup,
descr => "A list of subscriptions of a client on the node"}).
-export([ list/2
, lookup/2
]).
-export([ query/3
, format/1
]).
-define(query_fun, {?MODULE, query}).
-define(format_fun, {?MODULE, format}).
list(Bindings, Params) when map_size(Bindings) == 0 ->
case proplists:get_value(<<"topic">>, Params) of
undefined ->
return({ok, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)});
Topic ->
return({ok, emqx_mgmt:list_subscriptions_via_topic(emqx_mgmt_util:urldecode(Topic), ?format_fun)})
end;
list(#{node := Node} = Bindings, Params) ->
case proplists:get_value(<<"topic">>, Params) of
undefined ->
case Node =:= node() of
true ->
return({ok, emqx_mgmt_api:node_query(Node, Params, ?SUBS_QS_SCHEMA, ?query_fun)});
false ->
case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of
{badrpc, Reason} -> return({error, Reason});
Res -> Res
end
end;
Topic ->
return({ok, emqx_mgmt:list_subscriptions_via_topic(Node, emqx_mgmt_util:urldecode(Topic), ?format_fun)})
end.
lookup(#{node := Node, clientid := ClientId}, _Params) ->
return({ok, format(emqx_mgmt:lookup_subscriptions(Node, emqx_mgmt_util:urldecode(ClientId)))});
lookup(#{clientid := ClientId}, _Params) ->
return({ok, format(emqx_mgmt:lookup_subscriptions(emqx_mgmt_util:urldecode(ClientId)))}).
format(Items) when is_list(Items) ->
[format(Item) || Item <- Items];
format({{Subscriber, Topic}, Options}) ->
format({Subscriber, Topic, Options});
format({_Subscriber, Topic, Options = #{share := Group}}) ->
QoS = maps:get(qos, Options),
#{node => node(), topic => filename:join([<<"$share">>, Group, Topic]), clientid => maps:get(subid, Options), qos => QoS};
format({_Subscriber, Topic, Options}) ->
QoS = maps:get(qos, Options),
#{node => node(), topic => Topic, clientid => maps:get(subid, Options), qos => QoS}.
%%--------------------------------------------------------------------
%% Query Function
%%--------------------------------------------------------------------
query({Qs, []}, Start, Limit) ->
Ms = qs2ms(Qs),
emqx_mgmt_api:select_table(emqx_suboption, Ms, Start, Limit, fun format/1);
query({Qs, Fuzzy}, Start, Limit) ->
Ms = qs2ms(Qs),
MatchFun = match_fun(Ms, Fuzzy),
emqx_mgmt_api:traverse_table(emqx_suboption, MatchFun, Start, Limit, fun format/1).
match_fun(Ms, Fuzzy) ->
MsC = ets:match_spec_compile(Ms),
fun(Rows) ->
case ets:match_spec_run(Rows, MsC) of
[] -> [];
Ls -> lists:filter(fun(E) -> run_fuzzy_match(E, Fuzzy) end, Ls)
end
end.
run_fuzzy_match(_, []) ->
true;
run_fuzzy_match(E = {{_, Topic}, _}, [{topic, match, TopicFilter}|Fuzzy]) ->
emqx_topic:match(Topic, TopicFilter) andalso run_fuzzy_match(E, Fuzzy).
%%--------------------------------------------------------------------
%% Query String to Match Spec
qs2ms(Qs) ->
MtchHead = qs2ms(Qs, {{'_', '_'}, #{}}),
[{MtchHead, [], ['$_']}].
qs2ms([], MtchHead) ->
MtchHead;
qs2ms([{Key, '=:=', Value} | More], MtchHead) ->
qs2ms(More, update_ms(Key, Value, MtchHead)).
update_ms(clientid, X, {{Pid, Topic}, Opts}) ->
{{Pid, Topic}, Opts#{subid => X}};
update_ms(topic, X, {{Pid, _Topic}, Opts}) ->
{{Pid, X}, Opts};
update_ms(share, X, {{Pid, Topic}, Opts}) ->
{{Pid, Topic}, Opts#{share => X}};
update_ms(qos, X, {{Pid, Topic}, Opts}) ->
{{Pid, Topic}, Opts#{qos => X}}.

View File

@ -0,0 +1,130 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_topic_metrics).
-import(minirest, [return/1]).
-rest_api(#{name => list_all_topic_metrics,
method => 'GET',
path => "/topic-metrics",
func => list,
descr => "A list of all topic metrics of all nodes in the cluster"}).
-rest_api(#{name => list_topic_metrics,
method => 'GET',
path => "/topic-metrics/:bin:topic",
func => list,
descr => "A list of specfied topic metrics of all nodes in the cluster"}).
-rest_api(#{name => register_topic_metrics,
method => 'POST',
path => "/topic-metrics",
func => register,
descr => "Register topic metrics"}).
-rest_api(#{name => unregister_all_topic_metrics,
method => 'DELETE',
path => "/topic-metrics",
func => unregister,
descr => "Unregister all topic metrics"}).
-rest_api(#{name => unregister_topic_metrics,
method => 'DELETE',
path => "/topic-metrics/:bin:topic",
func => unregister,
descr => "Unregister topic metrics"}).
-export([ list/2
, register/2
, unregister/2
]).
list(#{topic := Topic0}, _Params) ->
execute_when_enabled(fun() ->
Topic = emqx_mgmt_util:urldecode(Topic0),
case safe_validate(Topic) of
true ->
case emqx_mgmt:get_topic_metrics(Topic) of
{error, Reason} -> return({error, Reason});
Metrics -> return({ok, maps:from_list(Metrics)})
end;
false ->
return({error, invalid_topic_name})
end
end);
list(_Bindings, _Params) ->
execute_when_enabled(fun() ->
case emqx_mgmt:get_all_topic_metrics() of
{error, Reason} -> return({error, Reason});
Metrics -> return({ok, Metrics})
end
end).
register(_Bindings, Params) ->
execute_when_enabled(fun() ->
case proplists:get_value(<<"topic">>, Params) of
undefined ->
return({error, missing_required_params});
Topic ->
case safe_validate(Topic) of
true ->
emqx_mgmt:register_topic_metrics(Topic),
return(ok);
false ->
return({error, invalid_topic_name})
end
end
end).
unregister(Bindings, _Params) when map_size(Bindings) =:= 0 ->
execute_when_enabled(fun() ->
emqx_mgmt:unregister_all_topic_metrics(),
return(ok)
end);
unregister(#{topic := Topic0}, _Params) ->
execute_when_enabled(fun() ->
Topic = emqx_mgmt_util:urldecode(Topic0),
case safe_validate(Topic) of
true ->
emqx_mgmt:unregister_topic_metrics(Topic),
return(ok);
false ->
return({error, invalid_topic_name})
end
end).
execute_when_enabled(Fun) ->
Enabled = case emqx_modules:find_module(emqx_mod_topic_metrics) of
[{_, false}] -> false;
[{_, true}] -> true
end,
case Enabled of
true ->
Fun();
false ->
return({error, module_not_loaded})
end.
safe_validate(Topic) ->
try emqx_topic:validate(name, Topic) of
true -> true
catch
error:_Error ->
false
end.

View File

@ -0,0 +1,35 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_app).
-behaviour(application).
-emqx_plugin(?MODULE).
-export([ start/2
, stop/1
]).
start(_Type, _Args) ->
{ok, Sup} = emqx_mgmt_sup:start_link(),
emqx_mgmt_auth:add_default_app(),
emqx_mgmt_http:start_listeners(),
emqx_mgmt_cli:load(),
{ok, Sup}.
stop(_State) ->
emqx_mgmt_http:stop_listeners().

View File

@ -0,0 +1,210 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_auth).
%% Mnesia Bootstrap
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}).
%% APP Management API
-export([ add_default_app/0
, add_app/2
, add_app/5
, add_app/6
, force_add_app/6
, lookup_app/1
, get_appsecret/1
, update_app/2
, update_app/5
, del_app/1
, list_apps/0
]).
%% APP Auth/ACL API
-export([is_authorized/2]).
-define(APP, emqx_management).
-record(mqtt_app, {id, secret, name, desc, status, expired}).
-type(appid() :: binary()).
-type(appsecret() :: binary()).
%%--------------------------------------------------------------------
%% Mnesia Bootstrap
%%--------------------------------------------------------------------
mnesia(boot) ->
ok = ekka_mnesia:create_table(mqtt_app, [
{disc_copies, [node()]},
{record_name, mqtt_app},
{attributes, record_info(fields, mqtt_app)}]);
mnesia(copy) ->
ok = ekka_mnesia:copy_table(mqtt_app, disc_copies).
%%--------------------------------------------------------------------
%% Manage Apps
%%--------------------------------------------------------------------
-spec(add_default_app() -> ok | {ok, appsecret()} | {error, term()}).
add_default_app() ->
AppId = application:get_env(?APP, default_application_id, undefined),
AppSecret = application:get_env(?APP, default_application_secret, undefined),
case {AppId, AppSecret} of
{undefined, _} -> ok;
{_, undefined} -> ok;
{_, _} ->
AppId1 = erlang:list_to_binary(AppId),
AppSecret1 = erlang:list_to_binary(AppSecret),
add_app(AppId1, <<"Default">>, AppSecret1, <<"Application user">>, true, undefined)
end.
-spec(add_app(appid(), binary()) -> {ok, appsecret()} | {error, term()}).
add_app(AppId, Name) when is_binary(AppId) ->
add_app(AppId, Name, <<"Application user">>, true, undefined).
-spec(add_app(appid(), binary(), binary(), boolean(), integer() | undefined)
-> {ok, appsecret()}
| {error, term()}).
add_app(AppId, Name, Desc, Status, Expired) when is_binary(AppId) ->
add_app(AppId, Name, undefined, Desc, Status, Expired).
-spec(add_app(appid(), binary(), binary(), binary(), boolean(), integer() | undefined)
-> {ok, appsecret()}
| {error, term()}).
add_app(AppId, Name, Secret, Desc, Status, Expired) when is_binary(AppId) ->
Secret1 = generate_appsecret_if_need(Secret),
App = #mqtt_app{id = AppId,
secret = Secret1,
name = Name,
desc = Desc,
status = Status,
expired = Expired},
AddFun = fun() ->
case mnesia:wread({mqtt_app, AppId}) of
[] -> mnesia:write(App);
_ -> mnesia:abort(alread_existed)
end
end,
case mnesia:transaction(AddFun) of
{atomic, ok} -> {ok, Secret1};
{aborted, Reason} -> {error, Reason}
end.
force_add_app(AppId, Name, Secret, Desc, Status, Expired) ->
AddFun = fun() ->
mnesia:write(#mqtt_app{id = AppId,
secret = Secret,
name = Name,
desc = Desc,
status = Status,
expired = Expired})
end,
case mnesia:transaction(AddFun) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end.
-spec(generate_appsecret_if_need(binary() | undefined) -> binary()).
generate_appsecret_if_need(InSecrt) when is_binary(InSecrt), byte_size(InSecrt) > 0 ->
InSecrt;
generate_appsecret_if_need(_) ->
AppConf = application:get_env(?APP, application, []),
case proplists:get_value(default_secret, AppConf) of
undefined -> emqx_guid:to_base62(emqx_guid:gen());
Secret when is_binary(Secret) -> Secret
end.
-spec(get_appsecret(appid()) -> {appsecret() | undefined}).
get_appsecret(AppId) when is_binary(AppId) ->
case mnesia:dirty_read(mqtt_app, AppId) of
[#mqtt_app{secret = Secret}] -> Secret;
[] -> undefined
end.
-spec(lookup_app(appid()) -> {{appid(), appsecret(), binary(), binary(), boolean(), integer() | undefined} | undefined}).
lookup_app(AppId) when is_binary(AppId) ->
case mnesia:dirty_read(mqtt_app, AppId) of
[#mqtt_app{id = AppId,
secret = AppSecret,
name = Name,
desc = Desc,
status = Status,
expired = Expired}] -> {AppId, AppSecret, Name, Desc, Status, Expired};
[] -> undefined
end.
-spec(update_app(appid(), boolean()) -> ok | {error, term()}).
update_app(AppId, Status) ->
case mnesia:dirty_read(mqtt_app, AppId) of
[App = #mqtt_app{}] ->
case mnesia:transaction(fun() -> mnesia:write(App#mqtt_app{status = Status}) end) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end;
[] ->
{error, not_found}
end.
-spec(update_app(appid(), binary(), binary(), boolean(), integer() | undefined) -> ok | {error, term()}).
update_app(AppId, Name, Desc, Status, Expired) ->
case mnesia:dirty_read(mqtt_app, AppId) of
[App = #mqtt_app{}] ->
case mnesia:transaction(fun() -> mnesia:write(App#mqtt_app{name = Name,
desc = Desc,
status = Status,
expired = Expired}) end) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end;
[] ->
{error, not_found}
end.
-spec(del_app(appid()) -> ok | {error, term()}).
del_app(AppId) when is_binary(AppId) ->
case mnesia:transaction(fun mnesia:delete/1, [{mqtt_app, AppId}]) of
{atomic, Ok} -> Ok;
{aborted, Reason} -> {error, Reason}
end.
-spec(list_apps() -> [{appid(), appsecret(), binary(), binary(), boolean(), integer() | undefined}]).
list_apps() ->
[ {AppId, AppSecret, Name, Desc, Status, Expired} || #mqtt_app{id = AppId,
secret = AppSecret,
name = Name,
desc = Desc,
status = Status,
expired = Expired} <- ets:tab2list(mqtt_app) ].
%%--------------------------------------------------------------------
%% Authenticate App
%%--------------------------------------------------------------------
-spec(is_authorized(appid(), appsecret()) -> boolean()).
is_authorized(AppId, AppSecret) ->
case lookup_app(AppId) of
{_, AppSecret1, _, _, Status, Expired} ->
Status andalso is_expired(Expired) andalso AppSecret =:= AppSecret1;
_ ->
false
end.
is_expired(undefined) -> true;
is_expired(Expired) -> Expired >= erlang:system_time(second).

View File

@ -0,0 +1,715 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_cli).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include("emqx_mgmt.hrl").
-define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~s~n", [Cmd, Descr])).
-import(lists, [foreach/2]).
-export([load/0]).
-export([ status/1
, broker/1
, cluster/1
, clients/1
, routes/1
, subscriptions/1
, plugins/1
, listeners/1
, vm/1
, mnesia/1
, trace/1
, log/1
, mgmt/1
, data/1
, modules/1
]).
-define(PROC_INFOKEYS, [status,
memory,
message_queue_len,
total_heap_size,
heap_size,
stack_size,
reductions]).
-define(MAX_LIMIT, 10000).
-define(APP, emqx).
-spec(load() -> ok).
load() ->
Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)],
lists:foreach(fun(Cmd) -> emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, []) end, Cmds).
is_cmd(Fun) ->
not lists:member(Fun, [init, load, module_info]).
mgmt(["insert", AppId, Name]) ->
case emqx_mgmt_auth:add_app(list_to_binary(AppId), list_to_binary(Name)) of
{ok, Secret} ->
emqx_ctl:print("AppSecret: ~s~n", [Secret]);
{error, already_existed} ->
emqx_ctl:print("Error: already existed~n");
{error, Reason} ->
emqx_ctl:print("Error: ~p~n", [Reason])
end;
mgmt(["lookup", AppId]) ->
case emqx_mgmt_auth:lookup_app(list_to_binary(AppId)) of
{AppId1, AppSecret, Name, Desc, Status, Expired} ->
emqx_ctl:print("app_id: ~s~nsecret: ~s~nname: ~s~ndesc: ~s~nstatus: ~s~nexpired: ~p~n",
[AppId1, AppSecret, Name, Desc, Status, Expired]);
undefined ->
emqx_ctl:print("Not Found.~n")
end;
mgmt(["update", AppId, Status]) ->
case emqx_mgmt_auth:update_app(list_to_binary(AppId), list_to_atom(Status)) of
ok ->
emqx_ctl:print("update successfully.~n");
{error, Reason} ->
emqx_ctl:print("Error: ~p~n", [Reason])
end;
mgmt(["delete", AppId]) ->
case emqx_mgmt_auth:del_app(list_to_binary(AppId)) of
ok -> emqx_ctl:print("ok~n");
{error, not_found} ->
emqx_ctl:print("Error: app not found~n");
{error, Reason} ->
emqx_ctl:print("Error: ~p~n", [Reason])
end;
mgmt(["list"]) ->
lists:foreach(fun({AppId, AppSecret, Name, Desc, Status, Expired}) ->
emqx_ctl:print("app_id: ~s, secret: ~s, name: ~s, desc: ~s, status: ~s, expired: ~p~n",
[AppId, AppSecret, Name, Desc, Status, Expired])
end, emqx_mgmt_auth:list_apps());
mgmt(_) ->
emqx_ctl:usage([{"mgmt list", "List Applications"},
{"mgmt insert <AppId> <Name>", "Add Application of REST API"},
{"mgmt update <AppId> <status>", "Update Application of REST API"},
{"mgmt lookup <AppId>", "Get Application of REST API"},
{"mgmt delete <AppId>", "Delete Application of REST API"}]).
%%--------------------------------------------------------------------
%% @doc Node status
status([]) ->
{InternalStatus, _ProvidedStatus} = init:get_status(),
emqx_ctl:print("Node ~p is ~p~n", [node(), InternalStatus]),
case lists:keysearch(?APP, 1, application:which_applications()) of
false ->
emqx_ctl:print("~s is not running~n", [?APP]);
{value, {?APP, _Desc, Vsn}} ->
emqx_ctl:print("~s ~s is running~n", [?APP, Vsn])
end;
status(_) ->
emqx_ctl:usage("status", "Show broker status").
%%--------------------------------------------------------------------
%% @doc Query broker
broker([]) ->
Funs = [sysdescr, version, uptime, datetime],
[emqx_ctl:print("~-10s: ~s~n", [Fun, emqx_sys:Fun()]) || Fun <- Funs];
broker(["stats"]) ->
[emqx_ctl:print("~-30s: ~w~n", [Stat, Val]) || {Stat, Val} <- lists:sort(emqx_stats:getstats())];
broker(["metrics"]) ->
[emqx_ctl:print("~-30s: ~w~n", [Metric, Val]) || {Metric, Val} <- lists:sort(emqx_metrics:all())];
broker(_) ->
emqx_ctl:usage([{"broker", "Show broker version, uptime and description"},
{"broker stats", "Show broker statistics of clients, topics, subscribers"},
{"broker metrics", "Show broker metrics"}]).
%%-----------------------------------------------------------------------------
%% @doc Cluster with other nodes
cluster(["join", SNode]) ->
case ekka:join(ekka_node:parse_name(SNode)) of
ok ->
emqx_ctl:print("Join the cluster successfully.~n"),
cluster(["status"]);
ignore ->
emqx_ctl:print("Ignore.~n");
{error, Error} ->
emqx_ctl:print("Failed to join the cluster: ~p~n", [Error])
end;
cluster(["leave"]) ->
case ekka:leave() of
ok ->
emqx_ctl:print("Leave the cluster successfully.~n"),
cluster(["status"]);
{error, Error} ->
emqx_ctl:print("Failed to leave the cluster: ~p~n", [Error])
end;
cluster(["force-leave", SNode]) ->
case ekka:force_leave(ekka_node:parse_name(SNode)) of
ok ->
emqx_ctl:print("Remove the node from cluster successfully.~n"),
cluster(["status"]);
ignore ->
emqx_ctl:print("Ignore.~n");
{error, Error} ->
emqx_ctl:print("Failed to remove the node from cluster: ~p~n", [Error])
end;
cluster(["status"]) ->
emqx_ctl:print("Cluster status: ~p~n", [ekka_cluster:info()]);
cluster(_) ->
emqx_ctl:usage([{"cluster join <Node>", "Join the cluster"},
{"cluster leave", "Leave the cluster"},
{"cluster force-leave <Node>","Force the node leave from cluster"},
{"cluster status", "Cluster status"}]).
%%--------------------------------------------------------------------
%% @doc Query clients
clients(["list"]) ->
dump(emqx_channel, client);
clients(["show", ClientId]) ->
if_client(ClientId, fun print/1);
clients(["kick", ClientId]) ->
case emqx_cm:kick_session(bin(ClientId)) of
ok -> emqx_ctl:print("ok~n");
_ -> emqx_ctl:print("Not Found.~n")
end;
clients(_) ->
emqx_ctl:usage([{"clients list", "List all clients"},
{"clients show <ClientId>", "Show a client"},
{"clients kick <ClientId>", "Kick out a client"}]).
if_client(ClientId, Fun) ->
case ets:lookup(emqx_channel, (bin(ClientId))) of
[] -> emqx_ctl:print("Not Found.~n");
[Channel] -> Fun({client, Channel})
end.
%%--------------------------------------------------------------------
%% @doc Routes Command
routes(["list"]) ->
dump(emqx_route);
routes(["show", Topic]) ->
Routes = ets:lookup(emqx_route, bin(Topic)),
[print({emqx_route, Route}) || Route <- Routes];
routes(_) ->
emqx_ctl:usage([{"routes list", "List all routes"},
{"routes show <Topic>", "Show a route"}]).
subscriptions(["list"]) ->
lists:foreach(fun(Suboption) ->
print({emqx_suboption, Suboption})
end, ets:tab2list(emqx_suboption));
subscriptions(["show", ClientId]) ->
case ets:lookup(emqx_subid, bin(ClientId)) of
[] ->
emqx_ctl:print("Not Found.~n");
[{_, Pid}] ->
case ets:match_object(emqx_suboption, {{Pid, '_'}, '_'}) of
[] -> emqx_ctl:print("Not Found.~n");
Suboption ->
[print({emqx_suboption, Sub}) || Sub <- Suboption]
end
end;
subscriptions(["add", ClientId, Topic, QoS]) ->
if_valid_qos(QoS, fun(IntQos) ->
case ets:lookup(emqx_channel, bin(ClientId)) of
[] -> emqx_ctl:print("Error: Channel not found!");
[{_, Pid}] ->
{Topic1, Options} = emqx_topic:parse(bin(Topic)),
Pid ! {subscribe, [{Topic1, Options#{qos => IntQos}}]},
emqx_ctl:print("ok~n")
end
end);
subscriptions(["del", ClientId, Topic]) ->
case ets:lookup(emqx_channel, bin(ClientId)) of
[] -> emqx_ctl:print("Error: Channel not found!");
[{_, Pid}] ->
Pid ! {unsubscribe, [emqx_topic:parse(bin(Topic))]},
emqx_ctl:print("ok~n")
end;
subscriptions(_) ->
emqx_ctl:usage([{"subscriptions list", "List all subscriptions"},
{"subscriptions show <ClientId>", "Show subscriptions of a client"},
{"subscriptions add <ClientId> <Topic> <QoS>", "Add a static subscription manually"},
{"subscriptions del <ClientId> <Topic>", "Delete a static subscription manually"}]).
if_valid_qos(QoS, Fun) ->
try list_to_integer(QoS) of
Int when ?IS_QOS(Int) -> Fun(Int);
_ -> emqx_ctl:print("QoS should be 0, 1, 2~n")
catch _:_ ->
emqx_ctl:print("QoS should be 0, 1, 2~n")
end.
plugins(["list"]) ->
foreach(fun print/1, emqx_plugins:list());
plugins(["load", Name]) ->
case emqx_plugins:load(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Plugin ~s loaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Load plugin ~s error: ~p.~n", [Name, Reason])
end;
plugins(["unload", "emqx_management"])->
emqx_ctl:print("Plugin emqx_management can not be unloaded.~n");
plugins(["unload", Name]) ->
case emqx_plugins:unload(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Plugin ~s unloaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Unload plugin ~s error: ~p.~n", [Name, Reason])
end;
plugins(["reload", Name]) ->
try list_to_existing_atom(Name) of
PluginName ->
case emqx_mgmt:reload_plugin(node(), PluginName) of
ok ->
emqx_ctl:print("Plugin ~s reloaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Reload plugin ~s error: ~p.~n", [Name, Reason])
end
catch
error:badarg ->
emqx_ctl:print("Reload plugin ~s error: The plugin doesn't exist.~n", [Name])
end;
plugins(_) ->
emqx_ctl:usage([{"plugins list", "Show loaded plugins"},
{"plugins load <Plugin>", "Load plugin"},
{"plugins unload <Plugin>", "Unload plugin"},
{"plugins reload <Plugin>", "Reload plugin"}
]).
%%--------------------------------------------------------------------
%% @doc Modules Command
modules(["list"]) ->
foreach(fun(Module) -> print({module, Module}) end, emqx_modules:list());
modules(["load", Name]) ->
case emqx_modules:load(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Module ~s loaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Load module ~s error: ~p.~n", [Name, Reason])
end;
modules(["unload", Name]) ->
case emqx_modules:unload(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Module ~s unloaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Unload module ~s error: ~p.~n", [Name, Reason])
end;
modules(["reload", "emqx_mod_acl_internal" = Name]) ->
case emqx_modules:reload(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Module ~s reloaded successfully.~n", [Name]);
{error, Reason} ->
emqx_ctl:print("Reload module ~s error: ~p.~n", [Name, Reason])
end;
modules(["reload", Name]) ->
emqx_ctl:print("Module: ~p does not need to be reloaded.~n", [Name]);
modules(_) ->
emqx_ctl:usage([{"modules list", "Show loaded modules"},
{"modules load <Module>", "Load module"},
{"modules unload <Module>", "Unload module"},
{"modules reload <Module>", "Reload module"}
]).
%%--------------------------------------------------------------------
%% @doc vm command
vm([]) ->
vm(["all"]);
vm(["all"]) ->
[vm([Name]) || Name <- ["load", "memory", "process", "io", "ports"]];
vm(["load"]) ->
[emqx_ctl:print("cpu/~-20s: ~s~n", [L, V]) || {L, V} <- emqx_vm:loads()];
vm(["memory"]) ->
[emqx_ctl:print("memory/~-17s: ~w~n", [Cat, Val]) || {Cat, Val} <- erlang:memory()];
vm(["process"]) ->
[emqx_ctl:print("process/~-16s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{limit, process_limit}, {count, process_count}]];
vm(["io"]) ->
IoInfo = lists:usort(lists:flatten(erlang:system_info(check_io))),
[emqx_ctl:print("io/~-21s: ~w~n", [Key, proplists:get_value(Key, IoInfo)]) || Key <- [max_fds, active_fds]];
vm(["ports"]) ->
[emqx_ctl:print("ports/~-16s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{count, port_count}, {limit, port_limit}]];
vm(_) ->
emqx_ctl:usage([{"vm all", "Show info of Erlang VM"},
{"vm load", "Show load of Erlang VM"},
{"vm memory", "Show memory of Erlang VM"},
{"vm process", "Show process of Erlang VM"},
{"vm io", "Show IO of Erlang VM"},
{"vm ports", "Show Ports of Erlang VM"}]).
%%--------------------------------------------------------------------
%% @doc mnesia Command
mnesia([]) ->
mnesia:system_info();
mnesia(_) ->
emqx_ctl:usage([{"mnesia", "Mnesia system info"}]).
%%--------------------------------------------------------------------
%% @doc Logger Command
log(["set-level", Level]) ->
case emqx_logger:set_log_level(list_to_atom(Level)) of
ok -> emqx_ctl:print("~s~n", [Level]);
Error -> emqx_ctl:print("[error] set overall log level failed: ~p~n", [Error])
end;
log(["primary-level"]) ->
Level = emqx_logger:get_primary_log_level(),
emqx_ctl:print("~s~n", [Level]);
log(["primary-level", Level]) ->
emqx_logger:set_primary_log_level(list_to_atom(Level)),
emqx_ctl:print("~s~n", [emqx_logger:get_primary_log_level()]);
log(["handlers", "list"]) ->
[emqx_ctl:print("LogHandler(id=~s, level=~s, destination=~s, status=~s)~n", [Id, Level, Dst, Status])
|| #{id := Id, level := Level, dst := Dst, status := Status} <- emqx_logger:get_log_handlers()],
ok;
log(["handlers", "start", HandlerId]) ->
case emqx_logger:start_log_handler(list_to_atom(HandlerId)) of
ok -> emqx_ctl:print("log handler ~s started~n", [HandlerId]);
{error, Reason} ->
emqx_ctl:print("[error] failed to start log handler ~s: ~p~n", [HandlerId, Reason])
end;
log(["handlers", "stop", HandlerId]) ->
case emqx_logger:stop_log_handler(list_to_atom(HandlerId)) of
ok -> emqx_ctl:print("log handler ~s stopped~n", [HandlerId]);
{error, Reason} ->
emqx_ctl:print("[error] failed to stop log handler ~s: ~p~n", [HandlerId, Reason])
end;
log(["handlers", "set-level", HandlerId, Level]) ->
case emqx_logger:set_log_handler_level(list_to_atom(HandlerId), list_to_atom(Level)) of
ok ->
#{level := NewLevel} = emqx_logger:get_log_handler(list_to_atom(HandlerId)),
emqx_ctl:print("~s~n", [NewLevel]);
{error, Error} ->
emqx_ctl:print("[error] ~p~n", [Error])
end;
log(_) ->
emqx_ctl:usage([{"log set-level <Level>", "Set the overall log level"},
{"log primary-level", "Show the primary log level now"},
{"log primary-level <Level>","Set the primary log level"},
{"log handlers list", "Show log handlers"},
{"log handlers start <HandlerId>", "Start a log handler"},
{"log handlers stop <HandlerId>", "Stop a log handler"},
{"log handlers set-level <HandlerId> <Level>", "Set log level of a log handler"}]).
%%--------------------------------------------------------------------
%% @doc Trace Command
trace(["list"]) ->
foreach(fun({{Who, Name}, {Level, LogFile}}) ->
emqx_ctl:print("Trace(~s=~s, level=~s, destination=~p)~n", [Who, Name, Level, LogFile])
end, emqx_tracer:lookup_traces());
trace(["stop", "client", ClientId]) ->
trace_off(clientid, ClientId);
trace(["start", "client", ClientId, LogFile]) ->
trace_on(clientid, ClientId, all, LogFile);
trace(["start", "client", ClientId, LogFile, Level]) ->
trace_on(clientid, ClientId, list_to_atom(Level), LogFile);
trace(["stop", "topic", Topic]) ->
trace_off(topic, Topic);
trace(["start", "topic", Topic, LogFile]) ->
trace_on(topic, Topic, all, LogFile);
trace(["start", "topic", Topic, LogFile, Level]) ->
trace_on(topic, Topic, list_to_atom(Level), LogFile);
trace(_) ->
emqx_ctl:usage([{"trace list", "List all traces started"},
{"trace start client <ClientId> <File> [<Level>]", "Traces for a client"},
{"trace stop client <ClientId>", "Stop tracing for a client"},
{"trace start topic <Topic> <File> [<Level>] ", "Traces for a topic"},
{"trace stop topic <Topic> ", "Stop tracing for a topic"}]).
trace_on(Who, Name, Level, LogFile) ->
case emqx_tracer:start_trace({Who, iolist_to_binary(Name)}, Level, LogFile) of
ok ->
emqx_ctl:print("trace ~s ~s successfully~n", [Who, Name]);
{error, Error} ->
emqx_ctl:print("[error] trace ~s ~s: ~p~n", [Who, Name, Error])
end.
trace_off(Who, Name) ->
case emqx_tracer:stop_trace({Who, iolist_to_binary(Name)}) of
ok ->
emqx_ctl:print("stop tracing ~s ~s successfully~n", [Who, Name]);
{error, Error} ->
emqx_ctl:print("[error] stop tracing ~s ~s: ~p~n", [Who, Name, Error])
end.
%%--------------------------------------------------------------------
%% @doc Listeners Command
listeners([]) ->
foreach(fun({{Protocol, ListenOn}, _Pid}) ->
Info = [{acceptors, esockd:get_acceptors({Protocol, ListenOn})},
{max_conns, esockd:get_max_connections({Protocol, ListenOn})},
{current_conn, esockd:get_current_connections({Protocol, ListenOn})},
{shutdown_count, esockd:get_shutdown_count({Protocol, ListenOn})}],
emqx_ctl:print("listener on ~s:~s~n", [Protocol, esockd:to_string(ListenOn)]),
foreach(fun({Key, Val}) ->
emqx_ctl:print(" ~-16s: ~w~n", [Key, Val])
end, Info)
end, esockd:listeners()),
foreach(fun({Protocol, Opts}) ->
Info = [{acceptors, maps:get(num_acceptors, proplists:get_value(transport_options, Opts, #{}), 0)},
{max_conns, proplists:get_value(max_connections, Opts)},
{current_conn, proplists:get_value(all_connections, Opts)},
{shutdown_count, []}],
emqx_ctl:print("listener on ~s:~p~n", [Protocol, proplists:get_value(port, Opts)]),
foreach(fun({Key, Val}) ->
emqx_ctl:print(" ~-16s: ~w~n", [Key, Val])
end, Info)
end, ranch:info());
listeners(["stop", Name = "http" ++ _N, ListenOn]) ->
case minirest:stop_http(list_to_atom(Name)) of
ok ->
emqx_ctl:print("Stop ~s listener on ~s successfully.~n", [Name, ListenOn]);
{error, Error} ->
emqx_ctl:print("Failed to stop ~s listener on ~s, error:~p~n", [Name, ListenOn, Error])
end;
listeners(["stop", Proto, ListenOn]) ->
ListenOn1 = case string:tokens(ListenOn, ":") of
[Port] -> list_to_integer(Port);
[IP, Port] -> {IP, list_to_integer(Port)}
end,
case emqx_listeners:stop_listener({list_to_atom(Proto), ListenOn1, []}) of
ok ->
emqx_ctl:print("Stop ~s listener on ~s successfully.~n", [Proto, ListenOn]);
{error, Error} ->
emqx_ctl:print("Failed to stop ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error])
end;
listeners(_) ->
emqx_ctl:usage([{"listeners", "List listeners"},
{"listeners stop <Proto> <Port>", "Stop a listener"}]).
%%--------------------------------------------------------------------
%% @doc data Command
data(["export"]) ->
Rules = emqx_mgmt:export_rules(),
Resources = emqx_mgmt:export_resources(),
Blacklist = emqx_mgmt:export_blacklist(),
Apps = emqx_mgmt:export_applications(),
Users = emqx_mgmt:export_users(),
AuthClientID = emqx_mgmt:export_auth_clientid(),
AuthUsername = emqx_mgmt:export_auth_username(),
AuthMnesia = emqx_mgmt:export_auth_mnesia(),
AclMnesia = emqx_mgmt:export_acl_mnesia(),
Schemas = emqx_mgmt:export_schemas(),
Seconds = erlang:system_time(second),
{{Y, M, D}, {H, MM, S}} = emqx_mgmt_util:datetime(Seconds),
Filename = io_lib:format("emqx-export-~p-~p-~p-~p-~p-~p.json", [Y, M, D, H, MM, S]),
NFilename = filename:join([emqx:get_env(data_dir), Filename]),
Version = string:sub_string(emqx_sys:version(), 1, 3),
Data = [{version, erlang:list_to_binary(Version)},
{date, erlang:list_to_binary(emqx_mgmt_util:strftime(Seconds))},
{rules, Rules},
{resources, Resources},
{blacklist, Blacklist},
{apps, Apps},
{users, Users},
{auth_clientid, AuthClientID},
{auth_username, AuthUsername},
{auth_mnesia, AuthMnesia},
{acl_mnesia, AclMnesia},
{schemas, Schemas}],
case file:write_file(NFilename, emqx_json:encode(Data)) of
ok ->
emqx_ctl:print("The emqx data has been successfully exported to ~s.~n", [NFilename]);
{error, Reason} ->
emqx_ctl:print("The emqx data export failed due to ~p.~n", [Reason])
end;
data(["import", Filename]) ->
case file:read_file(Filename) of
{ok, Json} ->
Data = emqx_json:decode(Json, [return_maps]),
Version = emqx_mgmt:to_version(maps:get(<<"version">>, Data)),
case lists:member(Version, ?VERSIONS) of
true ->
try
emqx_mgmt:import_resources(maps:get(<<"resources">>, Data, [])),
emqx_mgmt:import_rules(maps:get(<<"rules">>, Data, [])),
emqx_mgmt:import_blacklist(maps:get(<<"blacklist">>, Data, [])),
emqx_mgmt:import_applications(maps:get(<<"apps">>, Data, [])),
emqx_mgmt:import_users(maps:get(<<"users">>, Data, [])),
emqx_mgmt:import_auth_clientid(maps:get(<<"auth_clientid">>, Data, [])),
emqx_mgmt:import_auth_username(maps:get(<<"auth_username">>, Data, [])),
emqx_mgmt:import_auth_mnesia(maps:get(<<"auth_mnesia">>, Data, [])),
emqx_mgmt:import_acl_mnesia(maps:get(<<"acl_mnesia">>, Data, [])),
emqx_mgmt:import_schemas(maps:get(<<"schemas">>, Data, [])),
emqx_ctl:print("The emqx data has been imported successfully.~n")
catch Class:Reason:Stack ->
emqx_ctl:print("The emqx data import failed due: ~0p~n", [{Class,Reason,Stack}])
end;
false ->
emqx_ctl:print("Unsupported version: ~p~n", [Version])
end;
{error, Reason} ->
emqx_ctl:print("The emqx data import failed: ~0p while reading ~s.~n", [Reason, Filename])
end;
data(_) ->
emqx_ctl:usage([{"data import <File>", "Import data from the specified file"},
{"data export", "Export data"}]).
%%--------------------------------------------------------------------
%% Dump ETS
%%--------------------------------------------------------------------
dump(Table) ->
dump(Table, Table, ets:first(Table), []).
dump(Table, Tag) ->
dump(Table, Tag, ets:first(Table), []).
dump(_Table, _, '$end_of_table', Result) ->
lists:reverse(Result);
dump(Table, Tag, Key, Result) ->
PrintValue = [print({Tag, Record}) || Record <- ets:lookup(Table, Key)],
dump(Table, Tag, ets:next(Table, Key), [PrintValue | Result]).
print({_, []}) ->
ok;
print({client, {ClientId, ChanPid}}) ->
Attrs = case emqx_cm:get_chan_info(ClientId, ChanPid) of
undefined -> #{};
Attrs0 -> Attrs0
end,
Stats = case emqx_cm:get_chan_stats(ClientId, ChanPid) of
undefined -> #{};
Stats0 -> maps:from_list(Stats0)
end,
ClientInfo = maps:get(clientinfo, Attrs, #{}),
ConnInfo = maps:get(conninfo, Attrs, #{}),
Session = maps:get(session, Attrs, #{}),
Connected = case maps:get(conn_state, Attrs) of
connected -> true;
_ -> false
end,
Info = lists:foldl(fun(Items, Acc) ->
maps:merge(Items, Acc)
end, #{connected => Connected},
[maps:with([subscriptions_cnt, inflight_cnt, awaiting_rel_cnt,
mqueue_len, mqueue_dropped, send_msg], Stats),
maps:with([clientid, username], ClientInfo),
maps:with([peername, clean_start, keepalive, expiry_interval,
connected_at, disconnected_at], ConnInfo),
maps:with([created_at], Session)]),
InfoKeys = [clientid, username, peername,
clean_start, keepalive, expiry_interval,
subscriptions_cnt, inflight_cnt, awaiting_rel_cnt, send_msg, mqueue_len, mqueue_dropped,
connected, created_at, connected_at] ++ case maps:is_key(disconnected_at, Info) of
true -> [disconnected_at];
false -> []
end,
emqx_ctl:print("Client(~s, username=~s, peername=~s, "
"clean_start=~s, keepalive=~w, session_expiry_interval=~w, "
"subscriptions=~w, inflight=~w, awaiting_rel=~w, delivered_msgs=~w, enqueued_msgs=~w, dropped_msgs=~w, "
"connected=~s, created_at=~w, connected_at=~w" ++ case maps:is_key(disconnected_at, Info) of
true -> ", disconnected_at=~w)~n";
false -> ")~n"
end,
[format(K, maps:get(K, Info)) || K <- InfoKeys]);
print({emqx_route, #route{topic = Topic, dest = {_, Node}}}) ->
emqx_ctl:print("~s -> ~s~n", [Topic, Node]);
print({emqx_route, #route{topic = Topic, dest = Node}}) ->
emqx_ctl:print("~s -> ~s~n", [Topic, Node]);
print(#plugin{name = Name, descr = Descr, active = Active}) ->
emqx_ctl:print("Plugin(~s, description=~s, active=~s)~n",
[Name, Descr, Active]);
print({module, {Name, Active}}) ->
emqx_ctl:print("Module(~s, description=~s, active=~s)~n",
[Name, Name:description(), Active]);
print({emqx_suboption, {{Pid, Topic}, Options}}) when is_pid(Pid) ->
emqx_ctl:print("~s -> ~s~n", [maps:get(subid, Options), Topic]).
format(_, undefined) ->
undefined;
format(peername, {IPAddr, Port}) ->
IPStr = emqx_mgmt_util:ntoa(IPAddr),
io_lib:format("~s:~p", [IPStr, Port]);
format(_, Val) ->
Val.
bin(S) -> iolist_to_binary(S).

View File

@ -0,0 +1,125 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_http).
-import(proplists, [get_value/3]).
-export([ start_listeners/0
, handle_request/2
, stop_listeners/0
]).
-export([init/2]).
-include_lib("emqx/include/emqx.hrl").
-define(APP, emqx_management).
-define(EXCEPT_PLUGIN, [emqx_dashboard]).
-ifdef(TEST).
-define(EXCEPT, []).
-else.
-define(EXCEPT, [add_app, del_app, list_apps, lookup_app, update_app]).
-endif.
%%--------------------------------------------------------------------
%% Start/Stop Listeners
%%--------------------------------------------------------------------
start_listeners() ->
lists:foreach(fun start_listener/1, listeners()).
stop_listeners() ->
lists:foreach(fun stop_listener/1, listeners()).
start_listener({Proto, Port, Options}) when Proto == http ->
Dispatch = [{"/status", emqx_mgmt_http, []},
{"/api/v4/[...]", minirest, http_handlers()}],
minirest:start_http(listener_name(Proto), ranch_opts(Port, Options), Dispatch);
start_listener({Proto, Port, Options}) when Proto == https ->
Dispatch = [{"/status", emqx_mgmt_http, []},
{"/api/v4/[...]", minirest, http_handlers()}],
minirest:start_https(listener_name(Proto), ranch_opts(Port, Options), Dispatch).
ranch_opts(Port, Options0) ->
NumAcceptors = get_value(num_acceptors, Options0, 4),
MaxConnections = get_value(max_connections, Options0, 512),
Options = lists:foldl(fun({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors ->
Acc;
({inet6, true}, Acc) -> [inet6 | Acc];
({inet6, false}, Acc) -> Acc;
({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc];
({ipv6_v6only, false}, Acc) -> Acc;
({K, V}, Acc)->
[{K, V} | Acc]
end, [], Options0),
Res = #{num_acceptors => NumAcceptors,
max_connections => MaxConnections,
socket_opts => [{port, Port} | Options]},
Res.
stop_listener({Proto, _Port, _}) ->
minirest:stop_http(listener_name(Proto)).
listeners() ->
application:get_env(?APP, listeners, []).
listener_name(Proto) ->
list_to_atom(atom_to_list(Proto) ++ ":management").
http_handlers() ->
Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()),
[{"/api/v4", minirest:handler(#{apps => Plugins -- ?EXCEPT_PLUGIN,
except => ?EXCEPT,
filter => fun filter/1}),
[{authorization, fun authorize_appid/1}]}].
%%--------------------------------------------------------------------
%% Handle 'status' request
%%--------------------------------------------------------------------
init(Req, Opts) ->
Req1 = handle_request(cowboy_req:path(Req), Req),
{ok, Req1, Opts}.
handle_request(Path, Req) ->
handle_request(cowboy_req:method(Req), Path, Req).
handle_request(<<"GET">>, <<"/status">>, Req) ->
{InternalStatus, _ProvidedStatus} = init:get_status(),
AppStatus = case lists:keysearch(emqx, 1, application:which_applications()) of
false -> not_running;
{value, _Val} -> running
end,
Status = io_lib:format("Node ~s is ~s~nemqx is ~s",
[node(), InternalStatus, AppStatus]),
cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, Status, Req);
handle_request(_Method, _Path, Req) ->
cowboy_req:reply(400, #{<<"content-type">> => <<"text/plain">>}, <<"Not found.">>, Req).
authorize_appid(Req) ->
case cowboy_req:parse_header(<<"authorization">>, Req) of
{basic, AppId, AppSecret} -> emqx_mgmt_auth:is_authorized(AppId, AppSecret);
_ -> false
end.
filter(#{app := App}) ->
case emqx_plugins:find_plugin(App) of
false -> false;
Plugin -> Plugin#plugin.active
end.

View File

@ -0,0 +1,30 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, {{one_for_one, 1, 5}, []}}.

View File

@ -0,0 +1,84 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_util).
-export([ strftime/1
, datetime/1
, kmg/1
, ntoa/1
, merge_maps/2
]).
-export([urldecode/1]).
-define(KB, 1024).
-define(MB, (1024*1024)).
-define(GB, (1024*1024*1024)).
%%--------------------------------------------------------------------
%% Strftime
%%--------------------------------------------------------------------
strftime({MegaSecs, Secs, _MicroSecs}) ->
strftime(datetime(MegaSecs * 1000000 + Secs));
strftime(Secs) when is_integer(Secs) ->
strftime(datetime(Secs));
strftime({{Y,M,D}, {H,MM,S}}) ->
lists:flatten(
io_lib:format(
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
datetime(Timestamp) when is_integer(Timestamp) ->
Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}),
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
calendar:universal_time_to_local_time(Universal).
kmg(Byte) when Byte > ?GB ->
kmg(Byte / ?GB, "G");
kmg(Byte) when Byte > ?MB ->
kmg(Byte / ?MB, "M");
kmg(Byte) when Byte > ?KB ->
kmg(Byte / ?KB, "K");
kmg(Byte) ->
Byte.
kmg(F, S) ->
iolist_to_binary(io_lib:format("~.2f~s", [F, S])).
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});
ntoa(IP) ->
inet_parse:ntoa(IP).
merge_maps(Default, New) ->
maps:fold(fun(K, V, Acc) ->
case maps:get(K, Acc, undefined) of
OldV when is_map(OldV),
is_map(V) -> Acc#{K => merge_maps(OldV, V)};
_ -> Acc#{K => V}
end
end, Default, New).
-if(?OTP_RELEASE >= 23).
urldecode(S) ->
[{R, _}] = uri_string:dissect_query(S), R.
-else.
urldecode(S) ->
http_uri:decode(S).
-endif.

View File

@ -0,0 +1,288 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(LOG_LEVELS,["debug", "error", "info"]).
-define(LOG_HANDLER_ID, [file, default]).
all() ->
[{group, manage_apps},
{group, check_cli}].
groups() ->
[{manage_apps, [sequence],
[t_app
]},
{check_cli, [sequence],
[t_cli,
t_log_cmd,
t_mgmt_cmd,
t_status_cmd,
t_clients_cmd,
t_vm_cmd,
t_plugins_cmd,
t_modules_cmd,
t_trace_cmd,
t_broker_cmd,
t_router_cmd,
t_subscriptions_cmd,
t_listeners_cmd
]}].
apps() ->
[emqx, emqx_management, emqx_reloader].
init_per_suite(Config) ->
ekka_mnesia:start(),
emqx_mgmt_auth:mnesia(boot),
emqx_ct_helpers:start_apps([emqx_management, emqx_reloader]),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx_management, emqx_reloader, emqx]).
t_app(_Config) ->
{ok, AppSecret} = emqx_mgmt_auth:add_app(<<"app_id">>, <<"app_name">>),
?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret)),
?assertEqual(AppSecret, emqx_mgmt_auth:get_appsecret(<<"app_id">>)),
?assertEqual({<<"app_id">>, AppSecret,
<<"app_name">>, <<"Application user">>,
true, undefined},
lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())),
emqx_mgmt_auth:del_app(<<"app_id">>),
%% Use the default application secret
application:set_env(emqx_management, application, [{default_secret, <<"public">>}]),
{ok, AppSecret1} = emqx_mgmt_auth:add_app(<<"app_id">>, <<"app_name">>, <<"app_desc">>, true, undefined),
?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret1)),
?assertEqual(AppSecret1, emqx_mgmt_auth:get_appsecret(<<"app_id">>)),
?assertEqual(AppSecret1, <<"public">>),
?assertEqual({<<"app_id">>, AppSecret1, <<"app_name">>, <<"app_desc">>, true, undefined},
lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())),
emqx_mgmt_auth:del_app(<<"app_id">>),
application:set_env(emqx_management, application, []),
%% Specify the application secret
{ok, AppSecret2} = emqx_mgmt_auth:add_app(<<"app_id">>, <<"app_name">>, <<"secret">>, <<"app_desc">>, true, undefined),
?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret2)),
?assertEqual(AppSecret2, emqx_mgmt_auth:get_appsecret(<<"app_id">>)),
?assertEqual({<<"app_id">>, AppSecret2, <<"app_name">>, <<"app_desc">>, true, undefined},
lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())),
emqx_mgmt_auth:del_app(<<"app_id">>),
ok.
t_log_cmd(_) ->
print_mock(),
lists:foreach(fun(Level) ->
emqx_mgmt_cli:log(["primary-level", Level]),
?assertEqual(Level++"\n", emqx_mgmt_cli:log(["primary-level"]))
end, ?LOG_LEVELS),
lists:foreach(fun(Level) ->
emqx_mgmt_cli:log(["set-level", Level]),
?assertEqual(Level++"\n", emqx_mgmt_cli:log(["primary-level"]))
end, ?LOG_LEVELS),
[lists:foreach(fun(Level) ->
?assertEqual(Level++"\n", emqx_mgmt_cli:log(["handlers", "set-level",
atom_to_list(Id), Level]))
end, ?LOG_LEVELS)
|| #{id := Id} <- emqx_logger:get_log_handlers()].
t_mgmt_cmd(_) ->
ct:pal("start testing the mgmt command"),
print_mock(),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["lookup", "emqx_appid"]), "Not Found.")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["delete", "emqx_appid"]), "ok")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["insert", "emqx_appid", "emqx_name"]), "AppSecret:")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["insert", "emqx_appid", "emqx_name"]), "Error:")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["lookup", "emqx_appid"]), "app_id:")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["update", "emqx_appid", "ts"]), "update successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt(["delete", "emqx_appid"]), "ok")),
ok = emqx_mgmt_cli:mgmt(["list"]).
t_status_cmd(_) ->
ct:pal("start testing status command"),
print_mock(),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:status([]), "is running")).
t_broker_cmd(_) ->
ct:pal("start testing the broker command"),
print_mock(),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker([]), "sysdescr")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker(["stats"]), "subscriptions.shared")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker(["metrics"]), "bytes.sent")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker([undefined]), "broker")).
t_clients_cmd(_) ->
ct:pal("start testing the client command"),
print_mock(),
process_flag(trap_exit, true),
{ok, T} = emqtt:start_link([{host, "localhost"},
{clientid, <<"client12">>},
{username, <<"testuser1">>},
{password, <<"pass1">>}]),
{ok, _} = emqtt:connect(T),
timer:sleep(300),
emqx_mgmt_cli:clients(["list"]),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client12"]), "client12")),
?assertEqual((emqx_mgmt_cli:clients(["kick", "client12"])), "ok~n"),
timer:sleep(500),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client12"]), "Not Found")),
receive
{'EXIT', T, Reason} ->
ct:pal("Connection closed: ~p~n", [Reason])
after
500 ->
erlang:error("Client is not kick")
end,
WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()),
{ok, _} = rfc6455_client:open(WS),
Packet = raw_send_serialize(?CONNECT_PACKET(#mqtt_packet_connect{
clientid = <<"client13">>})),
ok = rfc6455_client:send_binary(WS, Packet),
Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT),
{binary, Bin} = rfc6455_client:recv(WS),
{ok, Connack, <<>>, _} = raw_recv_pase(Bin),
timer:sleep(300),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client13"]), "client13")).
% emqx_mgmt_cli:clients(["kick", "client13"]),
% timer:sleep(500),
% ?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client13"]), "Not Found")).
raw_recv_pase(Packet) ->
emqx_frame:parse(Packet).
raw_send_serialize(Packet) ->
emqx_frame:serialize(Packet).
t_vm_cmd(_) ->
ct:pal("start testing the vm command"),
print_mock(),
[[?assertMatch({match, _}, re:run(Result, Name)) || Result <- emqx_mgmt_cli:vm([Name])] || Name <- ["load", "memory", "process", "io", "ports"]],
[?assertMatch({match, _}, re:run(Result, "load")) || Result <- emqx_mgmt_cli:vm(["load"])],
[?assertMatch({match, _}, re:run(Result, "memory"))|| Result <- emqx_mgmt_cli:vm(["memory"])],
[?assertMatch({match, _}, re:run(Result, "process")) || Result <- emqx_mgmt_cli:vm(["process"])],
[?assertMatch({match, _}, re:run(Result, "io")) || Result <- emqx_mgmt_cli:vm(["io"])],
[?assertMatch({match, _}, re:run(Result, "ports")) || Result <- emqx_mgmt_cli:vm(["ports"])].
t_trace_cmd(_) ->
ct:pal("start testing the trace command"),
print_mock(),
logger:set_primary_config(level, debug),
{ok, T} = emqtt:start_link([{host, "localhost"},
{clientid, <<"client">>},
{username, <<"testuser">>},
{password, <<"pass">>}
]),
emqtt:connect(T),
emqtt:subscribe(T, <<"a/b/c">>),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["start", "client", "client", "log/clientid_trace.log"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["stop", "client", "client"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["start", "client", "client", "log/clientid_trace.log", "error"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["stop", "client", "client"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["start", "topic", "a/b/c", "log/clientid_trace.log"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["stop", "topic", "a/b/c"]), "successfully")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:trace(["start", "topic", "a/b/c", "log/clientid_trace.log", "error"]), "successfully")),
logger:set_primary_config(level, error).
t_router_cmd(_) ->
ct:pal("start testing the router command"),
print_mock(),
{ok, T} = emqtt:start_link([{host, "localhost"},
{clientid, <<"client1">>},
{username, <<"testuser1">>},
{password, <<"pass1">>}]),
emqtt:connect(T),
emqtt:subscribe(T, <<"a/b/c">>),
{ok, T1} = emqtt:start_link([{host, "localhost"},
{clientid, <<"client2">>},
{username, <<"testuser2">>},
{password, <<"pass2">>}
]),
emqtt:connect(T1),
emqtt:subscribe(T1, <<"a/b/c/d">>),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:routes(["list"]), "a/b/c | a/b/c")),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:routes(["show", "a/b/c"]), "a/b/c")).
t_subscriptions_cmd(_) ->
ct:pal("Start testing the subscriptions command"),
print_mock(),
{ok, T3} = emqtt:start_link([{host, "localhost"},
{clientid, <<"client">>},
{username, <<"testuser">>},
{password, <<"pass">>}]),
{ok, _} = emqtt:connect(T3),
{ok, _, _} = emqtt:subscribe(T3, <<"b/b/c">>),
timer:sleep(300),
[?assertMatch({match, _} , re:run(Result, "b/b/c"))
|| Result <- emqx_mgmt_cli:subscriptions(["show", <<"client">>])],
?assertEqual(emqx_mgmt_cli:subscriptions(["add", "client", "b/b/c", "0"]), "ok~n"),
?assertEqual(emqx_mgmt_cli:subscriptions(["del", "client", "b/b/c"]), "ok~n").
t_listeners_cmd(_) ->
print_mock(),
?assertEqual(emqx_mgmt_cli:listeners([]), ok),
?assertEqual(emqx_mgmt_cli:listeners(["stop", "wss", "8084"]), "Stop wss listener on 8084 successfully.\n").
t_plugins_cmd(_) ->
print_mock(),
meck:new(emqx_plugins, [non_strict, passthrough]),
meck:expect(emqx_plugins, load, fun(_) -> ok end),
meck:expect(emqx_plugins, unload, fun(_) -> ok end),
meck:expect(emqx_plugins, reload, fun(_) -> ok end),
?assertEqual(emqx_mgmt_cli:plugins(["list"]), ok),
?assertEqual(emqx_mgmt_cli:plugins(["unload", "emqx_reloader"]), "Plugin emqx_reloader unloaded successfully.\n"),
?assertEqual(emqx_mgmt_cli:plugins(["load", "emqx_reloader"]),"Plugin emqx_reloader loaded successfully.\n"),
?assertEqual(emqx_mgmt_cli:plugins(["unload", "emqx_management"]), "Plugin emqx_management can not be unloaded.~n").
t_modules_cmd(_) ->
print_mock(),
meck:new(emqx_modules, [non_strict, passthrough]),
meck:expect(emqx_modules, load, fun(_) -> ok end),
meck:expect(emqx_modules, unload, fun(_) -> ok end),
meck:expect(emqx_modules, reload, fun(_) -> ok end),
?assertEqual(emqx_mgmt_cli:modules(["list"]), ok),
?assertEqual(emqx_mgmt_cli:modules(["load", "emqx_mod_presence"]),"Module emqx_mod_presence loaded successfully.\n"),
?assertEqual(emqx_mgmt_cli:modules(["unload", "emqx_mod_presence"]), "Module emqx_mod_presence unloaded successfully.\n").
t_cli(_) ->
print_mock(),
?assertMatch({match, _}, re:run(emqx_mgmt_cli:status([""]), "status")),
[?assertMatch({match, _}, re:run(Value, "broker")) || Value <- emqx_mgmt_cli:broker([""])],
[?assertMatch({match, _}, re:run(Value, "cluster")) || Value <- emqx_mgmt_cli:cluster([""])],
[?assertMatch({match, _}, re:run(Value, "clients")) || Value <- emqx_mgmt_cli:clients([""])],
[?assertMatch({match, _}, re:run(Value, "routes")) || Value <- emqx_mgmt_cli:routes([""])],
[?assertMatch({match, _}, re:run(Value, "subscriptions")) || Value <- emqx_mgmt_cli:subscriptions([""])],
[?assertMatch({match, _}, re:run(Value, "plugins")) || Value <- emqx_mgmt_cli:plugins([""])],
[?assertMatch({match, _}, re:run(Value, "listeners")) || Value <- emqx_mgmt_cli:listeners([""])],
[?assertMatch({match, _}, re:run(Value, "vm")) || Value <- emqx_mgmt_cli:vm([""])],
[?assertMatch({match, _}, re:run(Value, "mnesia")) || Value <- emqx_mgmt_cli:mnesia([""])],
[?assertMatch({match, _}, re:run(Value, "trace")) || Value <- emqx_mgmt_cli:trace([""])],
[?assertMatch({match, _}, re:run(Value, "mgmt")) || Value <- emqx_mgmt_cli:mgmt([""])].
print_mock() ->
meck:new(emqx_ctl, [non_strict, passthrough]),
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end),
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end).

View File

@ -0,0 +1,645 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include("emqx_mgmt.hrl").
-define(CONTENT_TYPE, "application/x-www-form-urlencoded").
-define(HOST, "http://127.0.0.1:8081/").
-define(API_VERSION, "v4").
-define(BASE_PATH, "api").
all() ->
[{group, rest_api}].
groups() ->
[{rest_api,
[sequence],
[alarms,
apps,
banned,
brokers,
clients,
listeners,
metrics,
nodes,
plugins,
modules,
acl_cache,
pubsub,
routes_and_subscriptions,
stats]}].
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx, emqx_management, emqx_reloader]),
ekka_mnesia:start(),
emqx_mgmt_auth:mnesia(boot),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx_reloader, emqx_management, emqx]),
ekka_mnesia:ensure_stopped().
get(Key, ResponseBody) ->
maps:get(Key, jiffy:decode(list_to_binary(ResponseBody), [return_maps])).
lookup_alarm(Name, [#{<<"name">> := Name} | _More]) ->
true;
lookup_alarm(Name, [_Alarm | More]) ->
lookup_alarm(Name, More);
lookup_alarm(_Name, []) ->
false.
is_existing(Name, [#{name := Name} | _More]) ->
true;
is_existing(Name, [_Alarm | More]) ->
is_existing(Name, More);
is_existing(_Name, []) ->
false.
alarms(_) ->
emqx_alarm:activate(alarm1),
emqx_alarm:activate(alarm2),
?assert(is_existing(alarm1, emqx_alarm:get_alarms(activated))),
?assert(is_existing(alarm2, emqx_alarm:get_alarms(activated))),
{ok, Return1} = request_api(get, api_path(["alarms/activated"]), auth_header_()),
?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return1))))),
?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return1))))),
emqx_alarm:deactivate(alarm1),
{ok, Return2} = request_api(get, api_path(["alarms"]), auth_header_()),
?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return2))))),
?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return2))))),
{ok, Return3} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()),
?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return3))))),
?assertNot(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return3))))),
emqx_alarm:deactivate(alarm2),
{ok, Return4} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()),
?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return4))))),
?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return4))))),
{ok, _} = request_api(delete, api_path(["alarms/deactivated"]), auth_header_()),
{ok, Return5} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()),
?assertNot(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return5))))),
?assertNot(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return5))))).
apps(_) ->
AppId = <<"123456">>,
meck:new(emqx_mgmt_auth, [passthrough, no_history]),
meck:expect(emqx_mgmt_auth, add_app, 6, fun(_, _, _, _, _, _) -> {error, undefined} end),
{ok, Error1} = request_api(post, api_path(["apps"]), [],
auth_header_(), #{<<"app_id">> => AppId,
<<"name">> => <<"test">>,
<<"status">> => true}),
?assertEqual(?ERROR2, get(<<"code">>, Error1)),
meck:expect(emqx_mgmt_auth, del_app, 1, fun(_) -> {error, undefined} end),
{ok, Error2} = request_api(delete, api_path(["apps", binary_to_list(AppId)]), auth_header_()),
?assertEqual(?ERROR2, get(<<"code">>, Error2)),
meck:unload(emqx_mgmt_auth),
{ok, NoApp} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()),
?assertEqual(0, maps:size(get(<<"data">>, NoApp))),
{ok, NotFound} = request_api(put, api_path(["apps", binary_to_list(AppId)]), [],
auth_header_(), #{<<"name">> => <<"test 2">>,
<<"status">> => true}),
?assertEqual(<<"not_found">>, get(<<"message">>, NotFound)),
{ok, _} = request_api(post, api_path(["apps"]), [],
auth_header_(), #{<<"app_id">> => AppId,
<<"name">> => <<"test">>,
<<"status">> => true}),
{ok, _} = request_api(get, api_path(["apps"]), auth_header_()),
{ok, _} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()),
{ok, _} = request_api(put, api_path(["apps", binary_to_list(AppId)]), [],
auth_header_(), #{<<"name">> => <<"test 2">>,
<<"status">> => true}),
{ok, AppInfo} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()),
?assertEqual(<<"test 2">>, maps:get(<<"name">>, get(<<"data">>, AppInfo))),
{ok, _} = request_api(delete, api_path(["apps", binary_to_list(AppId)]), auth_header_()),
{ok, Result} = request_api(get, api_path(["apps"]), auth_header_()),
[App] = get(<<"data">>, Result),
?assertEqual(<<"admin">>, maps:get(<<"app_id">>, App)).
banned(_) ->
Who = <<"myclient">>,
{ok, _} = request_api(post, api_path(["banned"]), [],
auth_header_(), #{<<"who">> => Who,
<<"as">> => <<"clientid">>,
<<"reason">> => <<"test">>,
<<"by">> => <<"dashboard">>,
<<"at">> => erlang:system_time(second),
<<"until">> => erlang:system_time(second) + 10}),
{ok, Result} = request_api(get, api_path(["banned"]), auth_header_()),
[Banned] = get(<<"data">>, Result),
?assertEqual(Who, maps:get(<<"who">>, Banned)),
{ok, _} = request_api(delete, api_path(["banned", "clientid", binary_to_list(Who)]), auth_header_()),
{ok, Result2} = request_api(get, api_path(["banned"]), auth_header_()),
?assertEqual([], get(<<"data">>, Result2)).
brokers(_) ->
{ok, _} = request_api(get, api_path(["brokers"]), auth_header_()),
{ok, _} = request_api(get, api_path(["brokers", atom_to_list(node())]), auth_header_()),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, lookup_broker, 1, fun(_) -> {error, undefined} end),
{ok, Error} = request_api(get, api_path(["brokers", atom_to_list(node())]), auth_header_()),
?assertEqual(<<"undefined">>, get(<<"message">>, Error)),
meck:unload(emqx_mgmt).
clients(_) ->
process_flag(trap_exit, true),
Username1 = <<"user1">>,
Username2 = <<"user2">>,
ClientId1 = <<"client1">>,
ClientId2 = <<"client2">>,
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
{ok, _} = emqtt:connect(C1),
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
{ok, _} = emqtt:connect(C2),
timer:sleep(300),
{ok, Clients1} = request_api(get, api_path(["clients", binary_to_list(ClientId1)])
, auth_header_()),
?assertEqual(<<"client1">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients1)))),
{ok, Clients2} = request_api(get, api_path(["nodes", atom_to_list(node()),
"clients", binary_to_list(ClientId2)])
, auth_header_()),
?assertEqual(<<"client2">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients2)))),
{ok, Clients3} = request_api(get, api_path(["clients",
"username", binary_to_list(Username1)]),
auth_header_()),
?assertEqual(<<"client1">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients3)))),
{ok, Clients4} = request_api(get, api_path(["nodes", atom_to_list(node()),
"clients",
"username", binary_to_list(Username2)])
, auth_header_()),
?assertEqual(<<"client2">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients4)))),
{ok, Clients5} = request_api(get, api_path(["clients"]), "_limit=100&_page=1", auth_header_()),
?assertEqual(2, maps:get(<<"count">>, get(<<"meta">>, Clients5))),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, kickout_client, 1, fun(_) -> {error, undefined} end),
{ok, MeckRet1} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()),
?assertEqual(?ERROR1, get(<<"code">>, MeckRet1)),
meck:expect(emqx_mgmt, clean_acl_cache, 1, fun(_) -> {error, undefined} end),
{ok, MeckRet2} = request_api(delete, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()),
?assertEqual(?ERROR1, get(<<"code">>, MeckRet2)),
meck:expect(emqx_mgmt, list_acl_cache, 1, fun(_) -> {error, undefined} end),
{ok, MeckRet3} = request_api(get, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()),
?assertEqual(?ERROR1, get(<<"code">>, MeckRet3)),
meck:unload(emqx_mgmt),
{ok, Ok} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()),
?assertEqual(?SUCCESS, get(<<"code">>, Ok)),
{ok, NotFound0} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()),
?assertEqual(?ERROR12, get(<<"code">>, NotFound0)),
{ok, Clients6} = request_api(get, api_path(["clients"]), "_limit=100&_page=1", auth_header_()),
?assertEqual(1, maps:get(<<"count">>, get(<<"meta">>, Clients6))),
{ok, NotFound1} = request_api(get, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()),
?assertEqual(?ERROR12, get(<<"code">>, NotFound1)),
{ok, NotFound2} = request_api(delete, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()),
?assertEqual(?ERROR12, get(<<"code">>, NotFound2)),
{ok, EmptyAclCache} = request_api(get, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()),
?assertEqual(0, length(get(<<"data">>, EmptyAclCache))),
{ok, Ok1} = request_api(delete, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()),
?assertEqual(?SUCCESS, get(<<"code">>, Ok1)).
receive_exit(0) ->
ok;
receive_exit(Count) ->
receive
{'EXIT', Client, {shutdown, tcp_closed}} ->
ct:log("receive exit signal, Client: ~p", [Client]),
receive_exit(Count - 1);
{'EXIT', Client, _Reason} ->
ct:log("receive exit signal, Client: ~p", [Client]),
receive_exit(Count - 1)
after 1000 ->
ct:log("timeout")
end.
listeners(_) ->
{ok, _} = request_api(get, api_path(["listeners"]), auth_header_()),
{ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "listeners"]), auth_header_()),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, list_listeners, 0, fun() -> [{node(), {error, undefined}}] end),
{ok, Return} = request_api(get, api_path(["listeners"]), auth_header_()),
[Error] = get(<<"data">>, Return),
?assertEqual(<<"undefined">>,
maps:get(<<"error">>, maps:get(<<"listeners">>, Error))),
meck:unload(emqx_mgmt).
metrics(_) ->
{ok, _} = request_api(get, api_path(["metrics"]), auth_header_()),
{ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "metrics"]), auth_header_()),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, get_metrics, 1, fun(_) -> {error, undefined} end),
{ok, "{\"message\":\"undefined\"}"} = request_api(get, api_path(["nodes", atom_to_list(node()), "metrics"]), auth_header_()),
meck:unload(emqx_mgmt).
nodes(_) ->
{ok, _} = request_api(get, api_path(["nodes"]), auth_header_()),
{ok, _} = request_api(get, api_path(["nodes", atom_to_list(node())]), auth_header_()),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, list_nodes, 0, fun() -> [{node(), {error, undefined}}] end),
{ok, Return} = request_api(get, api_path(["nodes"]), auth_header_()),
[Error] = get(<<"data">>, Return),
?assertEqual(<<"undefined">>, maps:get(<<"error">>, Error)),
meck:unload(emqx_mgmt).
plugins(_) ->
{ok, Plugins1} = request_api(get, api_path(["plugins"]), auth_header_()),
[Plugins11] = filter(get(<<"data">>, Plugins1), <<"node">>, atom_to_binary(node(), utf8)),
[Plugin1] = filter(maps:get(<<"plugins">>, Plugins11), <<"name">>, <<"emqx_reloader">>),
?assertEqual(<<"emqx_reloader">>, maps:get(<<"name">>, Plugin1)),
?assertEqual(true, maps:get(<<"active">>, Plugin1)),
{ok, _} = request_api(put,
api_path(["plugins",
atom_to_list(emqx_reloader),
"unload"]),
auth_header_()),
{ok, Error1} = request_api(put,
api_path(["plugins",
atom_to_list(emqx_reloader),
"unload"]),
auth_header_()),
?assertEqual(<<"not_started">>, get(<<"message">>, Error1)),
{ok, Plugins2} = request_api(get,
api_path(["nodes", atom_to_list(node()), "plugins"]),
auth_header_()),
[Plugin2] = filter(get(<<"data">>, Plugins2), <<"name">>, <<"emqx_reloader">>),
?assertEqual(<<"emqx_reloader">>, maps:get(<<"name">>, Plugin2)),
?assertEqual(false, maps:get(<<"active">>, Plugin2)),
{ok, _} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"plugins",
atom_to_list(emqx_reloader),
"load"]),
auth_header_()),
{ok, Plugins3} = request_api(get,
api_path(["nodes", atom_to_list(node()), "plugins"]),
auth_header_()),
[Plugin3] = filter(get(<<"data">>, Plugins3), <<"name">>, <<"emqx_reloader">>),
?assertEqual(<<"emqx_reloader">>, maps:get(<<"name">>, Plugin3)),
?assertEqual(false, maps:get(<<"active">>, Plugin3)),
{ok, _} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"plugins",
atom_to_list(emqx_reloader),
"unload"]),
auth_header_()),
{ok, Error2} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"plugins",
atom_to_list(emqx_reloader),
"unload"]),
auth_header_()),
?assertEqual(<<"not_started">>, get(<<"message">>, Error2)).
modules(_) ->
emqx_modules:load_module(emqx_mod_presence, false),
timer:sleep(50),
{ok, Modules1} = request_api(get, api_path(["modules"]), auth_header_()),
[Modules11] = filter(get(<<"data">>, Modules1), <<"node">>, atom_to_binary(node(), utf8)),
[Module1] = filter(maps:get(<<"modules">>, Modules11), <<"name">>, <<"emqx_mod_presence">>),
?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module1)),
?assertEqual(true, maps:get(<<"active">>, Module1)),
{ok, _} = request_api(put,
api_path(["modules",
atom_to_list(emqx_mod_presence),
"unload"]),
auth_header_()),
{ok, Error1} = request_api(put,
api_path(["modules",
atom_to_list(emqx_mod_presence),
"unload"]),
auth_header_()),
?assertEqual(<<"not_started">>, get(<<"message">>, Error1)),
{ok, Modules2} = request_api(get,
api_path(["nodes", atom_to_list(node()), "modules"]),
auth_header_()),
[Module2] = filter(get(<<"data">>, Modules2), <<"name">>, <<"emqx_mod_presence">>),
?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module2)),
?assertEqual(false, maps:get(<<"active">>, Module2)),
{ok, _} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"modules",
atom_to_list(emqx_mod_presence),
"load"]),
auth_header_()),
{ok, Modules3} = request_api(get,
api_path(["nodes", atom_to_list(node()), "modules"]),
auth_header_()),
[Module3] = filter(get(<<"data">>, Modules3), <<"name">>, <<"emqx_mod_presence">>),
?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module3)),
?assertEqual(true, maps:get(<<"active">>, Module3)),
{ok, _} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"modules",
atom_to_list(emqx_mod_presence),
"unload"]),
auth_header_()),
{ok, Error2} = request_api(put,
api_path(["nodes",
atom_to_list(node()),
"modules",
atom_to_list(emqx_mod_presence),
"unload"]),
auth_header_()),
?assertEqual(<<"not_started">>, get(<<"message">>, Error2)),
emqx_modules:unload(emqx_mod_presence).
acl_cache(_) ->
ClientId = <<"client1">>,
Topic = <<"mytopic">>,
{ok, C1} = emqtt:start_link(#{clientid => ClientId}),
{ok, _} = emqtt:connect(C1),
{ok, _, _} = emqtt:subscribe(C1, Topic, 2),
%% get acl cache, should not be empty
{ok, Result} = request_api(get, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()),
#{<<"code">> := 0, <<"data">> := Caches} = jiffy:decode(list_to_binary(Result), [return_maps]),
?assert(length(Caches) > 0),
?assertMatch(#{<<"access">> := <<"subscribe">>,
<<"topic">> := Topic,
<<"result">> := <<"allow">>,
<<"updated_time">> := _}, hd(Caches)),
%% clear acl cache
{ok, Result2} = request_api(delete, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()),
?assertMatch(#{<<"code">> := 0}, jiffy:decode(list_to_binary(Result2), [return_maps])),
%% get acl cache again, after the acl cache is cleared
{ok, Result3} = request_api(get, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()),
#{<<"code">> := 0, <<"data">> := Caches3} = jiffy:decode(list_to_binary(Result3), [return_maps]),
?assertEqual(0, length(Caches3)),
ok = emqtt:disconnect(C1).
pubsub(_) ->
ClientId = <<"client1">>,
Options = #{clientid => ClientId,
proto_ver => 5},
Topic = <<"mytopic">>,
{ok, C1} = emqtt:start_link(Options),
{ok, _} = emqtt:connect(C1),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, subscribe, 2, fun(_, _) -> {error, undefined} end),
{ok, NotFound1} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => Topic,
<<"qos">> => 2}),
?assertEqual(?ERROR12, get(<<"code">>, NotFound1)),
meck:unload(emqx_mgmt),
{ok, BadTopic1} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topics">> => <<"">>,
<<"qos">> => 2}),
?assertEqual(?ERROR15, get(<<"code">>, BadTopic1)),
{ok, BadTopic2} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topics">> => <<"">>,
<<"qos">> => 1,
<<"payload">> => <<"hello">>}),
?assertEqual(?ERROR15, get(<<"code">>, BadTopic2)),
{ok, BadTopic3} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => <<"">>}),
?assertEqual(?ERROR15, get(<<"code">>, BadTopic3)),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, unsubscribe, 2, fun(_, _) -> {error, undefined} end),
{ok, NotFound2} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => Topic}),
?assertEqual(?ERROR12, get(<<"code">>, NotFound2)),
meck:unload(emqx_mgmt),
{ok, Code} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => Topic,
<<"qos">> => 2}),
?assertEqual(?SUCCESS, get(<<"code">>, Code)),
{ok, Code} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => <<"mytopic">>,
<<"qos">> => 1,
<<"payload">> => <<"hello">>}),
?assert(receive
{publish, #{payload := <<"hello">>}} ->
true
after 100 ->
false
end),
%% json payload
{ok, Code} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => <<"mytopic">>,
<<"qos">> => 1,
<<"payload">> => #{body => "hello world"}}),
Payload = emqx_json:encode(#{body => "hello world"}),
?assert(receive
{publish, #{payload := Payload}} ->
true
after 100 ->
false
end),
{ok, Code} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(),
#{<<"clientid">> => ClientId,
<<"topic">> => Topic}),
%% tests subscribe_batch
Topic_list = [<<"mytopic1">>, <<"mytopic2">>],
[ {ok, _, [2]} = emqtt:subscribe(C1, Topics, 2) || Topics <- Topic_list],
Body1 = [ #{<<"clientid">> => ClientId, <<"topic">> => Topics, <<"qos">> => 2} || Topics <- Topic_list],
{ok, Data1} = request_api(post, api_path(["mqtt/subscribe_batch"]), [], auth_header_(), Body1),
loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data1), [return_maps]))),
%% tests publish_batch
Body2 = [ #{<<"clientid">> => ClientId, <<"topic">> => Topics, <<"qos">> => 2, <<"retain">> => <<"false">>, <<"payload">> => #{body => "hello world"}} || Topics <- Topic_list ],
{ok, Data2} = request_api(post, api_path(["mqtt/publish_batch"]), [], auth_header_(), Body2),
loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data2), [return_maps]))),
[ ?assert(receive
{publish, #{topic := Topics}} ->
true
after 100 ->
false
end) || Topics <- Topic_list ],
%% tests unsubscribe_batch
Body3 = [#{<<"clientid">> => ClientId, <<"topic">> => Topics} || Topics <- Topic_list],
{ok, Data3} = request_api(post, api_path(["mqtt/unsubscribe_batch"]), [], auth_header_(), Body3),
loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data3), [return_maps]))),
ok = emqtt:disconnect(C1).
loop([]) -> [];
loop(Data) ->
[H | T] = Data,
ct:pal("H: ~p~n", [H]),
?assertEqual(0, maps:get(<<"code">>, H)),
loop(T).
routes_and_subscriptions(_) ->
ClientId = <<"myclient">>,
Topic = <<"mytopic">>,
{ok, NonRoute} = request_api(get, api_path(["routes"]), auth_header_()),
?assertEqual([], get(<<"data">>, NonRoute)),
{ok, NonSubscription} = request_api(get, api_path(["subscriptions"]), auth_header_()),
?assertEqual([], get(<<"data">>, NonSubscription)),
{ok, NonSubscription1} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions"]), auth_header_()),
?assertEqual([], get(<<"data">>, NonSubscription1)),
{ok, NonSubscription2} = request_api(get,
api_path(["subscriptions", binary_to_list(ClientId)]),
auth_header_()),
?assertEqual([], get(<<"data">>, NonSubscription2)),
{ok, NonSubscription3} = request_api(get, api_path(["nodes",
atom_to_list(node()),
"subscriptions",
binary_to_list(ClientId)])
, auth_header_()),
?assertEqual([], get(<<"data">>, NonSubscription3)),
{ok, C1} = emqtt:start_link(#{clean_start => true,
clientid => ClientId,
proto_ver => ?MQTT_PROTO_V5}),
{ok, _} = emqtt:connect(C1),
{ok, _, [2]} = emqtt:subscribe(C1, Topic, qos2),
{ok, Result} = request_api(get, api_path(["routes"]), auth_header_()),
[Route] = get(<<"data">>, Result),
?assertEqual(Topic, maps:get(<<"topic">>, Route)),
{ok, Result2} = request_api(get, api_path(["routes", binary_to_list(Topic)]), auth_header_()),
[Route] = get(<<"data">>, Result2),
{ok, Result3} = request_api(get, api_path(["subscriptions"]), auth_header_()),
[Subscription] = get(<<"data">>, Result3),
?assertEqual(Topic, maps:get(<<"topic">>, Subscription)),
?assertEqual(ClientId, maps:get(<<"clientid">>, Subscription)),
{ok, Result3} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions"]), auth_header_()),
{ok, Result4} = request_api(get, api_path(["subscriptions", binary_to_list(ClientId)]), auth_header_()),
[Subscription] = get(<<"data">>, Result4),
{ok, Result4} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions", binary_to_list(ClientId)])
, auth_header_()),
ok = emqtt:disconnect(C1).
stats(_) ->
{ok, _} = request_api(get, api_path(["stats"]), auth_header_()),
{ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "stats"]), auth_header_()),
meck:new(emqx_mgmt, [passthrough, no_history]),
meck:expect(emqx_mgmt, get_stats, 1, fun(_) -> {error, undefined} end),
{ok, Return} = request_api(get, api_path(["nodes", atom_to_list(node()), "stats"]), auth_header_()),
?assertEqual(<<"undefined">>, get(<<"message">>, Return)),
meck:unload(emqx_mgmt).
request_api(Method, Url, Auth) ->
request_api(Method, Url, [], Auth, []).
request_api(Method, Url, QueryParams, Auth) ->
request_api(Method, Url, QueryParams, Auth, []).
request_api(Method, Url, QueryParams, Auth, []) ->
NewUrl = case QueryParams of
"" -> Url;
_ -> Url ++ "?" ++ QueryParams
end,
do_request_api(Method, {NewUrl, [Auth]});
request_api(Method, Url, QueryParams, Auth, Body) ->
NewUrl = case QueryParams of
"" -> Url;
_ -> Url ++ "?" ++ QueryParams
end,
do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}).
do_request_api(Method, Request)->
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], []) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
{ok, {{"HTTP/1.1", Code, _}, _, Return} }
when Code =:= 200 orelse Code =:= 201 ->
{ok, Return};
{ok, {Reason, _, _}} ->
{error, Reason}
end.
auth_header_() ->
AppId = <<"admin">>,
AppSecret = <<"public">>,
auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)).
auth_header_(User, Pass) ->
Encoded = base64:encode_to_string(lists:append([User,":",Pass])),
{"Authorization","Basic " ++ Encoded}.
api_path(Parts)->
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts).
filter(List, Key, Value) ->
lists:filter(fun(Item) ->
maps:get(Key, Item) == Value
end, List).

View File

@ -0,0 +1,35 @@
##--------------------------------------------------------------------
## EMQ X Management Plugin
##--------------------------------------------------------------------
## Max Row Limit
management.max_row_limit = 10000
## Application default secret
#
# management.application.default_secret = public
##--------------------------------------------------------------------
## HTTP Listener
management.listener.http = 8080
management.listener.http.acceptors = 2
management.listener.http.max_clients = 512
management.listener.http.backlog = 512
management.listener.http.send_timeout = 15s
management.listener.http.send_timeout_close = on
##--------------------------------------------------------------------
## HTTPS Listener
## management.listener.https = 8081
## management.listener.https.acceptors = 2
## management.listener.https.max_clients = 512
## management.listener.https.backlog = 512
## management.listener.https.send_timeout = 15s
## management.listener.https.send_timeout_close = on
## management.listener.https.certfile = etc/certs/cert.pem
## management.listener.https.keyfile = etc/certs/key.pem
## management.listener.https.cacertfile = etc/certs/cacert.pem
## management.listener.https.verify = verify_peer
## management.listener.https.fail_if_no_peer_cert = true

View File

@ -0,0 +1,24 @@
##--------------------------------------------------------------------
## Reloader Plugin
##--------------------------------------------------------------------
## Interval of hot code reloading.
##
## Value: Duration
## - h: hour
## - m: minute
## - s: second
##
## Examples:
## - 2h: 2 hours
## - 30m: 30 minutes
## - 20s: 20 seconds
##
## Defaut: 60s
reloader.interval = 60s
## Logfile of reloader.
##
## Value: File
reloader.logfile = reloader.log

View File

@ -0,0 +1,252 @@
%% The contents of this file are subject to the Mozilla Public License
%% Version 1.1 (the "License"); you may not use this file except in
%% compliance with the License. You may obtain a copy of the License at
%% http://www.mozilla.org/MPL/
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
%% License for the specific language governing rights and limitations
%% under the License.
%%
%% The Original Code is RabbitMQ Management Console.
%%
%% The Initial Developer of the Original Code is GoPivotal, Inc.
%% Copyright (c) 2012-2016 Pivotal Software, Inc. All rights reserved.
%%
-module(rfc6455_client).
-export([new/2, open/1, recv/1, send/2, send_binary/2, close/1, close/2]).
-record(state, {host, port, addr, path, ppid, socket, data, phase}).
%% --------------------------------------------------------------------------
new(WsUrl, PPid) ->
crypto:start(),
"ws://" ++ Rest = WsUrl,
[Addr, Path] = split("/", Rest, 1),
[Host, MaybePort] = split(":", Addr, 1, empty),
Port = case MaybePort of
empty -> 80;
V -> {I, ""} = string:to_integer(V), I
end,
State = #state{host = Host,
port = Port,
addr = Addr,
path = "/" ++ Path,
ppid = PPid},
spawn(fun() ->
start_conn(State)
end).
open(WS) ->
receive
{rfc6455, open, WS, Opts} ->
{ok, Opts};
{rfc6455, close, WS, R} ->
{close, R}
end.
recv(WS) ->
receive
{rfc6455, recv, WS, Payload} ->
{ok, Payload};
{rfc6455, recv_binary, WS, Payload} ->
{binary, Payload};
{rfc6455, close, WS, R} ->
{close, R}
end.
send(WS, IoData) ->
WS ! {send, IoData},
ok.
send_binary(WS, IoData) ->
WS ! {send_binary, IoData},
ok.
close(WS) ->
close(WS, {1000, ""}).
close(WS, WsReason) ->
WS ! {close, WsReason},
receive
{rfc6455, close, WS, R} ->
{close, R}
end.
%% --------------------------------------------------------------------------
start_conn(State) ->
{ok, Socket} = gen_tcp:connect(State#state.host, State#state.port,
[binary,
{packet, 0}]),
Key = base64:encode_to_string(crypto:strong_rand_bytes(16)),
gen_tcp:send(Socket,
"GET " ++ State#state.path ++ " HTTP/1.1\r\n" ++
"Host: " ++ State#state.addr ++ "\r\n" ++
"Upgrade: websocket\r\n" ++
"Connection: Upgrade\r\n" ++
"Sec-WebSocket-Key: " ++ Key ++ "\r\n" ++
"Origin: null\r\n" ++
"Sec-WebSocket-Protocol: mqtt\r\n" ++
"Sec-WebSocket-Version: 13\r\n\r\n"),
loop(State#state{socket = Socket,
data = <<>>,
phase = opening}).
do_recv(State = #state{phase = opening, ppid = PPid, data = Data}) ->
case split("\r\n\r\n", binary_to_list(Data), 1, empty) of
[_Http, empty] -> State;
[Http, Data1] ->
%% TODO: don't ignore http response data, verify key
PPid ! {rfc6455, open, self(), [{http_response, Http}]},
State#state{phase = open,
data = Data1}
end;
do_recv(State = #state{phase = Phase, data = Data, socket = Socket, ppid = PPid})
when Phase =:= open orelse Phase =:= closing ->
R = case Data of
<<F:1, _:3, O:4, 0:1, L:7, Payload:L/binary, Rest/binary>>
when L < 126 ->
{F, O, Payload, Rest};
<<F:1, _:3, O:4, 0:1, 126:7, L2:16, Payload:L2/binary, Rest/binary>> ->
{F, O, Payload, Rest};
<<F:1, _:3, O:4, 0:1, 127:7, L2:64, Payload:L2/binary, Rest/binary>> ->
{F, O, Payload, Rest};
<<_:1, _:3, _:4, 1:1, _/binary>> ->
%% According o rfc6455 5.1 the server must not mask any frames.
die(Socket, PPid, {1006, "Protocol error"}, normal);
_ ->
moredata
end,
case R of
moredata ->
State;
_ -> do_recv2(State, R)
end.
do_recv2(State = #state{phase = Phase, socket = Socket, ppid = PPid}, R) ->
case R of
{1, 1, Payload, Rest} ->
PPid ! {rfc6455, recv, self(), Payload},
State#state{data = Rest};
{1, 2, Payload, Rest} ->
PPid ! {rfc6455, recv_binary, self(), Payload},
State#state{data = Rest};
{1, 8, Payload, _Rest} ->
WsReason = case Payload of
<<WC:16, WR/binary>> -> {WC, WR};
<<>> -> {1005, "No status received"}
end,
case Phase of
open -> %% echo
do_close(State, WsReason),
gen_tcp:close(Socket);
closing ->
ok
end,
die(Socket, PPid, WsReason, normal);
{_, _, _, _Rest2} ->
io:format("Unknown frame type~n"),
die(Socket, PPid, {1006, "Unknown frame type"}, normal)
end.
encode_frame(F, O, Payload) ->
Mask = crypto:strong_rand_bytes(4),
MaskedPayload = apply_mask(Mask, iolist_to_binary(Payload)),
L = byte_size(MaskedPayload),
IoData = case L of
_ when L < 126 ->
[<<F:1, 0:3, O:4, 1:1, L:7>>, Mask, MaskedPayload];
_ when L < 65536 ->
[<<F:1, 0:3, O:4, 1:1, 126:7, L:16>>, Mask, MaskedPayload];
_ ->
[<<F:1, 0:3, O:4, 1:1, 127:7, L:64>>, Mask, MaskedPayload]
end,
iolist_to_binary(IoData).
do_send(State = #state{socket = Socket}, Payload) ->
gen_tcp:send(Socket, encode_frame(1, 1, Payload)),
State.
do_send_binary(State = #state{socket = Socket}, Payload) ->
gen_tcp:send(Socket, encode_frame(1, 2, Payload)),
State.
do_close(State = #state{socket = Socket}, {Code, Reason}) ->
Payload = iolist_to_binary([<<Code:16>>, Reason]),
gen_tcp:send(Socket, encode_frame(1, 8, Payload)),
State#state{phase = closing}.
loop(State = #state{socket = Socket, ppid = PPid, data = Data,
phase = Phase}) ->
receive
{tcp, Socket, Bin} ->
State1 = State#state{data = iolist_to_binary([Data, Bin])},
loop(do_recv(State1));
{send, Payload} when Phase == open ->
loop(do_send(State, Payload));
{send_binary, Payload} when Phase == open ->
loop(do_send_binary(State, Payload));
{tcp_closed, Socket} ->
die(Socket, PPid, {1006, "Connection closed abnormally"}, normal);
{close, WsReason} when Phase == open ->
loop(do_close(State, WsReason))
end.
die(Socket, PPid, WsReason, Reason) ->
gen_tcp:shutdown(Socket, read_write),
PPid ! {rfc6455, close, self(), WsReason},
exit(Reason).
%% --------------------------------------------------------------------------
split(SubStr, Str, Limit) ->
split(SubStr, Str, Limit, "").
split(SubStr, Str, Limit, Default) ->
Acc = split(SubStr, Str, Limit, [], Default),
lists:reverse(Acc).
split(_SubStr, Str, 0, Acc, _Default) -> [Str | Acc];
split(SubStr, Str, Limit, Acc, Default) ->
{L, R} = case string:str(Str, SubStr) of
0 -> {Str, Default};
I -> {string:substr(Str, 1, I-1),
string:substr(Str, I+length(SubStr))}
end,
split(SubStr, R, Limit-1, [L | Acc], Default).
apply_mask(Mask, Data) when is_number(Mask) ->
apply_mask(<<Mask:32>>, Data);
apply_mask(<<0:32>>, Data) ->
Data;
apply_mask(Mask, Data) ->
iolist_to_binary(lists:reverse(apply_mask2(Mask, Data, []))).
apply_mask2(M = <<Mask:32>>, <<Data:32, Rest/binary>>, Acc) ->
T = Data bxor Mask,
apply_mask2(M, Rest, [<<T:32>> | Acc]);
apply_mask2(<<Mask:24, _:8>>, <<Data:24>>, Acc) ->
T = Data bxor Mask,
[<<T:24>> | Acc];
apply_mask2(<<Mask:16, _:16>>, <<Data:16>>, Acc) ->
T = Data bxor Mask,
[<<T:16>> | Acc];
apply_mask2(<<Mask:8, _:24>>, <<Data:8>>, Acc) ->
T = Data bxor Mask,
[<<T:8>> | Acc];
apply_mask2(_, <<>>, Acc) ->
Acc.

View File

@ -9,7 +9,8 @@
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.7.4"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.7.4"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.7.4"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.7.4"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.0"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.0"}}},
{cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}} {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}},
{minirest, {git, "https://github.com/emqx/minirest", {tag, "v0.3.0"}}}
]}. ]}.
{erl_opts, [warn_unused_vars, {erl_opts, [warn_unused_vars,