Merge remote-tracking branch 'origin/master' into release-51

This commit is contained in:
Zaiming (Stone) Shi 2023-06-12 10:35:47 +02:00
commit 12b73ba1c0
125 changed files with 3288 additions and 904 deletions

View File

@ -88,7 +88,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: ilammy/msvc-dev-cmd@v1.12.0 - uses: ilammy/msvc-dev-cmd@v1.12.0
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- name: build - name: build

View File

@ -96,7 +96,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ilammy/msvc-dev-cmd@v1.12.0 - uses: ilammy/msvc-dev-cmd@v1.12.0
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: ${{ matrix.otp }} otp-version: ${{ matrix.otp }}
- name: build - name: build

View File

@ -58,7 +58,7 @@ jobs:
arch: arch:
- amd64 - amd64
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
@ -133,7 +133,7 @@ jobs:
# - emqx-enterprise # TODO test enterprise # - emqx-enterprise # TODO test enterprise
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3

View File

@ -14,7 +14,7 @@ jobs:
outputs: outputs:
version: ${{ steps.build_docker.outputs.version}} version: ${{ steps.build_docker.outputs.version}}
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- name: download jmeter - name: download jmeter
@ -57,7 +57,7 @@ jobs:
needs: build_emqx_for_jmeter_tests needs: build_emqx_for_jmeter_tests
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -153,7 +153,7 @@ jobs:
needs: build_emqx_for_jmeter_tests needs: build_emqx_for_jmeter_tests
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -259,7 +259,7 @@ jobs:
needs: build_emqx_for_jmeter_tests needs: build_emqx_for_jmeter_tests
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -361,7 +361,7 @@ jobs:
needs: build_emqx_for_jmeter_tests needs: build_emqx_for_jmeter_tests
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -460,7 +460,7 @@ jobs:
needs: build_emqx_for_jmeter_tests needs: build_emqx_for_jmeter_tests
steps: steps:
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -71,7 +71,7 @@ jobs:
shell: bash shell: bash
steps: steps:
# setup Erlang to run lux # setup Erlang to run lux
- uses: erlef/setup-beam@v1.15.2 - uses: erlef/setup-beam@v1.15.4
with: with:
otp-version: 25.3.2 otp-version: 25.3.2
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -16,7 +16,7 @@ endif
# Dashbord version # Dashbord version
# from https://github.com/emqx/emqx-dashboard5 # from https://github.com/emqx/emqx-dashboard5
export EMQX_DASHBOARD_VERSION ?= v1.2.6-beta.1 export EMQX_DASHBOARD_VERSION ?= v1.2.6-beta.1
export EMQX_EE_DASHBOARD_VERSION ?= e1.1.0-beta.2 export EMQX_EE_DASHBOARD_VERSION ?= e1.1.0-beta.3
# `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used # `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used
# In make 4.4+, for backward-compatibility the value from the original environment is used. # In make 4.4+, for backward-compatibility the value from the original environment is used.

View File

@ -59,3 +59,27 @@
end end
end)() end)()
). ).
-define(retrying(CONFIG, NUM_RETRIES, TEST_BODY_FN), begin
__TEST_CASE = ?FUNCTION_NAME,
(fun
__GO(__CONFIG, __N) when __N >= NUM_RETRIES ->
TEST_BODY_FN(__CONFIG);
__GO(__CONFIG, __N) ->
try
TEST_BODY_FN(__CONFIG)
catch
__KIND:__REASON:__STACKTRACE ->
ct:pal("test errored; will retry\n ~p", [
#{kind => __KIND, reason => __REASON, stacktrace => __STACKTRACE}
]),
end_per_testcase(__TEST_CASE, __CONFIG),
garbage_collect(),
timer:sleep(1000),
__CONFIG1 = init_per_testcase(__TEST_CASE, __CONFIG),
__GO(__CONFIG1, __N + 1)
end
end)(
CONFIG, 0
)
end).

View File

@ -0,0 +1,24 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_config_backup).
-callback import_config(RawConf :: map()) ->
{ok, #{
root_key => emqx_utils_maps:config_key(),
changed => [emqx_utils_maps:config_path()]
}}
| {error, #{root_key => emqx_utils_maps:config_key(), reason => term()}}.

View File

@ -0,0 +1,19 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_db_backup).
-callback backup_tables() -> [mria:table()].

View File

@ -17,6 +17,7 @@
-module(emqx_banned). -module(emqx_banned).
-behaviour(gen_server). -behaviour(gen_server).
-behaviour(emqx_db_backup).
-include("emqx.hrl"). -include("emqx.hrl").
-include("logger.hrl"). -include("logger.hrl").
@ -50,6 +51,8 @@
code_change/3 code_change/3
]). ]).
-export([backup_tables/0]).
%% Internal exports (RPC) %% Internal exports (RPC)
-export([ -export([
expire_banned_items/1 expire_banned_items/1
@ -82,6 +85,11 @@ mnesia(boot) ->
{storage_properties, [{ets, [{read_concurrency, true}]}]} {storage_properties, [{ets, [{read_concurrency, true}]}]}
]). ]).
%%--------------------------------------------------------------------
%% Data backup
%%--------------------------------------------------------------------
backup_tables() -> [?BANNED_TAB].
%% @doc Start the banned server. %% @doc Start the banned server.
-spec start_link() -> startlink_ret(). -spec start_link() -> startlink_ret().
start_link() -> start_link() ->

View File

@ -930,7 +930,9 @@ maybe_update_zone([RootName | T], RootValue, Value) when is_atom(RootName) ->
end, end,
ExistingZones ExistingZones
), ),
persistent_term:put(?PERSIS_KEY(?CONF, zones), NewZones) ZonesKey = ?PERSIS_KEY(?CONF, zones),
persistent_term:put(ZonesKey, NewZones),
put_config_post_change_actions(ZonesKey, NewZones)
end, end,
NewRootValue. NewRootValue.

View File

@ -275,28 +275,17 @@ stats(#state{
async_set_keepalive(Idle, Interval, Probes) -> async_set_keepalive(Idle, Interval, Probes) ->
async_set_keepalive(os:type(), self(), Idle, Interval, Probes). async_set_keepalive(os:type(), self(), Idle, Interval, Probes).
async_set_keepalive({unix, linux}, Pid, Idle, Interval, Probes) -> async_set_keepalive(OS, Pid, Idle, Interval, Probes) ->
Options = [ case emqx_utils:tcp_keepalive_opts(OS, Idle, Interval, Probes) of
{keepalive, true}, {ok, Options} ->
{raw, 6, 4, <<Idle:32/native>>},
{raw, 6, 5, <<Interval:32/native>>},
{raw, 6, 6, <<Probes:32/native>>}
],
async_set_socket_options(Pid, Options); async_set_socket_options(Pid, Options);
async_set_keepalive({unix, darwin}, Pid, Idle, Interval, Probes) -> {error, {unsupported_os, OS}} ->
Options = [
{keepalive, true},
{raw, 6, 16#10, <<Idle:32/native>>},
{raw, 6, 16#101, <<Interval:32/native>>},
{raw, 6, 16#102, <<Probes:32/native>>}
],
async_set_socket_options(Pid, Options);
async_set_keepalive(OS, _Pid, _Idle, _Interval, _Probes) ->
?SLOG(warning, #{ ?SLOG(warning, #{
msg => "Unsupported operation: set TCP keepalive", msg => "Unsupported operation: set TCP keepalive",
os => OS os => OS
}), }),
ok. ok
end.
%% @doc Set custom socket options. %% @doc Set custom socket options.
%% This API is made async because the call might be originated from %% This API is made async because the call might be originated from

View File

@ -845,7 +845,9 @@ convert_certs(ListenerConf) ->
Listeners1 = Listeners1 =
maps:fold( maps:fold(
fun(Name, Conf, Acc1) -> fun(Name, Conf, Acc1) ->
Acc1#{Name => convert_certs(Type, Name, Conf)} Conf1 = convert_certs(Type, Name, Conf),
Conf2 = convert_authn_certs(Type, Name, Conf1),
Acc1#{Name => Conf2}
end, end,
#{}, #{},
Listeners0 Listeners0
@ -868,6 +870,19 @@ convert_certs(Type, Name, Conf) ->
throw({bad_ssl_config, Reason}) throw({bad_ssl_config, Reason})
end. end.
convert_authn_certs(Type, Name, #{<<"authentication">> := AuthNList} = Conf) ->
ChainName = listener_id(Type, Name),
AuthNList1 = lists:map(
fun(AuthN) ->
CertsDir = emqx_authentication_config:certs_dir(ChainName, AuthN),
emqx_authentication_config:convert_certs(CertsDir, AuthN)
end,
AuthNList
),
Conf#{<<"authentication">> => AuthNList1};
convert_authn_certs(_Type, _Name, Conf) ->
Conf.
filter_stacktrace({Reason, _Stacktrace}) -> Reason; filter_stacktrace({Reason, _Stacktrace}) -> Reason;
filter_stacktrace(Reason) -> Reason. filter_stacktrace(Reason) -> Reason.

View File

@ -307,6 +307,8 @@ fields("persistent_session_store") ->
boolean(), boolean(),
#{ #{
default => false, default => false,
%% TODO(5.2): change field name to 'enable' and keep 'enabled' as an alias
aliases => [enable],
desc => ?DESC(persistent_session_store_enabled) desc => ?DESC(persistent_session_store_enabled)
} }
)}, )},
@ -333,11 +335,11 @@ fields("persistent_session_store") ->
default => #{ default => #{
<<"type">> => <<"builtin">>, <<"type">> => <<"builtin">>,
<<"session">> => <<"session">> =>
#{<<"ram_cache">> => <<"true">>}, #{<<"ram_cache">> => true},
<<"session_messages">> => <<"session_messages">> =>
#{<<"ram_cache">> => <<"true">>}, #{<<"ram_cache">> => true},
<<"messages">> => <<"messages">> =>
#{<<"ram_cache">> => <<"false">>} #{<<"ram_cache">> => false}
}, },
desc => ?DESC(persistent_session_store_backend) desc => ?DESC(persistent_session_store_backend)
} }
@ -2005,6 +2007,8 @@ base_listener(Bind) ->
boolean(), boolean(),
#{ #{
default => true, default => true,
%% TODO(5.2): change field name to 'enable' and keep 'enabled' as an alias
aliases => [enable],
desc => ?DESC(fields_listener_enabled) desc => ?DESC(fields_listener_enabled)
} }
)}, )},

View File

@ -33,11 +33,11 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%---------------------------------------------------------------------- %%----------------------------------------------------------------------
% Based on `otp/lib/ssl/src/ssl_crl_cache.erl' %% Based on `otp/lib/ssl/src/ssl_crl_cache.erl'
%---------------------------------------------------------------------- %%----------------------------------------------------------------------
%---------------------------------------------------------------------- %%----------------------------------------------------------------------
%% Purpose: Simple default CRL cache %% Purpose: Simple default CRL cache
%%---------------------------------------------------------------------- %%----------------------------------------------------------------------

View File

@ -725,10 +725,17 @@ start_slave(Name, Opts) when is_map(Opts) ->
Node = node_name(Name), Node = node_name(Name),
put_peer_mod(Node, SlaveMod), put_peer_mod(Node, SlaveMod),
Cookie = atom_to_list(erlang:get_cookie()), Cookie = atom_to_list(erlang:get_cookie()),
PrivDataDir = maps:get(priv_data_dir, Opts, "/tmp"),
NodeDataDir = filename:join([
PrivDataDir,
Node,
integer_to_list(erlang:unique_integer())
]),
DoStart = DoStart =
fun() -> fun() ->
case SlaveMod of case SlaveMod of
ct_slave -> ct_slave ->
ct:pal("~p: node data dir: ~s", [Node, NodeDataDir]),
ct_slave:start( ct_slave:start(
Node, Node,
[ [
@ -739,7 +746,8 @@ start_slave(Name, Opts) when is_map(Opts) ->
{erl_flags, erl_flags()}, {erl_flags, erl_flags()},
{env, [ {env, [
{"HOCON_ENV_OVERRIDE_PREFIX", "EMQX_"}, {"HOCON_ENV_OVERRIDE_PREFIX", "EMQX_"},
{"EMQX_NODE__COOKIE", Cookie} {"EMQX_NODE__COOKIE", Cookie},
{"EMQX_NODE__DATA_DIR", NodeDataDir}
]} ]}
] ]
); );
@ -844,7 +852,14 @@ setup_node(Node, Opts) when is_map(Opts) ->
integer_to_list(erlang:unique_integer()), integer_to_list(erlang:unique_integer()),
"mnesia" "mnesia"
]), ]),
erpc:call(Node, application, set_env, [mnesia, dir, MnesiaDataDir]), case erpc:call(Node, application, get_env, [mnesia, dir, undefined]) of
undefined ->
ct:pal("~p: setting mnesia dir: ~p", [Node, MnesiaDataDir]),
erpc:call(Node, application, set_env, [mnesia, dir, MnesiaDataDir]);
PreviousMnesiaDir ->
ct:pal("~p: mnesia dir already set: ~p", [Node, PreviousMnesiaDir]),
ok
end,
%% Needs to be set explicitly because ekka:start() (which calls `gen`) is called without Handler %% Needs to be set explicitly because ekka:start() (which calls `gen`) is called without Handler
%% in emqx_common_test_helpers:start_apps(...) %% in emqx_common_test_helpers:start_apps(...)
@ -859,6 +874,12 @@ setup_node(Node, Opts) when is_map(Opts) ->
%% Setting env before starting any applications %% Setting env before starting any applications
set_envs(Node, Env), set_envs(Node, Env),
NodeDataDir = filename:join([
PrivDataDir,
node(),
integer_to_list(erlang:unique_integer())
]),
%% Here we start the apps %% Here we start the apps
EnvHandlerForRpc = EnvHandlerForRpc =
fun(App) -> fun(App) ->
@ -870,17 +891,10 @@ setup_node(Node, Opts) when is_map(Opts) ->
%% to avoid sharing data between executions and/or %% to avoid sharing data between executions and/or
%% nodes. these variables might not be in the %% nodes. these variables might not be in the
%% config file (e.g.: emqx_enterprise_schema). %% config file (e.g.: emqx_enterprise_schema).
NodeDataDir = filename:join([
PrivDataDir,
node(),
integer_to_list(erlang:unique_integer())
]),
Cookie = atom_to_list(erlang:get_cookie()), Cookie = atom_to_list(erlang:get_cookie()),
os:putenv("EMQX_NODE__DATA_DIR", NodeDataDir), set_env_once("EMQX_NODE__DATA_DIR", NodeDataDir),
os:putenv("EMQX_NODE__COOKIE", Cookie), set_env_once("EMQX_NODE__COOKIE", Cookie),
emqx_config:init_load(SchemaMod), emqx_config:init_load(SchemaMod),
os:unsetenv("EMQX_NODE__DATA_DIR"),
os:unsetenv("EMQX_NODE__COOKIE"),
application:set_env(emqx, init_config_load_done, true) application:set_env(emqx, init_config_load_done, true)
end, end,
@ -931,6 +945,15 @@ setup_node(Node, Opts) when is_map(Opts) ->
%% Helpers %% Helpers
set_env_once(Var, Value) ->
case os:getenv(Var) of
false ->
os:putenv(Var, Value);
_OldValue ->
ok
end,
ok.
put_peer_mod(Node, SlaveMod) -> put_peer_mod(Node, SlaveMod) ->
put({?MODULE, Node}, SlaveMod), put({?MODULE, Node}, SlaveMod),
ok. ok.
@ -1290,6 +1313,7 @@ call_janitor() ->
call_janitor(Timeout) -> call_janitor(Timeout) ->
Janitor = get_or_spawn_janitor(), Janitor = get_or_spawn_janitor(),
ok = emqx_test_janitor:stop(Janitor, Timeout), ok = emqx_test_janitor:stop(Janitor, Timeout),
erase({?MODULE, janitor_proc}),
ok. ok.
get_or_spawn_janitor() -> get_or_spawn_janitor() ->

View File

