emqx/apps/emqx_license/src/emqx_license.erl

186 lines
6.1 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license).
-include("emqx_license.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(emqx_config_handler).
-behaviour(emqx_config_backup).
-export([
pre_config_update/3,
post_config_update/5
]).
-export([
load/0,
check/2,
unload/0,
read_license/0,
read_license/1,
update_key/1,
update_setting/1
]).
-export([import_config/1]).
-define(CONF_KEY_PATH, [license]).
%% Give the license app the highest priority.
%% We don't define it in the emqx_hooks.hrl becasue that is an opensource code
%% and can be changed by the communitiy.
-define(HP_LICENSE, 2000).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
-spec read_license() -> {ok, emqx_license_parser:license()} | {error, term()}.
read_license() ->
read_license(emqx:get_config(?CONF_KEY_PATH)).
-spec load() -> ok.
load() ->
emqx_license_cli:load(),
emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
add_license_hook().
-spec unload() -> ok.
unload() ->
%% Delete the hook. This means that if the user calls
%% `application:stop(emqx_license).` from the shell, then here should no limitations!
del_license_hook(),
emqx_conf:remove_handler(?CONF_KEY_PATH),
emqx_license_cli:unload().
-spec update_key(binary() | string()) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
update_key(Value) when is_binary(Value); is_list(Value) ->
Result = exec_config_update({key, Value}),
handle_config_update_result(Result).
update_setting(Setting) when is_map(Setting) ->
Result = exec_config_update({setting, Setting}),
handle_config_update_result(Result).
exec_config_update(Param) ->
emqx_conf:update(
?CONF_KEY_PATH,
Param,
#{rawconf_with_defaults => true, override_to => cluster}
).
%%------------------------------------------------------------------------------
%% emqx_hooks
%%------------------------------------------------------------------------------
check(_ConnInfo, AckProps) ->
case emqx_license_checker:limits() of
{ok, #{max_connections := ?ERR_EXPIRED}} ->
?SLOG(error, #{msg => "connection_rejected_due_to_license_expired"}, #{tag => "LICENSE"}),
{stop, {error, ?RC_QUOTA_EXCEEDED}};
{ok, #{max_connections := MaxClients}} ->
case check_max_clients_exceeded(MaxClients) of
true ->
?SLOG_THROTTLE(
error,
#{msg => connection_rejected_due_to_license_limit_reached},
#{tag => "LICENSE"}
),
{stop, {error, ?RC_QUOTA_EXCEEDED}};
false ->
{ok, AckProps}
end;
{error, Reason} ->
?SLOG(
error,
#{
msg => "connection_rejected_due_to_license_not_loaded",
reason => Reason
},
#{tag => "LICENSE"}
),
{stop, {error, ?RC_QUOTA_EXCEEDED}}
end.
import_config(#{<<"license">> := Config}) ->
OldConf = emqx:get_config(?CONF_KEY_PATH),
case exec_config_update(Config) of
{ok, #{config := NewConf}} ->
Changed = maps:get(changed, emqx_utils_maps:diff_maps(NewConf, OldConf)),
Changed1 = lists:map(fun(Key) -> [license, Key] end, maps:keys(Changed)),
{ok, #{root_key => license, changed => Changed1}};
Error ->
{error, #{root_key => license, reason => Error}}
end;
import_config(_RawConf) ->
{ok, #{root_key => license, changed => []}}.
%%------------------------------------------------------------------------------
%% emqx_config_handler callbacks
%%------------------------------------------------------------------------------
pre_config_update(_, Cmd, Conf) ->
{ok, do_update(Cmd, Conf)}.
post_config_update(_Path, {setting, _}, NewConf, _Old, _AppEnvs) ->
{ok, NewConf};
post_config_update(_Path, _Cmd, NewConf, _Old, _AppEnvs) ->
case read_license(NewConf) of
{ok, License} ->
{ok, emqx_license_checker:update(License)};
{error, _} = Error ->
Error
end.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
add_license_hook() ->
ok = emqx_hooks:put('client.connect', {?MODULE, check, []}, ?HP_LICENSE).
del_license_hook() ->
_ = emqx_hooks:del('client.connect', {?MODULE, check, []}),
ok.
do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
case emqx_license_parser:parse(Content) of
{ok, _License} ->
Conf#{<<"key">> => Content};
{error, Reason} ->
erlang:throw(Reason)
end;
do_update({setting, Setting0}, Conf) ->
#{<<"key">> := Key} = Conf,
%% only allow updating dynamic_max_connections when it's BUSINESS_CRITICAL
Setting =
case emqx_license_parser:is_business_critical(Key) of
true ->
Setting0;
false ->
maps:without([<<"dynamic_max_connections">>], Setting0)
end,
maps:merge(Conf, Setting);
do_update(NewConf, _PrevConf) ->
#{<<"key">> := NewKey} = NewConf,
do_update({key, NewKey}, NewConf).
check_max_clients_exceeded(MaxClients) ->
emqx_license_resources:connection_count() > MaxClients * 1.1.
read_license(#{key := Content}) ->
emqx_license_parser:parse(Content).
handle_config_update_result({error, {post_config_update, ?MODULE, Error}}) ->
{error, Error};
handle_config_update_result({error, _} = Error) ->
Error;
handle_config_update_result({ok, #{post_config_update := #{emqx_license := Result}}}) ->
{ok, Result}.