feat(license): add license application

This commit is contained in:
Ilya Averyanov 2022-01-19 19:05:12 +03:00
parent bede3443a3
commit 17599432d1
48 changed files with 2302 additions and 106 deletions

View File

@ -40,6 +40,8 @@ jobs:
run: ./scripts/ensure-rebar3.sh 3.16.1-emqx-1
- name: check applications
run: ./scripts/check-elixir-applications.exs
- name: check applications started with emqx_machine
run: ./scripts/check-elixir-emqx-machine-boot-discrepancies.exs
env:
EMQX_RELEASE_TYPE: ${{ matrix.release_type }}
EMQX_PACKAGE_TYPE: ${{ matrix.package_type }}

View File

@ -6,6 +6,7 @@
{emqx_dashboard,1}.
{emqx_exhook,1}.
{emqx_gateway_cm,1}.
{emqx_license,1}.
{emqx_management,1}.
{emqx_mgmt_trace,1}.
{emqx_persistent_session,1}.

View File

@ -32,12 +32,17 @@ end_per_suite(_Config) ->
"If this test suite failed, and you are unsure why, read this:~n"
"https://github.com/emqx/emqx/blob/master/apps/emqx/src/bpapi/README.md", []).
check_if_versions_consistent(OldData, NewData) ->
%% OldData can contain a wider list of BPAPI versions
%% than the release being checked.
[] =:= NewData -- OldData.
t_run_check(_) ->
try
{ok, OldData} = file:consult(emqx_bpapi_static_checks:versions_file()),
?assert(emqx_bpapi_static_checks:run()),
{ok, NewData} = file:consult(emqx_bpapi_static_checks:versions_file()),
OldData =:= NewData orelse
check_if_versions_consistent(OldData, NewData) orelse
begin
logger:critical(
"BPAPI versions were changed, but not committed to the repo.\n"

View File

@ -113,6 +113,14 @@ node {
## Default: 23
backtrace_depth = 23
## Comma-separated list of applications to start with emqx_machine.
## These applications may restart on cluster leave/join.
##
## @doc node.applications
## ValueType: String
## Default: "gproc, esockd, ranch, cowboy, emqx"
applications = "{{ emqx_machine_boot_apps }}"
cluster_call {
retry_interval = 1s
max_history = 100

View File

@ -35,9 +35,15 @@ stop(_State) ->
init_conf() ->
{ok, TnxId} = copy_override_conf_from_core_node(),
emqx_app:set_init_tnx_id(TnxId),
emqx_config:init_load(emqx_conf_schema),
emqx_config:init_load(schema_module()),
emqx_app:set_init_config_load_done().
schema_module() ->
case os:getenv("SCHEMA_MOD") of
false -> emqx_conf_schema;
Value -> list_to_existing_atom(Value)
end.
copy_override_conf_from_core_node() ->
case nodes() of
[] -> %% The first core nodes is self.

View File

@ -306,6 +306,11 @@ a crash dump
#{ mapping => "emqx_machine.backtrace_depth"
, default => 23
})}
, {"applications",
sc(emqx_schema:comma_separated_atoms(),
#{ mapping => "emqx_machine.applications"
, default => []
})}
, {"etc_dir",
sc(string(),
#{ desc => "`etc` dir for the node"

View File

@ -26,6 +26,9 @@
-export([sorted_reboot_apps/1]).
-endif.
%% these apps are always (re)started by emqx_machine
-define(BASIC_REBOOT_APPS, [gproc, esockd, ranch, cowboy, emqx]).
post_boot() ->
ok = ensure_apps_started(),
ok = print_vsn(),
@ -80,29 +83,12 @@ start_one_app(App) ->
%% list of app names which should be rebooted when:
%% 1. due to static config change
%% 2. after join a cluster
%% the list of (re)started apps depends on release type/edition
%% and is configured in rebar.config.erl/mix.exs
reboot_apps() ->
[ gproc
, esockd
, ranch
, cowboy
, emqx
, emqx_prometheus
, emqx_modules
, emqx_dashboard
, emqx_connector
, emqx_gateway
, emqx_statsd
, emqx_resource
, emqx_rule_engine
, emqx_bridge
, emqx_plugin_libs
, emqx_management
, emqx_retainer
, emqx_exhook
, emqx_authn
, emqx_authz
, emqx_plugins
].
{ok, Apps} = application:get_env(emqx_machine, applications),
?BASIC_REBOOT_APPS ++ Apps.
sorted_reboot_apps() ->
Apps = [{App, app_deps(App)} || App <- reboot_apps()],

View File

@ -42,8 +42,24 @@ init_per_suite(Config) ->
%% Unload emqx_authz to avoid reboot this application
%%
application:unload(emqx_authz),
emqx_common_test_helpers:start_apps([emqx_conf]),
application:set_env(emqx_machine, applications, [ emqx_prometheus
, emqx_modules
, emqx_dashboard
, emqx_connector
, emqx_gateway
, emqx_statsd
, emqx_resource
, emqx_rule_engine
, emqx_bridge
, emqx_plugin_libs
, emqx_management
, emqx_retainer
, emqx_exhook
, emqx_authn
, emqx_authz
, emqx_plugin
]),
Config.
end_per_suite(_Config) ->

View File

@ -17,11 +17,11 @@ ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)"
export RUNNER_ROOT_DIR
export RUNNER_ETC_DIR
export REL_VSN
export SCHEMA_MOD
RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME"
CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"
REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN"
SCHEMA_MOD=emqx_conf_schema
WHOAMI=$(whoami)
@ -389,7 +389,7 @@ generate_config() {
## meaning, certain overrides will not be mapped to app.<time>.config file
## disable SC2086 to allow EMQX_LICENSE_CONF_OPTION to split
# shellcheck disable=SC2086
call_hocon -v -t "$NOW_TIME" -I "$CONFIGS_DIR/" -s $SCHEMA_MOD -c "$RUNNER_ETC_DIR"/emqx.conf $EMQX_LICENSE_CONF_OPTION -d "$RUNNER_DATA_DIR"/configs generate
call_hocon -v -t "$NOW_TIME" -I "$CONFIGS_DIR/" -s "$SCHEMA_MOD" -c "$RUNNER_ETC_DIR"/emqx.conf $EMQX_LICENSE_CONF_OPTION -d "$RUNNER_DATA_DIR"/configs generate
## filenames are per-hocon convention
local CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
@ -539,7 +539,7 @@ NAME="${EMQX_NODE__NAME:-}"
if [ -z "$NAME" ]; then
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
# for boot commands, inspect emqx.conf for node name
NAME="$(call_hocon -s $SCHEMA_MOD -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.name | tr -d \")"
NAME="$(call_hocon -s "$SCHEMA_MOD" -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.name | tr -d \")"
else
vm_args_file="$(latest_vm_args 'EMQX_NODE__NAME')"
NAME="$(grep -E '^-s?name' "${vm_args_file}" | awk '{print $2}')"
@ -570,7 +570,7 @@ fi
COOKIE="${EMQX_NODE__COOKIE:-}"
if [ -z "$COOKIE" ]; then
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
COOKIE="$(call_hocon -s $SCHEMA_MOD -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie | tr -d \")"
COOKIE="$(call_hocon -s "$SCHEMA_MOD" -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie | tr -d \")"
else
vm_args_file="$(latest_vm_args 'EMQX_NODE__COOKIE')"
COOKIE="$(grep -E '^-setcookie' "${vm_args_file}" | awk '{print $2}')"

19
lib-ee/emqx_enterprise_conf/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
*.iml
rebar3.crashdump
*~

View File

@ -0,0 +1,3 @@
# emqx_enterprise_conf
EMQ X Enterprise configuration schema

View File

@ -0,0 +1,2 @@
{erl_opts, [debug_info]}.
{deps, []}.

View File

@ -0,0 +1,14 @@
{application, emqx_enterprise_conf,
[{description, "EMQ X Enterprise configuration schema"},
{vsn, "0.1.0"},
{registered, []},
{applications,
[kernel,
stdlib
]},
{env,[]},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.

View File

@ -0,0 +1,32 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_enterprise_conf_schema).
-behaviour(hocon_schema).
-export([namespace/0, roots/0, fields/1, translations/0, translation/1]).
-define(EE_SCHEMA_MODULES, [emqx_license_schema
]).
namespace() ->
emqx_conf_schema:namespace().
roots() ->
lists:foldl(
fun(Module, Roots) ->
Roots ++ Module:roots()
end,
emqx_conf_schema:roots(),
?EE_SCHEMA_MODULES).
fields(Name) ->
emqx_conf_schema:fields(Name).
translations() ->
emqx_conf_schema:translations().
translation(Name) ->
emqx_conf_schema:translation(Name).

View File

@ -0,0 +1,46 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_enterprise_conf_schema_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_namespace(_Config) ->
?assertEqual(
emqx_conf_schema:namespace(),
emqx_enterprise_conf_schema:namespace()).
t_roots(_Config) ->
BaseRoots = emqx_conf_schema:roots(),
EnterpriseRoots = emqx_enterprise_conf_schema:roots(),
?assertEqual([], BaseRoots -- EnterpriseRoots),
?assert(lists:any(
fun({license, _}) -> true;
(_) -> false
end,
EnterpriseRoots)).
t_fields(_Config) ->
?assertEqual(
emqx_conf_schema:fields("node"),
emqx_enterprise_conf_schema:fields("node")).
t_translations(_Config) ->
[Root | _] = emqx_enterprise_conf_schema:translations(),
?assertEqual(
emqx_conf_schema:translation(Root),
emqx_enterprise_conf_schema:translation(Root)).

17
lib-ee/emqx_license/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
.eunit
deps
*.o
*.beam
*.plt
erl_crash.dump
ebin
rel/example_project
.concrete/DEV_MODE
.rebar
.DS_Store
.erlang.mk/
emqx_license.d
erlang.mk
_build/
rebar.lock
rebar3.crashdump

View File

@ -0,0 +1,3 @@
# emqx_license
EMQ X 5.0 License Manager.

View File

@ -0,0 +1,3 @@
license {
key = "MjIwMTExCjAKMTAKRm9vCmNvbnRhY3RAZm9vLmNvbQoyMDIyMDExMQoxMDAwMDAKMTAK.Iyle9eMrXSAZwJczR8MEI2dtpxLuL2OKRikTwYvFK/SgxfwZQLR7JJM2rKfkuT5eP4cxh0Y1+84hOoB7fj/MWA=="
}

View File

@ -0,0 +1,38 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQ X License Management CLI.
%%--------------------------------------------------------------------
-ifndef(_EMQX_LICENSE_).
-define(_EMQX_LICENSE_, true).
-define(EVALUATION_LOG,
"\n"
"===============================================================================\n"
"This is an evaluation license that is restricted to 10 concurrent connections.\n"
"If you already have a paid license, please apply it now.\n"
"Or you could visit https://emqx.com/apply-licenses/emqx to get a trial license.\n"
"===============================================================================\n"
).
-define(EXPIRY_LOG,
"\n"
"======================================================\n"
"Your license has expired.\n"
"Please visit https://emqx.com/apply-licenses/emqx or\n"
"contact our customer services for an updated license.\n"
"======================================================\n"
).
-define(OFFICIAL, 1).
-define(TRIAL, 0).
-define(SMALL_CUSTOMER, 0).
-define(MEDIUM_CUSTOMER, 1).
-define(LARGE_CUSTOMER, 2).
-define(EVALUATION_CUSTOMER, 10).
-define(EXPIRED_DAY, -90).
-endif.

View File

@ -0,0 +1 @@
{deps, [{emqx, {path, "../../apps/emqx"}}]}.

View File

@ -0,0 +1,7 @@
{application,emqx_license,
[{description,"EMQ X License"},
{vsn,"5.0.0"},
{modules,[]},
{registered,[emqx_license_sup]},
{applications,[kernel,stdlib]},
{mod,{emqx_license_app,[]}}]}.

View File

@ -0,0 +1,144 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license).
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(emqx_config_handler).
-export([pre_config_update/3,
post_config_update/5
]).
-export([load/0,
check/2,
unload/0,
read_license/0,
update_file/1,
update_key/1]).
-define(CONF_KEY_PATH, [license]).
%%------------------------------------------------------------------------------
%% 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_file(binary() | string()) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
update_file(Filename) when is_binary(Filename); is_list(Filename) ->
Result = emqx_conf:update(
?CONF_KEY_PATH,
{file, Filename},
#{rawconf_with_defaults => true, override_to => local}),
handle_config_update_result(Result).
-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 = emqx_conf:update(
?CONF_KEY_PATH,
{key, Value},
#{rawconf_with_defaults => true, override_to => cluster}),
handle_config_update_result(Result).
%%------------------------------------------------------------------------------
%% emqx_hooks
%%------------------------------------------------------------------------------
check(_ConnInfo, AckProps) ->
#{max_connections := MaxClients} = emqx_license_checker:limits(),
case MaxClients of
0 ->
?SLOG(error, #{msg => "Connection rejected due to the license expiration"}),
{stop, {error, ?RC_QUOTA_EXCEEDED}};
_ ->
case check_max_clients_exceeded(MaxClients) of
true ->
?SLOG(error, #{msg => "Connection rejected due to max clients limitation"}),
{stop, {error, ?RC_QUOTA_EXCEEDED}};
false ->
{ok, AckProps}
end
end.
%%------------------------------------------------------------------------------
%% emqx_config_handler callbacks
%%------------------------------------------------------------------------------
pre_config_update(_, Cmd, Conf) ->
{ok, do_update(Cmd, Conf)}.
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, []}).
del_license_hook() ->
_ = emqx_hooks:del('client.connect', {?MODULE, check, []}),
ok.
do_update({file, Filename}, _Conf) ->
case file:read_file(Filename) of
{ok, Content} ->
case emqx_license_parser:parse(Content) of
{ok, _License} ->
#{<<"file">> => Filename};
{error, Reason} ->
error(Reason)
end;
{error, Reason} ->
error({invalid_license_file, Reason})
end;
do_update({key, Content}, _Conf) when is_binary(Content); is_list(Content) ->
case emqx_license_parser:parse(Content) of
{ok, _License} ->
#{<<"key">> => Content};
{error, Reason} ->
error(Reason)
end.
check_max_clients_exceeded(MaxClients) ->
emqx_license_resources:connection_count() > MaxClients * 1.1.
read_license(#{file := Filename}) ->
case file:read_file(Filename) of
{ok, Content} -> emqx_license_parser:parse(Content);
{error, _} = Error -> Error
end;
read_license(#{key := Content}) ->
emqx_license_parser:parse(Content).
handle_config_update_result({error, _} = Error) -> Error;
handle_config_update_result({ok, #{post_config_update := #{emqx_license := Result}}}) -> {ok, Result}.

View File

@ -0,0 +1,20 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQ X License Management Application.
%%--------------------------------------------------------------------
-module(emqx_license_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_Type, _Args) ->
ok = emqx_license:load(),
{ok, Sup} = emqx_license_sup:start_link(),
{ok, Sup}.
stop(_State) ->
ok = emqx_license:unload(),
ok.

View File

@ -0,0 +1,141 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_checker).
-include("emqx_license.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-define(CHECK_INTERVAL, 5000).
-export([start_link/1,
start_link/2,
update/1,
dump/0,
limits/0]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
-type limits() :: #{max_connections := non_neg_integer()}.
-spec start_link(emqx_license_parser:license()) -> {ok, pid()}.
start_link(LicenseFetcher) ->
start_link(LicenseFetcher, ?CHECK_INTERVAL).
-spec start_link(emqx_license_parser:license(), timeout()) -> {ok, pid()}.
start_link(LicenseFetcher, CheckInterval) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
-spec update(emqx_license_parser:license()) -> ok.
update(License) ->
gen_server:call(?MODULE, {update, License}).
-spec dump() -> [{atom(), term()}].
dump() ->
gen_server:call(?MODULE, dump).
-spec limits() -> limits().
limits() ->
try ets:lookup(?MODULE, limits) of
[{limits, Limits}] -> Limits;
_ -> default_limits()
catch
error:badarg -> default_limits()
end.
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([LicenseFetcher, CheckInterval]) ->
case LicenseFetcher() of
{ok, License} ->
_ = ets:new(?MODULE, [set, protected, named_table]),
#{} = check_license(License),
State = ensure_timer(#{check_license_interval => CheckInterval,
license => License}),
{ok, State};
{error, _} = Error ->
Error
end.
handle_call({update, License}, _From, State) ->
{reply, check_license(License), State#{license => License}};
handle_call(dump, _From, #{license := License} = State) ->
{reply, emqx_license_parser:dump(License), State};
handle_call(_Req, _From, State) ->
{reply, unknown, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(check_license, #{license := License} = State) ->
#{} = check_license(License),
NewState = ensure_timer(State),
?tp(debug, emqx_license_checked, #{}),
{noreply, NewState};
handle_info(_Msg, State) ->
{noreply, State}.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
ensure_timer(#{check_license_interval := CheckInterval} = State) ->
_ = case State of
#{timer := Timer} -> erlang:cancel_timer(Timer);
_ -> ok
end,
State#{timer => erlang:send_after(CheckInterval, self(), check_license)}.
check_license(License) ->
NeedRestrict = need_restrict(License),
Limits = limits(License, NeedRestrict),
true = apply_limits(Limits),
#{warn_evaluation => warn_evaluation(License, NeedRestrict),
warn_expiry => warn_expiry(License, NeedRestrict)}.
warn_evaluation(License, false) ->
emqx_license_parser:customer_type(License) == ?EVALUATION_CUSTOMER;
warn_evaluation(_License, _NeedRestrict) -> false.
warn_expiry(_License, NeedRestrict) -> NeedRestrict.
limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)};
limits(_License, true) -> #{max_connections => 0}.
default_limits() -> #{max_connections => 0}.
days_left(License) ->
DateEnd = emqx_license_parser:expiry_date(License),
{DateNow, _} = calendar:universal_time(),
calendar:date_to_gregorian_days(DateEnd) - calendar:date_to_gregorian_days(DateNow).
need_restrict(License)->
DaysLeft = days_left(License),
CType = emqx_license_parser:customer_type(License),
Type = emqx_license_parser:license_type(License),
DaysLeft < 0
andalso (Type =/= ?OFFICIAL) or small_customer_overexpired(CType, DaysLeft).
small_customer_overexpired(?SMALL_CUSTOMER, DaysLeft)
when DaysLeft < ?EXPIRED_DAY -> true;
small_customer_overexpired(_CType, _DaysLeft) -> false.
apply_limits(Limits) ->
ets:insert(?MODULE, {limits, Limits}).

View File

@ -0,0 +1,77 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_cli).
-include("emqx_license.hrl").
-export([load/0, license/1, unload/0, print_warnings/1]).
-define(PRINT_MSG(Msg), io:format(Msg)).
-define(PRINT(Format, Args), io:format(Format, Args)).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
load() ->
ok = emqx_ctl:register_command(license, {?MODULE, license}, []).
license(["reload"]) ->
case emqx:get_config([license]) of
#{file := Filename} ->
license(["reload", Filename]);
#{key := _Key} ->
?PRINT_MSG("License is not configured as a file, please specify file explicitly~n")
end;
license(["reload", Filename]) ->
case emqx_license:update_file(Filename) of
{ok, Warnings} ->
ok = print_warnings(Warnings),
ok = ?PRINT_MSG("ok~n");
{error, Reason} -> ?PRINT("Error: ~p~n", [Reason])
end;
license(["update", EncodedLicense]) ->
case emqx_license:update_key(EncodedLicense) of
{ok, Warnings} ->
ok = print_warnings(Warnings),
ok = ?PRINT_MSG("ok~n");
{error, Reason} -> ?PRINT("Error: ~p~n", [Reason])
end;
license(["info"]) ->
lists:foreach(fun({K, V}) when is_binary(V); is_atom(V); is_list(V) ->
?PRINT("~-16s: ~s~n", [K, V]);
({K, V}) ->
?PRINT("~-16s: ~p~n", [K, V])
end, emqx_license_checker:dump());
license(_) ->
emqx_ctl:usage(
[ {"license info", "Show license info"},
{"license reload [<File>]", "Reload license from a file specified with an absolute path"},
{"license update License", "Update license given as a string"}
]).
unload() ->
ok = emqx_ctl:unregister_command(license).
print_warnings(Warnings) ->
ok = print_evaluation_warning(Warnings),
ok = print_expiry_warning(Warnings).
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
print_evaluation_warning(#{warn_evaluation := true}) ->
?PRINT_MSG(?EVALUATION_LOG);
print_evaluation_warning(_) -> ok.
print_expiry_warning(#{warn_expiry := true}) ->
?PRINT_MSG(?EXPIRY_LOG);
print_expiry_warning(_) -> ok.

View File

@ -0,0 +1,81 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_installer).
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-export([start_link/1,
start_link/4]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2]).
-define(NAME, emqx).
-define(INTERVAL, 5000).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
start_link(Callback) ->
start_link(?NAME, ?MODULE, ?INTERVAL, Callback).
start_link(Name, ServerName, Interval, Callback) ->
gen_server:start_link({local, ServerName}, ?MODULE, [Name, Interval, Callback], []).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([Name, Interval, Callback]) ->
Pid = whereis(Name),
State = #{interval => Interval,
name => Name,
pid => Pid,
callback => Callback
},
{ok, ensure_timer(State)}.
handle_call(_Req, _From, State) ->
{reply, unknown, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({timeout, Timer, check_pid}, #{timer := Timer} = State) ->
NewState = check_pid(State),
{noreply, ensure_timer(NewState)};
handle_info(_Msg, State) ->
{noreply, State}.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
ensure_timer(#{interval := Interval} = State) ->
_ = case State of
#{timer := Timer} -> erlang:cancel_timer(Timer);
_ -> ok
end,
State#{timer => erlang:start_timer(Interval, self(), check_pid)}.
check_pid(#{name := Name, pid := OldPid, callback := Callback} = State) ->
case whereis(Name) of
undefined ->
?tp(debug, emqx_license_installer_noproc, #{pid => OldPid}),
State;
OldPid ->
?tp(debug, emqx_license_installer_nochange, #{pid => OldPid}),
State;
NewPid ->
_ = Callback(),
?tp(debug, emqx_license_installer_called, #{pid => OldPid}),
State#{pid => NewPid}
end.

View File

@ -0,0 +1,114 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQ X License Management.
%%--------------------------------------------------------------------
-module(emqx_license_parser).
-include_lib("emqx/include/logger.hrl").
-include("emqx_license.hrl").
-define(PUBKEY, <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>).
-define(LICENSE_PARSE_MODULES, [emqx_license_parser_v20220101
]).
-type license_data() :: term().
-type customer_type() :: ?SMALL_CUSTOMER |
?MEDIUM_CUSTOMER |
?LARGE_CUSTOMER |
?EVALUATION_CUSTOMER.
-type license_type() :: ?OFFICIAL | ?TRIAL.
-type license() :: #{module := module(), data := license_data()}.
-export_type([license_data/0,
customer_type/0,
license_type/0,
license/0]).
-export([parse/1,
parse/2,
dump/1,
customer_type/1,
license_type/1,
expiry_date/1,
max_connections/1
]).
-ifdef(TEST).
-export([public_key/0
]).
-endif.
%%--------------------------------------------------------------------
%% Behaviour
%%--------------------------------------------------------------------
-callback parse(string() | binary(), binary()) -> {ok, license_data()} | {error, term()}.
-callback dump(license_data()) -> list({atom(), term()}).
-callback customer_type(license_data()) -> customer_type().
-callback license_type(license_data()) -> license_type().
-callback expiry_date(license_data()) -> calendar:date().
-callback max_connections(license_data()) -> non_neg_integer().
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
parse(Content) ->
DecodedKey = base64:decode(public_key()),
parse(Content, DecodedKey).
parse(Content, Key) ->
do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
-spec dump(license()) -> list({atom(), term()}).
dump(#{module := Module, data := LicenseData}) ->
Module:dump(LicenseData).
-spec customer_type(license()) -> customer_type().
customer_type(#{module := Module, data := LicenseData}) ->
Module:customer_type(LicenseData).
-spec license_type(license()) -> license_type().
license_type(#{module := Module, data := LicenseData}) ->
Module:license_type(LicenseData).
-spec expiry_date(license()) -> calendar:date().
expiry_date(#{module := Module, data := LicenseData}) ->
Module:expiry_date(LicenseData).
-spec max_connections(license()) -> non_neg_integer().
max_connections(#{module := Module, data := LicenseData}) ->
Module:max_connections(LicenseData).
%%--------------------------------------------------------------------
%% Private functions
%%--------------------------------------------------------------------
do_parse(_Content, _Key, [], Errors) ->
{error, {unknown_format, lists:reverse(Errors)}};
do_parse(Content, Key, [Module | Modules], Errors) ->
try Module:parse(Content, Key) of
{ok, LicenseData} ->
{ok, #{module => Module, data => LicenseData}};
{error, Error} ->
do_parse(Content, Key, Modules, [{Module, Error} | Errors])
catch
_Class:Error:_Stk ->
do_parse(Content, Key, Modules, [{Module, Error} | Errors])
end.
public_key() -> ?PUBKEY.

View File

@ -0,0 +1,148 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_parser_v20220101).
-behaviour(emqx_license_parser).
-include_lib("emqx/include/logger.hrl").
-include("emqx_license.hrl").
-define(DIGEST_TYPE, sha256).
-define(LICENSE_VERSION, <<"220111">>).
-export([parse/2,
dump/1,
customer_type/1,
license_type/1,
expiry_date/1,
max_connections/1]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
parse(Content, Key) ->
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
Payload = base64:decode(EncodedPayload),
Signature = base64:decode(EncodedSignature),
case verify_signature(Payload, Signature, Key) of
true -> parse_payload(Payload);
false -> {error, invalid_signature}
end.
dump(#{type := Type,
customer_type := CType,
customer := Customer,
email := Email,
date_start := DateStart,
max_connections := MaxConns} = License) ->
DateExpiry = expiry_date(License),
{DateNow, _} = calendar:universal_time(),
Expiry = DateNow > DateExpiry,
[{customer, Customer},
{email, Email},
{max_connections, MaxConns},
{start_at, format_date(DateStart)},
{expiry_at, format_date(DateExpiry)},
{type, format_type(Type)},
{customer_type, CType},
{expiry, Expiry}].
customer_type(#{customer_type := CType}) -> CType.
license_type(#{type := Type}) -> Type.
expiry_date(#{date_start := DateStart, days := Days}) ->
calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(DateStart) + Days).
max_connections(#{max_connections := MaxConns}) ->
MaxConns.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
verify_signature(Payload, Signature, Key) ->
RSAPublicKey = public_key:der_decode('RSAPublicKey', Key),
public_key:verify(Payload, ?DIGEST_TYPE, Signature, RSAPublicKey).
parse_payload(Payload) ->
Lines = lists:map(
fun string:trim/1,
string:split(string:trim(Payload), <<"\n">>, all)),
case Lines of
[?LICENSE_VERSION, Type, CType, Customer, Email, DateStart, Days, MaxConns] ->
collect_fields([{type, parse_type(Type)},
{customer_type, parse_customer_type(CType)},
{customer, {ok, Customer}},
{email, {ok, Email}},
{date_start, parse_date_start(DateStart)},
{days, parse_days(Days)},
{max_connections, parse_max_connections(MaxConns)}]);
[_Version, _Type, _CType, _Customer, _Email, _DateStart, _Days, _MaxConns] ->
{error, invalid_version};
_ ->
{error, invalid_field_number}
end.
parse_type(TypeStr) ->
case string:to_integer(TypeStr) of
{Type, <<"">>} -> {ok, Type};
_ -> {error, invalid_license_type}
end.
parse_customer_type(CTypeStr) ->
case string:to_integer(CTypeStr) of
{CType, <<"">>} -> {ok, CType};
_ -> {error, invalid_customer_type}
end.
parse_date_start(<<Y:4/binary, M:2/binary, D:2/binary>>) ->
Date = list_to_tuple([N || {N, <<>>} <- [string:to_integer(S) || S <- [Y, M, D]]]),
case calendar:valid_date(Date) of
true -> {ok, Date};
false -> {error, invalid_date}
end;
parse_date_start(_) -> {error, invalid_date}.
parse_days(DaysStr) ->
case string:to_integer(DaysStr) of
{Days, <<"">>} when Days > 0 -> {ok, Days};
_ -> {error, invalid_int_value}
end.
parse_max_connections(MaxConnStr) ->
case string:to_integer(MaxConnStr) of
{MaxConns, <<"">>} when MaxConns > 0 -> {ok, MaxConns};
_ -> {error, invalid_int_value}
end.
collect_fields(Fields) ->
Collected = lists:foldl(
fun({Name, {ok, Value}}, {FieldValues, Errors}) ->
{[{Name, Value} | FieldValues], Errors};
({Name, {error, Reason}}, {FieldValues, Errors}) ->
{FieldValues, [{Name, Reason} | Errors]}
end,
{[], []},
Fields),
case Collected of
{FieldValues, []} ->
{ok, maps:from_list(FieldValues)};
{_, Errors} ->
{error, lists:reverse(Errors)}
end.
format_date({Year, Month, Day}) ->
iolist_to_binary(
io_lib:format(
"~4..0w-~2..0w-~2..0w",
[Year, Month, Day])).
format_type(?OFFICIAL) -> <<"official">>;
format_type(?TRIAL) -> <<"trial">>.

View File

@ -0,0 +1,98 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_resources).
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-define(CHECK_INTERVAL, 5000).
-export([start_link/0,
start_link/1,
local_connection_count/0,
connection_count/0]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
-spec start_link() -> {ok, pid()}.
start_link() ->
start_link(?CHECK_INTERVAL).
-spec start_link(timeout()) -> {ok, pid()}.
start_link(CheckInterval) when is_integer(CheckInterval) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [CheckInterval], []).
-spec local_connection_count() -> non_neg_integer().
local_connection_count() ->
emqx_cm:get_connected_client_count().
-spec connection_count() -> non_neg_integer().
connection_count() ->
local_connection_count() + cached_remote_connection_count().
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([CheckInterval]) ->
_ = ets:new(?MODULE, [set, protected, named_table]),
State = ensure_timer(#{check_peer_interval => CheckInterval}),
{ok, State}.
handle_call(_Req, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(update_resources, State) ->
true = update_resources(),
?tp(debug, emqx_license_resources_updated, #{}),
{noreply, ensure_timer(State)}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
cached_remote_connection_count() ->
try ets:lookup(?MODULE, remote_connection_count) of
[{remote_connection_count, N}] -> N;
_ -> 0
catch
error:badarg -> 0
end.
update_resources() ->
ets:insert(?MODULE, {remote_connection_count, remote_connection_count()}).
ensure_timer(#{check_peer_interval := CheckInterval} = State) ->
_ = case State of
#{timer := Timer} -> erlang:cancel_timer(Timer);
_ -> ok
end,
State#{timer => erlang:send_after(CheckInterval, self(), update_resources)}.
remote_connection_count() ->
Nodes = mria_mnesia:running_nodes() -- [node()],
Results = emqx_license_proto_v1:remote_connection_counts(Nodes),
Counts = [Count || {ok, Count} <- Results],
lists:sum(Counts).

View File

@ -0,0 +1,27 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_schema).
-include_lib("typerefl/include/types.hrl").
%%------------------------------------------------------------------------------
%% hocon_schema callbacks
%%------------------------------------------------------------------------------
-behaviour(hocon_schema).
-export([roots/0, fields/1]).
roots() -> [{license, hoconsc:union(
[hoconsc:ref(?MODULE, key_license),
hoconsc:ref(?MODULE, file_license)])}].
fields(key_license) ->
[ {key, string()}
];
fields(file_license) ->
[ {file, string()}
].

View File

@ -0,0 +1,43 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQ X License Management Supervisor.
%%--------------------------------------------------------------------
-module(emqx_license_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, {#{strategy => one_for_one,
intensity => 10,
period => 100},
[#{id => license_checker,
start => {emqx_license_checker, start_link, [fun emqx_license:read_license/0]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_license_checker]},
#{id => license_resources,
start => {emqx_license_resources, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_license_resources]},
#{id => license_installer,
start => {emqx_license_installer, start_link, [fun emqx_license:load/0]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_license_installer]}
]}}.

View File

@ -0,0 +1,24 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_proto_v1).
-behaviour(emqx_bpapi).
-include_lib("emqx/include/bpapi.hrl").
-export([ introduced_in/0
]).
-export([ remote_connection_counts/1
]).
-define(TIMEOUT, 500).
introduced_in() ->
"5.0.0".
-spec remote_connection_counts(list(node())) -> list({atom(), term()}).
remote_connection_counts(Nodes) ->
erpc:multicall(Nodes, emqx_license_resources, local_connection_count, [], ?TIMEOUT).

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKHM3qUJR23ixg9CZYFgDdvxmTzsl+T7
UDXNxN36UJTRN+9eFIeARbPXw2jiUva/Wf7vQKhilzZ/N+E0mK2CdX0CAwEAAQ==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAKHM3qUJR23ixg9CZYFgDdvxmTzsl+T7UDXNxN36UJTRN+9eFIeA
RbPXw2jiUva/Wf7vQKhilzZ/N+E0mK2CdX0CAwEAAQJBAJCy2UKbA8hgEGTBKmoD
byGN9U8o/8aGgns7pJ4oKDyNWwM6Z3/omObDSTDcKn8Mfo26ccHUprIh+eiUW7TX
F4ECIQDMfCREBKniVK1yDZgqKFe1+uZqj7ylT1DQne2S9bn2UQIhAMqP3TIAED3C
MUfF3AN9oVDKJ/SFhQSKqI38XBmw9QVtAiEAqq801lHOPE3SOVF/ojDqhcxYaLpy
DMqX+orYs8LI5wECIQC/5tuf6v94Aum9HW36wKJ7b4m61mPWkaZuHY8Dp+n5YQIg
MrcXYujtNHEMWidC8S3ca1Ytp8kjMNcZVIil5CroP8E=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,168 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
emqx_config:save_schema_mod_and_names(emqx_license_schema),
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
emqx_common_test_helpers:stop_apps([emqx_license]),
ok.
init_per_testcase(Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
meck:new(emqx_license_parser, [passthrough]),
meck:expect(emqx_license_parser, public_key, fun public_key/0),
set_invalid_license_file(Case),
Config.
end_per_testcase(Case, _Config) ->
meck:unload(emqx_license_parser),
restore_valid_license_file(Case),
ok.
set_invalid_license_file(t_read_license_from_invalid_file) ->
Config = #{file => "/invalid/file"},
emqx_config:put([license], Config);
set_invalid_license_file(_) ->
ok.
restore_valid_license_file(t_read_license_from_invalid_file) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config);
restore_valid_license_file(_) ->
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config),
RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
emqx_config:put_raw([<<"license">>], RawConfig);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_update_file(_Config) ->
?assertMatch(
{error, {invalid_license_file, enoent}},
emqx_license:update_file("/unknown/path")),
ok = file:write_file("license_with_invalid_content.lic", <<"bad license">>),
?assertMatch(
{error, {unknown_format, _}},
emqx_license:update_file("license_with_invalid_content.lic")),
?assertMatch(
{ok, #{}},
emqx_license:update_file(emqx_license_test_lib:default_license())).
t_update_value(_Config) ->
?assertMatch(
{error, {unknown_format, _}},
emqx_license:update_key("invalid.license")),
{ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),
?assertMatch(
{ok, #{}},
emqx_license:update_key(LicenseValue)).
t_read_license_from_invalid_file(_Config) ->
?assertMatch(
{error, enoent},
emqx_license:read_license()).
t_check_exceeded(_Config) ->
License = mk_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]),
#{} = emqx_license_checker:update(License),
ok = lists:foreach(
fun(_) ->
{ok, C} = emqtt:start_link(),
{ok, _} = emqtt:connect(C)
end,
lists:seq(1, 12)),
?assertEqual(
{stop, {error, ?RC_QUOTA_EXCEEDED}},
emqx_license:check(#{}, #{})).
t_check_ok(_Config) ->
License = mk_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]),
#{} = emqx_license_checker:update(License),
ok = lists:foreach(
fun(_) ->
{ok, C} = emqtt:start_link(),
{ok, _} = emqtt:connect(C)
end,
lists:seq(1, 11)),
?assertEqual(
{ok, #{}},
emqx_license:check(#{}, #{})).
t_check_expired(_Config) ->
License = mk_license(
["220111",
"1", %% Official customer
"0", %% Small customer
"Foo",
"contact@foo.com",
"20210101", %% Expired long ago
"10",
"10"]),
#{} = emqx_license_checker:update(License),
?assertEqual(
{stop, {error, ?RC_QUOTA_EXCEEDED}},
emqx_license:check(#{}, #{})).
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
mk_license(Fields) ->
EncodedLicense = emqx_license_test_lib:make_license(Fields),
{ok, License} = emqx_license_parser:parse(
EncodedLicense,
emqx_license_test_lib:public_key_encoded()),
License.
public_key() -> <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>.

View File

@ -0,0 +1,208 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_checker_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
ok = emqx_common_test_helpers:stop_apps([emqx_license]).
init_per_testcase(t_default_limits, Config) ->
ok = emqx_common_test_helpers:stop_apps([emqx_license]),
Config;
init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(t_default_limits, _Config) ->
ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1);
end_per_testcase(_Case, _Config) ->
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_default_limits(_Config) ->
?assertMatch(#{max_connections := 0}, emqx_license_checker:limits()).
t_dump(_Config) ->
License = mk_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]),
#{} = emqx_license_checker:update(License),
?assertEqual(
[{customer,<<"Foo">>},
{email,<<"contact@foo.com">>},
{max_connections,10},
{start_at,<<"2022-01-11">>},
{expiry_at,<<"2295-10-27">>},
{type,<<"trial">>},
{customer_type,10},
{expiry,false}],
emqx_license_checker:dump()).
t_update(_Config) ->
License = mk_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"123"]),
#{} = emqx_license_checker:update(License),
?assertMatch(
#{max_connections := 123},
emqx_license_checker:limits()).
t_update_by_timer(_Config) ->
?check_trace(
begin
?wait_async_action(
begin
erlang:send(
emqx_license_checker,
check_license)
end,
#{?snk_kind := emqx_license_checked},
1000)
end,
fun(Trace) ->
?assertMatch([_ | _], ?of_kind(emqx_license_checked, Trace))
end).
t_expired_trial(_Config) ->
{NowDate, _} = calendar:universal_time(),
Date10DaysAgo = calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(NowDate) - 10),
License = mk_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
format_date(Date10DaysAgo),
"1",
"123"]),
#{} = emqx_license_checker:update(License),
?assertMatch(
#{max_connections := 0},
emqx_license_checker:limits()).
t_overexpired_small_client(_Config) ->
{NowDate, _} = calendar:universal_time(),
Date100DaysAgo = calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(NowDate) - 100),
License = mk_license(
["220111",
"1",
"0",
"Foo",
"contact@foo.com",
format_date(Date100DaysAgo),
"1",
"123"]),
#{} = emqx_license_checker:update(License),
?assertMatch(
#{max_connections := 0},
emqx_license_checker:limits()).
t_overexpired_medium_client(_Config) ->
{NowDate, _} = calendar:universal_time(),
Date100DaysAgo = calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(NowDate) - 100),
License = mk_license(
["220111",
"1",
"1",
"Foo",
"contact@foo.com",
format_date(Date100DaysAgo),
"1",
"123"]),
#{} = emqx_license_checker:update(License),
?assertMatch(
#{max_connections := 123},
emqx_license_checker:limits()).
t_recently_expired_small_client(_Config) ->
{NowDate, _} = calendar:universal_time(),
Date10DaysAgo = calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(NowDate) - 10),
License = mk_license(
["220111",
"1",
"0",
"Foo",
"contact@foo.com",
format_date(Date10DaysAgo),
"1",
"123"]),
#{} = emqx_license_checker:update(License),
?assertMatch(
#{max_connections := 123},
emqx_license_checker:limits()).
t_unknown_calls(_Config) ->
ok = gen_server:cast(emqx_license_checker, some_cast),
some_msg = erlang:send(emqx_license_checker, some_msg),
?assertEqual(unknown, gen_server:call(emqx_license_checker, some_request)).
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
mk_license(Fields) ->
EncodedLicense = emqx_license_test_lib:make_license(Fields),
{ok, License} = emqx_license_parser:parse(
EncodedLicense,
emqx_license_test_lib:public_key_encoded()),
License.
format_date({Year, Month, Day}) ->
lists:flatten(
io_lib:format(
"~4..0w~2..0w~2..0w",
[Year, Month, Day])).

View File

@ -0,0 +1,72 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_cli_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
emqx_config:save_schema_mod_and_names(emqx_license_schema),
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
emqx_common_test_helpers:stop_apps([emqx_license]),
ok.
init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
meck:new(emqx_license_parser, [passthrough]),
meck:expect(emqx_license_parser, public_key, fun public_key/0),
Config.
end_per_testcase(_Case, _Config) ->
meck:unload(emqx_license_parser),
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config),
RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
emqx_config:put_raw([<<"license">>], RawConfig);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_help(_Config) ->
_ = emqx_license_cli:license([]).
t_info(_Config) ->
_ = emqx_license_cli:license(["info"]).
t_reload(_Config) ->
_ = emqx_license_cli:license(["reload", "/invalid/path"]),
_ = emqx_license_cli:license(["reload", emqx_license_test_lib:default_license()]),
_ = emqx_license_cli:license(["reload"]).
t_update(_Config) ->
{ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),
_ = emqx_license_cli:license(["update", LicenseValue]),
_ = emqx_license_cli:license(["reload"]),
_ = emqx_license_cli:license(["update", "Invalid License Value"]).
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
public_key() -> <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>.
digest() -> <<"3jHg0zCb4NL5v8eIoKn+CNDMq8A04mXEOefqlUBSSVs=">>.

View File

@ -0,0 +1,81 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_installer_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
emqx_common_test_helpers:stop_apps([emqx_license]),
ok.
init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(_Case, _Config) ->
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_update(_Config) ->
?check_trace(
begin
?wait_async_action(
begin
Pid0 = spawn_link(fun() -> receive exit -> ok end end),
register(installer_test, Pid0),
{ok, _} = emqx_license_installer:start_link(
installer_test,
?MODULE,
10,
fun() -> ok end),
{ok, _} = ?block_until(
#{?snk_kind := emqx_license_installer_nochange},
100),
Pid0 ! exit,
{ok, _} = ?block_until(
#{?snk_kind := emqx_license_installer_noproc},
100),
Pid1 = spawn_link(fun() -> timer:sleep(100) end),
register(installer_test, Pid1)
end,
#{?snk_kind := emqx_license_installer_called},
1000)
end,
fun(Trace) ->
?assertMatch([_ | _], ?of_kind(emqx_license_installer_called, Trace))
end).
t_unknown_calls(_Config) ->
ok = gen_server:cast(emqx_license_installer, some_cast),
some_msg = erlang:send(emqx_license_installer, some_msg),
?assertEqual(unknown, gen_server:call(emqx_license_installer, some_request)).

View File

@ -0,0 +1,197 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_parser_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
emqx_common_test_helpers:stop_apps([emqx_license]),
ok.
init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(_Case, _Config) ->
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_parse(_Config) ->
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_encoded())),
%% invalid version
?assertMatch(
{error,
{unknown_format,
[{emqx_license_parser_v20220101,invalid_version}]}},
emqx_license_parser:parse(
emqx_license_test_lib:make_license(
["220101",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"
]),
public_key_encoded())),
%% invalid field number
?assertMatch(
{error,
{unknown_format,
[{emqx_license_parser_v20220101,invalid_field_number}]}},
emqx_license_parser:parse(
emqx_license_test_lib:make_license(
["220111",
"0",
"10",
"Foo", "Bar",
"contact@foo.com",
"20220111",
"100000",
"10"
]),
public_key_encoded())),
?assertMatch(
{error,
{unknown_format,
[{emqx_license_parser_v20220101,
[{type,invalid_license_type},
{customer_type,invalid_customer_type},
{date_start,invalid_date},
{days,invalid_int_value}]}]}},
emqx_license_parser:parse(
emqx_license_test_lib:make_license(
["220111",
"zero",
"ten",
"Foo",
"contact@foo.com",
"20220231",
"-10",
"10"
]),
public_key_encoded())),
%% invalid signature
[LicensePart, _] = binary:split(
emqx_license_test_lib:make_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]),
<<".">>),
[_, SignaturePart] = binary:split(
emqx_license_test_lib:make_license(
["220111",
"1",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]),
<<".">>),
?assertMatch(
{error,
{unknown_format,
[{emqx_license_parser_v20220101,invalid_signature}]}},
emqx_license_parser:parse(
iolist_to_binary([LicensePart, <<".">>, SignaturePart]),
public_key_encoded())),
%% totally invalid strings as license
?assertMatch(
{error, {unknown_format, _}},
emqx_license_parser:parse(
<<"badlicense">>,
public_key_encoded())),
?assertMatch(
{error, {unknown_format, _}},
emqx_license_parser:parse(
<<"bad.license">>,
public_key_encoded())).
t_dump(_Config) ->
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
?assertEqual(
[{customer,<<"Foo">>},
{email,<<"contact@foo.com">>},
{max_connections,10},
{start_at,<<"2022-01-11">>},
{expiry_at,<<"2295-10-27">>},
{type,<<"trial">>},
{customer_type,10},
{expiry,false}],
emqx_license_parser:dump(License)).
t_customer_type(_Config) ->
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
?assertEqual(10, emqx_license_parser:customer_type(License)).
t_license_type(_Config) ->
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
?assertEqual(0, emqx_license_parser:license_type(License)).
t_max_connections(_Config) ->
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
?assertEqual(10, emqx_license_parser:max_connections(License)).
t_expiry_date(_Config) ->
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
?assertEqual({2295,10,27}, emqx_license_parser:expiry_date(License)).
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
public_key_encoded() ->
emqx_license_test_lib:public_key_encoded().
sample_license() ->
emqx_license_test_lib:make_license(
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]).

View File

@ -0,0 +1,85 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_resources_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
_ = application:load(emqx_conf),
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
end_per_suite(_) ->
emqx_common_test_helpers:stop_apps([emqx_license]),
ok.
init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(_Case, _Config) ->
ok.
set_special_configs(emqx_license) ->
Config = #{file => emqx_license_test_lib:default_license()},
emqx_config:put([license], Config);
set_special_configs(_) -> ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_connection_count(_Config) ->
?check_trace(
begin
?wait_async_action(
whereis(emqx_license_resources) ! update_resources,
#{?snk_kind := emqx_license_resources_updated},
1000),
emqx_license_resources:connection_count()
end,
fun(ConnCount, Trace) ->
?assertEqual(0, ConnCount),
?assertMatch([_ | _], ?of_kind(emqx_license_resources_updated, Trace))
end),
meck:new(emqx_cm, [passthrough]),
meck:expect(emqx_cm, get_connected_client_count, fun() -> 10 end),
meck:new(emqx_license_proto_v1, [passthrough]),
meck:expect(
emqx_license_proto_v1,
remote_connection_counts,
fun(_Nodes) ->
[{ok, 5}, {error, some_error}]
end),
?check_trace(
begin
?wait_async_action(
whereis(emqx_license_resources) ! update_resources,
#{?snk_kind := emqx_license_resources_updated},
1000),
emqx_license_resources:connection_count()
end,
fun(ConnCount, _Trace) ->
?assertEqual(15, ConnCount)
end),
meck:unload(emqx_license_proto_v1),
meck:unload(emqx_cm).
t_emqx_license_proto(_Config) ->
?assert("5.0.0" =< emqx_license_proto_v1:introduced_in()).

View File

@ -0,0 +1,50 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_test_lib).
-compile(nowarn_export_all).
-compile(export_all).
-define(DEFAULT_LICENSE_VALUES,
["220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"]).
-define(DEFAULT_LICENSE_FILE, "emqx.lic").
private_key() ->
test_key("pvt.key").
public_key() ->
test_key("pub.pem").
public_key_encoded() ->
public_key:der_encode('RSAPublicKey', public_key()).
test_key(Filename) ->
Dir = code:lib_dir(emqx_license, test),
Path = filename:join([Dir, "data", Filename]),
{ok, KeyData} = file:read_file(Path),
[PemEntry] = public_key:pem_decode(KeyData),
Key = public_key:pem_entry_decode(PemEntry),
Key.
make_license(Values) ->
Key = private_key(),
Text = string:join(Values, "\n"),
EncodedText = base64:encode(Text),
Signature = public_key:sign(Text, sha256, Key),
EncodedSignature = base64:encode(Signature),
iolist_to_binary([EncodedText, ".", EncodedSignature]).
default_license() ->
License = make_license(?DEFAULT_LICENSE_VALUES),
ok = file:write_file(?DEFAULT_LICENSE_FILE, License),
?DEFAULT_LICENSE_FILE.

128
mix.exs
View File

@ -132,7 +132,7 @@ defmodule EMQXUmbrella.MixProject do
end
[
applications: applications(release_type),
applications: applications(release_type, edition_type),
skip_mode_validation_for: [
:emqx_gateway,
:emqx_dashboard,
@ -154,7 +154,7 @@ defmodule EMQXUmbrella.MixProject do
]
end
def applications(release_type) do
def applications(release_type, edition_type) do
[
crypto: :permanent,
public_key: :permanent,
@ -200,12 +200,52 @@ defmodule EMQXUmbrella.MixProject do
] ++
if(enable_quicer?(), do: [quicer: :permanent], else: []) ++
if(enable_bcrypt?(), do: [bcrypt: :permanent], else: []) ++
if(edition_type == :enterprise,
do: [
emqx_enterprise_conf: :load,
emqx_license: :permanent
],
else: []
) ++
if(release_type == :cloud,
do: [xmerl: :permanent, observer: :load],
else: []
)
end
def emqx_machine_boot_apps(:community) do
[
:emqx_prometheus,
:emqx_modules,
:emqx_dashboard,
:emqx_connector,
:emqx_gateway,
:emqx_statsd,
:emqx_resource,
:emqx_rule_engine,
:emqx_bridge,
:emqx_plugin_libs,
:emqx_management,
:emqx_retainer,
:emqx_exhook,
:emqx_authn,
:emqx_authz,
:emqx_plugin
]
end
def emqx_machine_boot_apps(:enterprise) do
emqx_machine_boot_apps(:community) ++
[]
end
defp emqx_machine_boot_app_list(edition_type) do
edition_type
|> emqx_machine_boot_apps()
|> Enum.map(&Atom.to_string/1)
|> Enum.join(", ")
end
def check_profile!() do
valid_envs = [
:dev,
@ -314,24 +354,24 @@ defmodule EMQXUmbrella.MixProject do
# This is generated by `scripts/merge-config.escript` or `make
# conf-segs`. So, this should be run before the release.
# TODO: run as a "compiler" step???
conf_rendered =
File.read!("apps/emqx_conf/etc/emqx.conf.all")
|> from_rebar_to_eex_template()
|> EEx.eval_string(assigns)
File.write!(
Path.join(etc, "emqx.conf"),
conf_rendered
render_template(
"apps/emqx_conf/etc/emqx.conf.all",
assigns,
Path.join(etc, "emqx.conf")
)
vars_rendered =
File.read!("rel/emqx_vars")
|> from_rebar_to_eex_template()
|> EEx.eval_string(assigns)
if edition_type == :enterprise do
render_template(
"apps/emqx_conf/etc/emqx_enterprise.conf.all",
assigns,
Path.join(etc, "emqx_enterprise.conf")
)
end
File.write!(
Path.join([release.path, "releases", "emqx_vars"]),
vars_rendered
render_template(
"rel/emqx_vars",
assigns,
Path.join([release.path, "releases", "emqx_vars"])
)
vm_args_template_path =
@ -343,19 +383,13 @@ defmodule EMQXUmbrella.MixProject do
"apps/emqx/etc/emqx_edge/vm.args"
end
vm_args_rendered =
File.read!(vm_args_template_path)
|> from_rebar_to_eex_template()
|> EEx.eval_string(assigns)
File.write!(
Path.join(etc, "vm.args"),
vm_args_rendered
)
File.write!(
Path.join(release.version_path, "vm.args"),
vm_args_rendered
render_template(
vm_args_template_path,
assigns,
[
Path.join(etc, "vm.args"),
Path.join(release.version_path, "vm.args")
]
)
for name <- [
@ -383,19 +417,30 @@ defmodule EMQXUmbrella.MixProject do
File.chmod!(Path.join(bin, name), 0o755)
end
built_on_rendered =
File.read!("rel/BUILT_ON")
|> from_rebar_to_eex_template()
|> EEx.eval_string(assigns)
File.write!(
Path.join([release.version_path, "BUILT_ON"]),
built_on_rendered
render_template(
"rel/BUILT_ON",
assigns,
Path.join(release.version_path, "BUILT_ON")
)
release
end
defp render_template(template, assigns, target) when is_binary(target) do
render_template(template, assigns, [target])
end
defp render_template(template, assigns, tartgets) when is_list(tartgets) do
rendered =
File.read!(template)
|> from_rebar_to_eex_template()
|> EEx.eval_string(assigns)
for target <- tartgets do
File.write!(target, rendered)
end
end
# needed by nodetool and by release_handler
defp create_RELEASES(release) do
apps =
@ -487,6 +532,8 @@ defmodule EMQXUmbrella.MixProject do
# FIXME: this is empty in `make emqx` ???
erl_opts: "",
emqx_description: emqx_description(release_type, edition_type),
emqx_schema_mod: emqx_schema_mod(edition_type),
emqx_machine_boot_apps: emqx_machine_boot_app_list(edition_type),
built_on_arch: built_on(),
is_elixir: "yes"
]
@ -513,6 +560,8 @@ defmodule EMQXUmbrella.MixProject do
erl_opts: "",
emqx_description: emqx_description(release_type, edition_type),
built_on_arch: built_on(),
emqx_schema_mod: emqx_schema_mod(edition_type),
emqx_machine_boot_apps: emqx_machine_boot_app_list(edition_type),
is_elixir: "yes"
]
end
@ -530,6 +579,9 @@ defmodule EMQXUmbrella.MixProject do
end
end
defp emqx_schema_mod(:enterprise), do: :emqx_enterprise_conf_schema
defp emqx_schema_mod(:community), do: :emqx_conf_schema
defp bcrypt_dep() do
if enable_bcrypt?(),
do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],

View File

@ -222,8 +222,10 @@ emqx_description(cloud, ee) -> "EMQ X Enterprise Edition";
emqx_description(cloud, ce) -> "EMQ X Community Edition";
emqx_description(edge, ce) -> "EMQ X Edge Edition".
overlay_vars(RelType, PkgType, _Edition) ->
overlay_vars_rel(RelType) ++ overlay_vars_pkg(PkgType).
overlay_vars(RelType, PkgType, Edition) ->
overlay_vars_rel(RelType)
++ overlay_vars_pkg(PkgType)
++ overlay_vars_edition(Edition).
%% vars per release type, cloud or edge
overlay_vars_rel(RelType) ->
@ -235,6 +237,15 @@ overlay_vars_rel(RelType) ->
[ {vm_args_file, VmArgs}
].
overlay_vars_edition(ce) ->
[ {emqx_schema_mod, emqx_conf_schema}
, {emqx_machine_boot_apps, emqx_machine_boot_app_list(ce)}
];
overlay_vars_edition(ee) ->
[ {emqx_schema_mod, emqx_enterprise_conf_schema}
, {emqx_machine_boot_apps, emqx_machine_boot_app_list(ee)}
].
%% vars per packaging type, bin(zip/tar.gz/docker) or pkg(rpm/deb)
overlay_vars_pkg(bin) ->
[ {platform_bin_dir, "bin"}
@ -316,10 +327,7 @@ relx_apps(ReleaseType, Edition) ->
%++ [emqx_license || is_enterprise(Edition)]
++ [bcrypt || provide_bcrypt_release(ReleaseType)]
++ relx_apps_per_rel(ReleaseType)
%% NOTE: applications below are only loaded after node start/restart
%% TODO: Add loaded/unloaded state to plugin apps
%% then we can always start plugin apps
++ [{N, load} || N <- relx_plugin_apps(ReleaseType, Edition)].
++ relx_additional_apps(ReleaseType, Edition).
relx_apps_per_rel(cloud) ->
[ xmerl
@ -335,19 +343,49 @@ is_app(Name) ->
_ -> false
end.
relx_plugin_apps(ReleaseType, Edition) ->
relx_additional_apps(ReleaseType, Edition) ->
relx_plugin_apps_per_rel(ReleaseType)
++ relx_plugin_apps_enterprise(Edition).
++ relx_apps_per_edition(Edition).
relx_plugin_apps_per_rel(cloud) ->
[];
relx_plugin_apps_per_rel(edge) ->
[].
relx_plugin_apps_enterprise(ee) ->
[list_to_atom(A) || A <- filelib:wildcard("*", "lib-ee"),
filelib:is_dir(filename:join(["lib-ee", A]))];
relx_plugin_apps_enterprise(ce) -> [].
relx_apps_per_edition(ee) ->
[ emqx_license
, {emqx_enterprise_conf, load}
];
relx_apps_per_edition(ce) -> [].
emqx_machine_boot_apps(ce) ->
[ emqx_prometheus
, emqx_modules
, emqx_dashboard
, emqx_connector
, emqx_gateway
, emqx_statsd
, emqx_resource
, emqx_rule_engine
, emqx_bridge
, emqx_plugin_libs
, emqx_management
, emqx_retainer
, emqx_exhook
, emqx_authn
, emqx_authz
, emqx_plugin
];
emqx_machine_boot_apps(ee) ->
emqx_machine_boot_apps(ce) ++
[].
emqx_machine_boot_app_list(Edition) ->
string:join(
[atom_to_list(AppName) || AppName <- emqx_machine_boot_apps(Edition)],
", ").
relx_overlay(ReleaseType, Edition) ->
[ {mkdir, "log/"}
@ -374,34 +412,38 @@ relx_overlay(ReleaseType, Edition) ->
, {copy, "bin/nodetool", "bin/nodetool-{{release_version}}"}
] ++ etc_overlay(ReleaseType, Edition).
etc_overlay(ReleaseType, _Edition) ->
Templates = emqx_etc_overlay(ReleaseType),
etc_overlay(ReleaseType, Edition) ->
Templates = emqx_etc_overlay(ReleaseType, Edition),
[ {mkdir, "etc/"}
, {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"}
] ++
lists:map(
fun({From, To}) -> {template, From, To};
(FromTo) -> {template, FromTo, FromTo}
end, Templates)
++ extra_overlay(ReleaseType).
end, Templates).
extra_overlay(cloud) ->
[
];
extra_overlay(edge) ->
[].
emqx_etc_overlay(cloud) ->
emqx_etc_overlay_common() ++
emqx_etc_overlay(ReleaseType, Edition) ->
emqx_etc_overlay_per_rel(ReleaseType)
++ emqx_etc_overlay_per_edition(Edition)
++ emqx_etc_overlay_common().
emqx_etc_overlay_per_rel(cloud) ->
[ {"{{base_dir}}/lib/emqx/etc/emqx_cloud/vm.args","etc/vm.args"}
];
emqx_etc_overlay(edge) ->
emqx_etc_overlay_common() ++
emqx_etc_overlay_per_rel(edge) ->
[ {"{{base_dir}}/lib/emqx/etc/emqx_edge/vm.args","etc/vm.args"}
].
emqx_etc_overlay_common() ->
[ {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}
].
emqx_etc_overlay_per_edition(ce) ->
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
, {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}
];
emqx_etc_overlay_per_edition(ee) ->
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx_enterprise.conf.all", "etc/emqx_enterprise.conf"}
, {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
].
get_vsn(Profile) ->

View File

@ -14,8 +14,8 @@ RUNNER_ETC_DIR="{{ runner_etc_dir }}"
RUNNER_DATA_DIR="{{ runner_data_dir }}"
RUNNER_USER="{{ runner_user }}"
IS_ELIXIR="{{ is_elixir }}"
SCHEMA_MOD="{{ emqx_schema_mod }}"
EMQX_LICENSE_CONF=''
export EMQX_DESCRIPTION='{{ emqx_description }}'
## computed vars

View File

@ -36,6 +36,9 @@ collect_deps([File | Files], Acc) ->
collect_deps(Files, do_collect_deps(Deps, File, Acc)).
do_collect_deps([], _File, Acc) -> Acc;
%% ignore relative app dependencies
do_collect_deps([{_Name, {path, _Path}} | Deps], File, Acc) ->
do_collect_deps(Deps, File, Acc);
do_collect_deps([{Name, Ref} | Deps], File, Acc) ->
Refs = maps:get(Name, Acc, []),
do_collect_deps(Deps, File, Acc#{Name => [{Ref, File} | Refs]}).

View File

@ -22,7 +22,7 @@ defmodule CheckElixirApplications do
env: [{"DEBUG", "1"}]
)
mix_apps = mix_applications(inputs.release_type)
mix_apps = mix_applications(inputs.release_type, inputs.edition_type)
rebar_apps = rebar_applications(profile)
results = diff_apps(mix_apps, rebar_apps)
@ -70,8 +70,8 @@ defmodule CheckElixirApplications do
end
end
defp mix_applications(release_type) do
EMQXUmbrella.MixProject.applications(release_type)
defp mix_applications(release_type, edition_type) do
EMQXUmbrella.MixProject.applications(release_type, edition_type)
end
defp rebar_applications(profile) do

View File

@ -0,0 +1,81 @@
#!/usr/bin/env elixir
defmodule CheckElixirEMQXMachineBootDiscrepancies do
alias EMQXUmbrella.MixProject
def main() do
{:ok, _} = Application.ensure_all_started(:mix)
File.cwd!()
|> Path.join("mix.exs")
|> Code.compile_file()
inputs = MixProject.check_profile!()
profile = Mix.env()
# produce `rebar.config.rendered` to consult
File.cwd!()
|> Path.join("rebar3")
|> System.cmd(["as", to_string(profile)],
env: [{"DEBUG", "1"}]
)
mix_apps = mix_emqx_machine_applications(inputs.edition_type)
rebar_apps = rebar_emqx_machine_applications(profile)
{mix_missing, rebar_missing} = diff_apps(mix_apps, rebar_apps)
if Enum.any?(mix_missing) do
IO.puts(
"For profile=#{profile}, edition=#{inputs.edition_type} " <>
"rebar.config.erl has the following emqx_machine_boot_apps " <>
"that are missing in mix.exs:"
)
IO.inspect(mix_missing, syntax_colors: [atom: :red])
end
if Enum.any?(rebar_missing) do
IO.puts(
"For profile=#{profile}, edition=#{inputs.edition_type} " <>
"mix.exs has the following emqx_machine_boot_apps " <>
"that are missing in rebar3.config.erl:"
)
IO.inspect(rebar_missing, syntax_colors: [atom: :red])
end
success? = Enum.empty?(mix_missing) and Enum.empty?(rebar_missing)
if not success? do
System.halt(1)
else
IO.puts(
IO.ANSI.green() <>
"Mix and Rebar emqx_machine_boot_apps OK!" <>
IO.ANSI.reset()
)
end
end
defp mix_emqx_machine_applications(edition_type) do
EMQXUmbrella.MixProject.emqx_machine_boot_apps(edition_type)
end
defp rebar_emqx_machine_applications(profile) do
{:ok, props} =
File.cwd!()
|> Path.join("rebar.config.rendered")
|> :file.consult()
props[:profiles][profile][:relx][:overlay_vars][:emqx_machine_boot_apps]
|> to_string()
|> String.split(~r/,\s+/)
|> Enum.map(&String.to_atom/1)
end
defp diff_apps(mix_apps, rebar_apps) do
mix_missing = rebar_apps -- mix_apps
rebar_missing = mix_apps -- rebar_apps
{mix_missing, rebar_missing}
end
end
CheckElixirEMQXMachineBootDiscrepancies.main()

View File

@ -12,16 +12,29 @@
main(_) ->
{ok, BaseConf} = file:read_file("apps/emqx_conf/etc/emqx_conf.conf"),
Cfgs = get_all_cfgs("apps/"),
Conf = lists:foldl(fun(CfgFile, Acc) ->
case filelib:is_regular(CfgFile) of
true ->
{ok, Bin1} = file:read_file(CfgFile),
[Acc, io_lib:nl(), Bin1];
false -> Acc
end
end, BaseConf, Cfgs),
ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf).
Conf = [merge(BaseConf, Cfgs),
io_lib:nl(),
"include emqx_enterprise.conf",
io_lib:nl()],
ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf),
EnterpriseCfgs = get_all_cfgs("lib-ee/"),
EnterpriseConf = merge("", EnterpriseCfgs),
ok = file:write_file("apps/emqx_conf/etc/emqx_enterprise.conf.all", EnterpriseConf).
merge(BaseConf, Cfgs) ->
lists:foldl(
fun(CfgFile, Acc) ->
case filelib:is_regular(CfgFile) of
true ->
{ok, Bin1} = file:read_file(CfgFile),
[Acc, io_lib:nl(), Bin1];
false -> Acc
end
end, BaseConf, Cfgs).
get_all_cfgs(Root) ->
Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_conf"],