@ -138,22 +138,32 @@ t_conf_update(_) ->
t_conf_update_timer(_Config) -> t_conf_update_timer(_Config) ->
_ = emqx_flapping:start_link(), _ = emqx_flapping:start_link(),
validate_timer([default]), validate_timer([{default, true}]),
%% change zones
{ok, _} = {ok, _} =
emqx:update_config([zones], #{ emqx:update_config([zones], #{
<<"timer_1">> => #{<<"flapping_detect">> => #{<<"enable">> => true}}, <<"timer_1">> => #{<<"flapping_detect">> => #{<<"enable">> => true}},
<<"timer_2">> => #{<<"flapping_detect">> => #{<<"enable">> => true}}, <<"timer_2">> => #{<<"flapping_detect">> => #{<<"enable">> => true}},
<<"timer_3">> => #{<<"flapping_detect">> => #{<<"enable">> => false}} <<"timer_3">> => #{<<"flapping_detect">> => #{<<"enable">> => false}}
}), }),
validate_timer([timer_1, timer_2, timer_3, default]), validate_timer([{timer_1, true}, {timer_2, true}, {timer_3, false}, {default, true}]),
%% change global flapping_detect
Global = emqx:get_raw_config([flapping_detect]),
{ok, _} = emqx:update_config([flapping_detect], Global#{<<"enable">> => false}),
validate_timer([{timer_1, true}, {timer_2, true}, {timer_3, false}, {default, false}]),
%% reset
{ok, _} = emqx:update_config([flapping_detect], Global#{<<"enable">> => true}),
validate_timer([{timer_1, true}, {timer_2, true}, {timer_3, false}, {default, true}]),
ok. ok.
validate_timer(Names) -> validate_timer(Lists) ->
{Names, _} = lists:unzip(Lists),
Zones = emqx:get_config([zones]), Zones = emqx:get_config([zones]),
?assertEqual(lists:sort(Names), lists:sort(maps:keys(Zones))), ?assertEqual(lists:sort(Names), lists:sort(maps:keys(Zones))),
Timers = sys:get_state(emqx_flapping), Timers = sys:get_state(emqx_flapping),
maps:foreach( maps:foreach(
fun(Name, #{flapping_detect := #{enable := Enable}}) -> fun(Name, #{flapping_detect := #{enable := Enable}}) ->
?assertEqual(lists:keyfind(Name, 1, Lists), {Name, Enable}),
?assertEqual(Enable, is_reference(maps:get(Name, Timers)), Timers) ?assertEqual(Enable, is_reference(maps:get(Name, Timers)), Timers)
end, end,
Zones Zones

View File

@ -60,13 +60,14 @@ init(Parent) ->
{ok, #{callbacks => [], owner => Parent}}. {ok, #{callbacks => [], owner => Parent}}.
terminate(_Reason, #{callbacks := Callbacks}) -> terminate(_Reason, #{callbacks := Callbacks}) ->
do_terminate(Callbacks). _ = do_terminate(Callbacks),
ok.
handle_call({push, Callback}, _From, State = #{callbacks := Callbacks}) -> handle_call({push, Callback}, _From, State = #{callbacks := Callbacks}) ->
{reply, ok, State#{callbacks := [Callback | Callbacks]}}; {reply, ok, State#{callbacks := [Callback | Callbacks]}};
handle_call(terminate, _From, State = #{callbacks := Callbacks}) -> handle_call(terminate, _From, State = #{callbacks := Callbacks}) ->
do_terminate(Callbacks), FailedCallbacks = do_terminate(Callbacks),
{stop, normal, ok, State}; {stop, normal, ok, State#{callbacks := FailedCallbacks}};
handle_call(_Req, _From, State) -> handle_call(_Req, _From, State) ->
{reply, error, State}. {reply, error, State}.
@ -83,17 +84,18 @@ handle_info(_Msg, State) ->
%%---------------------------------------------------------------------------------- %%----------------------------------------------------------------------------------
do_terminate(Callbacks) -> do_terminate(Callbacks) ->
lists:foreach( lists:foldl(
fun(Fun) -> fun(Fun, Failed) ->
try try
Fun() Fun(),
Failed
catch catch
K:E:S -> K:E:S ->
ct:pal("error executing callback ~p: ~p", [Fun, {K, E}]), ct:pal("error executing callback ~p: ~p", [Fun, {K, E}]),
ct:pal("stacktrace: ~p", [S]), ct:pal("stacktrace: ~p", [S]),
ok [Fun | Failed]
end end
end, end,
[],
Callbacks Callbacks
), ).
ok.

View File

@ -16,6 +16,8 @@
-module(emqx_authn). -module(emqx_authn).
-behaviour(emqx_config_backup).
-export([ -export([
providers/0, providers/0,
check_config/1, check_config/1,
@ -24,6 +26,11 @@
get_enabled_authns/0 get_enabled_authns/0
]). ]).
%% Data backup
-export([
import_config/1
]).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
providers() -> providers() ->
@ -126,3 +133,32 @@ get_enabled_authns() ->
tally_authenticators(#{id := AuthenticatorName}, Acc) -> tally_authenticators(#{id := AuthenticatorName}, Acc) ->
maps:update_with(AuthenticatorName, fun(N) -> N + 1 end, 1, Acc). maps:update_with(AuthenticatorName, fun(N) -> N + 1 end, 1, Acc).
%%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
-define(IMPORT_OPTS, #{override_to => cluster}).
import_config(RawConf) ->
AuthnList = authn_list(maps:get(?CONF_NS_BINARY, RawConf, [])),
OldAuthnList = emqx:get_raw_config([?CONF_NS_BINARY], []),
MergedAuthnList = emqx_utils:merge_lists(
OldAuthnList, AuthnList, fun emqx_authentication:authenticator_id/1
),
case emqx_conf:update([?CONF_NS_ATOM], MergedAuthnList, ?IMPORT_OPTS) of
{ok, #{raw_config := NewRawConf}} ->
{ok, #{root_key => ?CONF_NS_ATOM, changed => changed_paths(OldAuthnList, NewRawConf)}};
Error ->
{error, #{root_key => ?CONF_NS_ATOM, reason => Error}}
end.
changed_paths(OldAuthnList, NewAuthnList) ->
KeyFun = fun emqx_authentication:authenticator_id/1,
Changed = maps:get(changed, emqx_utils:diff_lists(NewAuthnList, OldAuthnList, KeyFun)),
[[?CONF_NS_BINARY, emqx_authentication:authenticator_id(OldAuthn)] || {OldAuthn, _} <- Changed].
authn_list(Authn) when is_list(Authn) ->
Authn;
authn_list(Authn) when is_map(Authn) ->
[Authn].

View File

@ -22,6 +22,7 @@
-behaviour(hocon_schema). -behaviour(hocon_schema).
-behaviour(emqx_authentication). -behaviour(emqx_authentication).
-behaviour(emqx_db_backup).
-export([ -export([
namespace/0, namespace/0,
@ -54,6 +55,8 @@
group_match_spec/1 group_match_spec/1
]). ]).
-export([backup_tables/0]).
%% Internal exports (RPC) %% Internal exports (RPC)
-export([ -export([
do_destroy/1, do_destroy/1,
@ -101,6 +104,12 @@ mnesia(boot) ->
{storage_properties, [{ets, [{read_concurrency, true}]}]} {storage_properties, [{ets, [{read_concurrency, true}]}]}
]). ]).
%%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
backup_tables() -> [?TAB].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Hocon Schema %% Hocon Schema
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -357,6 +366,9 @@ check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algori
add_user(UserGroup, UserID, Password, IsSuperuser, State) -> add_user(UserGroup, UserID, Password, IsSuperuser, State) ->
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
write_user(UserGroup, UserID, StoredKey, ServerKey, Salt, IsSuperuser).
write_user(UserGroup, UserID, StoredKey, ServerKey, Salt, IsSuperuser) ->
UserInfo = #user_info{ UserInfo = #user_info{
user_id = {UserGroup, UserID}, user_id = {UserGroup, UserID},
stored_key = StoredKey, stored_key = StoredKey,

View File

@ -435,19 +435,19 @@ parse_body(ContentType, _) ->
uri_encode(T) -> uri_encode(T) ->
emqx_http_lib:uri_encode(to_list(T)). emqx_http_lib:uri_encode(to_list(T)).
request_for_log(Credential, #{url := Url} = State) -> request_for_log(Credential, #{url := Url, method := Method} = State) ->
SafeCredential = emqx_authn_utils:without_password(Credential), SafeCredential = emqx_authn_utils:without_password(Credential),
case generate_request(SafeCredential, State) of case generate_request(SafeCredential, State) of
{PathQuery, Headers} -> {PathQuery, Headers} ->
#{ #{
method => post, method => Method,
base_url => Url, base_url => Url,
path_query => PathQuery, path_query => PathQuery,
headers => Headers headers => Headers
}; };
{PathQuery, Headers, Body} -> {PathQuery, Headers, Body} ->
#{ #{
method => post, method => Method,
base_url => Url, base_url => Url,
path_query => PathQuery, path_query => PathQuery,
headers => Headers, headers => Headers,

View File

@ -23,6 +23,7 @@
-behaviour(hocon_schema). -behaviour(hocon_schema).
-behaviour(emqx_authentication). -behaviour(emqx_authentication).
-behaviour(emqx_db_backup).
-export([ -export([
namespace/0, namespace/0,
@ -66,6 +67,10 @@
import_csv/3 import_csv/3
]). ]).
-export([mnesia/1]).
-export([backup_tables/0]).
-type user_group() :: binary(). -type user_group() :: binary().
-type user_id() :: binary(). -type user_id() :: binary().
@ -76,8 +81,6 @@
is_superuser :: boolean() is_superuser :: boolean()
}). }).
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
-define(TAB, ?MODULE). -define(TAB, ?MODULE).
@ -103,6 +106,11 @@ mnesia(boot) ->
{storage_properties, [{ets, [{read_concurrency, true}]}]} {storage_properties, [{ets, [{read_concurrency, true}]}]}
]). ]).
%%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
backup_tables() -> [?TAB].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Hocon Schema %% Hocon Schema
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -15,7 +15,9 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_authz). -module(emqx_authz).
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
-behaviour(emqx_config_backup).
-include("emqx_authz.hrl"). -include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -44,6 +46,13 @@
-export([acl_conf_file/0]). -export([acl_conf_file/0]).
%% Data backup
-export([
import_config/1,
maybe_read_acl_file/1,
maybe_write_acl_file/1
]).
-type source() :: map(). -type source() :: map().
-type match_result() :: {matched, allow} | {matched, deny} | nomatch. -type match_result() :: {matched, allow} | {matched, deny} | nomatch.
@ -326,9 +335,9 @@ init_metrics(Source) ->
) )
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% AuthZ callbacks %% AuthZ callbacks
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% @doc Check AuthZ %% @doc Check AuthZ
-spec authorize( -spec authorize(
@ -451,9 +460,58 @@ do_authorize(
get_enabled_authzs() -> get_enabled_authzs() ->
lists:usort([Type || #{type := Type, enable := true} <- lookup()]). lists:usort([Type || #{type := Type, enable := true} <- lookup()]).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
import_config(#{?CONF_NS_BINARY := AuthzConf}) ->
Sources = maps:get(<<"sources">>, AuthzConf, []),
OldSources = emqx:get_raw_config(?CONF_KEY_PATH, []),
MergedSources = emqx_utils:merge_lists(OldSources, Sources, fun type/1),
MergedAuthzConf = AuthzConf#{<<"sources">> => MergedSources},
case emqx_conf:update([?CONF_NS_ATOM], MergedAuthzConf, #{override_to => cluster}) of
{ok, #{raw_config := #{<<"sources">> := NewSources}}} ->
{ok, #{
root_key => ?CONF_NS_ATOM,
changed => changed_paths(OldSources, NewSources)
}};
Error ->
{error, #{root_key => ?CONF_NS_ATOM, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => ?CONF_NS_ATOM, changed => []}}.
changed_paths(OldSources, NewSources) ->
Changed = maps:get(changed, emqx_utils:diff_lists(NewSources, OldSources, fun type/1)),
[?CONF_KEY_PATH ++ [type(OldSource)] || {OldSource, _} <- Changed].
maybe_read_acl_file(RawConf) ->
maybe_convert_acl_file(RawConf, fun read_acl_file/1).
maybe_write_acl_file(RawConf) ->
maybe_convert_acl_file(RawConf, fun write_acl_file/1).
maybe_convert_acl_file(
#{?CONF_NS_BINARY := #{<<"sources">> := Sources} = AuthRawConf} = RawConf, Fun
) ->
Sources1 = lists:map(
fun
(#{<<"type">> := <<"file">>} = FileSource) -> Fun(FileSource);
(Source) -> Source
end,
Sources
),
RawConf#{?CONF_NS_BINARY => AuthRawConf#{<<"sources">> => Sources1}};
maybe_convert_acl_file(RawConf, _Fun) ->
RawConf.
read_acl_file(#{<<"path">> := Path} = Source) ->
{ok, Rules} = emqx_authz_file:read_file(Path),
maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
%%------------------------------------------------------------------------------
%% Internal function %% Internal function
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
client_info_source() -> client_info_source() ->
emqx_authz_client_info:create( emqx_authz_client_info:create(

View File

@ -42,6 +42,7 @@
}). }).
-behaviour(emqx_authz). -behaviour(emqx_authz).
-behaviour(emqx_db_backup).
%% AuthZ Callbacks %% AuthZ Callbacks
-export([ -export([
@ -65,6 +66,8 @@
record_count/0 record_count/0
]). ]).
-export([backup_tables/0]).
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
@ -119,6 +122,12 @@ authorize(
end, end,
do_authorize(Client, PubSub, Topic, Rules). do_authorize(Client, PubSub, Topic, Rules).
%%--------------------------------------------------------------------
%% Data backup
%%--------------------------------------------------------------------
backup_tables() -> [?ACL_TABLE].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Management API %% Management API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -16,6 +16,8 @@
-module(emqx_auto_subscribe). -module(emqx_auto_subscribe).
-behaviour(emqx_config_backup).
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
@ -24,7 +26,6 @@
-define(MAX_AUTO_SUBSCRIBE, 20). -define(MAX_AUTO_SUBSCRIBE, 20).
%
-export([load/0, unload/0]). -export([load/0, unload/0]).
-export([ -export([
@ -40,6 +41,11 @@
%% exported for `emqx_telemetry' %% exported for `emqx_telemetry'
-export([get_basic_usage_info/0]). -export([get_basic_usage_info/0]).
%% Data backup
-export([
import_config/1
]).
load() -> load() ->
ok = emqx_conf:add_handler([auto_subscribe, topics], ?MODULE), ok = emqx_conf:add_handler([auto_subscribe, topics], ?MODULE),
update_hook(). update_hook().
@ -73,8 +79,9 @@ post_config_update(_KeyPath, _Req, NewTopics, _OldConf, _AppEnvs) ->
Config = emqx_conf:get([auto_subscribe], #{}), Config = emqx_conf:get([auto_subscribe], #{}),
update_hook(Config#{topics => NewTopics}). update_hook(Config#{topics => NewTopics}).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% hook %% hook
%%------------------------------------------------------------------------------
on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) -> on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) ->
case erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]) of case erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]) of
@ -87,17 +94,38 @@ on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) ->
on_client_connected(_, _, _) -> on_client_connected(_, _, _) ->
ok. ok.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Telemetry %% Telemetry
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec get_basic_usage_info() -> #{auto_subscribe_count => non_neg_integer()}. -spec get_basic_usage_info() -> #{auto_subscribe_count => non_neg_integer()}.
get_basic_usage_info() -> get_basic_usage_info() ->
AutoSubscribe = emqx_conf:get([auto_subscribe, topics], []), AutoSubscribe = emqx_conf:get([auto_subscribe, topics], []),
#{auto_subscribe_count => length(AutoSubscribe)}. #{auto_subscribe_count => length(AutoSubscribe)}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
import_config(#{<<"auto_subscribe">> := #{<<"topics">> := Topics}}) ->
ConfPath = [auto_subscribe, topics],
OldTopics = emqx:get_raw_config(ConfPath, []),
KeyFun = fun(#{<<"topic">> := T}) -> T end,
MergedTopics = emqx_utils:merge_lists(OldTopics, Topics, KeyFun),
case emqx_conf:update(ConfPath, MergedTopics, #{override_to => cluster}) of
{ok, #{raw_config := NewTopics}} ->
Changed = maps:get(changed, emqx_utils:diff_lists(NewTopics, OldTopics, KeyFun)),
Changed1 = [ConfPath ++ [T] || {#{<<"topic">> := T}, _} <- Changed],
{ok, #{root_key => auto_subscribe, changed => Changed1}};
Error ->
{error, #{root_key => auto_subscribe, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => auto_subscribe, changed => []}}.
%%------------------------------------------------------------------------------
%% internal %% internal
%%------------------------------------------------------------------------------
format(Rules) when is_list(Rules) -> format(Rules) when is_list(Rules) ->
[format(Rule) || Rule <- Rules]; [format(Rule) || Rule <- Rules];

View File

@ -14,13 +14,19 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_bridge). -module(emqx_bridge).
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
-behaviour(emqx_config_backup).
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-export([post_config_update/5]). -export([
pre_config_update/3,
post_config_update/5
]).
-export([ -export([
load_hook/0, load_hook/0,
@ -53,6 +59,11 @@
%% exported for `emqx_telemetry' %% exported for `emqx_telemetry'
-export([get_basic_usage_info/0]). -export([get_basic_usage_info/0]).
%% Data backup
-export([
import_config/1
]).
-define(EGRESS_DIR_BRIDGES(T), -define(EGRESS_DIR_BRIDGES(T),
T == webhook; T == webhook;
T == mysql; T == mysql;
@ -80,8 +91,10 @@
T == iotdb T == iotdb
). ).
-define(ROOT_KEY, bridges).
load() -> load() ->
Bridges = emqx:get_config([bridges], #{}), Bridges = emqx:get_config([?ROOT_KEY], #{}),
lists:foreach( lists:foreach(
fun({Type, NamedConf}) -> fun({Type, NamedConf}) ->
lists:foreach( lists:foreach(
@ -98,7 +111,7 @@ load() ->
unload() -> unload() ->
unload_hook(), unload_hook(),
Bridges = emqx:get_config([bridges], #{}), Bridges = emqx:get_config([?ROOT_KEY], #{}),
lists:foreach( lists:foreach(
fun({Type, NamedConf}) -> fun({Type, NamedConf}) ->
lists:foreach( lists:foreach(
@ -139,7 +152,7 @@ reload_hook(Bridges) ->
ok = load_hook(Bridges). ok = load_hook(Bridges).
load_hook() -> load_hook() ->
Bridges = emqx:get_config([bridges], #{}), Bridges = emqx:get_config([?ROOT_KEY], #{}),
load_hook(Bridges). load_hook(Bridges).
load_hook(Bridges) -> load_hook(Bridges) ->
@ -210,7 +223,7 @@ send_message(BridgeId, Message) ->
send_message(BridgeType, BridgeName, ResId, Message). send_message(BridgeType, BridgeName, ResId, Message).
send_message(BridgeType, BridgeName, ResId, Message) -> send_message(BridgeType, BridgeName, ResId, Message) ->
case emqx:get_config([bridges, BridgeType, BridgeName], not_found) of case emqx:get_config([?ROOT_KEY, BridgeType, BridgeName], not_found) of
not_found -> not_found ->
{error, bridge_not_found}; {error, bridge_not_found};
#{enable := true} = Config -> #{enable := true} = Config ->
@ -231,9 +244,14 @@ query_opts(Config) ->
end. end.
config_key_path() -> config_key_path() ->
[bridges]. [?ROOT_KEY].
post_config_update(_, _Req, NewConf, OldConf, _AppEnv) -> pre_config_update([?ROOT_KEY], RawConf, RawConf) ->
{ok, RawConf};
pre_config_update([?ROOT_KEY], NewConf, _RawConf) ->
{ok, convert_certs(NewConf)}.
post_config_update([?ROOT_KEY], _Req, NewConf, OldConf, _AppEnv) ->
#{added := Added, removed := Removed, changed := Updated} = #{added := Added, removed := Removed, changed := Updated} =
diff_confs(NewConf, OldConf), diff_confs(NewConf, OldConf),
%% The config update will be failed if any task in `perform_bridge_changes` failed. %% The config update will be failed if any task in `perform_bridge_changes` failed.
@ -351,10 +369,74 @@ check_deps_and_remove(BridgeType, BridgeName, RemoveDeps) ->
remove(BridgeType, BridgeName) remove(BridgeType, BridgeName)
end. end.
%%----------------------------------------------------------------------------------------
%% Data backup
%%----------------------------------------------------------------------------------------
import_config(RawConf) ->
RootKeyPath = config_key_path(),
BridgesConf = maps:get(<<"bridges">>, RawConf, #{}),
OldBridgesConf = emqx:get_raw_config(RootKeyPath, #{}),
MergedConf = merge_confs(OldBridgesConf, BridgesConf),
case emqx_conf:update(RootKeyPath, MergedConf, #{override_to => cluster}) of
{ok, #{raw_config := NewRawConf}} ->
{ok, #{root_key => ?ROOT_KEY, changed => changed_paths(OldBridgesConf, NewRawConf)}};
Error ->
{error, #{root_key => ?ROOT_KEY, reason => Error}}
end.
merge_confs(OldConf, NewConf) ->
AllTypes = maps:keys(maps:merge(OldConf, NewConf)),
lists:foldr(
fun(Type, Acc) ->
NewBridges = maps:get(Type, NewConf, #{}),
OldBridges = maps:get(Type, OldConf, #{}),
Acc#{Type => maps:merge(OldBridges, NewBridges)}
end,
#{},
AllTypes
).
changed_paths(OldRawConf, NewRawConf) ->
maps:fold(
fun(Type, Bridges, ChangedAcc) ->
OldBridges = maps:get(Type, OldRawConf, #{}),
Changed = maps:get(changed, emqx_utils_maps:diff_maps(Bridges, OldBridges)),
[[?ROOT_KEY, Type, K] || K <- maps:keys(Changed)] ++ ChangedAcc
end,
[],
NewRawConf
).
%%======================================================================================== %%========================================================================================
%% Helper functions %% Helper functions
%%======================================================================================== %%========================================================================================
convert_certs(BridgesConf) ->
maps:map(
fun(Type, Bridges) ->
maps:map(
fun(Name, BridgeConf) ->
Path = filename:join([?ROOT_KEY, Type, Name]),
case emqx_connector_ssl:convert_certs(Path, BridgeConf) of
{error, Reason} ->
?SLOG(error, #{
msg => "bad_ssl_config",
type => Type,
name => Name,
reason => Reason
}),
throw({bad_ssl_config, Reason});
{ok, BridgeConf1} ->
BridgeConf1
end
end,
Bridges
)
end,
BridgesConf
).
perform_bridge_changes(Tasks) -> perform_bridge_changes(Tasks) ->
perform_bridge_changes(Tasks, ok). perform_bridge_changes(Tasks, ok).

View File

@ -141,14 +141,6 @@ request_config() ->
default => 2, default => 2,
desc => ?DESC("config_max_retries") desc => ?DESC("config_max_retries")
} }
)},
{request_timeout,
mk(
emqx_schema:timeout_duration_ms(),
#{
default => <<"15s">>,
desc => ?DESC("config_request_timeout")
}
)} )}
]. ].

View File

@ -132,7 +132,7 @@ bridge_config(TestCase, _TestGroup, Config) ->
" username = \"root\"\n" " username = \"root\"\n"
" password = \"root\"\n" " password = \"root\"\n"
" }\n" " }\n"
"iotdb_version = \"~s\"\n" " iotdb_version = \"~s\"\n"
" pool_size = 1\n" " pool_size = 1\n"
" resource_opts = {\n" " resource_opts = {\n"
" health_check_interval = 5000\n" " health_check_interval = 5000\n"

View File

@ -74,7 +74,8 @@ values(common_config) ->
socket_opts => #{ socket_opts => #{
sndbuf => <<"1024KB">>, sndbuf => <<"1024KB">>,
recbuf => <<"1024KB">>, recbuf => <<"1024KB">>,
nodelay => true nodelay => true,
tcp_keepalive => <<"none">>
} }
}; };
values(producer) -> values(producer) ->
@ -236,7 +237,13 @@ fields(socket_opts) ->
importance => ?IMPORTANCE_HIDDEN, importance => ?IMPORTANCE_HIDDEN,
desc => ?DESC(socket_nodelay) desc => ?DESC(socket_nodelay)
} }
)} )},
{tcp_keepalive,
mk(string(), #{
default => <<"none">>,
desc => ?DESC(socket_tcp_keepalive),
validator => fun emqx_schema:validate_tcp_keepalive/1
})}
]; ];
fields(producer_opts) -> fields(producer_opts) ->
[ [
@ -276,7 +283,7 @@ fields(producer_kafka_opts) ->
)}, )},
{partition_count_refresh_interval, {partition_count_refresh_interval,
mk( mk(
emqx_schema:duration_s(), emqx_schema:timeout_duration_s(),
#{ #{
default => <<"60s">>, default => <<"60s">>,
desc => ?DESC(partition_count_refresh_interval) desc => ?DESC(partition_count_refresh_interval)
@ -396,8 +403,8 @@ fields(consumer_kafka_opts) ->
)}, )},
{offset_commit_interval_seconds, {offset_commit_interval_seconds,
mk( mk(
pos_integer(), emqx_schema:timeout_duration_s(),
#{default => 5, desc => ?DESC(consumer_offset_commit_interval_seconds)} #{default => <<"5s">>, desc => ?DESC(consumer_offset_commit_interval_seconds)}
)} )}
]. ].

View File

@ -8,9 +8,12 @@
-export([ -export([
hosts/1, hosts/1,
make_client_id/2, make_client_id/2,
sasl/1 sasl/1,
socket_opts/1
]). ]).
-include_lib("emqx/include/logger.hrl").
%% Parse comma separated host:port list into a [{Host,Port}] list %% Parse comma separated host:port list into a [{Host,Port}] list
hosts(Hosts) when is_binary(Hosts) -> hosts(Hosts) when is_binary(Hosts) ->
hosts(binary_to_list(Hosts)); hosts(binary_to_list(Hosts));
@ -33,6 +36,51 @@ sasl(#{
}) -> }) ->
{callback, brod_gssapi, {gssapi, KeyTabFile, Principal}}. {callback, brod_gssapi, {gssapi, KeyTabFile, Principal}}.
%% Extra socket options, such as sndbuf size etc.
socket_opts(Opts) when is_map(Opts) ->
socket_opts(maps:to_list(Opts));
socket_opts(Opts) when is_list(Opts) ->
socket_opts_loop(Opts, []).
socket_opts_loop([], Acc) ->
lists:reverse(Acc);
socket_opts_loop([{tcp_keepalive, KeepAlive} | Rest], Acc) ->
Acc1 = tcp_keepalive(KeepAlive) ++ Acc,
socket_opts_loop(Rest, Acc1);
socket_opts_loop([{T, Bytes} | Rest], Acc) when
T =:= sndbuf orelse T =:= recbuf orelse T =:= buffer
->
Acc1 = [{T, Bytes} | adjust_socket_buffer(Bytes, Acc)],
socket_opts_loop(Rest, Acc1);
socket_opts_loop([Other | Rest], Acc) ->
socket_opts_loop(Rest, [Other | Acc]).
%% https://www.erlang.org/doc/man/inet.html
%% For TCP it is recommended to have val(buffer) >= val(recbuf)
%% to avoid performance issues because of unnecessary copying.
adjust_socket_buffer(Bytes, Opts) ->
case lists:keytake(buffer, 1, Opts) of
false ->
[{buffer, Bytes} | Opts];
{value, {buffer, Bytes1}, Acc1} ->
[{buffer, max(Bytes1, Bytes)} | Acc1]
end.
tcp_keepalive(None) when None =:= "none"; None =:= <<"none">> ->
[];
tcp_keepalive(KeepAlive) ->
{Idle, Interval, Probes} = emqx_schema:parse_tcp_keepalive(KeepAlive),
case emqx_utils:tcp_keepalive_opts(os:type(), Idle, Interval, Probes) of
{ok, Opts} ->
Opts;
{error, {unsupported_os, OS}} ->
?SLOG(warning, #{
msg => "Unsupported operation: set TCP keepalive",
os => OS
}),
[]
end.
to_bin(A) when is_atom(A) -> to_bin(A) when is_atom(A) ->
atom_to_binary(A); atom_to_binary(A);
to_bin(L) when is_list(L) -> to_bin(L) when is_list(L) ->

View File

@ -129,6 +129,7 @@ on_start(ResourceId, Config) ->
offset_commit_interval_seconds := _, offset_commit_interval_seconds := _,
offset_reset_policy := _ offset_reset_policy := _
}, },
socket_opts := SocketOpts0,
ssl := SSL, ssl := SSL,
topic_mapping := _ topic_mapping := _
} = Config, } = Config,
@ -142,8 +143,10 @@ on_start(ResourceId, Config) ->
Auth -> [{sasl, emqx_bridge_kafka_impl:sasl(Auth)}] Auth -> [{sasl, emqx_bridge_kafka_impl:sasl(Auth)}]
end, end,
ClientOpts = add_ssl_opts(ClientOpts0, SSL), ClientOpts = add_ssl_opts(ClientOpts0, SSL),
SocketOpts = emqx_bridge_kafka_impl:socket_opts(SocketOpts0),
ClientOpts1 = [{extra_sock_opts, SocketOpts} | ClientOpts],
ok = emqx_resource:allocate_resource(ResourceId, ?kafka_client_id, ClientID), ok = emqx_resource:allocate_resource(ResourceId, ?kafka_client_id, ClientID),
case brod:start_client(BootstrapHosts, ClientID, ClientOpts) of case brod:start_client(BootstrapHosts, ClientID, ClientOpts1) of
ok -> ok ->
?tp( ?tp(
kafka_consumer_client_started, kafka_consumer_client_started,

View File

@ -69,7 +69,7 @@ on_start(InstId, Config) ->
connect_timeout => ConnTimeout, connect_timeout => ConnTimeout,
client_id => ClientId, client_id => ClientId,
request_timeout => MetaReqTimeout, request_timeout => MetaReqTimeout,
extra_sock_opts => socket_opts(SocketOpts), extra_sock_opts => emqx_bridge_kafka_impl:socket_opts(SocketOpts),
sasl => emqx_bridge_kafka_impl:sasl(Auth), sasl => emqx_bridge_kafka_impl:sasl(Auth),
ssl => ssl(SSL) ssl => ssl(SSL)
}, },
@ -321,33 +321,6 @@ do_get_status(Client, KafkaTopic) ->
disconnected disconnected
end. end.
%% Extra socket options, such as sndbuf size etc.
socket_opts(Opts) when is_map(Opts) ->
socket_opts(maps:to_list(Opts));
socket_opts(Opts) when is_list(Opts) ->
socket_opts_loop(Opts, []).
socket_opts_loop([], Acc) ->
lists:reverse(Acc);
socket_opts_loop([{T, Bytes} | Rest], Acc) when
T =:= sndbuf orelse T =:= recbuf orelse T =:= buffer
->
Acc1 = [{T, Bytes} | adjust_socket_buffer(Bytes, Acc)],
socket_opts_loop(Rest, Acc1);
socket_opts_loop([Other | Rest], Acc) ->
socket_opts_loop(Rest, [Other | Acc]).
%% https://www.erlang.org/doc/man/inet.html
%% For TCP it is recommended to have val(buffer) >= val(recbuf)
%% to avoid performance issues because of unnecessary copying.
adjust_socket_buffer(Bytes, Opts) ->
case lists:keytake(buffer, 1, Opts) of
false ->
[{buffer, Bytes} | Opts];
{value, {buffer, Bytes1}, Acc1} ->
[{buffer, max(Bytes1, Bytes)} | Acc1]
end.
ssl(#{enable := true} = SSL) -> ssl(#{enable := true} = SSL) ->
emqx_tls_lib:to_client_opts(SSL); emqx_tls_lib:to_client_opts(SSL);
ssl(_) -> ssl(_) ->

View File

@ -168,6 +168,24 @@ message_key_dispatch_validations_test() ->
), ),
ok. ok.
tcp_keepalive_validation_test_() ->
ProducerConf = parse(kafka_producer_new_hocon()),
ConsumerConf = parse(kafka_consumer_hocon()),
test_keepalive_validation([<<"kafka">>, <<"myproducer">>], ProducerConf) ++
test_keepalive_validation([<<"kafka_consumer">>, <<"my_consumer">>], ConsumerConf).
test_keepalive_validation(Name, Conf) ->
Path = [<<"bridges">>] ++ Name ++ [<<"socket_opts">>, <<"tcp_keepalive">>],
Conf1 = emqx_utils_maps:deep_force_put(Path, Conf, <<"5,6,7">>),
Conf2 = emqx_utils_maps:deep_force_put(Path, Conf, <<"none">>),
ValidConfs = [Conf, Conf1, Conf2],
InvalidConf = emqx_utils_maps:deep_force_put(Path, Conf, <<"invalid">>),
InvalidConf1 = emqx_utils_maps:deep_force_put(Path, Conf, <<"5,6">>),
InvalidConf2 = emqx_utils_maps:deep_force_put(Path, Conf, <<"5,6,1000">>),
InvalidConfs = [InvalidConf, InvalidConf1, InvalidConf2],
[?_assertMatch(#{<<"bridges">> := _}, check(C)) || C <- ValidConfs] ++
[?_assertThrow(_, check(C)) || C <- InvalidConfs].
%%=========================================================================== %%===========================================================================
%% Helper functions %% Helper functions
%%=========================================================================== %%===========================================================================

View File

@ -9,6 +9,7 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-include_lib("emqx/include/asserts.hrl").
-import(emqx_common_test_helpers, [on_exit/1]). -import(emqx_common_test_helpers, [on_exit/1]).
@ -148,6 +149,7 @@ end_per_testcase(_Testcase, Config) ->
true -> true ->
ok; ok;
false -> false ->
ok = emqx_config:delete_override_conf_files(),
ProxyHost = ?config(proxy_host, Config), ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config), ProxyPort = ?config(proxy_port, Config),
emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort),
@ -157,6 +159,7 @@ end_per_testcase(_Testcase, Config) ->
%% machines struggle with all the containers running... %% machines struggle with all the containers running...
emqx_common_test_helpers:call_janitor(60_000), emqx_common_test_helpers:call_janitor(60_000),
ok = snabbkaffe:stop(), ok = snabbkaffe:stop(),
flush_consumed(),
ok ok
end. end.
@ -373,7 +376,9 @@ start_consumer(TestCase, Config) ->
(integer_to_binary(PulsarPort))/binary>> (integer_to_binary(PulsarPort))/binary>>
), ),
ConnOpts = #{}, ConnOpts = #{},
ConsumerClientId = TestCase, ConsumerClientId = list_to_atom(
atom_to_list(TestCase) ++ integer_to_list(erlang:unique_integer())
),
CertsPath = emqx_common_test_helpers:deps_path(emqx, "etc/certs"), CertsPath = emqx_common_test_helpers:deps_path(emqx, "etc/certs"),
SSLOpts = #{ SSLOpts = #{
enable => UseTLS, enable => UseTLS,
@ -393,12 +398,12 @@ start_consumer(TestCase, Config) ->
cb_init_args => #{send_to => self()}, cb_init_args => #{send_to => self()},
cb_module => pulsar_echo_consumer, cb_module => pulsar_echo_consumer,
sub_type => 'Shared', sub_type => 'Shared',
subscription => atom_to_list(TestCase), subscription => atom_to_list(TestCase) ++ integer_to_list(erlang:unique_integer()),
max_consumer_num => 1, max_consumer_num => 1,
%% Note! This must not coincide with the client %% Note! This must not coincide with the client
%% id, or else weird bugs will happen, like the %% id, or else weird bugs will happen, like the
%% consumer never starts... %% consumer never starts...
name => test_consumer, name => list_to_atom("test_consumer" ++ integer_to_list(erlang:unique_integer())),
consumer_id => 1, consumer_id => 1,
conn_opts => ConnOpts conn_opts => ConnOpts
}, },
@ -440,7 +445,10 @@ wait_until_connected(SupMod, Mod) ->
?retry( ?retry(
_Sleep = 300, _Sleep = 300,
_Attempts0 = 20, _Attempts0 = 20,
begin
true = length(Pids) > 0,
lists:foreach(fun(P) -> {connected, _} = sys:get_state(P) end, Pids) lists:foreach(fun(P) -> {connected, _} = sys:get_state(P) end, Pids)
end
), ),
ok. ok.
@ -483,6 +491,12 @@ receive_consumed(Timeout) ->
ct:fail("no message consumed") ct:fail("no message consumed")
end. end.
flush_consumed() ->
receive
{pulsar_message, _} -> flush_consumed()
after 0 -> ok
end.
try_decode_json(Payload) -> try_decode_json(Payload) ->
case emqx_utils_json:safe_decode(Payload, [return_maps]) of case emqx_utils_json:safe_decode(Payload, [return_maps]) of
{error, _} -> {error, _} ->
@ -1054,31 +1068,44 @@ t_resource_manager_crash_before_producers_started(Config) ->
), ),
ok. ok.
t_cluster(Config) -> t_cluster(Config0) ->
ct:timetrap({seconds, 120}),
?retrying(Config0, 3, fun do_t_cluster/1).
do_t_cluster(Config) ->
?check_trace(
begin
MQTTTopic = ?config(mqtt_topic, Config), MQTTTopic = ?config(mqtt_topic, Config),
ResourceId = resource_id(Config), ResourceId = resource_id(Config),
Cluster = cluster(Config), Cluster = cluster(Config),
ClientId = emqx_guid:to_hexstr(emqx_guid:gen()), ClientId = emqx_guid:to_hexstr(emqx_guid:gen()),
QoS = 0, QoS = 0,
Payload = emqx_guid:to_hexstr(emqx_guid:gen()), Payload = emqx_guid:to_hexstr(emqx_guid:gen()),
?check_trace( NumNodes = length(Cluster),
begin {ok, SRef0} = snabbkaffe:subscribe(
?match_event(#{?snk_kind := emqx_bridge_app_started}),
NumNodes,
25_000
),
Nodes = [N1, N2 | _] = start_cluster(Cluster), Nodes = [N1, N2 | _] = start_cluster(Cluster),
%% wait until bridge app supervisor is up; by that point, %% wait until bridge app supervisor is up; by that point,
%% `emqx_config_handler:add_handler' has been called and the node should be %% `emqx_config_handler:add_handler' has been called and the node should be
%% ready to create bridges. %% ready to create bridges.
NumNodes = length(Nodes), {ok, _} = snabbkaffe:receive_events(SRef0),
{ok, _} = snabbkaffe:block_until( {ok, SRef1} = snabbkaffe:subscribe(
?match_n_events(NumNodes, #{?snk_kind := emqx_bridge_app_started}),
15_000
),
{ok, SRef0} = snabbkaffe:subscribe(
?match_event(#{?snk_kind := pulsar_producer_bridge_started}), ?match_event(#{?snk_kind := pulsar_producer_bridge_started}),
NumNodes, NumNodes,
15_000 25_000
), ),
{ok, _} = erpc:call(N1, fun() -> create_bridge(Config) end), {ok, _} = erpc:call(N1, fun() -> create_bridge(Config) end),
{ok, _} = snabbkaffe:receive_events(SRef0), {ok, _} = snabbkaffe:receive_events(SRef1),
{ok, _} = snabbkaffe:block_until(
?match_n_events(
NumNodes,
#{?snk_kind := bridge_post_config_update_done}
),
25_000
),
lists:foreach( lists:foreach(
fun(N) -> fun(N) ->
?retry( ?retry(
@ -1095,6 +1122,7 @@ t_cluster(Config) ->
), ),
erpc:multicall(Nodes, fun wait_until_producer_connected/0), erpc:multicall(Nodes, fun wait_until_producer_connected/0),
Message0 = emqx_message:make(ClientId, QoS, MQTTTopic, Payload), Message0 = emqx_message:make(ClientId, QoS, MQTTTopic, Payload),
?tp(publishing_message, #{}),
erpc:call(N2, emqx, publish, [Message0]), erpc:call(N2, emqx, publish, [Message0]),
lists:foreach( lists:foreach(
@ -1108,10 +1136,7 @@ t_cluster(Config) ->
Nodes Nodes
), ),
ok Data0 = receive_consumed(30_000),
end,
fun(_Trace) ->
Data0 = receive_consumed(10_000),
?assertMatch( ?assertMatch(
[ [
#{ #{
@ -1123,7 +1148,9 @@ t_cluster(Config) ->
], ],
Data0 Data0
), ),
ok ok
end end,
[]
), ),
ok. ok.

View File

@ -1,4 +1,4 @@
%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -261,9 +261,13 @@ sync_data_from_node(Node) ->
{ok, DataBin} -> {ok, DataBin} ->
case zip:unzip(DataBin, [{cwd, emqx:data_dir()}]) of case zip:unzip(DataBin, [{cwd, emqx:data_dir()}]) of
{ok, []} -> {ok, []} ->
?SLOG(debug, #{node => Node, msg => "sync_data_from_node_ignore"}); ?SLOG(debug, #{node => Node, msg => "sync_data_from_node_empty_response"});
{ok, Files} -> {ok, Files} ->
?SLOG(debug, #{node => Node, msg => "sync_data_from_node_ok", files => Files}) ?SLOG(debug, #{
node => Node,
msg => "sync_data_from_node_non_empty_response",
files => Files
})
end, end,
ok; ok;
Error -> Error ->

View File

@ -20,14 +20,17 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
t_copy_conf_override_on_restarts(_Config) -> t_copy_conf_override_on_restarts(Config) ->
ct:timetrap({seconds, 120}), ct:timetrap({seconds, 120}),
snabbkaffe:fix_ct_logging(), snabbkaffe:fix_ct_logging(),
Cluster = cluster([cluster_spec({core, 1}), cluster_spec({core, 2}), cluster_spec({core, 3})]), Cluster = cluster(
[cluster_spec({core, 1}), cluster_spec({core, 2}), cluster_spec({core, 3})], Config
),
%% 1. Start all nodes %% 1. Start all nodes
Nodes = start_cluster(Cluster), Nodes = start_cluster(Cluster),
@ -50,16 +53,19 @@ t_copy_conf_override_on_restarts(_Config) ->
stop_cluster(Nodes) stop_cluster(Nodes)
end. end.
t_copy_new_data_dir(_Config) -> t_copy_new_data_dir(Config) ->
net_kernel:start(['master1@127.0.0.1', longnames]), net_kernel:start(['master1@127.0.0.1', longnames]),
ct:timetrap({seconds, 120}), ct:timetrap({seconds, 120}),
snabbkaffe:fix_ct_logging(), snabbkaffe:fix_ct_logging(),
Cluster = cluster([cluster_spec({core, 4}), cluster_spec({core, 5}), cluster_spec({core, 6})]), Cluster = cluster(
[cluster_spec({core, 4}), cluster_spec({core, 5}), cluster_spec({core, 6})], Config
),
%% 1. Start all nodes %% 1. Start all nodes
[First | Rest] = Nodes = start_cluster(Cluster), [First | Rest] = Nodes = start_cluster(Cluster),
try try
File = "/configs/cluster.hocon", NodeDataDir = erpc:call(First, emqx, data_dir, []),
File = NodeDataDir ++ "/configs/cluster.hocon",
assert_config_load_done(Nodes), assert_config_load_done(Nodes),
rpc:call(First, ?MODULE, create_data_dir, [File]), rpc:call(First, ?MODULE, create_data_dir, [File]),
{[ok, ok, ok], []} = rpc:multicall(Nodes, application, stop, [emqx_conf]), {[ok, ok, ok], []} = rpc:multicall(Nodes, application, stop, [emqx_conf]),
@ -74,16 +80,19 @@ t_copy_new_data_dir(_Config) ->
stop_cluster(Nodes) stop_cluster(Nodes)
end. end.
t_copy_deprecated_data_dir(_Config) -> t_copy_deprecated_data_dir(Config) ->
net_kernel:start(['master2@127.0.0.1', longnames]), net_kernel:start(['master2@127.0.0.1', longnames]),
ct:timetrap({seconds, 120}), ct:timetrap({seconds, 120}),
snabbkaffe:fix_ct_logging(), snabbkaffe:fix_ct_logging(),
Cluster = cluster([cluster_spec({core, 7}), cluster_spec({core, 8}), cluster_spec({core, 9})]), Cluster = cluster(
[cluster_spec({core, 7}), cluster_spec({core, 8}), cluster_spec({core, 9})], Config
),
%% 1. Start all nodes %% 1. Start all nodes
[First | Rest] = Nodes = start_cluster(Cluster), [First | Rest] = Nodes = start_cluster(Cluster),
try try
File = "/configs/cluster-override.conf", NodeDataDir = erpc:call(First, emqx, data_dir, []),
File = NodeDataDir ++ "/configs/cluster-override.conf",
assert_config_load_done(Nodes), assert_config_load_done(Nodes),
rpc:call(First, ?MODULE, create_data_dir, [File]), rpc:call(First, ?MODULE, create_data_dir, [File]),
{[ok, ok, ok], []} = rpc:multicall(Nodes, application, stop, [emqx_conf]), {[ok, ok, ok], []} = rpc:multicall(Nodes, application, stop, [emqx_conf]),
@ -98,11 +107,13 @@ t_copy_deprecated_data_dir(_Config) ->
stop_cluster(Nodes) stop_cluster(Nodes)
end. end.
t_no_copy_from_newer_version_node(_Config) -> t_no_copy_from_newer_version_node(Config) ->
net_kernel:start(['master2@127.0.0.1', longnames]), net_kernel:start(['master2@127.0.0.1', longnames]),
ct:timetrap({seconds, 120}), ct:timetrap({seconds, 120}),
snabbkaffe:fix_ct_logging(), snabbkaffe:fix_ct_logging(),
Cluster = cluster([cluster_spec({core, 10}), cluster_spec({core, 11}), cluster_spec({core, 12})]), Cluster = cluster(
[cluster_spec({core, 10}), cluster_spec({core, 11}), cluster_spec({core, 12})], Config
),
OKs = [ok, ok, ok], OKs = [ok, ok, ok],
[First | Rest] = Nodes = start_cluster(Cluster), [First | Rest] = Nodes = start_cluster(Cluster),
try try
@ -131,56 +142,60 @@ t_no_copy_from_newer_version_node(_Config) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create_data_dir(File) -> create_data_dir(File) ->
Node = atom_to_list(node()), NodeDataDir = emqx:data_dir(),
ok = filelib:ensure_dir(Node ++ "/certs/"), ok = filelib:ensure_dir(NodeDataDir ++ "/certs/"),
ok = filelib:ensure_dir(Node ++ "/authz/"), ok = filelib:ensure_dir(NodeDataDir ++ "/authz/"),
ok = filelib:ensure_dir(Node ++ "/configs/"), ok = filelib:ensure_dir(NodeDataDir ++ "/configs/"),
ok = file:write_file(Node ++ "/certs/fake-cert", list_to_binary(Node)), ok = file:write_file(NodeDataDir ++ "/certs/fake-cert", list_to_binary(NodeDataDir)),
ok = file:write_file(Node ++ "/authz/fake-authz", list_to_binary(Node)), ok = file:write_file(NodeDataDir ++ "/authz/fake-authz", list_to_binary(NodeDataDir)),
Telemetry = <<"telemetry.enable = false">>, Telemetry = <<"telemetry.enable = false">>,
ok = file:write_file(Node ++ File, Telemetry). ok = file:write_file(File, Telemetry).
set_data_dir_env() -> set_data_dir_env() ->
Node = atom_to_list(node()), NodeDataDir = emqx:data_dir(),
NodeStr = atom_to_list(node()),
%% will create certs and authz dir %% will create certs and authz dir
ok = filelib:ensure_dir(Node ++ "/configs/"), ok = filelib:ensure_dir(NodeDataDir ++ "/configs/"),
{ok, [ConfigFile]} = application:get_env(emqx, config_files), {ok, [ConfigFile]} = application:get_env(emqx, config_files),
NewConfigFile = ConfigFile ++ "." ++ Node, NewConfigFile = ConfigFile ++ "." ++ NodeStr,
ok = filelib:ensure_dir(NewConfigFile),
{ok, _} = file:copy(ConfigFile, NewConfigFile), {ok, _} = file:copy(ConfigFile, NewConfigFile),
Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]~n", [NewConfigFile])), Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]~n", [NewConfigFile])),
ok = file:write_file(NewConfigFile, Bin, [append]), ok = file:write_file(NewConfigFile, Bin, [append]),
DataDir = iolist_to_binary(io_lib:format("node.data_dir = ~p~n", [Node])), DataDir = iolist_to_binary(io_lib:format("node.data_dir = ~p~n", [NodeDataDir])),
ok = file:write_file(NewConfigFile, DataDir, [append]), ok = file:write_file(NewConfigFile, DataDir, [append]),
application:set_env(emqx, config_files, [NewConfigFile]), application:set_env(emqx, config_files, [NewConfigFile]),
application:set_env(emqx, data_dir, Node), %% application:set_env(emqx, data_dir, Node),
%% We set env both cluster.hocon and cluster-override.conf, but only one will be used %% We set env both cluster.hocon and cluster-override.conf, but only one will be used
application:set_env(emqx, cluster_hocon_file, Node ++ "/configs/cluster.hocon"), application:set_env(emqx, cluster_hocon_file, NodeDataDir ++ "/configs/cluster.hocon"),
application:set_env(emqx, cluster_override_conf_file, Node ++ "/configs/cluster-override.conf"), application:set_env(
emqx, cluster_override_conf_file, NodeDataDir ++ "/configs/cluster-override.conf"
),
ok. ok.
assert_data_copy_done([First0 | Rest], File) -> assert_data_copy_done([_First | Rest], File) ->
First = atom_to_list(First0), FirstDataDir = filename:dirname(filename:dirname(File)),
{ok, FakeCertFile} = file:read_file(First ++ "/certs/fake-cert"), {ok, FakeCertFile} = file:read_file(FirstDataDir ++ "/certs/fake-cert"),
{ok, FakeAuthzFile} = file:read_file(First ++ "/authz/fake-authz"), {ok, FakeAuthzFile} = file:read_file(FirstDataDir ++ "/authz/fake-authz"),
{ok, FakeOverrideFile} = file:read_file(First ++ File), {ok, FakeOverrideFile} = file:read_file(File),
{ok, ExpectFake} = hocon:binary(FakeOverrideFile), {ok, ExpectFake} = hocon:binary(FakeOverrideFile),
lists:foreach( lists:foreach(
fun(Node0) -> fun(Node0) ->
Node = atom_to_list(Node0), NodeDataDir = erpc:call(Node0, emqx, data_dir, []),
?assertEqual( ?assertEqual(
{ok, FakeCertFile}, {ok, FakeCertFile},
file:read_file(Node ++ "/certs/fake-cert"), file:read_file(NodeDataDir ++ "/certs/fake-cert"),
#{node => Node} #{node => Node0}
), ),
?assertEqual( ?assertEqual(
{ok, ExpectFake}, {ok, ExpectFake},
hocon:files([Node ++ File]), hocon:files([File]),
#{node => Node} #{node => Node0}
), ),
?assertEqual( ?assertEqual(
{ok, FakeAuthzFile}, {ok, FakeAuthzFile},
file:read_file(Node ++ "/authz/fake-authz"), file:read_file(NodeDataDir ++ "/authz/fake-authz"),
#{node => Node} #{node => Node0}
) )
end, end,
Rest Rest
@ -207,7 +222,7 @@ assert_config_load_done(Nodes) ->
). ).
stop_cluster(Nodes) -> stop_cluster(Nodes) ->
[emqx_common_test_helpers:stop_slave(Node) || Node <- Nodes]. emqx_utils:pmap(fun emqx_common_test_helpers:stop_slave/1, Nodes).
start_cluster(Specs) -> start_cluster(Specs) ->
[emqx_common_test_helpers:start_slave(Name, Opts) || {Name, Opts} <- Specs]. [emqx_common_test_helpers:start_slave(Name, Opts) || {Name, Opts} <- Specs].
@ -222,7 +237,8 @@ start_cluster_async(Specs) ->
|| {Name, Opts} <- Specs || {Name, Opts} <- Specs
]. ].
cluster(Specs) -> cluster(Specs, Config) ->
PrivDataDir = ?config(priv_dir, Config),
Env = [ Env = [
{emqx, init_config_load_done, false}, {emqx, init_config_load_done, false},
{emqx, boot_modules, []} {emqx, boot_modules, []}
@ -232,6 +248,7 @@ cluster(Specs) ->
{apps, [emqx_conf]}, {apps, [emqx_conf]},
{load_schema, false}, {load_schema, false},
{join_to, true}, {join_to, true},
{priv_data_dir, PrivDataDir},
{env_handler, fun {env_handler, fun
(emqx) -> (emqx) ->
application:set_env(emqx, boot_modules, []), application:set_env(emqx, boot_modules, []),

View File

@ -72,8 +72,7 @@ start_listeners(Listeners) ->
{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}},
{"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}},
{emqx_mgmt_api_status:path(), emqx_mgmt_api_status, []}, {emqx_mgmt_api_status:path(), emqx_mgmt_api_status, []},
{emqx_dashboard_swagger:relative_uri("/[...]"), emqx_dashboard_bad_api, []}, {'_', emqx_dashboard_not_found, []}
{'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
], ],
BaseMinirest = #{ BaseMinirest = #{
base_path => emqx_dashboard_swagger:base_path(), base_path => emqx_dashboard_swagger:base_path(),

View File

@ -24,6 +24,8 @@
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
-behaviour(emqx_db_backup).
%% Mnesia bootstrap %% Mnesia bootstrap
-export([mnesia/1]). -export([mnesia/1]).
@ -54,6 +56,8 @@
default_username/0 default_username/0
]). ]).
-export([backup_tables/0]).
-type emqx_admin() :: #?ADMIN{}. -type emqx_admin() :: #?ADMIN{}.
-define(BOOTSTRAP_USER_TAG, <<"bootstrap user">>). -define(BOOTSTRAP_USER_TAG, <<"bootstrap user">>).
@ -76,6 +80,12 @@ mnesia(boot) ->
]} ]}
]). ]).
%%--------------------------------------------------------------------
%% Data backup
%%--------------------------------------------------------------------
backup_tables() -> [?ADMIN].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% bootstrap API %% bootstrap API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -63,7 +63,7 @@ schema("/error_codes/:code") ->
'operationId' => error_code, 'operationId' => error_code,
get => #{ get => #{
security => [], security => [],
description => ?DESC(error_codes_u), description => ?DESC(error_codes),
tags => [<<"Error Codes">>], tags => [<<"Error Codes">>],
parameters => [ parameters => [
{code, {code,

View File

@ -65,7 +65,7 @@ schema("/monitor_current") ->
'operationId' => monitor_current, 'operationId' => monitor_current,
get => #{ get => #{
tags => [<<"Metrics">>], tags => [<<"Metrics">>],
description => ?DESC(current_status), description => ?DESC(current_stats),
responses => #{ responses => #{
200 => hoconsc:mk(hoconsc:ref(sampler_current), #{}) 200 => hoconsc:mk(hoconsc:ref(sampler_current), #{})
} }
@ -76,7 +76,7 @@ schema("/monitor_current/nodes/:node") ->
'operationId' => monitor_current, 'operationId' => monitor_current,
get => #{ get => #{
tags => [<<"Metrics">>], tags => [<<"Metrics">>],
description => ?DESC(current_status_node), description => ?DESC(current_stats_node),
parameters => [parameter_node()], parameters => [parameter_node()],
responses => #{ responses => #{
200 => hoconsc:mk(hoconsc:ref(sampler_current), #{}), 200 => hoconsc:mk(hoconsc:ref(sampler_current), #{}),

View File

@ -14,7 +14,7 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_dashboard_bad_api). -module(emqx_dashboard_not_found).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -23,10 +23,22 @@
init(Req0, State) -> init(Req0, State) ->
RedactedReq = emqx_utils:redact(Req0), RedactedReq = emqx_utils:redact(Req0),
?SLOG(warning, #{msg => "unexpected_api_access", request => RedactedReq}), ?SLOG(warning, #{msg => "unexpected_api_access", request => RedactedReq}),
CT = ct(cowboy_req:header(<<"accept">>, Req0, <<"text/html">>)),
Req = cowboy_req:reply( Req = cowboy_req:reply(
404, 404,
#{<<"content-type">> => <<"application/json">>}, #{<<"content-type">> => CT},
<<"{\"code\": \"API_NOT_EXIST\", \"message\": \"Request Path Not Found\"}">>, ct_body(CT),
RedactedReq RedactedReq
), ),
{ok, Req, State}. {ok, Req, State}.
ct(<<"text/plain", _/binary>>) -> <<"text/plain">>;
ct(<<"application/json", _/binary>>) -> <<"application/json">>;
ct(_AnyOther) -> <<"text/html">>.
ct_body(<<"text/html">>) ->
<<"<html><head><title>404 - NOT FOUND</title></head><body><h1>404 - NOT FOUND</h1></body></html>">>;
ct_body(<<"text/plain">>) ->
<<"404 - NOT FOUND">>;
ct_body(<<"application/json">>) ->
<<"{\"code\": \"NOT_FOUND\", \"message\": \"Request Path Not Found\"}">>.

View File

@ -99,9 +99,7 @@ t_monitor_current_api_live_connections(_) ->
ok = emqtt:disconnect(C), ok = emqtt:disconnect(C),
{ok, C1} = emqtt:start_link([{clean_start, true}, {clientid, ClientId1}]), {ok, C1} = emqtt:start_link([{clean_start, true}, {clientid, ClientId1}]),
{ok, _} = emqtt:connect(C1), {ok, _} = emqtt:connect(C1),
%% waiting for emqx_stats ticker ok = waiting_emqx_stats_and_monitor_update('live_connections.max'),
timer:sleep(1500),
_ = emqx_dashboard_monitor:current_rate(),
{ok, Rate} = request(["monitor_current"]), {ok, Rate} = request(["monitor_current"]),
?assertEqual(1, maps:get(<<"live_connections">>, Rate)), ?assertEqual(1, maps:get(<<"live_connections">>, Rate)),
?assertEqual(2, maps:get(<<"connections">>, Rate)), ?assertEqual(2, maps:get(<<"connections">>, Rate)),
@ -181,3 +179,24 @@ wait_new_monitor(OldMonitor, Count) ->
timer:sleep(100), timer:sleep(100),
wait_new_monitor(OldMonitor, Count - 1) wait_new_monitor(OldMonitor, Count - 1)
end. end.
waiting_emqx_stats_and_monitor_update(WaitKey) ->
Self = self(),
meck:new(emqx_stats, [passthrough]),
meck:expect(
emqx_stats,
setstat,
fun(Stat, MaxStat, Val) ->
(Stat =:= WaitKey orelse MaxStat =:= WaitKey) andalso (Self ! updated),
meck:passthrough([Stat, MaxStat, Val])
end
),
receive
updated -> ok
after 5000 ->
error(waiting_emqx_stats_update_timeout)
end,
meck:unload([emqx_stats]),
%% manually call monitor update
_ = emqx_dashboard_monitor:current_rate(),
ok.

View File

@ -14,7 +14,7 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_dashboard_bad_api_SUITE). -module(emqx_dashboard_not_found_SUITE).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
@ -23,7 +23,7 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(SERVER, "http://127.0.0.1:18083/api/v5"). -define(SERVER, "http://127.0.0.1:18083/").
-import(emqx_mgmt_api_test_util, [request/2]). -import(emqx_mgmt_api_test_util, [request/2]).

View File

@ -18,6 +18,7 @@
-module(emqx_exhook_mgr). -module(emqx_exhook_mgr).
-behaviour(gen_server). -behaviour(gen_server).
-behaviour(emqx_config_backup).
-include("emqx_exhook.hrl"). -include("emqx_exhook.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -66,6 +67,11 @@
-export([roots/0]). -export([roots/0]).
%% Data backup
-export([
import_config/1
]).
%% Running servers %% Running servers
-type state() :: #{servers := servers()}. -type state() :: #{servers := servers()}.
@ -98,9 +104,9 @@
-export_type([servers/0, server/0]). -export_type([servers/0, server/0]).
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% APIs %% APIs
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
-spec start_link() -> -spec start_link() ->
ignore ignore
@ -137,7 +143,7 @@ call(Req) ->
init_ref_counter_table() -> init_ref_counter_table() ->
_ = ets:new(?HOOKS_REF_COUNTER, [named_table, public]). _ = ets:new(?HOOKS_REF_COUNTER, [named_table, public]).
%%===================================================================== %%========================================================================================
%% Hocon schema %% Hocon schema
roots() -> roots() ->
emqx_exhook_schema:server_config(). emqx_exhook_schema:server_config().
@ -179,9 +185,30 @@ post_config_update(_KeyPath, UpdateReq, NewConf, OldConf, _AppEnvs) ->
Result = call({update_config, UpdateReq, NewConf, OldConf}), Result = call({update_config, UpdateReq, NewConf, OldConf}),
{ok, Result}. {ok, Result}.
%%-------------------------------------------------------------------- %%========================================================================================
%%----------------------------------------------------------------------------------------
%% Data backup
%%----------------------------------------------------------------------------------------
import_config(#{<<"exhook">> := #{<<"servers">> := Servers} = ExHook}) ->
OldServers = emqx:get_raw_config(?SERVERS, []),
KeyFun = fun(#{<<"name">> := Name}) -> Name end,
ExHook1 = ExHook#{<<"servers">> => emqx_utils:merge_lists(OldServers, Servers, KeyFun)},
case emqx_conf:update(?EXHOOK, ExHook1, #{override_to => cluster}) of
{ok, #{raw_config := #{<<"servers">> := NewRawServers}}} ->
Changed = maps:get(changed, emqx_utils:diff_lists(NewRawServers, OldServers, KeyFun)),
ChangedPaths = [?SERVERS ++ [Name] || {#{<<"name">> := Name}, _} <- Changed],
{ok, #{root_key => ?EXHOOK, changed => ChangedPaths}};
Error ->
{error, #{root_key => ?EXHOOK, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => ?EXHOOK, changed => []}}.
%%----------------------------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
init([]) -> init([]) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
@ -333,9 +360,9 @@ terminate(Reason, State = #{servers := Servers}) ->
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% Internal funcs %% Internal funcs
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
unload_exhooks() -> unload_exhooks() ->
[ [
@ -572,7 +599,7 @@ update_servers(Servers, State) ->
set_disable(Server) -> set_disable(Server) ->
Server#{status := disabled, timer := undefined}. Server#{status := disabled, timer := undefined}.
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% Server state persistent %% Server state persistent
save(Name, ServerState) -> save(Name, ServerState) ->
Saved = persistent_term:get(?APP, []), Saved = persistent_term:get(?APP, []),

View File

@ -76,6 +76,7 @@ fields(file_transfer) ->
#{ #{
desc => ?DESC("init_timeout"), desc => ?DESC("init_timeout"),
required => false, required => false,
importance => ?IMPORTANCE_LOW,
default => "10s" default => "10s"
} }
)}, )},
@ -85,6 +86,7 @@ fields(file_transfer) ->
#{ #{
desc => ?DESC("store_segment_timeout"), desc => ?DESC("store_segment_timeout"),
required => false, required => false,
importance => ?IMPORTANCE_LOW,
default => "5m" default => "5m"
} }
)}, )},
@ -94,6 +96,7 @@ fields(file_transfer) ->
#{ #{
desc => ?DESC("assemble_timeout"), desc => ?DESC("assemble_timeout"),
required => false, required => false,
importance => ?IMPORTANCE_LOW,
default => "5m" default => "5m"
} }
)}, )},

