Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
a7b7bb19f7 | |
![]() |
4d8fd6b427 |
33
Makefile
33
Makefile
|
@ -1,5 +1,6 @@
|
|||
## shallow clone for speed
|
||||
|
||||
REBAR3 ?= ./rebar3
|
||||
REBAR_GIT_CLONE_OPTIONS += --depth 1
|
||||
export REBAR_GIT_CLONE_OPTIONS
|
||||
|
||||
|
@ -19,8 +20,8 @@ tests: eunit ct
|
|||
|
||||
.PHONY: run
|
||||
run: run_setup unlock
|
||||
@rebar3 as test get-deps
|
||||
@rebar3 as test auto --name $(RUN_NODE_NAME) --script scripts/run_emqx.escript
|
||||
@$(REBAR3) as test get-deps
|
||||
@$(REBAR3) as test auto --name $(RUN_NODE_NAME) --script scripts/run_emqx.escript
|
||||
|
||||
.PHONY: run_setup
|
||||
run_setup:
|
||||
|
@ -46,13 +47,13 @@ run_setup:
|
|||
|
||||
.PHONY: shell
|
||||
shell:
|
||||
@rebar3 as test auto
|
||||
@$(REBAR3) as test auto
|
||||
|
||||
compile: unlock
|
||||
@rebar3 compile
|
||||
@$(REBAR3) compile
|
||||
|
||||
unlock:
|
||||
@rebar3 unlock
|
||||
@$(REBAR3) unlock
|
||||
|
||||
clean: distclean
|
||||
|
||||
|
@ -61,35 +62,35 @@ CUTTLEFISH_SCRIPT := _build/default/lib/cuttlefish/cuttlefish
|
|||
|
||||
.PHONY: cover
|
||||
cover:
|
||||
@rebar3 cover
|
||||
@$(REBAR3) cover
|
||||
|
||||
.PHONY: coveralls
|
||||
coveralls:
|
||||
@rebar3 as test coveralls send
|
||||
@$(REBAR3) as test coveralls send
|
||||
|
||||
.PHONY: xref
|
||||
xref:
|
||||
@rebar3 xref
|
||||
@$(REBAR3) xref
|
||||
|
||||
.PHONY: dialyzer
|
||||
dialyzer:
|
||||
@rebar3 dialyzer
|
||||
@$(REBAR3) dialyzer
|
||||
|
||||
.PHONY: proper
|
||||
proper:
|
||||
@rebar3 proper -d test/props -c
|
||||
@$(REBAR3) proper -d test/props -c
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
@rebar3 get-deps
|
||||
@$(REBAR3) get-deps
|
||||
|
||||
.PHONY: eunit
|
||||
eunit:
|
||||
@rebar3 eunit -v
|
||||
@$(REBAR3) eunit -v
|
||||
|
||||
.PHONY: ct_setup
|
||||
ct_setup:
|
||||
rebar3 as test compile
|
||||
$(REBAR3) as test compile
|
||||
@mkdir -p data
|
||||
@if [ ! -f data/loaded_plugins ]; then touch data/loaded_plugins; fi
|
||||
@ln -s -f '../../../../etc' _build/test/lib/emqx/
|
||||
|
@ -97,20 +98,20 @@ ct_setup:
|
|||
|
||||
.PHONY: ct
|
||||
ct: ct_setup
|
||||
@rebar3 ct -v --name $(CT_NODE_NAME) --suite=$(shell echo $(foreach var,$(CT_SUITES),test/$(var)_SUITE) | tr ' ' ',')
|
||||
@$(REBAR3) ct -v --name $(CT_NODE_NAME) --suite=$(shell echo $(foreach var,$(CT_SUITES),test/$(var)_SUITE) | tr ' ' ',')
|
||||
|
||||
## Run one single CT with rebar3
|
||||
## e.g. make ct-one-suite suite=emqx_bridge
|
||||
.PHONY: $(SUITES:%=ct-%)
|
||||
$(CT_SUITES:%=ct-%): ct_setup
|
||||
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover
|
||||
@$(REBAR3) ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover
|
||||
|
||||
.PHONY: app.config
|
||||
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf
|
||||
$(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/
|
||||
|
||||
$(CUTTLEFISH_SCRIPT):
|
||||
@rebar3 get-deps
|
||||
@$(REBAR3) get-deps
|
||||
@if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi
|
||||
|
||||
bbmustache:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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"]).
|
|
@ -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}.
|
||||
|
|
@ -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}.
|
|
@ -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"}
|
||||
]}
|
||||
]}.
|
|
@ -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.
|
||||
|
|
@ -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}].
|
|
@ -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.
|
|
@ -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.
|
|
@ -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}].
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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}].
|
||||
|
|
@ -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.
|
||||
|
|
@ -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}.
|
||||
|
|
@ -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)}.
|
||||
|
|
@ -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}.
|
||||
|
|
@ -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.
|
|
@ -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}.
|
||||
|
|
@ -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.
|
|
@ -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}}.
|
|
@ -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.
|
|
@ -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().
|
|
@ -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).
|
||||
|
|
@ -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).
|
|
@ -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.
|
|
@ -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}, []}}.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
|
@ -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).
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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.
|
|
@ -9,7 +9,8 @@
|
|||
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.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"}}},
|
||||
{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,
|
||||
|
|
Loading…
Reference in New Issue