View File

@ -26,6 +26,51 @@
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
%% a
%% b
%% foo
%% 42
%% Я
%% link -> ../c
%% c
%% bar
%%
%% link -> ../a
%% d
%% e
%% baz
%% needle
%% haystack
init_per_suite(Config) ->
Root = ?config(data_dir, Config),
A = filename:join([Root, "a", "b", "foo"]),
C = filename:join([Root, "c", "bar"]),
D = filename:join([Root, "d", "e", "baz"]),
F42 = filename:join([A, "42"]),
F42_1 = filename:join([A, "Я"]),
FBar = filename:join([C, "中文"]),
FNeedle = filename:join([D, "needle"]),
FHayStack = filename:join([Root, "d", "haystack"]),
Files = [F42, F42_1, FBar, FNeedle, FHayStack],
lists:foreach(fun filelib:ensure_dir/1, Files),
%% create files
lists:foreach(fun(File) -> file:write_file(File, <<"">>, [write]) end, Files),
%% create links
ALink = filename:join([Root, "a", "link"]),
CLink = filename:join([Root, "c", "link"]),
make_symlink("../c", ALink),
make_symlink("../a", CLink),
Config.
end_per_suite(Config) ->
Root = ?config(data_dir, Config),
ok = file:del_dir_r(filename:join([Root, "a"])),
ok = file:del_dir_r(filename:join([Root, "c"])),
ok = file:del_dir_r(filename:join([Root, "d"])),
ok.
t_fold_single_level(Config) -> t_fold_single_level(Config) ->
Root = ?config(data_dir, Config), Root = ?config(data_dir, Config),
?assertMatch( ?assertMatch(
@ -248,3 +293,7 @@ cons(Entry, Acc) ->
sort(L) when is_list(L) -> sort(L) when is_list(L) ->
lists:sort(L). lists:sort(L).
make_symlink(FileOrDir, NewLink) ->
_ = file:delete(NewLink),
ok = file:make_symlink(FileOrDir, NewLink).

View File

@ -1 +0,0 @@
Zhōngwén

View File

@ -304,8 +304,7 @@ do_listeners_cluster_status(Listeners) ->
status => #{ status => #{
running => Running, running => Running,
current_connections => Curr, current_connections => Curr,
%% XXX: Since it is taken from raw-conf, it is possible a string max_connections => ensure_integer_or_infinity(Max)
max_connections => int(Max)
} }
} }
} }
@ -314,10 +313,15 @@ do_listeners_cluster_status(Listeners) ->
Listeners Listeners
). ).
int(B) when is_binary(B) -> ensure_integer_or_infinity(infinity) ->
infinity;
ensure_integer_or_infinity(<<"infinity">>) ->
infinity;
ensure_integer_or_infinity(B) when is_binary(B) ->
binary_to_integer(B); binary_to_integer(B);
int(I) when is_integer(I) -> ensure_integer_or_infinity(I) when is_integer(I) ->
I. I.
aggregate_listener_status(NodeStatus) -> aggregate_listener_status(NodeStatus) ->
aggregate_listener_status(NodeStatus, 0, 0, undefined). aggregate_listener_status(NodeStatus, 0, 0, undefined).
@ -330,8 +334,9 @@ aggregate_listener_status(
CurrAcc, CurrAcc,
RunningAcc RunningAcc
) -> ) ->
NMaxAcc = emqx_gateway_utils:plus_max_connections(MaxAcc, Max),
NRunning = aggregate_running(Running, RunningAcc), NRunning = aggregate_running(Running, RunningAcc),
aggregate_listener_status(T, MaxAcc + Max, Current + CurrAcc, NRunning); aggregate_listener_status(T, NMaxAcc, Current + CurrAcc, NRunning);
aggregate_listener_status([], MaxAcc, CurrAcc, RunningAcc) -> aggregate_listener_status([], MaxAcc, CurrAcc, RunningAcc) ->
{MaxAcc, CurrAcc, RunningAcc}. {MaxAcc, CurrAcc, RunningAcc}.

View File

@ -389,7 +389,7 @@ open_session(
end, end,
case takeover_session(GwName, ClientId) of case takeover_session(GwName, ClientId) of
{ok, ConnMod, ChanPid, Session} -> {ok, ConnMod, ChanPid, Session} ->
ok = emqx_session:resume(ClientInfo, Session), ok = SessionMod:resume(ClientInfo, Session),
case request_stepdown({takeover, 'end'}, ConnMod, ChanPid) of case request_stepdown({takeover, 'end'}, ConnMod, ChanPid) of
{ok, Pendings} -> {ok, Pendings} ->
register_channel( register_channel(

View File

@ -18,6 +18,7 @@
-module(emqx_gateway_conf). -module(emqx_gateway_conf).
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
-behaviour(emqx_config_backup).
%% Load/Unload %% Load/Unload
-export([ -export([
@ -64,6 +65,11 @@
post_config_update/5 post_config_update/5
]). ]).
%% Data backup
-export([
import_config/1
]).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_authentication.hrl"). -include_lib("emqx/include/emqx_authentication.hrl").
-define(AUTHN_BIN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY). -define(AUTHN_BIN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY).
@ -76,9 +82,9 @@
-define(IS_SSL(T), (T == <<"ssl_options">> orelse T == <<"dtls_options">>)). -define(IS_SSL(T), (T == <<"ssl_options">> orelse T == <<"dtls_options">>)).
-define(IGNORE_KEYS, [<<"listeners">>, ?AUTHN_BIN]). -define(IGNORE_KEYS, [<<"listeners">>, ?AUTHN_BIN]).
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% Load/Unload %% Load/Unload
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
-define(GATEWAY, [gateway]). -define(GATEWAY, [gateway]).
-spec load() -> ok. -spec load() -> ok.
@ -89,7 +95,7 @@ load() ->
unload() -> unload() ->
emqx_conf:remove_handler(?GATEWAY). emqx_conf:remove_handler(?GATEWAY).
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% APIs %% APIs
-spec load_gateway(atom_or_bin(), map()) -> map_or_err(). -spec load_gateway(atom_or_bin(), map()) -> map_or_err().
@ -365,9 +371,26 @@ ret_listener_or_err(GwName, {LType, LName}, {ok, #{raw_config := GwConf}}) ->
ret_listener_or_err(_, _, Err) -> ret_listener_or_err(_, _, Err) ->
Err. Err.
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% Data backup
%%----------------------------------------------------------------------------------------
import_config(RawConf) ->
GatewayConf = maps:get(<<"gateway">>, RawConf, #{}),
OldGatewayConf = emqx:get_raw_config([<<"gateway">>], #{}),
MergedConf = maps:merge(OldGatewayConf, GatewayConf),
case emqx_conf:update([gateway], MergedConf, #{override_to => cluster}) of
{ok, #{raw_config := NewRawConf}} ->
Changed = maps:get(changed, emqx_utils_maps:diff_maps(NewRawConf, OldGatewayConf)),
ChangedPaths = [[gateway, GwName] || GwName <- maps:keys(Changed)],
{ok, #{root_key => gateway, changed => ChangedPaths}};
Error ->
{error, #{root_key => gateway, reason => Error}}
end.
%%----------------------------------------------------------------------------------------
%% Config Handler %% Config Handler
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
-spec pre_config_update( -spec pre_config_update(
list(atom()), list(atom()),
@ -793,9 +816,9 @@ post_config_update(?GATEWAY, _Req = #{}, NewConfig, OldConfig, _AppEnvs) ->
), ),
ok. ok.
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
%% Internal functions %% Internal funcs
%%-------------------------------------------------------------------- %%----------------------------------------------------------------------------------------
tune_gw_certs(Fun, GwName, Conf) -> tune_gw_certs(Fun, GwName, Conf) ->
apply_to_gateway_basic_confs( apply_to_gateway_basic_confs(

View File

@ -161,7 +161,10 @@ max_connections_count(Config) ->
Listeners = emqx_gateway_utils:normalize_config(Config), Listeners = emqx_gateway_utils:normalize_config(Config),
lists:foldl( lists:foldl(
fun({_, _, _, SocketOpts, _}, Acc) -> fun({_, _, _, SocketOpts, _}, Acc) ->
Acc + proplists:get_value(max_connections, SocketOpts, 0) emqx_gateway_utils:plus_max_connections(
Acc,
proplists:get_value(max_connections, SocketOpts, 0)
)
end, end,
0, 0,
Listeners Listeners
@ -588,10 +591,12 @@ sum_cluster_connections(List) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal funcs %% Internal funcs
sum_cluster_connections( sum_cluster_connections(
[#{max_connections := Max, current_connections := Current} | T], MaxAcc, CurrAcc [#{max_connections := Max, current_connections := Current} | T], MaxAcc, CurrAcc
) -> ) ->
sum_cluster_connections(T, MaxAcc + Max, Current + CurrAcc); NMaxAcc = emqx_gateway_utils:plus_max_connections(MaxAcc, Max),
sum_cluster_connections(T, NMaxAcc, Current + CurrAcc);
sum_cluster_connections([_ | T], MaxAcc, CurrAcc) -> sum_cluster_connections([_ | T], MaxAcc, CurrAcc) ->
sum_cluster_connections(T, MaxAcc, CurrAcc); sum_cluster_connections(T, MaxAcc, CurrAcc);
sum_cluster_connections([], MaxAcc, CurrAcc) -> sum_cluster_connections([], MaxAcc, CurrAcc) ->

View File

@ -266,7 +266,7 @@ common_listener_opts() ->
)}, )},
{max_connections, {max_connections,
sc( sc(
integer(), hoconsc:union([pos_integer(), infinity]),
#{ #{
default => 1024, default => 1024,
desc => ?DESC(gateway_common_listener_max_connections) desc => ?DESC(gateway_common_listener_max_connections)

View File

@ -45,7 +45,8 @@
is_running/2, is_running/2,
global_chain/1, global_chain/1,
listener_chain/3, listener_chain/3,
find_gateway_definitions/0 find_gateway_definitions/0,
plus_max_connections/2
]). ]).
-export([stringfy/1]). -export([stringfy/1]).
@ -607,3 +608,12 @@ ignore_lib_apps(Apps) ->
wx wx
], ],
[AppName || {AppName, _, _} <- Apps, not lists:member(AppName, LibApps)]. [AppName || {AppName, _, _} <- Apps, not lists:member(AppName, LibApps)].
-spec plus_max_connections(non_neg_integer() | infinity, non_neg_integer() | infinity) ->
pos_integer() | infinity.
plus_max_connections(_, infinity) ->
infinity;
plus_max_connections(infinity, _) ->
infinity;
plus_max_connections(A, B) when is_integer(A) andalso is_integer(B) ->
A + B.

View File

@ -411,6 +411,45 @@ t_listeners_tcp(_) ->
{404, _} = request(get, "/gateways/stomp/listeners/stomp:tcp:def"), {404, _} = request(get, "/gateways/stomp/listeners/stomp:tcp:def"),
ok. ok.
t_listeners_max_conns(_) ->
{204, _} = request(put, "/gateways/stomp", #{}),
{404, _} = request(get, "/gateways/stomp/listeners"),
LisConf = #{
name => <<"def">>,
type => <<"tcp">>,
bind => <<"127.0.0.1:61613">>,
max_connections => 1024
},
{201, _} = request(post, "/gateways/stomp/listeners", LisConf),
{200, ConfResp} = request(get, "/gateways/stomp/listeners"),
assert_confs([LisConf], ConfResp),
{200, ConfResp1} = request(get, "/gateways/stomp/listeners/stomp:tcp:def"),
assert_confs(LisConf, ConfResp1),
LisConf2 = maps:merge(LisConf, #{max_connections => <<"infinity">>}),
{200, _} = request(
put,
"/gateways/stomp/listeners/stomp:tcp:def",
LisConf2
),
{200, ConfResp2} = request(get, "/gateways/stomp/listeners/stomp:tcp:def"),
assert_confs(LisConf2, ConfResp2),
{200, [Listeners]} = request(get, "/gateways/stomp/listeners"),
?assertMatch(#{max_connections := <<"infinity">>}, Listeners),
{200, Gateways} = request(get, "/gateways"),
[StompGwOverview] = lists:filter(
fun(Gw) -> maps:get(name, Gw) =:= <<"stomp">> end,
Gateways
),
?assertMatch(#{max_connections := <<"infinity">>}, StompGwOverview),
{204, _} = request(delete, "/gateways/stomp/listeners/stomp:tcp:def"),
{404, _} = request(get, "/gateways/stomp/listeners/stomp:tcp:def"),
ok.
t_listeners_authn(_) -> t_listeners_authn(_) ->
GwConf = #{ GwConf = #{
name => <<"stomp">>, name => <<"stomp">>,

View File

@ -381,6 +381,8 @@ ensure_keepalive_timer(Fun, #channel{keepalive = KeepAlive} = Channel) ->
Heartbeat = emqx_keepalive:info(interval, KeepAlive), Heartbeat = emqx_keepalive:info(interval, KeepAlive),
Fun(keepalive, Heartbeat, keepalive, Channel). Fun(keepalive, Heartbeat, keepalive, Channel).
check_auth_state(Msg, #channel{connection_required = false} = Channel) ->
call_session(handle_request, Msg, Channel);
check_auth_state(Msg, #channel{connection_required = true} = Channel) -> check_auth_state(Msg, #channel{connection_required = true} = Channel) ->
case is_create_connection_request(Msg) of case is_create_connection_request(Msg) of
true -> true ->

View File

@ -58,14 +58,14 @@ all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
application:load(emqx_gateway_coap), application:load(emqx_gateway_coap),
ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, ?CONF_DEFAULT), ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, ?CONF_DEFAULT),
emqx_mgmt_api_test_util:init_suite([emqx_authn, emqx_gateway]), emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authn, emqx_gateway]),
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
Config. Config.
end_per_suite(_) -> end_per_suite(_) ->
meck:unload(emqx_access_control), meck:unload(emqx_access_control),
{ok, _} = emqx:remove_config([<<"gateway">>, <<"coap">>]), {ok, _} = emqx:remove_config([<<"gateway">>, <<"coap">>]),
emqx_mgmt_api_test_util:end_suite([emqx_gateway, emqx_authn]). emqx_mgmt_api_test_util:end_suite([emqx_gateway, emqx_authn, emqx_conf]).
init_per_testcase(t_connection_with_authn_failed, Config) -> init_per_testcase(t_connection_with_authn_failed, Config) ->
ok = meck:expect( ok = meck:expect(
@ -92,6 +92,13 @@ mqtt_prefix() ->
ps_prefix() -> ps_prefix() ->
?PS_PREFIX. ?PS_PREFIX.
restart_coap_with_connection_mode(Bool) ->
Conf = emqx:get_raw_config([gateway, coap]),
emqx_gateway_conf:update_gateway(
coap,
Conf#{<<"connection_required">> => atom_to_binary(Bool)}
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Test Cases %% Test Cases
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -373,6 +380,35 @@ t_on_offline_event(_) ->
end, end,
do(Fun). do(Fun).
t_connectionless_pubsub(_) ->
restart_coap_with_connection_mode(false),
Fun = fun(Channel) ->
Topic = <<"t/a">>,
Payload = <<"123">>,
URI = pubsub_uri(binary_to_list(Topic)),
Req = make_req(get, Payload, [{observe, 0}]),
{ok, content, _} = do_request(Channel, URI, Req),
?LOGT("observer topic:~ts~n", [Topic]),
%% ensure subscribe succeed
timer:sleep(100),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
%% publish a message
Req2 = make_req(post, Payload),
{ok, changed, _} = do_request(Channel, URI, Req2),
{ok, content, Notify} = with_response(Channel),
?LOGT("observer get Notif=~p", [Notify]),
#coap_content{payload = PayloadRecv} = Notify,
?assertEqual(Payload, PayloadRecv)
end,
do(Fun),
restart_coap_with_connection_mode(true).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% helpers %% helpers
@ -402,6 +438,9 @@ observe(Channel, Token, false) ->
{ok, nocontent, _Data} = do_request(Channel, URI, Req), {ok, nocontent, _Data} = do_request(Channel, URI, Req),
ok. ok.
pubsub_uri(Topic) when is_list(Topic) ->
?PS_PREFIX ++ "/" ++ Topic.
pubsub_uri(Topic, Token) when is_list(Topic), is_list(Token) -> pubsub_uri(Topic, Token) when is_list(Topic), is_list(Token) ->
?PS_PREFIX ++ "/" ++ Topic ++ "?clientid=client1&token=" ++ Token. ?PS_PREFIX ++ "/" ++ Topic ++ "?clientid=client1&token=" ++ Token.

View File

@ -14,6 +14,8 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-define(SN_MAX_PREDEF_TOPIC_ID, 1024).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MQTT-SN Types %% MQTT-SN Types
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -56,8 +56,7 @@ on_gateway_load(
}, },
Ctx Ctx
) -> ) ->
%% We Also need to start `emqx_mqttsn_broadcast` & %% We Also need to start `emqx_mqttsn_broadcast`
%% `emqx_mqttsn_registry` process
case maps:get(broadcast, Config, false) of case maps:get(broadcast, Config, false) of
false -> false ->
ok; ok;
@ -70,12 +69,9 @@ on_gateway_load(
end, end,
PredefTopics = maps:get(predefined, Config, []), PredefTopics = maps:get(predefined, Config, []),
{ok, RegistrySvr} = emqx_mqttsn_registry:start_link(GwName, PredefTopics), ok = emqx_mqttsn_registry:persist_predefined_topics(PredefTopics),
NConfig = maps:without( NConfig = maps:without([broadcast, predefined], Config),
[broadcast, predefined],
Config#{registry => emqx_mqttsn_registry:lookup_name(RegistrySvr)}
),
Listeners = emqx_gateway_utils:normalize_config(NConfig), Listeners = emqx_gateway_utils:normalize_config(NConfig),
@ -125,5 +121,7 @@ on_gateway_unload(
}, },
_GwState _GwState
) -> ) ->
PredefTopics = maps:get(predefined, Config, []),
ok = emqx_mqttsn_registry:clear_predefined_topics(PredefTopics),
Listeners = normalize_config(Config), Listeners = normalize_config(Config),
stop_listeners(GwName, Listeners). stop_listeners(GwName, Listeners).

View File

@ -23,6 +23,7 @@
-include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/types.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
%% API %% API
-export([ -export([
@ -50,20 +51,16 @@
-record(channel, { -record(channel, {
%% Context %% Context
ctx :: emqx_gateway_ctx:context(), ctx :: emqx_gateway_ctx:context(),
%% Registry
registry :: emqx_mqttsn_registry:registry(),
%% Gateway Id %% Gateway Id
gateway_id :: integer(), gateway_id :: integer(),
%% Enable QoS3 %% Enable negative_qos
enable_negative_qos :: boolean(),
%% XXX: Get confs from ctx ?
enable_qos3 :: boolean(),
%% MQTT-SN Connection Info %% MQTT-SN Connection Info
conninfo :: emqx_types:conninfo(), conninfo :: emqx_types:conninfo(),
%% MQTT-SN Client Info %% MQTT-SN Client Info
clientinfo :: emqx_types:clientinfo(), clientinfo :: emqx_types:clientinfo(),
%% Session %% Session
session :: emqx_session:session() | undefined, session :: emqx_mqttsn_session:session() | undefined,
%% Keepalive %% Keepalive
keepalive :: emqx_keepalive:keepalive() | undefined, keepalive :: emqx_keepalive:keepalive() | undefined,
%% Will Msg %% Will Msg
@ -148,9 +145,8 @@ init(
) -> ) ->
Peercert = maps:get(peercert, ConnInfo, undefined), Peercert = maps:get(peercert, ConnInfo, undefined),
Mountpoint = maps:get(mountpoint, Option, undefined), Mountpoint = maps:get(mountpoint, Option, undefined),
Registry = maps:get(registry, Option),
GwId = maps:get(gateway_id, Option), GwId = maps:get(gateway_id, Option),
EnableQoS3 = maps:get(enable_qos3, Option, true), EnableNegQoS = maps:get(enable_qos3, Option, true),
ListenerId = ListenerId =
case maps:get(listener, Option, undefined) of case maps:get(listener, Option, undefined) of
undefined -> undefined; undefined -> undefined;
@ -181,9 +177,8 @@ init(
), ),
#channel{ #channel{
ctx = Ctx, ctx = Ctx,
registry = Registry,
gateway_id = GwId, gateway_id = GwId,
enable_qos3 = EnableQoS3, enable_negative_qos = EnableNegQoS,
conninfo = ConnInfo, conninfo = ConnInfo,
clientinfo = ClientInfo, clientinfo = ClientInfo,
clientinfo_override = Override, clientinfo_override = Override,
@ -218,7 +213,7 @@ info(conn_state, #channel{conn_state = ConnState}) ->
info(clientinfo, #channel{clientinfo = ClientInfo}) -> info(clientinfo, #channel{clientinfo = ClientInfo}) ->
ClientInfo; ClientInfo;
info(session, #channel{session = Session}) -> info(session, #channel{session = Session}) ->
emqx_utils:maybe_apply(fun emqx_session:info/1, Session); emqx_utils:maybe_apply(fun emqx_mqttsn_session:info/1, Session);
info(will_msg, #channel{will_msg = WillMsg}) -> info(will_msg, #channel{will_msg = WillMsg}) ->
WillMsg; WillMsg;
info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> info(clientid, #channel{clientinfo = #{clientid := ClientId}}) ->
@ -230,7 +225,7 @@ info(ctx, #channel{ctx = Ctx}) ->
stats(#channel{session = undefined}) -> stats(#channel{session = undefined}) ->
[]; [];
stats(#channel{session = Session}) -> stats(#channel{session = Session}) ->
emqx_session:stats(Session). emqx_mqttsn_session:stats(Session).
set_conn_state(ConnState, Channel) -> set_conn_state(ConnState, Channel) ->
Channel#channel{conn_state = ConnState}. Channel#channel{conn_state = ConnState}.
@ -389,19 +384,15 @@ process_connect(
clientinfo = ClientInfo clientinfo = ClientInfo
} }
) -> ) ->
SessFun = fun(ClientInfoT, _) -> SessFun = fun(ClientInfoT, _) -> emqx_mqttsn_session:init(ClientInfoT) end,
Conf = emqx_cm:get_session_confs(
ClientInfoT, #{receive_maximum => 1, expiry_interval => 0}
),
emqx_session:init(Conf)
end,
case case
emqx_gateway_ctx:open_session( emqx_gateway_ctx:open_session(
Ctx, Ctx,
CleanStart, CleanStart,
ClientInfo, ClientInfo,
ConnInfo, ConnInfo,
SessFun SessFun,
_SessMod = emqx_mqttsn_session
) )
of of
{ok, #{ {ok, #{
@ -461,30 +452,30 @@ handle_in(?SN_ADVERTISE_MSG(_GwId, _Radius), Channel) ->
% ignore % ignore
shutdown(normal, Channel); shutdown(normal, Channel);
handle_in( handle_in(
Publish =
?SN_PUBLISH_MSG( ?SN_PUBLISH_MSG(
#mqtt_sn_flags{ #mqtt_sn_flags{
qos = ?QOS_NEG1, qos = ?QOS_NEG1,
topic_id_type = TopicIdType topic_id_type = TopicIdType
}, },
TopicId, TopicId,
_MsgId, MsgId,
Data Data
), ),
Channel = #channel{conn_state = idle, registry = Registry} Channel = #channel{conn_state = idle}
) -> ) ->
%% FIXME: check enable_qos3 ?? case check_negative_qos_enable(Publish, Channel) of
ok ->
TopicName = TopicName =
case (TopicIdType =:= ?SN_SHORT_TOPIC) of case TopicIdType of
true -> ?SN_SHORT_TOPIC ->
<<TopicId:16>>; TopicId;
false -> ?SN_PREDEFINED_TOPIC ->
emqx_mqttsn_registry:lookup_topic( Registry = emqx_mqttsn_registry:init(),
Registry, emqx_mqttsn_registry:lookup_topic(TopicId, Registry);
?NEG_QOS_CLIENT_ID, _ ->
TopicId undefined
)
end, end,
_ =
case TopicName =/= undefined of case TopicName =/= undefined of
true -> true ->
Msg = emqx_message:make( Msg = emqx_message:make(
@ -493,16 +484,26 @@ handle_in(
TopicName, TopicName,
Data Data
), ),
emqx_broker:publish(Msg);
false ->
ok
end,
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "receive_qo3_message_in_idle_mode", msg => "receive_qo3_message_in_idle_mode",
topic => TopicName, topic => TopicName,
data => Data data => Data
}), }),
{ok, Channel}; _ = emqx_broker:publish(Msg),
ok;
false ->
ok
end,
shutdown(normal, Channel);
{error, Rc} ->
?tp(info, ignore_negative_qos, #{
topic_id => TopicId,
msg_id => MsgId,
return_code => Rc
}),
PubAck = ?SN_PUBACK_MSG(TopicId, MsgId, Rc),
shutdown(normal, PubAck, Channel)
end;
handle_in( handle_in(
Pkt = #mqtt_sn_message{type = Type}, Pkt = #mqtt_sn_message{type = Type},
Channel = #channel{conn_state = idle} Channel = #channel{conn_state = idle}
@ -619,20 +620,19 @@ handle_in(
end; end;
handle_in( handle_in(
?SN_REGISTER_MSG(_TopicId, MsgId, TopicName), ?SN_REGISTER_MSG(_TopicId, MsgId, TopicName),
Channel = #channel{ Channel = #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
case emqx_mqttsn_registry:register_topic(Registry, ClientId, TopicName) of Registry = emqx_mqttsn_session:registry(Session),
TopicId when is_integer(TopicId) -> case emqx_mqttsn_registry:reg(TopicName, Registry) of
{ok, TopicId, NRegistry} ->
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "registered_topic_name", msg => "registered_topic_name",
topic_name => TopicName, topic_name => TopicName,
topic_id => TopicId topic_id => TopicId
}), }),
AckPacket = ?SN_REGACK_MSG(TopicId, MsgId, ?SN_RC_ACCEPTED), AckPacket = ?SN_REGACK_MSG(TopicId, MsgId, ?SN_RC_ACCEPTED),
{ok, {outgoing, AckPacket}, Channel}; NSession = emqx_mqttsn_session:set_registry(NRegistry, Session),
{ok, {outgoing, AckPacket}, Channel#channel{session = NSession}};
{error, too_large} -> {error, too_large} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "register_topic_failed", msg => "register_topic_failed",
@ -720,7 +720,7 @@ handle_in(PubPkt = ?SN_PUBLISH_MSG(_Flags, TopicId0, MsgId, _Data), Channel) ->
case case
emqx_utils:pipeline( emqx_utils:pipeline(
[ [
fun check_qos3_enable/2, fun check_negative_qos_enable/2,
fun preproc_pub_pkt/2, fun preproc_pub_pkt/2,
fun convert_topic_id_to_name/2, fun convert_topic_id_to_name/2,
fun check_pub_authz/2, fun check_pub_authz/2,
@ -733,20 +733,25 @@ handle_in(PubPkt = ?SN_PUBLISH_MSG(_Flags, TopicId0, MsgId, _Data), Channel) ->
{ok, Msg, NChannel} -> {ok, Msg, NChannel} ->
do_publish(TopicId, MsgId, Msg, NChannel); do_publish(TopicId, MsgId, Msg, NChannel);
{error, ReturnCode, NChannel} -> {error, ReturnCode, NChannel} ->
?tp(info, publish_msg_rejected, #{
topic_id => TopicId,
msg_id => MsgId,
return_code => ReturnCode
}),
handle_out(puback, {TopicId, MsgId, ReturnCode}, NChannel) handle_out(puback, {TopicId, MsgId, ReturnCode}, NChannel)
end; end;
handle_in( handle_in(
?SN_PUBACK_MSG(TopicId, MsgId, ReturnCode), ?SN_PUBACK_MSG(TopicId, MsgId, ReturnCode),
Channel = #channel{ Channel = #channel{
ctx = Ctx, ctx = Ctx,
registry = Registry,
session = Session, session = Session,
clientinfo = ClientInfo = #{clientid := ClientId} clientinfo = ClientInfo
} }
) -> ) ->
Registry = emqx_mqttsn_session:registry(Session),
case ReturnCode of case ReturnCode of
?SN_RC_ACCEPTED -> ?SN_RC_ACCEPTED ->
case emqx_session:puback(ClientInfo, MsgId, Session) of case emqx_mqttsn_session:puback(ClientInfo, MsgId, Session) of
{ok, Msg, NSession} -> {ok, Msg, NSession} ->
ok = after_message_acked(ClientInfo, Msg, Channel), ok = after_message_acked(ClientInfo, Msg, Channel),
{Replies, NChannel} = goto_asleep_if_buffered_msgs_sent( {Replies, NChannel} = goto_asleep_if_buffered_msgs_sent(
@ -778,7 +783,7 @@ handle_in(
{ok, Channel} {ok, Channel}
end; end;
?SN_RC_INVALID_TOPIC_ID -> ?SN_RC_INVALID_TOPIC_ID ->
case emqx_mqttsn_registry:lookup_topic(Registry, ClientId, TopicId) of case emqx_mqttsn_registry:lookup_topic(TopicId, Registry) of
undefined -> undefined ->
{ok, Channel}; {ok, Channel};
TopicName -> TopicName ->
@ -803,7 +808,7 @@ handle_in(
clientinfo = ClientInfo clientinfo = ClientInfo
} }
) -> ) ->
case emqx_session:pubrec(ClientInfo, MsgId, Session) of case emqx_mqttsn_session:pubrec(ClientInfo, MsgId, Session) of
{ok, Msg, NSession} -> {ok, Msg, NSession} ->
ok = after_message_acked(ClientInfo, Msg, Channel), ok = after_message_acked(ClientInfo, Msg, Channel),
NChannel = Channel#channel{session = NSession}, NChannel = Channel#channel{session = NSession},
@ -829,7 +834,7 @@ handle_in(
?SN_PUBREC_MSG(?SN_PUBREL, MsgId), ?SN_PUBREC_MSG(?SN_PUBREL, MsgId),
Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo} Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo}
) -> ) ->
case emqx_session:pubrel(ClientInfo, MsgId, Session) of case emqx_mqttsn_session:pubrel(ClientInfo, MsgId, Session) of
{ok, NSession} -> {ok, NSession} ->
NChannel = Channel#channel{session = NSession}, NChannel = Channel#channel{session = NSession},
handle_out(pubcomp, MsgId, NChannel); handle_out(pubcomp, MsgId, NChannel);
@ -846,7 +851,7 @@ handle_in(
?SN_PUBREC_MSG(?SN_PUBCOMP, MsgId), ?SN_PUBREC_MSG(?SN_PUBCOMP, MsgId),
Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo} Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo}
) -> ) ->
case emqx_session:pubcomp(ClientInfo, MsgId, Session) of case emqx_mqttsn_session:pubcomp(ClientInfo, MsgId, Session) of
{ok, NSession} -> {ok, NSession} ->
{Replies, NChannel} = goto_asleep_if_buffered_msgs_sent( {Replies, NChannel} = goto_asleep_if_buffered_msgs_sent(
Channel#channel{session = NSession} Channel#channel{session = NSession}
@ -1044,18 +1049,13 @@ send_next_register_or_replay_publish(
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle Publish %% Handle Publish
check_qos3_enable( check_negative_qos_enable(
?SN_PUBLISH_MSG(Flags, TopicId, _MsgId, Data), ?SN_PUBLISH_MSG(Flags, _TopicId, _MsgId, _Data),
#channel{enable_qos3 = EnableQoS3} #channel{enable_negative_qos = EnableNegQoS}
) -> ) ->
#mqtt_sn_flags{qos = QoS} = Flags, #mqtt_sn_flags{qos = QoS} = Flags,
case EnableQoS3 =:= false andalso QoS =:= ?QOS_NEG1 of case EnableNegQoS =:= false andalso QoS =:= ?QOS_NEG1 of
true -> true ->
?SLOG(debug, #{
msg => "ignore_msg_due_to_qos3_disabled",
topic_id => TopicId,
data => Data
}),
{error, ?SN_RC_NOT_SUPPORTED}; {error, ?SN_RC_NOT_SUPPORTED};
false -> false ->
ok ok
@ -1088,12 +1088,10 @@ convert_topic_id_to_name({{name, TopicName}, Flags, Data}, Channel) ->
{ok, {TopicName, Flags, Data}, Channel}; {ok, {TopicName, Flags, Data}, Channel};
convert_topic_id_to_name( convert_topic_id_to_name(
{{id, TopicId}, Flags, Data}, {{id, TopicId}, Flags, Data},
Channel = #channel{ Channel = #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
case emqx_mqttsn_registry:lookup_topic(Registry, ClientId, TopicId) of Registry = emqx_mqttsn_session:registry(Session),
case emqx_mqttsn_registry:lookup_topic(TopicId, Registry) of
undefined -> undefined ->
{error, ?SN_RC_INVALID_TOPIC_ID}; {error, ?SN_RC_INVALID_TOPIC_ID};
TopicName -> TopicName ->
@ -1162,7 +1160,7 @@ do_publish(
Msg = #message{qos = ?QOS_2}, Msg = #message{qos = ?QOS_2},
Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo} Channel = #channel{ctx = Ctx, session = Session, clientinfo = ClientInfo}
) -> ) ->
case emqx_session:publish(ClientInfo, MsgId, Msg, Session) of case emqx_mqttsn_session:publish(ClientInfo, MsgId, Msg, Session) of
{ok, _PubRes, NSession} -> {ok, _PubRes, NSession} ->
NChannel1 = ensure_timer( NChannel1 = ensure_timer(
await_timer, await_timer,
@ -1195,15 +1193,13 @@ preproc_subs_type(
TopicName, TopicName,
QoS QoS
), ),
Channel = #channel{ Channel = #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
Registry = emqx_mqttsn_session:registry(Session),
%% If the gateway is able accept the subscription, %% If the gateway is able accept the subscription,
%% it assigns a topic id to the received topic name %% it assigns a topic id to the received topic name
%% and returns it within a SUBACK message %% and returns it within a SUBACK message
case emqx_mqttsn_registry:register_topic(Registry, ClientId, TopicName) of case emqx_mqttsn_registry:reg(TopicName, Registry) of
{error, too_large} -> {error, too_large} ->
{error, ?SN_RC2_EXCEED_LIMITATION}; {error, ?SN_RC2_EXCEED_LIMITATION};
{error, wildcard_topic} -> {error, wildcard_topic} ->
@ -1214,8 +1210,9 @@ preproc_subs_type(
%% value when it has the first PUBLISH message with a matching %% value when it has the first PUBLISH message with a matching
%% topic name to be sent to the client, see also Section 6.10. %% topic name to be sent to the client, see also Section 6.10.
{ok, {?SN_INVALID_TOPIC_ID, TopicName, QoS}, Channel}; {ok, {?SN_INVALID_TOPIC_ID, TopicName, QoS}, Channel};
TopicId when is_integer(TopicId) -> {ok, TopicId, NRegistry} ->
{ok, {TopicId, TopicName, QoS}, Channel} NSession = emqx_mqttsn_session:set_registry(NRegistry, Session),
{ok, {TopicId, TopicName, QoS}, Channel#channel{session = NSession}}
end; end;
preproc_subs_type( preproc_subs_type(
?SN_SUBSCRIBE_MSG_TYPE( ?SN_SUBSCRIBE_MSG_TYPE(
@ -1223,18 +1220,10 @@ preproc_subs_type(
TopicId, TopicId,
QoS QoS
), ),
Channel = #channel{ Channel = #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
case Registry = emqx_mqttsn_session:registry(Session),
emqx_mqttsn_registry:lookup_topic( case emqx_mqttsn_registry:lookup_topic(TopicId, Registry) of
Registry,
ClientId,
TopicId
)
of
undefined -> undefined ->
{error, ?SN_RC_INVALID_TOPIC_ID}; {error, ?SN_RC_INVALID_TOPIC_ID};
TopicName -> TopicName ->
@ -1311,7 +1300,7 @@ do_subscribe(
) -> ) ->
NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName),
NSubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts), NSubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts),
case emqx_session:subscribe(ClientInfo, NTopicName, NSubOpts, Session) of case emqx_mqttsn_session:subscribe(ClientInfo, NTopicName, NSubOpts, Session) of
{ok, NSession} -> {ok, NSession} ->
{ok, {TopicId, NTopicName, NSubOpts}, Channel#channel{session = NSession}}; {ok, {TopicId, NTopicName, NSubOpts}, Channel#channel{session = NSession}};
{error, ?RC_QUOTA_EXCEEDED} -> {error, ?RC_QUOTA_EXCEEDED} ->
@ -1339,18 +1328,10 @@ preproc_unsub_type(
?SN_PREDEFINED_TOPIC, ?SN_PREDEFINED_TOPIC,
TopicId TopicId
), ),
Channel = #channel{ Channel = #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
case Registry = emqx_mqttsn_session:registry(Session),
emqx_mqttsn_registry:lookup_topic( case emqx_mqttsn_registry:lookup_topic(TopicId, Registry) of
Registry,
ClientId,
TopicId
)
of
undefined -> undefined ->
{error, not_found}; {error, not_found};
TopicName -> TopicName ->
@ -1410,7 +1391,7 @@ do_unsubscribe(
SubOpts SubOpts
), ),
case case
emqx_session:unsubscribe( emqx_mqttsn_session:unsubscribe(
ClientInfo, ClientInfo,
NTopicName, NTopicName,
NSubOpts, NSubOpts,
@ -1455,9 +1436,9 @@ awake(
clientid => ClientId, clientid => ClientId,
previous_state => ConnState previous_state => ConnState
}), }),
{ok, Publishes, Session1} = emqx_session:replay(ClientInfo, Session), {ok, Publishes, Session1} = emqx_mqttsn_session:replay(ClientInfo, Session),
{NPublishes, NSession} = {NPublishes, NSession} =
case emqx_session:deliver(ClientInfo, [], Session1) of case emqx_mqttsn_session:deliver(ClientInfo, [], Session1) of
{ok, Session2} -> {ok, Session2} ->
{Publishes, Session2}; {Publishes, Session2};
{ok, More, Session2} -> {ok, More, Session2} ->
@ -1485,8 +1466,8 @@ goto_asleep_if_buffered_msgs_sent(
} }
) -> ) ->
case case
emqx_mqueue:is_empty(emqx_session:info(mqueue, Session)) andalso emqx_mqueue:is_empty(emqx_mqttsn_session:info(mqueue, Session)) andalso
emqx_inflight:is_empty(emqx_session:info(inflight, Session)) emqx_inflight:is_empty(emqx_mqttsn_session:info(inflight, Session))
of of
true -> true ->
?SLOG(info, #{ ?SLOG(info, #{
@ -1579,7 +1560,7 @@ handle_out(
register_inflight = undefined register_inflight = undefined
} }
) -> ) ->
{MsgId, NSession} = emqx_session:obtain_next_pkt_id(Session), {MsgId, NSession} = emqx_mqttsn_session:obtain_next_pkt_id(Session),
Outgoing = {outgoing, ?SN_REGISTER_MSG(TopicId, MsgId, TopicName)}, Outgoing = {outgoing, ?SN_REGISTER_MSG(TopicId, MsgId, TopicName)},
NChannel = Channel#channel{ NChannel = Channel#channel{
session = NSession, session = NSession,
@ -1655,7 +1636,7 @@ maybe_resume_session(
resuming = true resuming = true
} }
) -> ) ->
Subs = emqx_session:info(subscriptions, Session), Subs = emqx_mqttsn_session:info(subscriptions, Session),
case subs_resume() andalso map_size(Subs) =/= 0 of case subs_resume() andalso map_size(Subs) =/= 0 of
true -> true ->
TopicNames = lists:filter(fun(T) -> not emqx_topic:wildcard(T) end, maps:keys(Subs)), TopicNames = lists:filter(fun(T) -> not emqx_topic:wildcard(T) end, maps:keys(Subs)),
@ -1680,9 +1661,9 @@ resume_or_replay_messages(
false -> false ->
{[], Channel} {[], Channel}
end, end,
{ok, Publishes, Session1} = emqx_session:replay(ClientInfo, Session), {ok, Publishes, Session1} = emqx_mqttsn_session:replay(ClientInfo, Session),
{NPublishes, NSession} = {NPublishes, NSession} =
case emqx_session:deliver(ClientInfo, NPendings, Session1) of case emqx_mqttsn_session:deliver(ClientInfo, NPendings, Session1) of
{ok, Session2} -> {ok, Session2} ->
{Publishes, Session2}; {Publishes, Session2};
{ok, More, Session2} -> {ok, More, Session2} ->
@ -1753,10 +1734,7 @@ outgoing_deliver_and_register({Packets, Channel}) ->
message_to_packet( message_to_packet(
MsgId, MsgId,
Message, Message,
#channel{ #channel{session = Session}
registry = Registry,
clientinfo = #{clientid := ClientId}
}
) -> ) ->
QoS = emqx_message:qos(Message), QoS = emqx_message:qos(Message),
Topic = emqx_message:topic(Message), Topic = emqx_message:topic(Message),
@ -1766,7 +1744,8 @@ message_to_packet(
?QOS_0 -> 0; ?QOS_0 -> 0;
_ -> MsgId _ -> MsgId
end, end,
case emqx_mqttsn_registry:lookup_topic_id(Registry, ClientId, Topic) of Registry = emqx_mqttsn_session:registry(Session),
case emqx_mqttsn_registry:lookup_topic_id(Topic, Registry) of
{predef, PredefTopicId} -> {predef, PredefTopicId} ->
Flags = #mqtt_sn_flags{qos = QoS, topic_id_type = ?SN_PREDEFINED_TOPIC}, Flags = #mqtt_sn_flags{qos = QoS, topic_id_type = ?SN_PREDEFINED_TOPIC},
?SN_PUBLISH_MSG(Flags, PredefTopicId, NMsgId, Payload); ?SN_PUBLISH_MSG(Flags, PredefTopicId, NMsgId, Payload);
@ -1801,7 +1780,7 @@ handle_call({unsubscribe, Topic}, _From, Channel) ->
{ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel),
reply_and_update(ok, NChannel); reply_and_update(ok, NChannel);
handle_call(subscriptions, _From, Channel = #channel{session = Session}) -> handle_call(subscriptions, _From, Channel = #channel{session = Session}) ->
reply({ok, maps:to_list(emqx_session:info(subscriptions, Session))}, Channel); reply({ok, maps:to_list(emqx_mqttsn_session:info(subscriptions, Session))}, Channel);
handle_call(kick, _From, Channel) -> handle_call(kick, _From, Channel) ->
NChannel = ensure_disconnected(kicked, Channel), NChannel = ensure_disconnected(kicked, Channel),
shutdown_and_reply(kicked, ok, NChannel); shutdown_and_reply(kicked, ok, NChannel);
@ -1822,7 +1801,7 @@ handle_call(
pendings = Pendings pendings = Pendings
} }
) -> ) ->
ok = emqx_session:takeover(Session), ok = emqx_mqttsn_session:takeover(Session),
%% TODO: Should not drain deliver here (side effect) %% TODO: Should not drain deliver here (side effect)
Delivers = emqx_utils:drain_deliver(), Delivers = emqx_utils:drain_deliver(),
AllPendings = lists:append(Delivers, Pendings), AllPendings = lists:append(Delivers, Pendings),
@ -1899,8 +1878,9 @@ handle_info(clean_authz_cache, Channel) ->
{ok, Channel}; {ok, Channel};
handle_info({subscribe, _}, Channel) -> handle_info({subscribe, _}, Channel) ->
{ok, Channel}; {ok, Channel};
handle_info({register, TopicName}, Channel) -> handle_info({register, TopicName}, Channel = #channel{session = Session}) ->
case ensure_registered_topic_name(TopicName, Channel) of Registry = emqx_mqttsn_session:registry(Session),
case emqx_mqttsn_registry:reg(TopicName, Registry) of
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "register_topic_failed", msg => "register_topic_failed",
@ -1908,8 +1888,9 @@ handle_info({register, TopicName}, Channel) ->
reason => Reason reason => Reason
}), }),
{ok, Channel}; {ok, Channel};
{ok, TopicId} -> {ok, TopicId, NRegistry} ->
handle_out(register, {TopicId, TopicName}, Channel) NSession = emqx_mqttsn_session:set_registry(NRegistry, Session),
handle_out(register, {TopicId, TopicName}, Channel#channel{session = NSession})
end; end;
handle_info(Info, Channel) -> handle_info(Info, Channel) ->
?SLOG(error, #{ ?SLOG(error, #{
@ -1928,21 +1909,6 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) ->
shutdown(Reason, Channel) shutdown(Reason, Channel)
end. end.
ensure_registered_topic_name(
TopicName,
Channel = #channel{registry = Registry}
) ->
ClientId = clientid(Channel),
case emqx_mqttsn_registry:lookup_topic_id(Registry, ClientId, TopicName) of
undefined ->
case emqx_mqttsn_registry:register_topic(Registry, ClientId, TopicName) of
{error, Reason} -> {error, Reason};
TopicId -> {ok, TopicId}
end;
TopicId ->
{ok, TopicId}
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Ensure disconnected %% Ensure disconnected
@ -1991,7 +1957,7 @@ handle_deliver(
ConnState =:= disconnected; ConnState =:= disconnected;
ConnState =:= asleep ConnState =:= asleep
-> ->
NSession = emqx_session:enqueue( NSession = emqx_mqttsn_session:enqueue(
ClientInfo, ClientInfo,
ignore_local(maybe_nack(Delivers), ClientId, Session, Ctx), ignore_local(maybe_nack(Delivers), ClientId, Session, Ctx),
Session Session
@ -2027,7 +1993,7 @@ handle_deliver(
} }
) -> ) ->
case case
emqx_session:deliver( emqx_mqttsn_session:deliver(
ClientInfo, ClientInfo,
ignore_local(Delivers, ClientId, Session, Ctx), ignore_local(Delivers, ClientId, Session, Ctx),
Session Session
@ -2045,7 +2011,7 @@ handle_deliver(
end. end.
ignore_local(Delivers, Subscriber, Session, Ctx) -> ignore_local(Delivers, Subscriber, Session, Ctx) ->
Subs = emqx_session:info(subscriptions, Session), Subs = emqx_mqttsn_session:info(subscriptions, Session),
lists:filter( lists:filter(
fun({deliver, Topic, #message{from = Publisher}}) -> fun({deliver, Topic, #message{from = Publisher}}) ->
case maps:find(Topic, Subs) of case maps:find(Topic, Subs) of
@ -2120,7 +2086,7 @@ handle_timeout(
retry_delivery, retry_delivery,
Channel = #channel{session = Session, clientinfo = ClientInfo} Channel = #channel{session = Session, clientinfo = ClientInfo}
) -> ) ->
case emqx_session:retry(ClientInfo, Session) of case emqx_mqttsn_session:retry(ClientInfo, Session) of
{ok, NSession} -> {ok, NSession} ->
{ok, clean_timer(retry_timer, Channel#channel{session = NSession})}; {ok, clean_timer(retry_timer, Channel#channel{session = NSession})};
{ok, Publishes, Timeout, NSession} -> {ok, Publishes, Timeout, NSession} ->
@ -2145,7 +2111,7 @@ handle_timeout(
expire_awaiting_rel, expire_awaiting_rel,
Channel = #channel{session = Session, clientinfo = ClientInfo} Channel = #channel{session = Session, clientinfo = ClientInfo}
) -> ) ->
case emqx_session:expire(ClientInfo, awaiting_rel, Session) of case emqx_mqttsn_session:expire(ClientInfo, awaiting_rel, Session) of
{ok, NSession} -> {ok, NSession} ->
{ok, clean_timer(await_timer, Channel#channel{session = NSession})}; {ok, clean_timer(await_timer, Channel#channel{session = NSession})};
{ok, Timeout, NSession} -> {ok, Timeout, NSession} ->
@ -2289,17 +2255,14 @@ clean_timer(Name, Channel = #channel{timers = Timers}) ->
interval(alive_timer, #channel{keepalive = KeepAlive}) -> interval(alive_timer, #channel{keepalive = KeepAlive}) ->
emqx_keepalive:info(interval, KeepAlive); emqx_keepalive:info(interval, KeepAlive);
interval(retry_timer, #channel{session = Session}) -> interval(retry_timer, #channel{session = Session}) ->
emqx_session:info(retry_interval, Session); emqx_mqttsn_session:info(retry_interval, Session);
interval(await_timer, #channel{session = Session}) -> interval(await_timer, #channel{session = Session}) ->
emqx_session:info(await_rel_timeout, Session). emqx_mqttsn_session:info(await_rel_timeout, Session).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Helper functions %% Helper functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
clientid(#channel{clientinfo = #{clientid := ClientId}}) ->
ClientId.
run_hooks(Ctx, Name, Args) -> run_hooks(Ctx, Name, Args) ->
emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_gateway_ctx:metrics_inc(Ctx, Name),
emqx_hooks:run(Name, Args). emqx_hooks:run(Name, Args).

View File

@ -17,64 +17,92 @@
%% @doc The MQTT-SN Topic Registry %% @doc The MQTT-SN Topic Registry
-module(emqx_mqttsn_registry). -module(emqx_mqttsn_registry).
-behaviour(gen_server).
-include("emqx_mqttsn.hrl"). -include("emqx_mqttsn.hrl").
-include_lib("emqx/include/logger.hrl").
-export([start_link/2]).
-export([ -export([
register_topic/3, persist_predefined_topics/1,
unregister_topic/2 clear_predefined_topics/1
]). ]).
-export([ -export([
lookup_topic/3, init/0,
lookup_topic_id/3 reg/2,
unreg/2,
lookup_topic/2,
lookup_topic_id/2
]). ]).
%% gen_server callbacks -define(PKEY(Id), {mqttsn, predef_topics, Id}).
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
%% Internal exports (RPC) -type registry() :: #{
-export([ %% The last topic id aallocated
do_register/4 last_topic_id := pos_integer(),
]). %% The mapping from topic id to topic name
id_to_name := map(),
%% The mapping from topic name to topic id
name_to_id := map()
}.
-export([lookup_name/1]). -type predef_topic() :: #{
id := 1..1024,
-define(SN_SHARD, emqx_mqttsn_shard). topic := iolist()
}.
-record(state, {tabname, max_predef_topic_id = 0}).
-record(emqx_mqttsn_registry, {key, value}).
-type registry() :: {Tab :: atom(), RegistryPid :: pid()}.
%%----------------------------------------------------------------------------- %%-----------------------------------------------------------------------------
%% APIs
-spec start_link(atom(), list()) -> -spec persist_predefined_topics([predef_topic()]) -> ok.
ignore persist_predefined_topics(PredefTopics) when is_list(PredefTopics) ->
| {ok, pid()} try
| {error, Reason :: term()}. F = fun(#{id := TopicId, topic := TopicName0}) when TopicId =< 1024 ->
start_link(InstaId, PredefTopics) -> TopicName = iolist_to_binary(TopicName0),
gen_server:start_link(?MODULE, [InstaId, PredefTopics], []). persistent_term:put(?PKEY(TopicId), TopicName),
persistent_term:put(?PKEY(TopicName), TopicId)
end,
lists:foreach(F, PredefTopics)
catch
_:_ ->
clear_predefined_topics(PredefTopics),
error(badarg)
end.
-spec register_topic(registry(), emqx_types:clientid(), emqx_types:topic()) -> -spec clear_predefined_topics([predef_topic()]) -> ok.
integer() clear_predefined_topics(PredefTopics) ->
lists:foreach(
fun(#{id := TopicId, topic := TopicName0}) ->
TopicName = iolist_to_binary(TopicName0),
persistent_term:erase(?PKEY(TopicId)),
persistent_term:erase(?PKEY(TopicName))
end,
PredefTopics
),
ok.
-spec init() -> registry().
init() ->
#{
last_topic_id => ?SN_MAX_PREDEF_TOPIC_ID,
id_to_name => #{},
name_to_id => #{}
}.
-spec reg(emqx_types:topic(), registry()) ->
{ok, integer(), registry()}
| {error, term()}. | {error, term()}.
register_topic({_, Pid}, ClientId, TopicName) when is_binary(TopicName) -> reg(
TopicName,
Registry
) when is_binary(TopicName) ->
case emqx_topic:wildcard(TopicName) of case emqx_topic:wildcard(TopicName) of
false -> false ->
gen_server:call(Pid, {register, ClientId, TopicName}); case lookup_topic_id(TopicName, Registry) of
{predef, TopicId} when is_integer(TopicId) ->
{ok, TopicId, Registry};
TopicId when is_integer(TopicId) ->
{ok, TopicId, Registry};
undefined ->
do_reg(TopicName, Registry)
end;
%% TopicId: in case of accepted the value that will be used as topic %% TopicId: in case of accepted the value that will be used as topic
%% id by the gateway when sending PUBLISH messages to the client (not %% id by the gateway when sending PUBLISH messages to the client (not
%% relevant in case of subscriptions to a short topic name or to a topic %% relevant in case of subscriptions to a short topic name or to a topic
@ -83,182 +111,64 @@ register_topic({_, Pid}, ClientId, TopicName) when is_binary(TopicName) ->
{error, wildcard_topic} {error, wildcard_topic}
end. end.
-spec lookup_topic(registry(), emqx_types:clientid(), pos_integer()) -> do_reg(
TopicName,
Registry = #{
last_topic_id := TopicId0,
id_to_name := IdMap,
name_to_id := NameMap
}
) ->
case next_topic_id(TopicId0) of
{error, too_large} ->
{error, too_large};
NextTopicId ->
NRegistry = Registry#{
last_topic_id := NextTopicId,
id_to_name := maps:put(NextTopicId, TopicName, IdMap),
name_to_id := maps:put(TopicName, NextTopicId, NameMap)
},
{ok, NextTopicId, NRegistry}
end.
next_topic_id(Id) when is_integer(Id) andalso (Id < 16#FFFF) ->
Id + 1;
next_topic_id(Id) when is_integer(Id) ->
{error, too_large}.
-spec lookup_topic(pos_integer(), registry()) ->
undefined undefined
| binary(). | binary().
lookup_topic({Tab, _}, ClientId, TopicId) when is_integer(TopicId) -> lookup_topic(TopicId, _Registry = #{id_to_name := IdMap}) when is_integer(TopicId) ->
case lookup_element(Tab, {predef, TopicId}, 3) of case persistent_term:get(?PKEY(TopicId), undefined) of
undefined -> undefined ->
lookup_element(Tab, {ClientId, TopicId}, 3); maps:get(TopicId, IdMap, undefined);
Topic -> Topic ->
Topic Topic
end. end.
-spec lookup_topic_id(registry(), emqx_types:clientid(), emqx_types:topic()) -> -spec lookup_topic_id(emqx_types:topic(), registry()) ->
undefined undefined
| pos_integer() | pos_integer()
| {predef, integer()}. | {predef, integer()}.
lookup_topic_id({Tab, _}, ClientId, TopicName) when is_binary(TopicName) -> lookup_topic_id(TopicName, _Registry = #{name_to_id := NameMap}) when is_binary(TopicName) ->
case lookup_element(Tab, {predef, TopicName}, 3) of case persistent_term:get(?PKEY(TopicName), undefined) of
undefined -> undefined ->
lookup_element(Tab, {ClientId, TopicName}, 3); maps:get(TopicName, NameMap, undefined);
TopicId -> TopicId ->
{predef, TopicId} {predef, TopicId}
end. end.
%% @private -spec unreg(emqx_types:topic(), registry()) -> registry().
lookup_element(Tab, Key, Pos) -> unreg(TopicName, Registry = #{name_to_id := NameMap, id_to_name := IdMap}) when
try is_binary(TopicName)
ets:lookup_element(Tab, Key, Pos) ->
catch case maps:find(TopicName, NameMap) of
error:badarg -> undefined {ok, TopicId} ->
end. Registry#{
name_to_id := maps:remove(TopicName, NameMap),
-spec unregister_topic(registry(), emqx_types:clientid()) -> ok. id_to_name := maps:remove(TopicId, IdMap)
unregister_topic({_, Pid}, ClientId) -> };
gen_server:call(Pid, {unregister, ClientId}). error ->
lookup_name(Pid) ->
gen_server:call(Pid, name).
%%-----------------------------------------------------------------------------
name(InstaId) ->
list_to_atom(lists:concat([emqx_mqttsn_, InstaId, '_registry'])).
init([InstaId, PredefTopics]) ->
%% {predef, TopicId} -> TopicName
%% {predef, TopicName} -> TopicId
%% {ClientId, TopicId} -> TopicName
%% {ClientId, TopicName} -> TopicId
Tab = name(InstaId),
ok = mria:create_table(Tab, [
{storage, ram_copies},
{record_name, emqx_mqttsn_registry},
{attributes, record_info(fields, emqx_mqttsn_registry)},
{storage_properties, [{ets, [{read_concurrency, true}]}]},
{rlog_shard, ?SN_SHARD}
]),
ok = mria:wait_for_tables([Tab]),
MaxPredefId = lists:foldl(
fun(#{id := TopicId, topic := TopicName0}, AccId) ->
TopicName = iolist_to_binary(TopicName0),
mria:dirty_write(Tab, #emqx_mqttsn_registry{
key = {predef, TopicId},
value = TopicName
}),
mria:dirty_write(Tab, #emqx_mqttsn_registry{
key = {predef, TopicName},
value = TopicId
}),
case TopicId > AccId of
true -> TopicId;
false -> AccId
end
end,
0,
PredefTopics
),
{ok, #state{tabname = Tab, max_predef_topic_id = MaxPredefId}}.
handle_call(
{register, ClientId, TopicName},
_From,
State = #state{tabname = Tab, max_predef_topic_id = PredefId}
) ->
case lookup_topic_id({Tab, self()}, ClientId, TopicName) of
{predef, PredefTopicId} when is_integer(PredefTopicId) ->
{reply, PredefTopicId, State};
TopicId when is_integer(TopicId) ->
{reply, TopicId, State};
undefined ->
case next_topic_id(Tab, PredefId, ClientId) of
TopicId when TopicId >= 16#FFFF ->
{reply, {error, too_large}, State};
TopicId ->
case
mria:transaction(?SN_SHARD, fun ?MODULE:do_register/4, [
Tab, ClientId, TopicId, TopicName
])
of
{atomic, ok} ->
{reply, TopicId, State};
{aborted, Error} ->
{reply, {error, Error}, State}
end
end
end;
handle_call({unregister, ClientId}, _From, State = #state{tabname = Tab}) ->
Registry = mnesia:dirty_match_object(
Tab,
{emqx_mqttsn_registry, {ClientId, '_'}, '_'}
),
lists:foreach(
fun(R) ->
mria:dirty_delete_object(Tab, R)
end,
Registry Registry
),
{reply, ok, State};
handle_call(name, _From, State = #state{tabname = Tab}) ->
{reply, {Tab, self()}, State};
handle_call(Req, _From, State) ->
?SLOG(error, #{
msg => "unexpected_call",
call => Req
}),
{reply, ignored, State}.
handle_cast(Msg, State) ->
?SLOG(error, #{
msg => "unexpected_cast",
cast => Msg
}),
{noreply, State}.
handle_info(Info, State) ->
?SLOG(error, #{
msg => "unexpected_info",
info => Info
}),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
do_register(Tab, ClientId, TopicId, TopicName) ->
mnesia:write(
Tab,
#emqx_mqttsn_registry{
key = {ClientId, next_topic_id},
value = TopicId + 1
},
write
),
mnesia:write(
Tab,
#emqx_mqttsn_registry{
key = {ClientId, TopicName},
value = TopicId
},
write
),
mnesia:write(
Tab,
#emqx_mqttsn_registry{
key = {ClientId, TopicId},
value = TopicName
},
write
).
%%-----------------------------------------------------------------------------
next_topic_id(Tab, PredefId, ClientId) ->
case mnesia:dirty_read(Tab, {ClientId, next_topic_id}) of
[#emqx_mqttsn_registry{value = Id}] -> Id;
[] -> PredefId + 1
end. end.

View File

@ -16,6 +16,7 @@
-module(emqx_mqttsn_schema). -module(emqx_mqttsn_schema).
-include("emqx_mqttsn.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
@ -41,7 +42,6 @@ fields(mqttsn) ->
desc => ?DESC(mqttsn_broadcast) desc => ?DESC(mqttsn_broadcast)
} }
)}, )},
%% TODO: rename
{enable_qos3, {enable_qos3,
sc( sc(
boolean(), boolean(),
@ -73,7 +73,7 @@ fields(mqttsn) ->
fields(mqttsn_predefined) -> fields(mqttsn_predefined) ->
[ [
{id, {id,
sc(integer(), #{ sc(range(1, ?SN_MAX_PREDEF_TOPIC_ID), #{
required => true, required => true,
desc => ?DESC(mqttsn_predefined_id) desc => ?DESC(mqttsn_predefined_id)
})}, })},

View File

@ -0,0 +1,144 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mqttsn_session).
-export([registry/1, set_registry/2]).
-export([
init/1,
info/1,
info/2,
stats/1,
resume/2
]).
-export([
publish/4,
subscribe/4,
unsubscribe/4,
puback/3,
pubrec/3,
pubrel/3,
pubcomp/3
]).
-export([
replay/2,
deliver/3,
obtain_next_pkt_id/1,
takeover/1,
enqueue/3,
retry/2,
expire/3
]).
-type session() :: #{
registry := emqx_mqttsn_registry:registry(),
session := emqx_session:session()
}.
-export_type([session/0]).
init(ClientInfo) ->
Conf = emqx_cm:get_session_confs(
ClientInfo, #{receive_maximum => 1, expiry_interval => 0}
),
#{
registry => emqx_mqttsn_registry:init(),
session => emqx_session:init(Conf)
}.
registry(#{registry := Registry}) ->
Registry.
set_registry(Registry, Session) ->
Session#{registry := Registry}.
info(#{session := Session}) ->
emqx_session:info(Session).
info(Key, #{session := Session}) ->
emqx_session:info(Key, Session).
stats(#{session := Session}) ->
emqx_session:stats(Session).
puback(ClientInfo, MsgId, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, MsgId], Session).
pubrec(ClientInfo, MsgId, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, MsgId], Session).
pubrel(ClientInfo, MsgId, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, MsgId], Session).
pubcomp(ClientInfo, MsgId, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, MsgId], Session).
publish(ClientInfo, MsgId, Msg, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, MsgId, Msg], Session).
subscribe(ClientInfo, Topic, SubOpts, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, Topic, SubOpts], Session).
unsubscribe(ClientInfo, Topic, SubOpts, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, Topic, SubOpts], Session).
replay(ClientInfo, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo], Session).
deliver(ClientInfo, Delivers, Session1) ->
with_sess(?FUNCTION_NAME, [ClientInfo, Delivers], Session1).
obtain_next_pkt_id(Session = #{session := Sess}) ->
{Id, Sess1} = emqx_session:obtain_next_pkt_id(Sess),
{Id, Session#{session := Sess1}}.
takeover(_Session = #{session := Sess}) ->
emqx_session:takeover(Sess).
enqueue(ClientInfo, Delivers, Session = #{session := Sess}) ->
Sess1 = emqx_session:enqueue(ClientInfo, Delivers, Sess),
Session#{session := Sess1}.
retry(ClientInfo, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo], Session).
expire(ClientInfo, awaiting_rel, Session) ->
with_sess(?FUNCTION_NAME, [ClientInfo, awaiting_rel], Session).
resume(ClientInfo, #{session := Sess}) ->
emqx_session:resume(ClientInfo, Sess).
%%--------------------------------------------------------------------
%% internal funcs
with_sess(Fun, Args, Session = #{session := Sess}) ->
case apply(emqx_session, Fun, Args ++ [Sess]) of
%% for subscribe
{error, Reason} ->
{error, Reason};
%% for pubrel
{ok, Sess1} ->
{ok, Session#{session := Sess1}};
%% for publish and puback
{ok, Result, Sess1} ->
{ok, Result, Session#{session := Sess1}};
%% for puback
{ok, Msgs, Replies, Sess1} ->
{ok, Msgs, Replies, Session#{session := Sess1}}
end.

View File

@ -35,6 +35,8 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(HOST, {127, 0, 0, 1}). -define(HOST, {127, 0, 0, 1}).
-define(PORT, 1884). -define(PORT, 1884).
@ -45,12 +47,15 @@
-define(LOG(Format, Args), ct:log("TEST: " ++ Format, Args)). -define(LOG(Format, Args), ct:log("TEST: " ++ Format, Args)).
-define(MAX_PRED_TOPIC_ID, 2). -define(MAX_PRED_TOPIC_ID, ?SN_MAX_PREDEF_TOPIC_ID).
-define(PREDEF_TOPIC_ID1, 1). -define(PREDEF_TOPIC_ID1, 1).
-define(PREDEF_TOPIC_ID2, 2). -define(PREDEF_TOPIC_ID2, 2).
-define(PREDEF_TOPIC_NAME1, <<"/predefined/topic/name/hello">>). -define(PREDEF_TOPIC_NAME1, <<"/predefined/topic/name/hello">>).
-define(PREDEF_TOPIC_NAME2, <<"/predefined/topic/name/nice">>). -define(PREDEF_TOPIC_NAME2, <<"/predefined/topic/name/nice">>).
-define(ENABLE_QOS3, true). -define(DEFAULT_PREDEFINED_TOPICS, [
#{<<"id">> => ?PREDEF_TOPIC_ID1, <<"topic">> => ?PREDEF_TOPIC_NAME1},
#{<<"id">> => ?PREDEF_TOPIC_ID2, <<"topic">> => ?PREDEF_TOPIC_NAME2}
]).
% FLAG NOT USED % FLAG NOT USED
-define(FNU, 0). -define(FNU, 0).
@ -120,6 +125,20 @@ restart_mqttsn_with_subs_resume_off() ->
Conf#{<<"subs_resume">> => <<"false">>} Conf#{<<"subs_resume">> => <<"false">>}
). ).
restart_mqttsn_with_neg_qos_on() ->
Conf = emqx:get_raw_config([gateway, mqttsn]),
emqx_gateway_conf:update_gateway(
mqttsn,
Conf#{<<"enable_qos3">> => <<"true">>}
).
restart_mqttsn_with_neg_qos_off() ->
Conf = emqx:get_raw_config([gateway, mqttsn]),
emqx_gateway_conf:update_gateway(
mqttsn,
Conf#{<<"enable_qos3">> => <<"false">>}
).
restart_mqttsn_with_mountpoint(Mp) -> restart_mqttsn_with_mountpoint(Mp) ->
Conf = emqx:get_raw_config([gateway, mqttsn]), Conf = emqx:get_raw_config([gateway, mqttsn]),
emqx_gateway_conf:update_gateway( emqx_gateway_conf:update_gateway(
@ -127,6 +146,13 @@ restart_mqttsn_with_mountpoint(Mp) ->
Conf#{<<"mountpoint">> => Mp} Conf#{<<"mountpoint">> => Mp}
). ).
restart_mqttsn_with_predefined_topics(Topics) ->
Conf = emqx:get_raw_config([gateway, mqttsn]),
emqx_gateway_conf:update_gateway(
mqttsn,
Conf#{<<"predefined">> => Topics}
).
default_config() -> default_config() ->
?CONF_DEFAULT. ?CONF_DEFAULT.
@ -471,7 +497,36 @@ t_subscribe_case08(_) ->
?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)), ?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)),
gen_udp:close(Socket). gen_udp:close(Socket).
t_publish_negqos_case09(_) -> t_subscribe_predefined_topic(_) ->
Dup = 0,
QoS = 0,
Retain = 0,
Will = 0,
CleanSession = 0,
MsgId = 1,
Socket = ensure_connected_client(?CLIENTID),
send_subscribe_msg_predefined_topic(Socket, 0, ?PREDEF_TOPIC_ID1, 1),
?assertEqual(
<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, ?SN_NORMAL_TOPIC:2,
?PREDEF_TOPIC_ID1:16, MsgId:16, ?SN_RC_ACCEPTED>>,
receive_response(Socket)
),
send_disconnect_msg(Socket, undefined),
gen_udp:close(Socket),
restart_mqttsn_with_predefined_topics([]),
Socket1 = ensure_connected_client(?CLIENTID),
send_subscribe_msg_predefined_topic(Socket1, 0, ?PREDEF_TOPIC_ID1, 1),
?assertEqual(
<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, ?SN_NORMAL_TOPIC:2, 0:16,
MsgId:16, ?SN_RC_INVALID_TOPIC_ID>>,
receive_response(Socket1)
),
send_disconnect_msg(Socket1, undefined),
restart_mqttsn_with_predefined_topics(?DEFAULT_PREDEFINED_TOPICS),
gen_udp:close(Socket1).
t_publish_negqos_enabled(_) ->
Dup = 0, Dup = 0,
QoS = 0, QoS = 0,
NegQoS = 3, NegQoS = 3,
@ -497,19 +552,40 @@ t_publish_negqos_case09(_) ->
Payload1 = <<20, 21, 22, 23>>, Payload1 = <<20, 21, 22, 23>>,
send_publish_msg_normal_topic(Socket, NegQoS, MsgId1, TopicId1, Payload1), send_publish_msg_normal_topic(Socket, NegQoS, MsgId1, TopicId1, Payload1),
timer:sleep(100), timer:sleep(100),
case ?ENABLE_QOS3 of
true ->
Eexp = Eexp =
<<11, ?SN_PUBLISH, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, <<11, ?SN_PUBLISH, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, ?SN_NORMAL_TOPIC:2,
?SN_NORMAL_TOPIC:2, TopicId1:16, (mid(0)):16, <<20, 21, 22, 23>>/binary>>, TopicId1:16, (mid(0)):16, <<20, 21, 22, 23>>/binary>>,
What = receive_response(Socket), What = receive_response(Socket),
?assertEqual(Eexp, What) ?assertEqual(Eexp, What),
end,
send_disconnect_msg(Socket, undefined), send_disconnect_msg(Socket, undefined),
?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)), ?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)),
gen_udp:close(Socket). gen_udp:close(Socket).
t_publish_negqos_disabled(_) ->
restart_mqttsn_with_neg_qos_off(),
NegQoS = 3,
MsgId = 1,
Payload = <<"abc">>,
TopicId = ?MAX_PRED_TOPIC_ID,
{ok, Socket} = gen_udp:open(0, [binary]),
?check_trace(
begin
send_publish_msg_predefined_topic(Socket, NegQoS, MsgId, TopicId, Payload),
?assertEqual(
<<7, ?SN_PUBACK, TopicId:16, MsgId:16, ?SN_RC_NOT_SUPPORTED>>,
receive_response(Socket)
),
receive_response(Socket)
end,
fun(Trace0) ->
Trace = ?of_kind(ignore_negative_qos, Trace0),
?assertMatch([#{return_code := ?SN_RC_NOT_SUPPORTED}], Trace)
end
),
restart_mqttsn_with_neg_qos_on(),
gen_udp:close(Socket).
t_publish_qos0_case01(_) -> t_publish_qos0_case01(_) ->
Dup = 0, Dup = 0,
QoS = 0, QoS = 0,
@ -2737,3 +2813,9 @@ flush(Msgs) ->
M -> flush([M | Msgs]) M -> flush([M | Msgs])
after 0 -> lists:reverse(Msgs) after 0 -> lists:reverse(Msgs)
end. end.
ensure_connected_client(ClientId) ->
{ok, Socket} = gen_udp:open(0, [binary]),
send_connect_msg(Socket, ClientId),
?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)),
Socket.

View File

@ -19,10 +19,11 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_mqttsn.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(REGISTRY, emqx_mqttsn_registry). -define(REGISTRY, emqx_mqttsn_registry).
-define(MAX_PREDEF_ID, 2). -define(MAX_PREDEF_ID, ?SN_MAX_PREDEF_TOPIC_ID).
-define(PREDEF_TOPICS, [ -define(PREDEF_TOPICS, [
#{id => 1, topic => <<"/predefined/topic/name/hello">>}, #{id => 1, topic => <<"/predefined/topic/name/hello">>},
#{id => 2, topic => <<"/predefined/topic/name/nice">>} #{id => 2, topic => <<"/predefined/topic/name/nice">>}
@ -36,96 +37,64 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
application:ensure_all_started(ekka),
mria:start(),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
application:stop(ekka),
ok. ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
{ok, Pid} = ?REGISTRY:start_link('mqttsn', ?PREDEF_TOPICS), emqx_mqttsn_registry:persist_predefined_topics(?PREDEF_TOPICS),
{Tab, Pid} = ?REGISTRY:lookup_name(Pid), Config.
[{reg, {Tab, Pid}} | Config].
end_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, Config) ->
{Tab, _Pid} = proplists:get_value(reg, Config), emqx_mqttsn_registry:clear_predefined_topics(?PREDEF_TOPICS),
mria:clear_table(Tab),
Config. Config.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Test cases %% Test cases
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
t_register(Config) -> t_register(_) ->
Reg = proplists:get_value(reg, Config), Reg = ?REGISTRY:init(),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), {ok, ?MAX_PREDEF_ID + 1, Reg1} = ?REGISTRY:reg(<<"Topic1">>, Reg),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic2">>)), {ok, ?MAX_PREDEF_ID + 2, Reg2} = ?REGISTRY:reg(<<"Topic2">>, Reg1),
?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 1)), ?assertMatch({ok, ?MAX_PREDEF_ID + 1, Reg2}, ?REGISTRY:reg(<<"Topic1">>, Reg2)),
?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 2)), ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(?MAX_PREDEF_ID + 1, Reg2)),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)), ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(?MAX_PREDEF_ID + 2, Reg2)),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)), ?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:lookup_topic_id(<<"Topic1">>, Reg2)),
emqx_mqttsn_registry:unregister_topic(Reg, <<"ClientId">>), ?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(<<"Topic2">>, Reg2)),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 1)),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 2)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)).
t_register_case2(Config) -> Reg3 = emqx_mqttsn_registry:unreg(<<"Topic1">>, Reg2),
Reg = proplists:get_value(reg, Config), ?assertEqual(undefined, ?REGISTRY:lookup_topic(?MAX_PREDEF_ID + 1, Reg3)),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"Topic1">>, Reg3)),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic2">>)), ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(?MAX_PREDEF_ID + 2, Reg3)),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), ?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(<<"Topic2">>, Reg3)),
?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 1)),
?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 2)),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic3">>)),
?REGISTRY:unregister_topic(Reg, <<"ClientId">>),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 1)),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 2)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)).
t_reach_maximum(Config) -> ?assertMatch({ok, ?MAX_PREDEF_ID + 3, _Reg4}, ?REGISTRY:reg(<<"Topic3">>, Reg3)).
Reg = proplists:get_value(reg, Config),
register_a_lot(?MAX_PREDEF_ID + 1, 16#ffff, Reg),
?assertEqual({error, too_large}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicABC">>)),
Topic1 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID + 1])),
Topic2 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID + 2])),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic1)),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic2)),
?REGISTRY:unregister_topic(Reg, <<"ClientId">>),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 1)),
?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID + 2)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic1)),
?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic2)).
t_register_case4(Config) -> t_reach_maximum(_) ->
Reg = proplists:get_value(reg, Config), Reg0 = ?REGISTRY:init(),
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicA">>)), Reg = register_a_lot(?MAX_PREDEF_ID + 1, 16#ffff, Reg0),
?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicB">>)), ?assertEqual({error, too_large}, ?REGISTRY:reg(<<"TopicABC">>, Reg)),
?assertEqual(?MAX_PREDEF_ID + 3, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicC">>)), ?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:lookup_topic_id(<<"Topic1025">>, Reg)),
?REGISTRY:unregister_topic(Reg, <<"ClientId">>), ?assertEqual(?MAX_PREDEF_ID + 2, ?REGISTRY:lookup_topic_id(<<"Topic1026">>, Reg)).
?assertEqual(?MAX_PREDEF_ID + 1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicD">>)).
t_deny_wildcard_topic(Config) -> t_deny_wildcard_topic(_) ->
Reg = proplists:get_value(reg, Config), Reg = ?REGISTRY:init(),
?assertEqual( ?assertEqual({error, wildcard_topic}, ?REGISTRY:reg(<<"/TopicA/#">>, Reg)),
{error, wildcard_topic}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"/TopicA/#">>) ?assertEqual({error, wildcard_topic}, ?REGISTRY:reg(<<"/+/TopicB">>, Reg)).
),
?assertEqual(
{error, wildcard_topic}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"/+/TopicB">>)
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Helper funcs %% Helper funcs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
register_a_lot(Max, Max, _Reg) -> register_a_lot(N, Max, Reg) when N =< Max ->
ok;
register_a_lot(N, Max, Reg) when N < Max ->
Topic = iolist_to_binary(["Topic", integer_to_list(N)]), Topic = iolist_to_binary(["Topic", integer_to_list(N)]),
?assertEqual(N, ?REGISTRY:register_topic(Reg, <<"ClientId">>, Topic)), {ok, ReturnedId, Reg1} = ?REGISTRY:reg(Topic, Reg),
register_a_lot(N + 1, Max, Reg). ?assertEqual(N, ReturnedId),
case N == Max of
true ->
Reg1;
_ ->
register_a_lot(N + 1, Max, Reg1)
end.

View File

@ -183,7 +183,7 @@ delete(Keys, Fields) ->
lists:foldl(fun(Key, Acc) -> lists:keydelete(Key, 1, Acc) end, Fields, Keys). lists:foldl(fun(Key, Acc) -> lists:keydelete(Key, 1, Acc) end, Fields, Keys).
api_key(get, _) -> api_key(get, _) ->
{200, [format(App) || App <- emqx_mgmt_auth:list()]}; {200, [emqx_mgmt_auth:format(App) || App <- emqx_mgmt_auth:list()]};
api_key(post, #{body := App}) -> api_key(post, #{body := App}) ->
#{ #{
<<"name">> := Name, <<"name">> := Name,
@ -194,7 +194,7 @@ api_key(post, #{body := App}) ->
Desc = unicode:characters_to_binary(Desc0, unicode), Desc = unicode:characters_to_binary(Desc0, unicode),
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
{ok, NewApp} -> {ok, NewApp} ->
{200, format(NewApp)}; {200, emqx_mgmt_auth:format(NewApp)};
{error, Reason} -> {error, Reason} ->
{400, #{ {400, #{
code => 'BAD_REQUEST', code => 'BAD_REQUEST',
@ -206,7 +206,7 @@ api_key(post, #{body := App}) ->
api_key_by_name(get, #{bindings := #{name := Name}}) -> api_key_by_name(get, #{bindings := #{name := Name}}) ->
case emqx_mgmt_auth:read(Name) of case emqx_mgmt_auth:read(Name) of
{ok, App} -> {200, format(App)}; {ok, App} -> {200, emqx_mgmt_auth:format(App)};
{error, not_found} -> {404, ?NOT_FOUND_RESPONSE} {error, not_found} -> {404, ?NOT_FOUND_RESPONSE}
end; end;
api_key_by_name(delete, #{bindings := #{name := Name}}) -> api_key_by_name(delete, #{bindings := #{name := Name}}) ->
@ -219,20 +219,9 @@ api_key_by_name(put, #{bindings := #{name := Name}, body := Body}) ->
ExpiredAt = ensure_expired_at(Body), ExpiredAt = ensure_expired_at(Body),
Desc = maps:get(<<"desc">>, Body, undefined), Desc = maps:get(<<"desc">>, Body, undefined),
case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc) of case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc) of
{ok, App} -> {200, format(App)}; {ok, App} -> {200, emqx_mgmt_auth:format(App)};
{error, not_found} -> {404, ?NOT_FOUND_RESPONSE} {error, not_found} -> {404, ?NOT_FOUND_RESPONSE}
end. end.
format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
ExpiredAt =
case ExpiredAt0 of
infinity -> <<"infinity">>;
_ -> list_to_binary(calendar:system_time_to_rfc3339(ExpiredAt0))
end,
App#{
expired_at => ExpiredAt,
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
}.
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt; ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
ensure_expired_at(_) -> infinity. ensure_expired_at(_) -> infinity.

View File

@ -19,7 +19,6 @@
-behaviour(minirest_api). -behaviour(minirest_api).
-export([namespace/0, api_spec/0, paths/0, schema/1, fields/1]). -export([namespace/0, api_spec/0, paths/0, schema/1, fields/1]).
-import(emqx_dashboard_swagger, [error_codes/2, error_codes/1]).
-export([ -export([
listener_type_status/2, listener_type_status/2,
@ -36,6 +35,16 @@
do_list_listeners/0 do_list_listeners/0
]). ]).
-import(emqx_dashboard_swagger, [error_codes/2, error_codes/1]).
-import(emqx_mgmt_listeners_conf, [
action/4,
create/3,
ensure_remove/2,
get_raw/2,
update/3
]).
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
@ -44,7 +53,6 @@
-define(LISTENER_NOT_FOUND, <<"Listener id not found">>). -define(LISTENER_NOT_FOUND, <<"Listener id not found">>).
-define(LISTENER_ID_INCONSISTENT, <<"Path and body's listener id not match">>). -define(LISTENER_ID_INCONSISTENT, <<"Path and body's listener id not match">>).
-define(ADDR_PORT_INUSE, <<"Addr port in use">>). -define(ADDR_PORT_INUSE, <<"Addr port in use">>).
-define(OPTS(_OverrideTo_), #{rawconf_with_defaults => true, override_to => _OverrideTo_}).
namespace() -> "listeners". namespace() -> "listeners".
@ -387,14 +395,13 @@ crud_listeners_by_id(get, #{bindings := #{id := Id0}}) ->
crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) -> crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
case parse_listener_conf(Body0) of case parse_listener_conf(Body0) of
{Id, Type, Name, Conf} -> {Id, Type, Name, Conf} ->
Path = [listeners, Type, Name], case get_raw(Type, Name) of
case emqx_conf:get_raw(Path, undefined) of
undefined -> undefined ->
{404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}}; {404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
PrevConf -> PrevConf ->
MergeConfT = emqx_utils_maps:deep_merge(PrevConf, Conf), MergeConfT = emqx_utils_maps:deep_merge(PrevConf, Conf),
MergeConf = emqx_listeners:ensure_override_limiter_conf(MergeConfT, Conf), MergeConf = emqx_listeners:ensure_override_limiter_conf(MergeConfT, Conf),
case update(Path, MergeConf) of case update(Type, Name, MergeConf) of
{ok, #{raw_config := _RawConf}} -> {ok, #{raw_config := _RawConf}} ->
crud_listeners_by_id(get, #{bindings => #{id => Id}}); crud_listeners_by_id(get, #{bindings => #{id => Id}});
{error, not_found} -> {error, not_found} ->
@ -412,7 +419,7 @@ crud_listeners_by_id(post, #{body := Body}) ->
create_listener(Body); create_listener(Body);
crud_listeners_by_id(delete, #{bindings := #{id := Id}}) -> crud_listeners_by_id(delete, #{bindings := #{id := Id}}) ->
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id), {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id),
case ensure_remove([listeners, Type, Name]) of case ensure_remove(Type, Name) of
{ok, _} -> {204}; {ok, _} -> {204};
{error, Reason} -> {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}} {error, Reason} -> {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
end. end.
@ -457,12 +464,11 @@ restart_listeners_by_id(Method, Body = #{bindings := Bindings}) ->
action_listeners_by_id(post, #{bindings := #{id := Id, action := Action}}) -> action_listeners_by_id(post, #{bindings := #{id := Id, action := Action}}) ->
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id), {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id),
Path = [listeners, Type, Name], case get_raw(Type, Name) of
case emqx_conf:get_raw(Path, undefined) of
undefined -> undefined ->
{404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}}; {404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
_PrevConf -> _PrevConf ->
case action(Path, Action, enabled(Action)) of case action(Type, Name, Action, enabled(Action)) of
{ok, #{raw_config := _RawConf}} -> {ok, #{raw_config := _RawConf}} ->
{200}; {200};
{error, not_found} -> {error, not_found} ->
@ -634,23 +640,6 @@ max_conn(_Int1, <<"infinity">>) -> <<"infinity">>;
max_conn(<<"infinity">>, _Int) -> <<"infinity">>; max_conn(<<"infinity">>, _Int) -> <<"infinity">>;
max_conn(Int1, Int2) -> Int1 + Int2. max_conn(Int1, Int2) -> Int1 + Int2.
update(Path, Conf) ->
wrap(emqx_conf:update(Path, {update, Conf}, ?OPTS(cluster))).
action(Path, Action, Conf) ->
wrap(emqx_conf:update(Path, {action, Action, Conf}, ?OPTS(cluster))).
create(Path, Conf) ->
wrap(emqx_conf:update(Path, {create, Conf}, ?OPTS(cluster))).
ensure_remove(Path) ->
wrap(emqx_conf:tombstone(Path, ?OPTS(cluster))).
wrap({error, {post_config_update, emqx_listeners, Reason}}) -> {error, Reason};
wrap({error, {pre_config_update, emqx_listeners, Reason}}) -> {error, Reason};
wrap({error, Reason}) -> {error, Reason};
wrap(Ok) -> Ok.
listener_type_status_example() -> listener_type_status_example() ->
[ [
#{ #{
@ -813,8 +802,7 @@ tcp_schema_example() ->
create_listener(Body) -> create_listener(Body) ->
case parse_listener_conf(Body) of case parse_listener_conf(Body) of
{Id, Type, Name, Conf} -> {Id, Type, Name, Conf} ->
Path = [listeners, Type, Name], case create(Type, Name, Conf) of
case create(Path, Conf) of
{ok, #{raw_config := _RawConf}} -> {ok, #{raw_config := _RawConf}} ->
crud_listeners_by_id(get, #{bindings => #{id => Id}}); crud_listeners_by_id(get, #{bindings => #{id => Id}});
{error, already_exist} -> {error, already_exist} ->

View File

@ -206,7 +206,7 @@ fields(log_file_detail) ->
fields(node) ++ fields(node) ++
[ [
{size, hoconsc:mk(integer(), #{description => ?DESC(file_size)})}, {size, hoconsc:mk(integer(), #{description => ?DESC(file_size)})},
{mtime, hoconsc:mk(integer(), #{description => ?DESC(modification_date)})} {mtime, hoconsc:mk(integer(), #{description => ?DESC(file_mtime)})}
]; ];
fields(trace) -> fields(trace) ->
[ [
@ -214,7 +214,7 @@ fields(trace) ->
hoconsc:mk( hoconsc:mk(
binary(), binary(),
#{ #{
description => ?DESC(format), description => ?DESC(trace_name),
validator => fun ?MODULE:validate_name/1, validator => fun ?MODULE:validate_name/1,
required => true, required => true,
example => <<"EMQX-TRACE-1">> example => <<"EMQX-TRACE-1">>

View File

@ -17,6 +17,8 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-behaviour(emqx_db_backup).
%% API %% API
-export([mnesia/1]). -export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
@ -28,12 +30,15 @@
update/4, update/4,
delete/1, delete/1,
list/0, list/0,
init_bootstrap_file/0 init_bootstrap_file/0,
format/1
]). ]).
-export([authorize/3]). -export([authorize/3]).
-export([post_config_update/5]). -export([post_config_update/5]).
-export([backup_tables/0]).
%% Internal exports (RPC) %% Internal exports (RPC)
-export([ -export([
do_update/4, do_update/4,
@ -67,6 +72,12 @@ mnesia(boot) ->
{attributes, record_info(fields, ?APP)} {attributes, record_info(fields, ?APP)}
]). ]).
%%--------------------------------------------------------------------
%% Data backup
%%--------------------------------------------------------------------
backup_tables() -> [?APP].
post_config_update([api_key], _Req, NewConf, _OldConf, _AppEnvs) -> post_config_update([api_key], _Req, NewConf, _OldConf, _AppEnvs) ->
#{bootstrap_file := File} = NewConf, #{bootstrap_file := File} = NewConf,
case init_bootstrap_file(File) of case init_bootstrap_file(File) of
@ -127,6 +138,17 @@ do_delete(Name) ->
[_App] -> mnesia:delete({?APP, Name}) [_App] -> mnesia:delete({?APP, Name})
end. end.
format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
ExpiredAt =
case ExpiredAt0 of
infinity -> <<"infinity">>;
_ -> list_to_binary(calendar:system_time_to_rfc3339(ExpiredAt0))
end,
App#{
expired_at => ExpiredAt,
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
}.
list() -> list() ->
to_map(ets:match_object(?APP, #?APP{_ = '_'})). to_map(ets:match_object(?APP, #?APP{_ = '_'})).

View File

@ -25,6 +25,7 @@
-include("emqx_mgmt.hrl"). -include("emqx_mgmt.hrl").
-define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~ts~n", [Cmd, Descr])). -define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~ts~n", [Cmd, Descr])).
-define(DATA_BACKUP_OPTS, #{print_fun => fun emqx_ctl:print/2}).
-export([load/0]). -export([load/0]).
@ -44,7 +45,8 @@
log/1, log/1,
authz/1, authz/1,
pem_cache/1, pem_cache/1,
olp/1 olp/1,
data/1
]). ]).
-define(PROC_INFOKEYS, [ -define(PROC_INFOKEYS, [
@ -739,6 +741,37 @@ olp(_) ->
{"olp disable", "Disable overload protection"} {"olp disable", "Disable overload protection"}
]). ]).
%%--------------------------------------------------------------------
%% @doc data Command
data(["export"]) ->
case emqx_mgmt_data_backup:export(?DATA_BACKUP_OPTS) of
{ok, #{filename := Filename}} ->
emqx_ctl:print("Data has been successfully exported to ~s.~n", [Filename]);
{error, Reason} ->
Reason1 = emqx_mgmt_data_backup:format_error(Reason),
emqx_ctl:print("[error] Data export failed, reason: ~p.~n", [Reason1])
end;
data(["import", Filename]) ->
case emqx_mgmt_data_backup:import(Filename, ?DATA_BACKUP_OPTS) of
{ok, #{db_errors := DbErrs, config_errors := ConfErrs}} when
map_size(DbErrs) =:= 0, map_size(ConfErrs) =:= 0
->
emqx_ctl:print("Data has been imported successfully.~n");
{ok, _} ->
emqx_ctl:print(
"Data has been imported, but some errors occurred, see the the log above.~n"
);
{error, Reason} ->
Reason1 = emqx_mgmt_data_backup:format_error(Reason),
emqx_ctl:print("[error] Data import failed, reason: ~p.~n", [Reason1])
end;
data(_) ->
emqx_ctl:usage([
{"data import <File>", "Import data from the specified tar archive file"},
{"data export", "Export data"}
]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Dump ETS %% Dump ETS
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -0,0 +1,690 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_data_backup).
-export([
export/0,
export/1,
import/1,
import/2,
format_error/1
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
-include_lib("kernel/include/file.hrl").
-include_lib("emqx/include/logger.hrl").
-define(ROOT_BACKUP_DIR, "backup").
-define(BACKUP_MNESIA_DIR, "mnesia").
-define(TAR_SUFFIX, ".tar.gz").
-define(META_FILENAME, "META.hocon").
-define(CLUSTER_HOCON_FILENAME, "cluster.hocon").
-define(CONF_KEYS, [
<<"delayed">>,
<<"rewrite">>,
<<"retainer">>,
<<"mqtt">>,
<<"alarm">>,
<<"sysmon">>,
<<"sys_topics">>,
<<"limiter">>,
<<"log">>,
<<"persistent_session_store">>,
<<"prometheus">>,
<<"crl_cache">>,
<<"conn_congestion">>,
<<"force_shutdown">>,
<<"flapping_detect">>,
<<"broker">>,
<<"force_gc">>,
<<"zones">>
]).
-define(DEFAULT_OPTS, #{}).
-define(tar(_FileName_), _FileName_ ++ ?TAR_SUFFIX).
-define(fmt_tar_err(_Expr_),
fun() ->
case _Expr_ of
{error, _Reason_} -> {error, erl_tar:format_error(_Reason_)};
_Other_ -> _Other_
end
end()
).
-type backup_file_info() :: #{
filename => binary(),
size => non_neg_integer(),
created_at => binary(),
node => node(),
atom() => _
}.
-type db_error_details() :: #{mria:table() => {error, _}}.
-type config_error_details() :: #{emqx_utils_maps:config_path() => {error, _}}.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
-spec export() -> {ok, backup_file_info()} | {error, _}.
export() ->
export(?DEFAULT_OPTS).
-spec export(map()) -> {ok, backup_file_info()} | {error, _}.
export(Opts) ->
{BackupName, TarDescriptor} = prepare_new_backup(Opts),
try
do_export(BackupName, TarDescriptor, Opts)
catch
Class:Reason:Stack ->
?SLOG(error, #{
msg => "emqx_data_export_failed",
exception => Class,
reason => Reason,
stacktrace => Stack
}),
{error, Reason}
after
%% erl_tar:close/1 raises error if called on an already closed tar
catch erl_tar:close(TarDescriptor),
file:del_dir_r(BackupName)
end.
-spec import(file:filename_all()) ->
{ok, #{db_errors => db_error_details(), config_errors => config_error_details()}}
| {error, _}.
import(BackupFileName) ->
import(BackupFileName, ?DEFAULT_OPTS).
-spec import(file:filename_all(), map()) ->
{ok, #{db_errors => db_error_details(), config_errors => config_error_details()}}
| {error, _}.
import(BackupFileName, Opts) ->
case is_import_allowed() of
true ->
case lookup_file(str(BackupFileName)) of
{ok, FilePath} ->
do_import(FilePath, Opts);
Err ->
Err
end;
false ->
{error, not_core_node}
end.
format_error(not_core_node) ->
str(
io_lib:format(
"backup data import is only allowed on core EMQX nodes, but requested node ~p is not core",
[node()]
)
);
format_error(ee_to_ce_backup) ->
"importing EMQX Enterprise data backup to EMQX is not allowed";
format_error(missing_backup_meta) ->
"invalid backup archive file: missing " ?META_FILENAME;
format_error(invalid_edition) ->
"invalid backup archive content: wrong EMQX edition value in " ?META_FILENAME;
format_error(invalid_version) ->
"invalid backup archive content: wrong EMQX version value in " ?META_FILENAME;
format_error(bad_archive_dir) ->
"invalid backup archive content: all files in the archive must be under <backup name> directory";
format_error(not_found) ->
"backup file not found";
format_error(bad_backup_name) ->
"invalid backup name: file name must have " ?TAR_SUFFIX " extension";
format_error({unsupported_version, ImportVersion}) ->
str(
io_lib:format(
"[warning] Backup version ~p is newer than EMQX version ~p, import is not allowed.~n",
[str(ImportVersion), str(emqx_release:version())]
)
);
format_error(Reason) ->
Reason.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
prepare_new_backup(Opts) ->
Ts = erlang:system_time(millisecond),
{{Y, M, D}, {HH, MM, SS}} = local_datetime(Ts),
BackupBaseName = str(
io_lib:format(
"emqx-export-~0p-~2..0b-~2..0b-~2..0b-~2..0b-~2..0b.~3..0b",
[Y, M, D, HH, MM, SS, Ts rem 1000]
)
),
BackupName = filename:join(root_backup_dir(), BackupBaseName),
BackupTarName = ?tar(BackupName),
maybe_print("Exporting data to ~p...~n", [BackupTarName], Opts),
{ok, TarDescriptor} = ?fmt_tar_err(erl_tar:open(BackupTarName, [write, compressed])),
{BackupName, TarDescriptor}.
do_export(BackupName, TarDescriptor, Opts) ->
BackupBaseName = filename:basename(BackupName),
BackupTarName = ?tar(BackupName),
Meta = #{
version => emqx_release:version(),
edition => emqx_release:edition()
},
MetaBin = bin(hocon_pp:do(Meta, #{})),
MetaFileName = filename:join(BackupBaseName, ?META_FILENAME),
ok = ?fmt_tar_err(erl_tar:add(TarDescriptor, MetaBin, MetaFileName, [])),
ok = export_cluster_hocon(TarDescriptor, BackupBaseName, Opts),
ok = export_mnesia_tabs(TarDescriptor, BackupName, BackupBaseName, Opts),
ok = ?fmt_tar_err(erl_tar:close(TarDescriptor)),
{ok, #file_info{
size = Size,
ctime = {{Y1, M1, D1}, {H1, MM1, S1}}
}} = file:read_file_info(BackupTarName),
CreatedAt = io_lib:format("~p-~p-~p ~p:~p:~p", [Y1, M1, D1, H1, MM1, S1]),
{ok, #{
filename => bin(BackupTarName),
size => Size,
created_at => bin(CreatedAt),
node => node()
}}.
export_cluster_hocon(TarDescriptor, BackupBaseName, Opts) ->
maybe_print("Exporting cluster configuration...~n", [], Opts),
RawConf = emqx_config:read_override_conf(#{override_to => cluster}),
maybe_print(
"Exporting additional files from EMQX data_dir: ~p...~n", [str(emqx:data_dir())], Opts
),
RawConf1 = read_data_files(RawConf),
RawConfBin = bin(hocon_pp:do(RawConf1, #{})),
NameInArchive = filename:join(BackupBaseName, ?CLUSTER_HOCON_FILENAME),
ok = ?fmt_tar_err(erl_tar:add(TarDescriptor, RawConfBin, NameInArchive, [])).
export_mnesia_tabs(TarDescriptor, BackupName, BackupBaseName, Opts) ->
maybe_print("Exporting built-in database...~n", [], Opts),
lists:foreach(
fun(Tab) -> export_mnesia_tab(TarDescriptor, Tab, BackupName, BackupBaseName, Opts) end,
tabs_to_backup()
).
export_mnesia_tab(TarDescriptor, TabName, BackupName, BackupBaseName, Opts) ->
maybe_print("Exporting ~p database table...~n", [TabName], Opts),
{ok, MnesiaBackupName} = do_export_mnesia_tab(TabName, BackupName),
NameInArchive = mnesia_backup_name(BackupBaseName, TabName),
ok = ?fmt_tar_err(erl_tar:add(TarDescriptor, MnesiaBackupName, NameInArchive, [])),
_ = file:delete(MnesiaBackupName),
ok.
do_export_mnesia_tab(TabName, BackupName) ->
Node = node(),
try
{ok, TabName, [Node]} = mnesia:activate_checkpoint(
[{name, TabName}, {min, [TabName]}, {allow_remote, false}]
),
MnesiaBackupName = mnesia_backup_name(BackupName, TabName),
ok = filelib:ensure_dir(MnesiaBackupName),
ok = mnesia:backup_checkpoint(TabName, MnesiaBackupName),
{ok, MnesiaBackupName}
after
mnesia:deactivate_checkpoint(TabName)
end.
-ifdef(TEST).
tabs_to_backup() ->
%% Allow mocking in tests
?MODULE:mnesia_tabs_to_backup().
-else.
tabs_to_backup() ->
mnesia_tabs_to_backup().
-endif.
mnesia_tabs_to_backup() ->
lists:flatten([M:backup_tables() || M <- find_behaviours(emqx_db_backup)]).
mnesia_backup_name(Path, TabName) ->
filename:join([Path, ?BACKUP_MNESIA_DIR, atom_to_list(TabName)]).
is_import_allowed() ->
mria_rlog:role() =:= core.
validate_backup(BackupDir) ->
case hocon:files([filename:join(BackupDir, ?META_FILENAME)]) of
{ok, #{
<<"edition">> := Edition,
<<"version">> := Version
}} = Meta ->
validate(
[
fun() -> check_edition(Edition) end,
fun() -> check_version(Version) end
],
Meta
);
_ ->
?SLOG(error, #{msg => "missing_backup_meta", backup => BackupDir}),
{error, missing_backup_meta}
end.
validate([ValidatorFun | T], OkRes) ->
case ValidatorFun() of
ok -> validate(T, OkRes);
Err -> Err
end;
validate([], OkRes) ->
OkRes.
check_edition(BackupEdition) when BackupEdition =:= <<"ce">>; BackupEdition =:= <<"ee">> ->
Edition = bin(emqx_release:edition()),
case {BackupEdition, Edition} of
{<<"ee">>, <<"ce">>} ->
{error, ee_to_ce_backup};
_ ->
ok
end;
check_edition(BackupEdition) ->
?SLOG(error, #{msg => "invalid_backup_edition", edition => BackupEdition}),
{error, invalid_edition}.
check_version(ImportVersion) ->
case parse_version_no_patch(ImportVersion) of
{ok, {ImportMajorInt, ImportMinorInt}} ->
Version = emqx_release:version(),
{ok, {MajorInt, MinorInt}} = parse_version_no_patch(bin(Version)),
case ImportMajorInt > MajorInt orelse ImportMinorInt > MinorInt of
true ->
%% 4.x backup files are anyway not compatible and will be treated as invalid,
%% before this step,
{error, {unsupported_version, str(ImportVersion)}};
false ->
ok
end;
Err ->
Err
end.
parse_version_no_patch(VersionBin) ->
case string:split(VersionBin, ".", all) of
[Major, Minor | _] ->
{MajorInt, _} = emqx_utils_binary:bin_to_int(Major),
{MinorInt, _} = emqx_utils_binary:bin_to_int(Minor),
{ok, {MajorInt, MinorInt}};
_ ->
?SLOG(error, #{msg => "failed_to_parse_backup_version", version => VersionBin}),
{error, invalid_version}
end.
do_import(BackupFileName, Opts) ->
BackupDir = filename:join(root_backup_dir(), filename:basename(BackupFileName, ?TAR_SUFFIX)),
maybe_print("Importing data from ~p...~n", [BackupFileName], Opts),
try
ok = validate_backup_name(BackupFileName),
ok = extract_backup(BackupFileName),
{ok, _} = validate_backup(BackupDir),
ConfErrors = import_cluster_hocon(BackupDir, Opts),
MnesiaErrors = import_mnesia_tabs(BackupDir, Opts),
?SLOG(info, #{msg => "emqx_data_import_success"}),
{ok, #{db_errors => MnesiaErrors, config_errors => ConfErrors}}
catch
error:{badmatch, {error, Reason}}:Stack ->
?SLOG(error, #{msg => "emqx_data_import_failed", reason => Reason, stacktrace => Stack}),
{error, Reason};
Class:Reason:Stack ->
?SLOG(error, #{
msg => "emqx_data_import_failed",
exception => Class,
reason => Reason,
stacktrace => Stack
}),
{error, Reason}
after
file:del_dir_r(BackupDir)
end.
import_mnesia_tabs(BackupDir, Opts) ->
maybe_print("Importing built-in database...~n", [], Opts),
filter_errors(
lists:foldr(
fun(Tab, Acc) -> Acc#{Tab => import_mnesia_tab(BackupDir, Tab, Opts)} end,
#{},
tabs_to_backup()
)
).
import_mnesia_tab(BackupDir, TabName, Opts) ->
MnesiaBackupFileName = mnesia_backup_name(BackupDir, TabName),
case filelib:is_regular(MnesiaBackupFileName) of
true ->
maybe_print("Importing ~p database table...~n", [TabName], Opts),
restore_mnesia_tab(BackupDir, MnesiaBackupFileName, TabName, Opts);
false ->
maybe_print("No backup file for ~p database table...~n", [TabName], Opts),
?SLOG(info, #{msg => "missing_mnesia_backup", table => TabName, backup => BackupDir}),
ok
end.
restore_mnesia_tab(BackupDir, MnesiaBackupFileName, TabName, Opts) ->
BackupNameToImport = MnesiaBackupFileName ++ "_for_import",
Prepared =
catch mnesia:traverse_backup(
MnesiaBackupFileName, BackupNameToImport, fun backup_converter/2, 0
),
try
case Prepared of
{ok, _} ->
Restored = mnesia:restore(BackupNameToImport, [{default_op, keep_tables}]),
case Restored of
{atomic, [TabName]} ->
ok;
RestoreErr ->
?SLOG(error, #{
msg => "failed_to_restore_mnesia_backup",
table => TabName,
backup => BackupDir,
reason => RestoreErr
}),
maybe_print_mnesia_import_err(TabName, RestoreErr, Opts),
{error, RestoreErr}
end;
PrepareErr ->
?SLOG(error, #{
msg => "failed_to_prepare_mnesia_backup_for_restoring",
table => TabName,
backup => BackupDir,
reason => PrepareErr
}),
maybe_print_mnesia_import_err(TabName, PrepareErr, Opts),
PrepareErr
end
after
%% Cleanup files as soon as they are not needed any more for more efficient disk usage
_ = file:delete(BackupNameToImport),
_ = file:delete(MnesiaBackupFileName)
end.
backup_converter({schema, Tab, CreateList}, Acc) ->
check_rec_attributes(Tab, CreateList),
{[{schema, Tab, lists:map(fun convert_copies/1, CreateList)}], Acc};
backup_converter(Other, Acc) ->
{[Other], Acc}.
check_rec_attributes(Tab, CreateList) ->
ImportAttributes = proplists:get_value(attributes, CreateList),
Attributes = mnesia:table_info(Tab, attributes),
case ImportAttributes =/= Attributes of
true ->
throw({error, different_table_schema});
false ->
ok
end.
convert_copies({K, [_ | _]}) when K == ram_copies; K == disc_copies; K == disc_only_copies ->
{K, [node()]};
convert_copies(Other) ->
Other.
extract_backup(BackupFileName) ->
BackupDir = root_backup_dir(),
ok = validate_filenames(BackupFileName),
?fmt_tar_err(erl_tar:extract(BackupFileName, [{cwd, BackupDir}, compressed])).
validate_filenames(BackupFileName) ->
{ok, FileNames} = ?fmt_tar_err(erl_tar:table(BackupFileName, [compressed])),
BackupName = filename:basename(BackupFileName, ?TAR_SUFFIX),
IsValid = lists:all(
fun(FileName) ->
[Root | _] = filename:split(FileName),
Root =:= BackupName
end,
FileNames
),
case IsValid of
true -> ok;
false -> {error, bad_archive_dir}
end.
import_cluster_hocon(BackupDir, Opts) ->
HoconFileName = filename:join(BackupDir, ?CLUSTER_HOCON_FILENAME),
case filelib:is_regular(HoconFileName) of
true ->
{ok, RawConf} = hocon:files([HoconFileName]),
{ok, _} = validate_cluster_hocon(RawConf),
maybe_print("Importing cluster configuration...~n", [], Opts),
%% At this point, when all validations have been passed, we want to log errors (if any)
%% but proceed with the next items, instead of aborting the whole import operation
do_import_conf(RawConf, Opts);
false ->
maybe_print("No cluster configuration to be imported.~n", [], Opts),
?SLOG(info, #{
msg => "no_backup_hocon_config_to_import",
backup => BackupDir
}),
#{}
end.
read_data_files(RawConf) ->
DataDir = bin(emqx:data_dir()),
{ok, Cwd} = file:get_cwd(),
AbsDataDir = bin(filename:join(Cwd, DataDir)),
RawConf1 = emqx_authz:maybe_read_acl_file(RawConf),
emqx_utils_maps:deep_convert(RawConf1, fun read_data_file/4, [DataDir, AbsDataDir]).
-define(dir_pattern(_Dir_), <<_Dir_:(byte_size(_Dir_))/binary, _/binary>>).
read_data_file(Key, Val, DataDir, AbsDataDir) ->
Val1 =
case Val of
?dir_pattern(DataDir) = FileName ->
do_read_file(FileName);
?dir_pattern(AbsDataDir) = FileName ->
do_read_file(FileName);
V ->
V
end,
{Key, Val1}.
do_read_file(FileName) ->
case file:read_file(FileName) of
{ok, Content} ->
Content;
{error, Reason} ->
?SLOG(warning, #{
msg => "failed_to_read_data_file",
filename => FileName,
reason => Reason
}),
FileName
end.
validate_cluster_hocon(RawConf) ->
%% write ACL file to comply with the schema...
RawConf1 = emqx_authz:maybe_write_acl_file(RawConf),
emqx_hocon:check(
emqx_conf:schema_module(),
maps:merge(emqx:get_raw_config([]), RawConf1),
#{atom_key => false, required => false}
).
do_import_conf(RawConf, Opts) ->
GenConfErrs = filter_errors(maps:from_list(import_generic_conf(RawConf))),
maybe_print_errors(GenConfErrs, Opts),
Errors =
lists:foldr(
fun(Module, ErrorsAcc) ->
Module:import_config(RawConf),
case Module:import_config(RawConf) of
{ok, #{changed := Changed}} ->
maybe_print_changed(Changed, Opts),
ErrorsAcc;
{error, #{root_key := RootKey, reason := Reason}} ->
ErrorsAcc#{[RootKey] => Reason}
end
end,
GenConfErrs,
find_behaviours(emqx_config_backup)
),
maybe_print_errors(Errors, Opts),
Errors.
import_generic_conf(Data) ->
lists:map(
fun(Key) ->
case maps:get(Key, Data, undefined) of
undefined -> {[Key], ok};
Conf -> {[Key], emqx_conf:update([Key], Conf, #{override_to => cluster})}
end
end,
?CONF_KEYS
).
maybe_print_changed(Changed, Opts) ->
lists:foreach(
fun(ChangedPath) ->
maybe_print(
"Config key path ~p was present before import and "
"has been overwritten.~n",
[pretty_path(ChangedPath)],
Opts
)
end,
Changed
).
maybe_print_errors(Errors, Opts) ->
maps:foreach(
fun(Path, Err) ->
maybe_print(
"Failed to import the following config path: ~p, reason: ~p~n",
[pretty_path(Path), Err],
Opts
)
end,
Errors
).
filter_errors(Results) ->
maps:filter(
fun
(_Path, {error, _}) -> true;
(_, _) -> false
end,
Results
).
pretty_path(Path) ->
str(lists:join(".", [str(Part) || Part <- Path])).
str(Data) when is_atom(Data) ->
atom_to_list(Data);
str(Data) ->
unicode:characters_to_list(Data).
bin(Data) when is_atom(Data) ->
atom_to_binary(Data, utf8);
bin(Data) ->
unicode:characters_to_binary(Data).
validate_backup_name(FileName) ->
BaseName = filename:basename(FileName, ?TAR_SUFFIX),
ValidName = BaseName ++ ?TAR_SUFFIX,
case filename:basename(FileName) of
ValidName -> ok;
_ -> {error, bad_backup_name}
end.
lookup_file(FileName) ->
case filelib:is_regular(FileName) of
true ->
{ok, FileName};
false ->
%% Only lookup by basename, don't allow to lookup by file path
case FileName =:= filename:basename(FileName) of
true ->
FilePath = filename:join(root_backup_dir(), FileName),
case filelib:is_file(FilePath) of
true -> {ok, FilePath};
false -> {error, not_found}
end;
false ->
{error, not_found}
end
end.
root_backup_dir() ->
Dir = filename:join(emqx:data_dir(), ?ROOT_BACKUP_DIR),
ok = ensure_path(Dir),
Dir.
-if(?OTP_RELEASE < 25).
ensure_path(Path) -> filelib:ensure_dir(filename:join([Path, "dummy"])).
-else.
ensure_path(Path) -> filelib:ensure_path(Path).
-endif.
local_datetime(MillisecondTs) ->
calendar:system_time_to_local_time(MillisecondTs, millisecond).
maybe_print(Format, Args, #{print_fun := PrintFun}) ->
PrintFun(Format, Args);
maybe_print(_Format, _Args, _Opts) ->
ok.
maybe_print_mnesia_import_err(TabName, Error, Opts) ->
maybe_print(
"[error] Failed to import built-in database table: ~p, reason: ~p~n",
[TabName, Error],
Opts
).
find_behaviours(Behaviour) ->
find_behaviours(Behaviour, apps(), []).
%% Based on minirest_api:find_api_modules/1
find_behaviours(_Behaviour, [] = _Apps, Acc) ->
Acc;
find_behaviours(Behaviour, [App | Apps], Acc) ->
case application:get_key(App, modules) of
undefined ->
Acc;
{ok, Modules} ->
NewAcc = lists:filter(
fun(Module) ->
Info = Module:module_info(attributes),
Bhvrs = lists:flatten(
proplists:get_all_values(behavior, Info) ++
proplists:get_all_values(behaviour, Info)
),
lists:member(Behaviour, Bhvrs)
end,
Modules
),
find_behaviours(Behaviour, Apps, NewAcc ++ Acc)
end.
apps() ->
[
App
|| {App, _, _} <- application:loaded_applications(),
case re:run(atom_to_list(App), "^emqx") of
{match, [{0, 4}]} -> true;
_ -> false
end
].

View File

@ -0,0 +1,96 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_listeners_conf).
-behaviour(emqx_config_backup).
-export([
action/4,
create/3,
ensure_remove/2,
get_raw/2,
update/3
]).
%% Data backup
-export([
import_config/1
]).
-include_lib("emqx/include/logger.hrl").
-define(CONF_ROOT_KEY, listeners).
-define(path(_Type_, _Name_), [?CONF_ROOT_KEY, _Type_, _Name_]).
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
-define(IMPORT_OPTS, #{override_to => cluster}).
action(Type, Name, Action, Conf) ->
wrap(emqx_conf:update(?path(Type, Name), {action, Action, Conf}, ?OPTS)).
create(Type, Name, Conf) ->
wrap(emqx_conf:update(?path(Type, Name), {create, Conf}, ?OPTS)).
ensure_remove(Type, Name) ->
wrap(emqx_conf:tombstone(?path(Type, Name), ?OPTS)).
get_raw(Type, Name) -> emqx_conf:get_raw(?path(Type, Name), undefined).
update(Type, Name, Conf) ->
wrap(emqx_conf:update(?path(Type, Name), {update, Conf}, ?OPTS)).
wrap({error, {post_config_update, emqx_listeners, Reason}}) -> {error, Reason};
wrap({error, {pre_config_update, emqx_listeners, Reason}}) -> {error, Reason};
wrap({error, Reason}) -> {error, Reason};
wrap(Ok) -> Ok.
%%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
import_config(RawConf) ->
NewConf = maps:get(<<"listeners">>, RawConf, #{}),
OldConf = emqx:get_raw_config([?CONF_ROOT_KEY], #{}),
MergedConf = merge_confs(OldConf, NewConf),
case emqx_conf:update([?CONF_ROOT_KEY], MergedConf, ?IMPORT_OPTS) of
{ok, #{raw_config := NewRawConf}} ->
{ok, #{root_key => ?CONF_ROOT_KEY, changed => changed_paths(OldConf, NewRawConf)}};
Error ->
{error, #{root_key => ?CONF_ROOT_KEY, reason => Error}}
end.
merge_confs(OldConf, NewConf) ->
AllTypes = maps:keys(maps:merge(OldConf, NewConf)),
lists:foldr(
fun(Type, Acc) ->
NewListeners = maps:get(Type, NewConf, #{}),
OldListeners = maps:get(Type, OldConf, #{}),
Acc#{Type => maps:merge(OldListeners, NewListeners)}
end,
#{},
AllTypes
).
changed_paths(OldRawConf, NewRawConf) ->
maps:fold(
fun(Type, Listeners, ChangedAcc) ->
OldListeners = maps:get(Type, OldRawConf, #{}),
Changed = maps:get(changed, emqx_utils_maps:diff_maps(Listeners, OldListeners)),
[?path(Type, K) || K <- maps:keys(Changed)] ++ ChangedAcc
end,
[],
NewRawConf
).

View File

@ -0,0 +1,519 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_data_backup_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
[application:load(App) || App <- apps_to_start() ++ apps_to_load()],
Config.
end_per_suite(_Config) ->
ok.
init_per_testcase(t_import_on_cluster, Config) ->
%% Don't import listeners to avoid port conflicts
%% when the same conf will be imported to another cluster
meck:new(emqx_mgmt_listeners_conf, [passthrough]),
meck:new(emqx_gateway_conf, [passthrough]),
meck:expect(
emqx_mgmt_listeners_conf,
import_config,
1,
{ok, #{changed => [], root_key => listeners}}
),
meck:expect(
emqx_gateway_conf,
import_config,
1,
{ok, #{changed => [], root_key => gateway}}
),
[{cluster, cluster(Config)} | setup(Config)];
init_per_testcase(t_verify_imported_mnesia_tab_on_cluster, Config) ->
[{cluster, cluster(Config)} | setup(Config)];
init_per_testcase(t_mnesia_bad_tab_schema, Config) ->
meck:new(emqx_mgmt_data_backup, [passthrough]),
meck:expect(emqx_mgmt_data_backup, mnesia_tabs_to_backup, 0, [data_backup_test]),
setup(Config);
init_per_testcase(_TestCase, Config) ->
setup(Config).
end_per_testcase(t_import_on_cluster, Config) ->
cleanup_cluster(?config(cluster, Config)),
cleanup(Config),
meck:unload(emqx_mgmt_listeners_conf),
meck:unload(emqx_gateway_conf);
end_per_testcase(t_verify_imported_mnesia_tab_on_cluster, Config) ->
cleanup_cluster(?config(cluster, Config)),
cleanup(Config);
end_per_testcase(t_mnesia_bad_tab_schema, Config) ->
cleanup(Config),
meck:unload(emqx_mgmt_data_backup);
end_per_testcase(_TestCase, Config) ->
cleanup(Config).
t_empty_export_import(_Config) ->
ExpRawConf = emqx:get_raw_config([]),
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
Exp = {ok, #{db_errors => #{}, config_errors => #{}}},
?assertEqual(Exp, emqx_mgmt_data_backup:import(FileName)),
?assertEqual(ExpRawConf, emqx:get_raw_config([])),
%% idempotent update assert
?assertEqual(Exp, emqx_mgmt_data_backup:import(FileName)),
?assertEqual(ExpRawConf, emqx:get_raw_config([])).
t_cluster_hocon_export_import(Config) ->
RawConfBeforeImport = emqx:get_raw_config([]),
BootstrapFile = filename:join(?config(data_dir, Config), ?BOOTSTRAP_BACKUP),
Exp = {ok, #{db_errors => #{}, config_errors => #{}}},
?assertEqual(Exp, emqx_mgmt_data_backup:import(BootstrapFile)),
RawConfAfterImport = emqx:get_raw_config([]),
?assertNotEqual(RawConfBeforeImport, RawConfAfterImport),
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
?assertEqual(Exp, emqx_mgmt_data_backup:import(FileName)),
?assertEqual(RawConfAfterImport, emqx:get_raw_config([])),
%% idempotent update assert
?assertEqual(Exp, emqx_mgmt_data_backup:import(FileName)),
?assertEqual(RawConfAfterImport, emqx:get_raw_config([])),
%% lookup file inside <data_dir>/backup
?assertEqual(Exp, emqx_mgmt_data_backup:import(filename:basename(FileName))).
t_ee_to_ce_backup(Config) ->
case emqx_release:edition() of
ce ->
EEBackupFileName = filename:join(?config(priv_dir, Config), "export-backup-ee.tar.gz"),
Meta = unicode:characters_to_binary(
hocon_pp:do(#{edition => ee, version => emqx_release:version()}, #{})
),
ok = erl_tar:create(
EEBackupFileName,
[
{"export-backup-ee/cluster.hocon", <<>>},
{"export-backup-ee/META.hocon", Meta}
],
[compressed]
),
ExpReason = ee_to_ce_backup,
?assertEqual(
{error, ExpReason}, emqx_mgmt_data_backup:import(EEBackupFileName)
),
%% Must be translated to a readable string
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(ExpReason));
ee ->
%% Don't fail if the test is run with emqx-enterprise profile
ok
end.
t_no_backup_file(_Config) ->
ExpReason = not_found,
?assertEqual(
{error, not_found}, emqx_mgmt_data_backup:import("no_such_backup.tar.gz")
),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(ExpReason)).
t_bad_backup_file(Config) ->
BadFileName = filename:join(?config(priv_dir, Config), "export-bad-backup-tar-gz"),
ok = file:write_file(BadFileName, <<>>),
NoMetaFileName = filename:join(?config(priv_dir, Config), "export-no-meta.tar.gz"),
ok = erl_tar:create(NoMetaFileName, [{"export-no-meta/cluster.hocon", <<>>}], [compressed]),
BadArchiveDirFileName = filename:join(?config(priv_dir, Config), "export-bad-dir.tar.gz"),
ok = erl_tar:create(
BadArchiveDirFileName,
[
{"tmp/cluster.hocon", <<>>},
{"export-bad-dir-inside/META.hocon", <<>>},
{"/export-bad-dir-inside/mnesia/test_tab", <<>>}
],
[compressed]
),
InvalidEditionFileName = filename:join(
?config(priv_dir, Config), "export-invalid-edition.tar.gz"
),
Meta = unicode:characters_to_binary(
hocon_pp:do(#{edition => "test", version => emqx_release:version()}, #{})
),
ok = erl_tar:create(
InvalidEditionFileName,
[
{"export-invalid-edition/cluster.hocon", <<>>},
{"export-invalid-edition/META.hocon", Meta}
],
[compressed]
),
InvalidVersionFileName = filename:join(
?config(priv_dir, Config), "export-invalid-version.tar.gz"
),
Meta1 = unicode:characters_to_binary(
hocon_pp:do(#{edition => emqx_release:edition(), version => "test"}, #{})
),
ok = erl_tar:create(
InvalidVersionFileName,
[
{"export-invalid-version/cluster.hocon", <<>>},
{"export-invalid-version/META.hocon", Meta1}
],
[compressed]
),
BadFileNameReason = bad_backup_name,
NoMetaReason = missing_backup_meta,
BadArchiveDirReason = bad_archive_dir,
InvalidEditionReason = invalid_edition,
InvalidVersionReason = invalid_version,
?assertEqual({error, BadFileNameReason}, emqx_mgmt_data_backup:import(BadFileName)),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(BadFileNameReason)),
?assertEqual({error, NoMetaReason}, emqx_mgmt_data_backup:import(NoMetaFileName)),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(NoMetaReason)),
?assertEqual(
{error, BadArchiveDirReason},
emqx_mgmt_data_backup:import(BadArchiveDirFileName)
),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(BadArchiveDirReason)),
?assertEqual(
{error, InvalidEditionReason},
emqx_mgmt_data_backup:import(InvalidEditionFileName)
),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(InvalidEditionReason)),
?assertEqual(
{error, InvalidVersionReason},
emqx_mgmt_data_backup:import(InvalidVersionFileName)
),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(InvalidVersionReason)).
t_future_version(Config) ->
CurrentVersion = list_to_binary(emqx_release:version()),
[_, _ | Patch] = string:split(CurrentVersion, ".", all),
{ok, {MajorInt, MinorInt}} = emqx_mgmt_data_backup:parse_version_no_patch(CurrentVersion),
FutureMajorVersion = recompose_version(MajorInt + 1, MinorInt, Patch),
FutureMinorVersion = recompose_version(MajorInt, MinorInt + 1, Patch),
[MajorMeta, MinorMeta] =
[
unicode:characters_to_binary(
hocon_pp:do(#{edition => emqx_release:edition(), version => V}, #{})
)
|| V <- [FutureMajorVersion, FutureMinorVersion]
],
MajorFileName = filename:join(?config(priv_dir, Config), "export-future-major-ver.tar.gz"),
MinorFileName = filename:join(?config(priv_dir, Config), "export-future-minor-ver.tar.gz"),
ok = erl_tar:create(
MajorFileName,
[
{"export-future-major-ver/cluster.hocon", <<>>},
{"export-future-major-ver/META.hocon", MajorMeta}
],
[compressed]
),
ok = erl_tar:create(
MinorFileName,
[
{"export-future-minor-ver/cluster.hocon", <<>>},
{"export-future-minor-ver/META.hocon", MinorMeta}
],
[compressed]
),
ExpMajorReason = {unsupported_version, FutureMajorVersion},
ExpMinorReason = {unsupported_version, FutureMinorVersion},
?assertEqual({error, ExpMajorReason}, emqx_mgmt_data_backup:import(MajorFileName)),
?assertEqual({error, ExpMinorReason}, emqx_mgmt_data_backup:import(MinorFileName)),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(ExpMajorReason)),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(ExpMinorReason)).
t_bad_config(Config) ->
BadConfigFileName = filename:join(?config(priv_dir, Config), "export-bad-config-backup.tar.gz"),
Meta = unicode:characters_to_binary(
hocon_pp:do(#{edition => emqx_release:edition(), version => emqx_release:version()}, #{})
),
BadConfigMap = #{
<<"listeners">> =>
#{
<<"bad-type">> =>
#{<<"bad-name">> => #{<<"bad-field">> => <<"bad-val">>}}
}
},
BadConfig = unicode:characters_to_binary(hocon_pp:do(BadConfigMap, #{})),
ok = erl_tar:create(
BadConfigFileName,
[
{"export-bad-config-backup/cluster.hocon", BadConfig},
{"export-bad-config-backup/META.hocon", Meta}
],
[compressed]
),
Res = emqx_mgmt_data_backup:import(BadConfigFileName),
?assertMatch({error, #{kind := validation_error}}, Res).
t_import_on_cluster(Config) ->
%% Randomly chosen config key to verify import result additionally
?assertEqual([], emqx:get_config([authentication])),
BootstrapFile = filename:join(?config(data_dir, Config), ?BOOTSTRAP_BACKUP),
ExpImportRes = {ok, #{db_errors => #{}, config_errors => #{}}},
?assertEqual(ExpImportRes, emqx_mgmt_data_backup:import(BootstrapFile)),
ImportedAuthnConf = emqx:get_config([authentication]),
?assertMatch([_ | _], ImportedAuthnConf),
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
{ok, Cwd} = file:get_cwd(),
AbsFilePath = filename:join(Cwd, FileName),
[CoreNode1, _CoreNode2, ReplicantNode] = NodesList = ?config(cluster, Config),
ReplImportReason = not_core_node,
?assertEqual(
{error, ReplImportReason},
rpc:call(ReplicantNode, emqx_mgmt_data_backup, import, [AbsFilePath])
),
?assertMatch([_ | _], emqx_mgmt_data_backup:format_error(ReplImportReason)),
[?assertEqual([], rpc:call(N, emqx, get_config, [[authentication]])) || N <- NodesList],
?assertEqual(
ExpImportRes,
rpc:call(CoreNode1, emqx_mgmt_data_backup, import, [AbsFilePath])
),
[
?assertEqual(
authn_ids(ImportedAuthnConf),
authn_ids(rpc:call(N, emqx, get_config, [[authentication]]))
)
|| N <- NodesList
].
t_verify_imported_mnesia_tab_on_cluster(Config) ->
UsersToExport = users(<<"user_to_export_">>),
UsersBeforeImport = users(<<"user_before_import_">>),
[{ok, _} = emqx_dashboard_admin:add_user(U, U, U) || U <- UsersToExport],
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
{ok, Cwd} = file:get_cwd(),
AbsFilePath = filename:join(Cwd, FileName),
[CoreNode1, CoreNode2, ReplicantNode] = NodesList = ?config(cluster, Config),
[
{ok, _} = rpc:call(CoreNode1, emqx_dashboard_admin, add_user, [U, U, U])
|| U <- UsersBeforeImport
],
?assertEqual(
{ok, #{db_errors => #{}, config_errors => #{}}},
rpc:call(CoreNode1, emqx_mgmt_data_backup, import, [AbsFilePath])
),
[Tab] = emqx_dashboard_admin:backup_tables(),
AllUsers = lists:sort(mnesia:dirty_all_keys(Tab) ++ UsersBeforeImport),
[
?assertEqual(
AllUsers,
lists:sort(rpc:call(N, mnesia, dirty_all_keys, [Tab]))
)
|| N <- [CoreNode1, CoreNode2]
],
%% Give some extra time to replicant to import data...
timer:sleep(3000),
?assertEqual(AllUsers, lists:sort(rpc:call(ReplicantNode, mnesia, dirty_all_keys, [Tab]))),
[rpc:call(N, ekka, leave, []) || N <- lists:reverse(NodesList)],
[emqx_common_test_helpers:stop_slave(N) || N <- NodesList].
t_mnesia_bad_tab_schema(_Config) ->
OldAttributes = [id, name, description],
ok = create_test_tab(OldAttributes),
ok = mria:dirty_write({data_backup_test, <<"id">>, <<"old_name">>, <<"old_description">>}),
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
{atomic, ok} = mnesia:delete_table(data_backup_test),
NewAttributes = [id, name, description, new_field],
ok = create_test_tab(NewAttributes),
NewRec =
{data_backup_test, <<"id">>, <<"new_name">>, <<"new_description">>, <<"new_field_value">>},
ok = mria:dirty_write(NewRec),
?assertEqual(
{ok, #{
db_errors =>
#{data_backup_test => {error, {"Backup traversal failed", different_table_schema}}},
config_errors => #{}
}},
emqx_mgmt_data_backup:import(FileName)
),
?assertEqual([NewRec], mnesia:dirty_read(data_backup_test, <<"id">>)),
?assertEqual([<<"id">>], mnesia:dirty_all_keys(data_backup_test)).
t_read_files(_Config) ->
DataDir = emqx:data_dir(),
%% Relative "data" path is set in init_per_testcase/2, asserting it must be safe
?assertEqual("data", DataDir),
{ok, Cwd} = file:get_cwd(),
AbsDataDir = filename:join(Cwd, DataDir),
FileBaseName = "t_read_files_tmp_file",
TestFileAbsPath = iolist_to_binary(filename:join(AbsDataDir, FileBaseName)),
TestFilePath = iolist_to_binary(filename:join(DataDir, FileBaseName)),
TestFileContent = <<"test_file_content">>,
ok = file:write_file(TestFileAbsPath, TestFileContent),
RawConf = #{
<<"test_rootkey">> => #{
<<"test_field">> => <<"test_field_path">>,
<<"abs_data_dir_path_file">> => TestFileAbsPath,
<<"rel_data_dir_path_file">> => TestFilePath,
<<"path_outside_data_dir">> => <<"/tmp/some-file">>
}
},
RawConf1 = emqx_utils_maps:deep_put(
[<<"test_rootkey">>, <<"abs_data_dir_path_file">>], RawConf, TestFileContent
),
ExpectedConf = emqx_utils_maps:deep_put(
[<<"test_rootkey">>, <<"rel_data_dir_path_file">>], RawConf1, TestFileContent
),
?assertEqual(ExpectedConf, emqx_mgmt_data_backup:read_data_files(RawConf)).
%%------------------------------------------------------------------------------
%% Internal test helpers
%%------------------------------------------------------------------------------
setup(Config) ->
%% avoid port conflicts if the cluster is started
AppHandler = fun
(emqx_dashboard) ->
ok = emqx_config:put([dashboard, listeners, http, bind], 0);
(_) ->
ok
end,
ok = emqx_common_test_helpers:start_apps(apps_to_start(), AppHandler),
PrevDataDir = application:get_env(emqx, data_dir),
application:set_env(emqx, data_dir, "data"),
[{previous_emqx_data_dir, PrevDataDir} | Config].
cleanup(Config) ->
emqx_common_test_helpers:stop_apps(apps_to_start()),
case ?config(previous_emqx_data_dir, Config) of
undefined ->
application:unset_env(emqx, data_dir);
{ok, Val} ->
application:set_env(emqx, data_dir, Val)
end.
cleanup_cluster(ClusterNodes) ->
[rpc:call(N, ekka, leave, []) || N <- lists:reverse(ClusterNodes)],
[emqx_common_test_helpers:stop_slave(N) || N <- ClusterNodes].
users(Prefix) ->
[
<<Prefix/binary, (integer_to_binary(abs(erlang:unique_integer())))/binary>>
|| _ <- lists:seq(1, 10)
].
authn_ids(AuthnConf) ->
lists:sort([emqx_authentication:authenticator_id(Conf) || Conf <- AuthnConf]).
recompose_version(MajorInt, MinorInt, Patch) ->
unicode:characters_to_list(
[integer_to_list(MajorInt + 1), $., integer_to_list(MinorInt), $. | Patch]
).
cluster(Config) ->
PrivDataDir = ?config(priv_dir, Config),
[{Core1, Core1Opts}, {Core2, Core2Opts}, {Replicant, ReplOpts}] =
emqx_common_test_helpers:emqx_cluster(
[
{core, data_backup_core1},
{core, data_backup_core2},
{replicant, data_backup_replicant}
],
#{
priv_data_dir => PrivDataDir,
schema_mod => emqx_conf_schema,
apps => apps_to_start(),
load_apps => apps_to_start() ++ apps_to_load(),
env => [{mria, db_backend, rlog}],
load_schema => true,
start_autocluster => true,
join_to => true,
listener_ports => [],
conf => [{[dashboard, listeners, http, bind], 0}],
env_handler =>
fun(_) ->
application:set_env(emqx, boot_modules, [broker, router])
end
}
),
Node1 = emqx_common_test_helpers:start_slave(Core1, Core1Opts),
Node2 = emqx_common_test_helpers:start_slave(Core2, Core2Opts),
#{conf := _ReplConf, env := ReplEnv} = ReplOpts,
ClusterDiscovery = {static, [{seeds, [Node1, Node2]}]},
ReplOpts1 = maps:remove(
join_to,
ReplOpts#{
env => [{ekka, cluster_discovery, ClusterDiscovery} | ReplEnv],
env_handler => fun(_) ->
application:set_env(emqx, boot_modules, [broker, router]),
application:set_env(
ekka,
cluster_discovery,
ClusterDiscovery
)
end
}
),
ReplNode = emqx_common_test_helpers:start_slave(Replicant, ReplOpts1),
[Node1, Node2, ReplNode].
create_test_tab(Attributes) ->
ok = mria:create_table(data_backup_test, [
{type, set},
{rlog_shard, data_backup_test_shard},
{storage, disc_copies},
{record_name, data_backup_test},
{attributes, Attributes},
{storage_properties, [
{ets, [
{read_concurrency, true},
{write_concurrency, true}
]}
]}
]),
ok = mria:wait_for_tables([data_backup_test]).
apps_to_start() ->
[
emqx,
emqx_conf,
emqx_psk,
emqx_management,
emqx_dashboard,
emqx_authz,
emqx_authn,
emqx_rule_engine,
emqx_retainer,
emqx_prometheus,
emqx_modules,
emqx_gateway,
emqx_exhook,
emqx_bridge,
emqx_auto_subscribe
].
apps_to_load() ->
[
emqx_gateway_lwm2m,
emqx_gateway_coap,
emqx_gateway_exproto,
emqx_gateway_stomp,
emqx_gateway_mqttsn
].

View File

@ -98,9 +98,9 @@
-define(FORMAT_FUN, {?MODULE, format_delayed}). -define(FORMAT_FUN, {?MODULE, format_delayed}).
-define(NOW, erlang:system_time(milli_seconds)). -define(NOW, erlang:system_time(milli_seconds)).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Mnesia bootstrap %% Mnesia bootstrap
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
mnesia(boot) -> mnesia(boot) ->
ok = mria:create_table(?TAB, [ ok = mria:create_table(?TAB, [
{type, ordered_set}, {type, ordered_set},
@ -110,9 +110,9 @@ mnesia(boot) ->
{attributes, record_info(fields, delayed_message)} {attributes, record_info(fields, delayed_message)}
]). ]).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Hooks %% Hooks
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
on_message_publish( on_message_publish(
Msg = #message{ Msg = #message{
id = Id, id = Id,
@ -143,9 +143,9 @@ on_message_publish(
on_message_publish(Msg) -> on_message_publish(Msg) ->
{ok, Msg}. {ok, Msg}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Start delayed publish server %% Start delayed publish server
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec start_link() -> emqx_types:startlink_ret(). -spec start_link() -> emqx_types:startlink_ret().
start_link() -> start_link() ->
@ -270,9 +270,9 @@ post_config_update(_KeyPath, _ConfigReq, NewConf, _OldConf, _AppEnvs) ->
Enable = maps:get(enable, NewConf, undefined), Enable = maps:get(enable, NewConf, undefined),
load_or_unload(Enable). load_or_unload(Enable).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% gen_server callback %% gen_server callback
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
init([]) -> init([]) ->
ok = mria:wait_for_tables([?TAB]), ok = mria:wait_for_tables([?TAB]),
@ -335,9 +335,9 @@ terminate(_Reason, #{stats_timer := StatsTimer} = State) ->
code_change(_Vsn, State, _Extra) -> code_change(_Vsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Telemetry %% Telemetry
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec get_basic_usage_info() -> #{delayed_message_count => non_neg_integer()}. -spec get_basic_usage_info() -> #{delayed_message_count => non_neg_integer()}.
get_basic_usage_info() -> get_basic_usage_info() ->
@ -348,9 +348,9 @@ get_basic_usage_info() ->
end, end,
#{delayed_message_count => DelayedCount}. #{delayed_message_count => DelayedCount}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Ensure the stats %% Ensure the stats
-spec ensure_stats_event(state()) -> state(). -spec ensure_stats_event(state()) -> state().

View File

@ -49,9 +49,12 @@
%% exported for `emqx_telemetry' %% exported for `emqx_telemetry'
-export([get_basic_usage_info/0]). -export([get_basic_usage_info/0]).
%%-------------------------------------------------------------------- -define(update(_Rules_),
emqx_conf:update([rewrite], _Rules_, #{override_to => cluster})
).
%%------------------------------------------------------------------------------
%% Load/Unload %% Load/Unload
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
enable() -> enable() ->
emqx_conf:add_handler([rewrite], ?MODULE), emqx_conf:add_handler([rewrite], ?MODULE),
@ -67,7 +70,7 @@ list() ->
emqx_conf:get_raw([<<"rewrite">>], []). emqx_conf:get_raw([<<"rewrite">>], []).
update(Rules0) -> update(Rules0) ->
case emqx_conf:update([rewrite], Rules0, #{override_to => cluster}) of case ?update(Rules0) of
{ok, _} -> {ok, _} ->
ok; ok;
{error, Reason} -> {error, Reason} ->
@ -109,18 +112,19 @@ rewrite_publish(Message = #message{topic = Topic}, Rules) ->
Binds = fill_client_binds(Message), Binds = fill_client_binds(Message),
{ok, Message#message{topic = match_and_rewrite(Topic, Rules, Binds)}}. {ok, Message#message{topic = match_and_rewrite(Topic, Rules, Binds)}}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Telemetry %% Telemetry
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec get_basic_usage_info() -> #{topic_rewrite_rule_count => non_neg_integer()}. -spec get_basic_usage_info() -> #{topic_rewrite_rule_count => non_neg_integer()}.
get_basic_usage_info() -> get_basic_usage_info() ->
RewriteRules = list(), RewriteRules = list(),
#{topic_rewrite_rule_count => length(RewriteRules)}. #{topic_rewrite_rule_count => length(RewriteRules)}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
compile(Rules) -> compile(Rules) ->
lists:foldl( lists:foldl(
fun(Rule, {Publish, Subscribe, Error}) -> fun(Rule, {Publish, Subscribe, Error}) ->

View File

@ -2,7 +2,7 @@
{application, emqx_psk, [ {application, emqx_psk, [
{description, "EMQX PSK"}, {description, "EMQX PSK"},
% strict semver, bump manually! % strict semver, bump manually!
{vsn, "5.0.1"}, {vsn, "5.0.2"},
{modules, []}, {modules, []},
{registered, [emqx_psk_sup]}, {registered, [emqx_psk_sup]},
{applications, [kernel, stdlib]}, {applications, [kernel, stdlib]},

View File

@ -17,6 +17,8 @@
-module(emqx_psk). -module(emqx_psk).
-behaviour(gen_server). -behaviour(gen_server).
-behaviour(emqx_db_backup).
-behaviour(emqx_config_backup).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
@ -48,6 +50,12 @@
insert_psks/1 insert_psks/1
]). ]).
%% Data backup
-export([
import_config/1,
backup_tables/0
]).
-record(psk_entry, { -record(psk_entry, {
psk_id :: binary(), psk_id :: binary(),
shared_secret :: binary(), shared_secret :: binary(),
@ -86,6 +94,12 @@ mnesia(boot) ->
{storage_properties, [{ets, [{read_concurrency, true}]}]} {storage_properties, [{ets, [{read_concurrency, true}]}]}
]). ]).
%%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
backup_tables() -> [?TAB].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -115,9 +129,27 @@ start_link() ->
stop() -> stop() ->
gen_server:stop(?MODULE). gen_server:stop(?MODULE).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Data backup
%%------------------------------------------------------------------------------
import_config(#{<<"psk_authentication">> := PskConf}) ->
case emqx_conf:update([psk_authentication], PskConf, #{override_to => cluster}) of
{ok, _} ->
case get_config(enable) of
true -> load();
false -> ok
end,
{ok, #{root_key => psk_authentication, changed => []}};
Error ->
{error, #{root_key => psk_authentication, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => psk_authentication, changed => []}}.
%%------------------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
init(_Opts) -> init(_Opts) ->
_ = _ =

View File

@ -23,6 +23,8 @@
-export([namespace/0, roots/0, fields/1, desc/1]). -export([namespace/0, roots/0, fields/1, desc/1]).
-export([create_opts/1]).
%% range interval in ms %% range interval in ms
-define(HEALTH_CHECK_INTERVAL_RANGE_MIN, 1). -define(HEALTH_CHECK_INTERVAL_RANGE_MIN, 1).
-define(HEALTH_CHECK_INTERVAL_RANGE_MAX, 3_600_000). -define(HEALTH_CHECK_INTERVAL_RANGE_MAX, 3_600_000).
@ -43,6 +45,10 @@ fields("resource_opts") ->
)} )}
]; ];
fields("creation_opts") -> fields("creation_opts") ->
create_opts([]).
create_opts(Overrides) ->
override(
[ [
{buffer_mode, fun buffer_mode/1}, {buffer_mode, fun buffer_mode/1},
{worker_pool_size, fun worker_pool_size/1}, {worker_pool_size, fun worker_pool_size/1},
@ -61,7 +67,19 @@ fields("creation_opts") ->
{enable_queue, fun enable_queue/1}, {enable_queue, fun enable_queue/1},
{max_buffer_bytes, fun max_buffer_bytes/1}, {max_buffer_bytes, fun max_buffer_bytes/1},
{buffer_seg_bytes, fun buffer_seg_bytes/1} {buffer_seg_bytes, fun buffer_seg_bytes/1}
]. ],
Overrides
).
override([], _) ->
[];
override([{Name, Sc} | Rest], Overrides) ->
case lists:keyfind(Name, 1, Overrides) of
{Name, Override} ->
[{Name, hocon_schema:override(Sc, Override)} | override(Rest, Overrides)];
false ->
[{Name, Sc} | override(Rest, Overrides)]
end.
resource_opts_meta() -> resource_opts_meta() ->
#{ #{
@ -142,6 +160,7 @@ request_ttl(_) -> undefined.
enable_batch(type) -> boolean(); enable_batch(type) -> boolean();
enable_batch(required) -> false; enable_batch(required) -> false;
enable_batch(default) -> true; enable_batch(default) -> true;
enable_batch(importance) -> ?IMPORTANCE_HIDDEN;
enable_batch(deprecated) -> {since, "v5.0.14"}; enable_batch(deprecated) -> {since, "v5.0.14"};
enable_batch(desc) -> ?DESC("enable_batch"); enable_batch(desc) -> ?DESC("enable_batch");
enable_batch(_) -> undefined. enable_batch(_) -> undefined.
@ -169,6 +188,7 @@ batch_size(_) -> undefined.
batch_time(type) -> emqx_schema:timeout_duration_ms(); batch_time(type) -> emqx_schema:timeout_duration_ms();
batch_time(desc) -> ?DESC("batch_time"); batch_time(desc) -> ?DESC("batch_time");
batch_time(default) -> ?DEFAULT_BATCH_TIME_RAW; batch_time(default) -> ?DEFAULT_BATCH_TIME_RAW;
batch_time(importance) -> ?IMPORTANCE_LOW;
batch_time(required) -> false; batch_time(required) -> false;
batch_time(_) -> undefined. batch_time(_) -> undefined.

View File

@ -82,9 +82,9 @@
-callback clean(context()) -> ok. -callback clean(context()) -> ok.
-callback size(context()) -> non_neg_integer(). -callback size(context()) -> non_neg_integer().
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Hook API %% Hook API
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec on_session_subscribed(_, _, emqx_types:subopts(), _) -> any(). -spec on_session_subscribed(_, _, emqx_types:subopts(), _) -> any().
on_session_subscribed(_, _, #{share := ShareName}, _) when ShareName =/= undefined -> on_session_subscribed(_, _, #{share := ShareName}, _) when ShareName =/= undefined ->
ok; ok;
@ -118,9 +118,9 @@ on_message_publish(Msg = #message{flags = #{retain := true}}, Context) ->
on_message_publish(Msg, _) -> on_message_publish(Msg, _) ->
{ok, Msg}. {ok, Msg}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% APIs %% APIs
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% @doc Start the retainer %% @doc Start the retainer
-spec start_link() -> emqx_types:startlink_ret(). -spec start_link() -> emqx_types:startlink_ret().
@ -169,9 +169,9 @@ call(Req) ->
stats_fun() -> stats_fun() ->
gen_server:cast(?MODULE, ?FUNCTION_NAME). gen_server:cast(?MODULE, ?FUNCTION_NAME).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% APIs %% APIs
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec get_basic_usage_info() -> #{retained_messages => non_neg_integer()}. -spec get_basic_usage_info() -> #{retained_messages => non_neg_integer()}.
get_basic_usage_info() -> get_basic_usage_info() ->
@ -183,9 +183,9 @@ get_basic_usage_info() ->
#{retained_messages => 0} #{retained_messages => 0}
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
init([]) -> init([]) ->
erlang:process_flag(trap_exit, true), erlang:process_flag(trap_exit, true),
@ -248,9 +248,9 @@ terminate(_Reason, #{clear_timer := ClearTimer}) ->
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
-spec new_state() -> state(). -spec new_state() -> state().
new_state() -> new_state() ->
#{ #{

View File

@ -18,6 +18,7 @@
-behaviour(gen_server). -behaviour(gen_server).
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
-behaiour(emqx_config_backup).
-include("rule_engine.hrl"). -include("rule_engine.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -78,6 +79,11 @@
code_change/3 code_change/3
]). ]).
%% Data backup
-export([
import_config/1
]).
-define(RULE_ENGINE, ?MODULE). -define(RULE_ENGINE, ?MODULE).
-define(T_CALL, infinity). -define(T_CALL, infinity).
@ -105,7 +111,7 @@
start_link() -> start_link() ->
gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []). gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []).
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% The config handler for emqx_rule_engine %% The config handler for emqx_rule_engine
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
post_config_update(?RULE_PATH(RuleId), _Req, NewRule, undefined, _AppEnvs) -> post_config_update(?RULE_PATH(RuleId), _Req, NewRule, undefined, _AppEnvs) ->
@ -142,9 +148,9 @@ post_config_update([rule_engine], _Req, #{rules := NewRules}, #{rules := OldRule
{error, Error} {error, Error}
end. end.
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% APIs for rules %% APIs for rules
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
-spec load_rules() -> ok. -spec load_rules() -> ok.
load_rules() -> load_rules() ->
@ -185,9 +191,9 @@ delete_rule(RuleId) when is_binary(RuleId) ->
insert_rule(Rule) -> insert_rule(Rule) ->
gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL). gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL).
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% Rule Management %% Rule Management
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
-spec get_rules() -> [rule()]. -spec get_rules() -> [rule()].
get_rules() -> get_rules() ->
@ -301,9 +307,9 @@ unload_hooks_for_rule(#{id := Id, from := Topics}) ->
Topics Topics
). ).
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% Telemetry helper functions %% Telemetry helper functions
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
-spec get_basic_usage_info() -> -spec get_basic_usage_info() ->
#{ #{
@ -362,9 +368,27 @@ tally_referenced_bridges(BridgeIDs, Acc0) ->
BridgeIDs BridgeIDs
). ).
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% Data backup
%%----------------------------------------------------------------------------------------
import_config(#{<<"rule_engine">> := #{<<"rules">> := NewRules} = RuleEngineConf}) ->
OldRules = emqx:get_raw_config(?KEY_PATH, #{}),
RuleEngineConf1 = RuleEngineConf#{<<"rules">> => maps:merge(OldRules, NewRules)},
case emqx_conf:update([rule_engine], RuleEngineConf1, #{override_to => cluster}) of
{ok, #{raw_config := #{<<"rules">> := NewRawRules}}} ->
Changed = maps:get(changed, emqx_utils_maps:diff_maps(NewRawRules, OldRules)),
ChangedPaths = [?RULE_PATH(Id) || Id <- maps:keys(Changed)],
{ok, #{root_key => rule_engine, changed => ChangedPaths}};
Error ->
{error, #{root_key => rule_engine, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => rule_engine, changed => []}}.
%%----------------------------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
init([]) -> init([]) ->
_TableId = ets:new(?KV_TAB, [ _TableId = ets:new(?KV_TAB, [
@ -404,9 +428,9 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
%% Internal Functions %% Internal Functions
%%------------------------------------------------------------------------------ %%----------------------------------------------------------------------------------------
parse_and_insert(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt) -> parse_and_insert(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt) ->
case emqx_rule_sqlparser:parse(Sql) of case emqx_rule_sqlparser:parse(Sql) of

View File

@ -56,7 +56,9 @@
safe_to_existing_atom/2, safe_to_existing_atom/2,
pub_props_to_packet/1, pub_props_to_packet/1,
safe_filename/1, safe_filename/1,
diff_lists/3 diff_lists/3,
merge_lists/3,
tcp_keepalive_opts/4
]). ]).
-export([ -export([
@ -487,6 +489,26 @@ safe_to_existing_atom(Atom, _Encoding) when is_atom(Atom) ->
safe_to_existing_atom(_Any, _Encoding) -> safe_to_existing_atom(_Any, _Encoding) ->
{error, invalid_type}. {error, invalid_type}.
-spec tcp_keepalive_opts(term(), non_neg_integer(), non_neg_integer(), non_neg_integer()) ->
{ok, [{keepalive, true} | {raw, non_neg_integer(), non_neg_integer(), binary()}]}
| {error, {unsupported_os, term()}}.
tcp_keepalive_opts({unix, linux}, Idle, Interval, Probes) ->
{ok, [
{keepalive, true},
{raw, 6, 4, <<Idle:32/native>>},
{raw, 6, 5, <<Interval:32/native>>},
{raw, 6, 6, <<Probes:32/native>>}
]};
tcp_keepalive_opts({unix, darwin}, Idle, Interval, Probes) ->
{ok, [
{keepalive, true},
{raw, 6, 16#10, <<Idle:32/native>>},
{raw, 6, 16#101, <<Interval:32/native>>},
{raw, 6, 16#102, <<Probes:32/native>>}
]};
tcp_keepalive_opts(OS, _Idle, _Interval, _Probes) ->
{error, {unsupported_os, OS}}.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal Functions %% Internal Functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -578,15 +600,18 @@ try_to_existing_atom(Convert, Data, Encoding) ->
_:Reason -> {error, Reason} _:Reason -> {error, Reason}
end. end.
is_sensitive_key(token) -> true;
is_sensitive_key("token") -> true;
is_sensitive_key(<<"token">>) -> true;
is_sensitive_key(authorization) -> true; is_sensitive_key(authorization) -> true;
is_sensitive_key("authorization") -> true; is_sensitive_key("authorization") -> true;
is_sensitive_key(<<"authorization">>) -> true; is_sensitive_key(<<"authorization">>) -> true;
is_sensitive_key(aws_secret_access_key) -> true;
is_sensitive_key("aws_secret_access_key") -> true;
is_sensitive_key(<<"aws_secret_access_key">>) -> true;
is_sensitive_key(password) -> true; is_sensitive_key(password) -> true;
is_sensitive_key("password") -> true; is_sensitive_key("password") -> true;
is_sensitive_key(<<"password">>) -> true; is_sensitive_key(<<"password">>) -> true;
is_sensitive_key('proxy-authorization') -> true;
is_sensitive_key("proxy-authorization") -> true;
is_sensitive_key(<<"proxy-authorization">>) -> true;
is_sensitive_key(secret) -> true; is_sensitive_key(secret) -> true;
is_sensitive_key("secret") -> true; is_sensitive_key("secret") -> true;
is_sensitive_key(<<"secret">>) -> true; is_sensitive_key(<<"secret">>) -> true;
@ -596,9 +621,9 @@ is_sensitive_key(<<"secret_key">>) -> true;
is_sensitive_key(security_token) -> true; is_sensitive_key(security_token) -> true;
is_sensitive_key("security_token") -> true; is_sensitive_key("security_token") -> true;
is_sensitive_key(<<"security_token">>) -> true; is_sensitive_key(<<"security_token">>) -> true;
is_sensitive_key(aws_secret_access_key) -> true; is_sensitive_key(token) -> true;
is_sensitive_key("aws_secret_access_key") -> true; is_sensitive_key("token") -> true;
is_sensitive_key(<<"aws_secret_access_key">>) -> true; is_sensitive_key(<<"token">>) -> true;
is_sensitive_key(_) -> false. is_sensitive_key(_) -> false.
redact(Term) -> redact(Term) ->
@ -709,9 +734,14 @@ redact_test_() ->
Types = [atom, string, binary], Types = [atom, string, binary],
Keys = [ Keys = [
token, authorization,
aws_secret_access_key,
password, password,
secret 'proxy-authorization',
secret,
secret_key,
security_token,
token
], ],
[{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types]. [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
@ -819,6 +849,42 @@ diff_lists(New, Old, KeyFunc) when is_list(New) andalso is_list(Old) ->
changed => lists:reverse(Changed) changed => lists:reverse(Changed)
}. }.
%% @doc Merges two lists preserving the original order of elements in both lists.
%% KeyFunc must extract a unique key from each element.
%% If two keys exist in both lists, the value in List1 is superseded by the value in List2, but
%% the element position in the result list will equal its position in List1.
%% Example:
%% emqx_utils:merge_append_lists(
%% [#{id => a, val => old}, #{id => b, val => old}],
%% [#{id => a, val => new}, #{id => c}, #{id => b, val => new}, #{id => d}],
%% fun(#{id := Id}) -> Id end).
%% [#{id => a,val => new},
%% #{id => b,val => new},
%% #{id => c},
%% #{id => d}]
-spec merge_lists(list(T), list(T), KeyFunc) -> list(T) when
KeyFunc :: fun((T) -> any()),
T :: any().
merge_lists(List1, List2, KeyFunc) ->
WithKeysList2 = lists:map(fun(E) -> {KeyFunc(E), E} end, List2),
WithKeysList1 = lists:map(
fun(E) ->
K = KeyFunc(E),
case lists:keyfind(K, 1, WithKeysList2) of
false -> {K, E};
WithKey1 -> WithKey1
end
end,
List1
),
NewWithKeysList2 = lists:filter(
fun({K, _}) ->
not lists:keymember(K, 1, WithKeysList1)
end,
WithKeysList2
),
[E || {_, E} <- WithKeysList1 ++ NewWithKeysList2].
search(_ExpectValue, _KeyFunc, []) -> search(_ExpectValue, _KeyFunc, []) ->
false; false;
search(ExpectValue, KeyFunc, [Item | List]) -> search(ExpectValue, KeyFunc, [Item | List]) ->

View File

@ -26,6 +26,33 @@
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
Root = ?config(data_dir, Config),
D1 = filename:join([Root, "nonempty", "d1/"]),
D2 = filename:join([Root, "nonempty", "d2/"]),
F1 = filename:join([D1, "1"]),
F2 = filename:join([D1, "2"]),
DeepDir = filename:join([Root, "nonempty", "d2", "deep", "down/"]),
DeepFile = filename:join([DeepDir, "here"]),
Files = [F1, F2, DeepFile],
lists:foreach(fun filelib:ensure_dir/1, Files),
D1LinkMutrec = filename:join([D1, "mutrec"]),
D2LinkMutrec = filename:join([D2, "deep", "mutrec"]),
lists:foreach(fun(File) -> file:write_file(File, <<"">>, [write]) end, Files),
chmod_file(D1, 8#00777),
chmod_file(DeepFile, 8#00600),
make_symlink(DeepDir, D1LinkMutrec),
%% can't file:make_link("../../d1", D2Mutrec) on mac, return {error, eperm}
make_symlink("../../d1", D2LinkMutrec),
{ok, D2MutrecInfo} = file:read_link_info(D2LinkMutrec),
ct:pal("~ts 's file_info is ~p~n", [D2LinkMutrec, D2MutrecInfo]),
Config.
end_per_suite(Config) ->
Root = ?config(data_dir, Config),
ok = file:del_dir_r(filename:join([Root, "nonempty"])),
ok.
%% %%
t_traverse_dir(Config) -> t_traverse_dir(Config) ->
@ -115,3 +142,11 @@ t_canonicalize_non_utf8(_) ->
badarg, badarg,
emqx_utils_fs:canonicalize(<<128, 128, 128>>) emqx_utils_fs:canonicalize(<<128, 128, 128>>)
). ).
chmod_file(File, Mode) ->
{ok, FileInfo} = file:read_file_info(File),
ok = file:write_file_info(File, FileInfo#file_info{mode = Mode}).
make_symlink(FileOrDir, NewLink) ->
_ = file:delete(NewLink),
ok = file:make_symlink(FileOrDir, NewLink).

View File

@ -1 +0,0 @@
../d2/deep/down

View File

@ -0,0 +1,4 @@
Implement configuration and user data import/export CLI.
The `emqx ctl export` and `emqx ctl import` commands allow to export configuration and built-in database
data from a running EMQX cluster and later import it to the same or another running EMQX cluster.

View File

@ -0,0 +1,5 @@
Allow 'enable' as well as 'enabled' as the state flag for listeners.
Prior to this change, listener can be enable/disabled by setting the 'true' or 'false' on the 'enabled' config.
This is slightly different naming comparing to other state flags in the system.
No the 'enable' flag is added as an aliase on listeners.

View File

@ -0,0 +1,3 @@
Adds support for unlimited max connections for gateway listeners by allowing
infinity as a valid value for the `max_connections` field in the configuration
and HTTP API

View File

@ -0,0 +1 @@
Fix the issue where the `enable_qos` option does not take effect in the MQTT-SN gateway.

View File

@ -0,0 +1 @@
Fix the issue in MQTT-SN gateway where deleting Predefined Topics configuration does not work.

Some files were not shown because too many files have changed in this diff Show More