Merge pull request #8598 from thalesmg/license-fixes-50
license fixes (5.0)
This commit is contained in:
commit
a9b176b8bd
|
@ -14,6 +14,7 @@
|
||||||
{emqx_gateway_cm,1}.
|
{emqx_gateway_cm,1}.
|
||||||
{emqx_gateway_http,1}.
|
{emqx_gateway_http,1}.
|
||||||
{emqx_license,1}.
|
{emqx_license,1}.
|
||||||
|
{emqx_license,2}.
|
||||||
{emqx_management,1}.
|
{emqx_management,1}.
|
||||||
{emqx_management,2}.
|
{emqx_management,2}.
|
||||||
{emqx_mgmt_api_plugins,1}.
|
{emqx_mgmt_api_plugins,1}.
|
||||||
|
|
|
@ -595,6 +595,7 @@ setup_node(Node, Opts) when is_map(Opts) ->
|
||||||
EnvHandler = maps:get(env_handler, Opts, fun(_) -> ok end),
|
EnvHandler = maps:get(env_handler, Opts, fun(_) -> ok end),
|
||||||
ConfigureGenRpc = maps:get(configure_gen_rpc, Opts, true),
|
ConfigureGenRpc = maps:get(configure_gen_rpc, Opts, true),
|
||||||
LoadSchema = maps:get(load_schema, Opts, true),
|
LoadSchema = maps:get(load_schema, Opts, true),
|
||||||
|
SchemaMod = maps:get(schema_mod, Opts, emqx_schema),
|
||||||
LoadApps = maps:get(load_apps, Opts, [gen_rpc, emqx, ekka, mria] ++ Apps),
|
LoadApps = maps:get(load_apps, Opts, [gen_rpc, emqx, ekka, mria] ++ Apps),
|
||||||
Env = maps:get(env, Opts, []),
|
Env = maps:get(env, Opts, []),
|
||||||
Conf = maps:get(conf, Opts, []),
|
Conf = maps:get(conf, Opts, []),
|
||||||
|
@ -630,7 +631,7 @@ setup_node(Node, Opts) when is_map(Opts) ->
|
||||||
%% Otherwise, configuration get's loaded and all preset env in envhandler is lost
|
%% Otherwise, configuration get's loaded and all preset env in envhandler is lost
|
||||||
LoadSchema andalso
|
LoadSchema andalso
|
||||||
begin
|
begin
|
||||||
emqx_config:init_load(emqx_schema),
|
emqx_config:init_load(SchemaMod),
|
||||||
application:set_env(emqx, init_config_load_done, true)
|
application:set_env(emqx, init_config_load_done, true)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
2
bin/emqx
2
bin/emqx
|
@ -416,7 +416,7 @@ call_hocon() {
|
||||||
## and parsing HOCON config + environment variables is a non-trivial task
|
## and parsing HOCON config + environment variables is a non-trivial task
|
||||||
CONF_KEYS=( 'node.data_dir' 'node.name' 'node.cookie' 'node.db_backend' 'cluster.proto_dist' )
|
CONF_KEYS=( 'node.data_dir' 'node.name' 'node.cookie' 'node.db_backend' 'cluster.proto_dist' )
|
||||||
if [ "$IS_ENTERPRISE" = 'yes' ]; then
|
if [ "$IS_ENTERPRISE" = 'yes' ]; then
|
||||||
CONF_KEYS+=( 'license.file' 'license.key' )
|
CONF_KEYS+=( 'license.type' 'license.file' 'license.key' )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
|
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
|
||||||
|
|
|
@ -25,9 +25,9 @@ main(Args) ->
|
||||||
%% forward the call to hocon_cli
|
%% forward the call to hocon_cli
|
||||||
hocon_cli:main(Rest);
|
hocon_cli:main(Rest);
|
||||||
["check_license_key", Key] ->
|
["check_license_key", Key] ->
|
||||||
check_license(#{key => list_to_binary(Key)});
|
check_license(#{type => key, key => list_to_binary(Key)});
|
||||||
["check_license_file", File] ->
|
["check_license_file", File] ->
|
||||||
check_license(#{file => list_to_binary(File)});
|
check_license(#{type => file, file => list_to_binary(File)});
|
||||||
_ ->
|
_ ->
|
||||||
do(Args)
|
do(Args)
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
license {
|
license {
|
||||||
|
type = key
|
||||||
# The default license has 1000 connections limit, it is issued on 20220419 and valid for 5 years (1825 days)
|
# The default license has 1000 connections limit, it is issued on 20220419 and valid for 5 years (1825 days)
|
||||||
key = "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KZGVmYXVsdAoyMDIyMDQxOQoxODI1CjEwMDAK.MEQCICbgRVijCQov2hrvZXR1mk9Oa+tyV1F5oJ6iOZeSHjnQAiB9dUiVeaZekDOjztk+NCWjhk4PG8tWfw2uFZWruSzD6g=="
|
key = "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KZGVmYXVsdAoyMDIyMDQxOQoxODI1CjEwMDAK.MEQCICbgRVijCQov2hrvZXR1mk9Oa+tyV1F5oJ6iOZeSHjnQAiB9dUiVeaZekDOjztk+NCWjhk4PG8tWfw2uFZWruSzD6g=="
|
||||||
connection_low_watermark = 75%,
|
connection_low_watermark = 75%,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{application, emqx_license, [
|
{application, emqx_license, [
|
||||||
{description, "EMQX License"},
|
{description, "EMQX License"},
|
||||||
{vsn, "5.0.0"},
|
{vsn, "5.0.1"},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{registered, [emqx_license_sup]},
|
{registered, [emqx_license_sup]},
|
||||||
{applications, [kernel, stdlib]},
|
{applications, [kernel, stdlib]},
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
read_license/0,
|
read_license/0,
|
||||||
read_license/1,
|
read_license/1,
|
||||||
update_file/1,
|
update_file/1,
|
||||||
update_key/1
|
update_key/1,
|
||||||
|
license_dir/0,
|
||||||
|
save_and_backup_license/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(CONF_KEY_PATH, [license]).
|
-define(CONF_KEY_PATH, [license]).
|
||||||
|
@ -54,15 +56,29 @@ unload() ->
|
||||||
emqx_conf:remove_handler(?CONF_KEY_PATH),
|
emqx_conf:remove_handler(?CONF_KEY_PATH),
|
||||||
emqx_license_cli:unload().
|
emqx_license_cli:unload().
|
||||||
|
|
||||||
|
-spec license_dir() -> file:filename().
|
||||||
|
license_dir() ->
|
||||||
|
filename:join([emqx:data_dir(), licenses]).
|
||||||
|
|
||||||
|
%% Subdirectory relative to data dir.
|
||||||
|
-spec relative_license_path() -> file:filename().
|
||||||
|
relative_license_path() ->
|
||||||
|
filename:join([licenses, "emqx.lic"]).
|
||||||
|
|
||||||
-spec update_file(binary() | string()) ->
|
-spec update_file(binary() | string()) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
||||||
update_file(Filename) when is_binary(Filename); is_list(Filename) ->
|
update_file(Filename) when is_binary(Filename); is_list(Filename) ->
|
||||||
Result = emqx_conf:update(
|
case file:read_file(Filename) of
|
||||||
?CONF_KEY_PATH,
|
{ok, Contents} ->
|
||||||
{file, Filename},
|
Result = emqx_conf:update(
|
||||||
#{rawconf_with_defaults => true, override_to => local}
|
?CONF_KEY_PATH,
|
||||||
),
|
{file, Contents},
|
||||||
handle_config_update_result(Result).
|
#{rawconf_with_defaults => true, override_to => local}
|
||||||
|
),
|
||||||
|
handle_config_update_result(Result);
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
end.
|
||||||
|
|
||||||
-spec update_key(binary() | string()) ->
|
-spec update_key(binary() | string()) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
||||||
|
@ -125,22 +141,18 @@ del_license_hook() ->
|
||||||
_ = emqx_hooks:del('client.connect', {?MODULE, check, []}),
|
_ = emqx_hooks:del('client.connect', {?MODULE, check, []}),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
do_update({file, Filename}, Conf) ->
|
do_update({file, NewContents}, Conf) ->
|
||||||
case file:read_file(Filename) of
|
Res = emqx_license_proto_v2:save_and_backup_license(mria_mnesia:running_nodes(), NewContents),
|
||||||
{ok, Content} ->
|
%% assert
|
||||||
case emqx_license_parser:parse(Content) of
|
true = lists:all(fun(X) -> X =:= {ok, ok} end, Res),
|
||||||
{ok, _License} ->
|
%% Must be relative to the data dir, since different nodes might
|
||||||
maps:remove(<<"key">>, Conf#{<<"file">> => Filename});
|
%% have different data directories configured...
|
||||||
{error, Reason} ->
|
LicensePath = relative_license_path(),
|
||||||
erlang:throw(Reason)
|
maps:remove(<<"key">>, Conf#{<<"type">> => file, <<"file">> => LicensePath});
|
||||||
end;
|
|
||||||
{error, Reason} ->
|
|
||||||
erlang:throw({invalid_license_file, Reason})
|
|
||||||
end;
|
|
||||||
do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
|
do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
|
||||||
case emqx_license_parser:parse(Content) of
|
case emqx_license_parser:parse(Content) of
|
||||||
{ok, _License} ->
|
{ok, _License} ->
|
||||||
maps:remove(<<"file">>, Conf#{<<"key">> => Content});
|
maps:remove(<<"file">>, Conf#{<<"type">> => key, <<"key">> => Content});
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
erlang:throw(Reason)
|
erlang:throw(Reason)
|
||||||
end;
|
end;
|
||||||
|
@ -148,17 +160,61 @@ do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
|
||||||
do_update(_Other, Conf) ->
|
do_update(_Other, Conf) ->
|
||||||
Conf.
|
Conf.
|
||||||
|
|
||||||
|
save_and_backup_license(NewLicenseKey) ->
|
||||||
|
%% Must be relative to the data dir, since different nodes might
|
||||||
|
%% have different data directories configured...
|
||||||
|
CurrentLicensePath = filename:join(emqx:data_dir(), relative_license_path()),
|
||||||
|
LicenseDir = filename:dirname(CurrentLicensePath),
|
||||||
|
case filelib:ensure_dir(CurrentLicensePath) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, EnsureError} -> throw({error_creating_license_dir, EnsureError})
|
||||||
|
end,
|
||||||
|
case file:read_file(CurrentLicensePath) of
|
||||||
|
{ok, NewLicenseKey} ->
|
||||||
|
%% same contents; nothing to do.
|
||||||
|
ok;
|
||||||
|
{ok, _OldContents} ->
|
||||||
|
Time = calendar:system_time_to_rfc3339(erlang:system_time(second)),
|
||||||
|
BackupPath = filename:join([
|
||||||
|
LicenseDir,
|
||||||
|
"emqx.lic." ++ Time ++ ".backup"
|
||||||
|
]),
|
||||||
|
case file:copy(CurrentLicensePath, BackupPath) of
|
||||||
|
{ok, _} -> ok;
|
||||||
|
{error, CopyError} -> throw({error_backing_up_license, CopyError})
|
||||||
|
end,
|
||||||
|
ok;
|
||||||
|
{error, enoent} ->
|
||||||
|
ok;
|
||||||
|
{error, Error} ->
|
||||||
|
throw({error_reading_existing_license, Error})
|
||||||
|
end,
|
||||||
|
case file:write_file(CurrentLicensePath, NewLicenseKey) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, WriteError} -> throw({error_writing_license, WriteError})
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
check_max_clients_exceeded(MaxClients) ->
|
check_max_clients_exceeded(MaxClients) ->
|
||||||
emqx_license_resources:connection_count() > MaxClients * 1.1.
|
emqx_license_resources:connection_count() > MaxClients * 1.1.
|
||||||
|
|
||||||
read_license(#{file := Filename}) ->
|
read_license(#{type := file, file := Filename}) ->
|
||||||
case file:read_file(Filename) of
|
case file:read_file(Filename) of
|
||||||
{ok, Content} -> emqx_license_parser:parse(Content);
|
{ok, Content} ->
|
||||||
{error, _} = Error -> Error
|
emqx_license_parser:parse(Content);
|
||||||
|
{error, _} = Error ->
|
||||||
|
%% Could be a relative path in data folder after update.
|
||||||
|
FilenameDataDir = filename:join(emqx:data_dir(), Filename),
|
||||||
|
case file:read_file(FilenameDataDir) of
|
||||||
|
{ok, Content} -> emqx_license_parser:parse(Content);
|
||||||
|
_Error -> Error
|
||||||
|
end
|
||||||
end;
|
end;
|
||||||
read_license(#{key := Content}) ->
|
read_license(#{type := key, key := Content}) ->
|
||||||
emqx_license_parser:parse(Content).
|
emqx_license_parser:parse(Content).
|
||||||
|
|
||||||
|
handle_config_update_result({error, {post_config_update, ?MODULE, Error}}) ->
|
||||||
|
{error, Error};
|
||||||
handle_config_update_result({error, _} = Error) ->
|
handle_config_update_result({error, _} = Error) ->
|
||||||
Error;
|
Error;
|
||||||
handle_config_update_result({ok, #{post_config_update := #{emqx_license := Result}}}) ->
|
handle_config_update_result({ok, #{post_config_update := #{emqx_license := Result}}}) ->
|
||||||
|
|
|
@ -128,6 +128,6 @@ ensure_timer(#{check_peer_interval := CheckInterval} = State) ->
|
||||||
|
|
||||||
remote_connection_count() ->
|
remote_connection_count() ->
|
||||||
Nodes = mria_mnesia:running_nodes() -- [node()],
|
Nodes = mria_mnesia:running_nodes() -- [node()],
|
||||||
Results = emqx_license_proto_v1:remote_connection_counts(Nodes),
|
Results = emqx_license_proto_v2:remote_connection_counts(Nodes),
|
||||||
Counts = [Count || {ok, Count} <- Results],
|
Counts = [Count || {ok, Count} <- Results],
|
||||||
lists:sum(Counts).
|
lists:sum(Counts).
|
||||||
|
|
|
@ -14,14 +14,15 @@
|
||||||
|
|
||||||
-export([roots/0, fields/1, validations/0, desc/1]).
|
-export([roots/0, fields/1, validations/0, desc/1]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
license_type/0
|
||||||
|
]).
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[
|
[
|
||||||
{license,
|
{license,
|
||||||
hoconsc:mk(
|
hoconsc:mk(
|
||||||
hoconsc:union([
|
license_type(),
|
||||||
hoconsc:ref(?MODULE, key_license),
|
|
||||||
hoconsc:ref(?MODULE, file_license)
|
|
||||||
]),
|
|
||||||
#{
|
#{
|
||||||
desc =>
|
desc =>
|
||||||
"EMQX Enterprise license.\n"
|
"EMQX Enterprise license.\n"
|
||||||
|
@ -36,16 +37,35 @@ roots() ->
|
||||||
|
|
||||||
fields(key_license) ->
|
fields(key_license) ->
|
||||||
[
|
[
|
||||||
|
{type, #{
|
||||||
|
type => key,
|
||||||
|
required => true
|
||||||
|
}},
|
||||||
{key, #{
|
{key, #{
|
||||||
type => string(),
|
type => string(),
|
||||||
%% so it's not logged
|
%% so it's not logged
|
||||||
sensitive => true,
|
sensitive => true,
|
||||||
|
required => true,
|
||||||
desc => "License string"
|
desc => "License string"
|
||||||
|
}},
|
||||||
|
{file, #{
|
||||||
|
type => string(),
|
||||||
|
required => false
|
||||||
}}
|
}}
|
||||||
| common_fields()
|
| common_fields()
|
||||||
];
|
];
|
||||||
fields(file_license) ->
|
fields(file_license) ->
|
||||||
[
|
[
|
||||||
|
{type, #{
|
||||||
|
type => file,
|
||||||
|
required => true
|
||||||
|
}},
|
||||||
|
{key, #{
|
||||||
|
type => string(),
|
||||||
|
%% so it's not logged
|
||||||
|
sensitive => true,
|
||||||
|
required => false
|
||||||
|
}},
|
||||||
{file, #{
|
{file, #{
|
||||||
type => string(),
|
type => string(),
|
||||||
desc => "Path to the license file"
|
desc => "Path to the license file"
|
||||||
|
@ -77,6 +97,12 @@ common_fields() ->
|
||||||
validations() ->
|
validations() ->
|
||||||
[{check_license_watermark, fun check_license_watermark/1}].
|
[{check_license_watermark, fun check_license_watermark/1}].
|
||||||
|
|
||||||
|
license_type() ->
|
||||||
|
hoconsc:union([
|
||||||
|
hoconsc:ref(?MODULE, key_license),
|
||||||
|
hoconsc:ref(?MODULE, file_license)
|
||||||
|
]).
|
||||||
|
|
||||||
check_license_watermark(Conf) ->
|
check_license_watermark(Conf) ->
|
||||||
case hocon_maps:get("license.connection_low_watermark", Conf) of
|
case hocon_maps:get("license.connection_low_watermark", Conf) of
|
||||||
undefined ->
|
undefined ->
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_license_proto_v2).
|
||||||
|
|
||||||
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
-export([introduced_in/0]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
remote_connection_counts/1,
|
||||||
|
save_and_backup_license/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(TIMEOUT, 500).
|
||||||
|
-define(BACKUP_TIMEOUT, 15_000).
|
||||||
|
|
||||||
|
introduced_in() ->
|
||||||
|
"5.0.5".
|
||||||
|
|
||||||
|
-spec remote_connection_counts(list(node())) -> list({atom(), term()}).
|
||||||
|
remote_connection_counts(Nodes) ->
|
||||||
|
erpc:multicall(Nodes, emqx_license_resources, local_connection_count, [], ?TIMEOUT).
|
||||||
|
|
||||||
|
-spec save_and_backup_license(list(node()), binary()) -> list({atom(), term()}).
|
||||||
|
save_and_backup_license(Nodes, NewLicenseKey) ->
|
||||||
|
erpc:multicall(Nodes, emqx_license, save_and_backup_license, [NewLicenseKey], ?BACKUP_TIMEOUT).
|
|
@ -28,39 +28,190 @@ end_per_suite(_) ->
|
||||||
init_per_testcase(Case, Config) ->
|
init_per_testcase(Case, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
set_invalid_license_file(Case),
|
set_invalid_license_file(Case),
|
||||||
Config.
|
Paths = set_override_paths(Case),
|
||||||
|
Config0 = setup_test(Case, Config),
|
||||||
|
Paths ++ Config0 ++ Config.
|
||||||
|
|
||||||
end_per_testcase(Case, _Config) ->
|
end_per_testcase(Case, Config) ->
|
||||||
restore_valid_license_file(Case),
|
restore_valid_license_file(Case),
|
||||||
|
clean_overrides(Case, Config),
|
||||||
|
teardown_test(Case, Config),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_override_paths(TestCase) when
|
||||||
|
TestCase =:= t_change_from_file_to_key;
|
||||||
|
TestCase =:= t_change_from_key_to_file
|
||||||
|
->
|
||||||
|
LocalOverridePath = filename:join([
|
||||||
|
"/tmp",
|
||||||
|
"local-" ++ atom_to_list(TestCase) ++ ".conf"
|
||||||
|
]),
|
||||||
|
ClusterOverridePath = filename:join([
|
||||||
|
"/tmp",
|
||||||
|
"local-" ++ atom_to_list(TestCase) ++ ".conf"
|
||||||
|
]),
|
||||||
|
application:set_env(emqx, local_override_conf_file, LocalOverridePath),
|
||||||
|
application:set_env(emqx, cluster_override_conf_file, ClusterOverridePath),
|
||||||
|
[
|
||||||
|
{local_override_path, LocalOverridePath},
|
||||||
|
{cluster_override_path, ClusterOverridePath}
|
||||||
|
];
|
||||||
|
set_override_paths(_TestCase) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
clean_overrides(TestCase, Config) when
|
||||||
|
TestCase =:= t_change_from_file_to_key;
|
||||||
|
TestCase =:= t_change_from_key_to_file
|
||||||
|
->
|
||||||
|
LocalOverridePath = ?config(local_override_path, Config),
|
||||||
|
ClusterOverridePath = ?config(cluster_override_path, Config),
|
||||||
|
file:delete(LocalOverridePath),
|
||||||
|
file:delete(ClusterOverridePath),
|
||||||
|
application:unset_env(emqx, local_override_conf_file),
|
||||||
|
application:unset_env(emqx, cluster_override_conf_file),
|
||||||
|
ok;
|
||||||
|
clean_overrides(_TestCase, _Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
setup_test(TestCase, Config) when
|
||||||
|
TestCase =:= t_update_file_cluster_backup
|
||||||
|
->
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
{LicenseKey, _License} = mk_license(
|
||||||
|
[
|
||||||
|
%% license format version
|
||||||
|
"220111",
|
||||||
|
%% license type
|
||||||
|
"0",
|
||||||
|
%% customer type
|
||||||
|
"10",
|
||||||
|
%% customer name
|
||||||
|
"Foo",
|
||||||
|
%% customer email
|
||||||
|
"contact@foo.com",
|
||||||
|
%% deplayment name
|
||||||
|
"bar-deployment",
|
||||||
|
%% start date
|
||||||
|
"20220111",
|
||||||
|
%% days
|
||||||
|
"100000",
|
||||||
|
%% max connections
|
||||||
|
"19"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
Cluster = emqx_common_test_helpers:emqx_cluster(
|
||||||
|
[core, core],
|
||||||
|
[
|
||||||
|
{apps, [emqx_conf, emqx_license]},
|
||||||
|
{load_schema, false},
|
||||||
|
{schema_mod, emqx_enterprise_conf_schema},
|
||||||
|
{env_handler, fun
|
||||||
|
(emqx) ->
|
||||||
|
emqx_config:save_schema_mod_and_names(emqx_enterprise_conf_schema),
|
||||||
|
%% emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
||||||
|
application:set_env(emqx, boot_modules, []),
|
||||||
|
application:set_env(
|
||||||
|
emqx,
|
||||||
|
data_dir,
|
||||||
|
filename:join([
|
||||||
|
DataDir,
|
||||||
|
TestCase,
|
||||||
|
node()
|
||||||
|
])
|
||||||
|
),
|
||||||
|
ok;
|
||||||
|
(emqx_conf) ->
|
||||||
|
emqx_config:save_schema_mod_and_names(emqx_enterprise_conf_schema),
|
||||||
|
%% emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
||||||
|
application:set_env(
|
||||||
|
emqx,
|
||||||
|
data_dir,
|
||||||
|
filename:join([
|
||||||
|
DataDir,
|
||||||
|
TestCase,
|
||||||
|
node()
|
||||||
|
])
|
||||||
|
),
|
||||||
|
ok;
|
||||||
|
(emqx_license) ->
|
||||||
|
LicensePath = filename:join(emqx_license:license_dir(), "emqx.lic"),
|
||||||
|
filelib:ensure_dir(LicensePath),
|
||||||
|
ok = file:write_file(LicensePath, LicenseKey),
|
||||||
|
LicConfig = #{type => file, file => LicensePath},
|
||||||
|
emqx_config:put([license], LicConfig),
|
||||||
|
RawConfig = #{<<"type">> => file, <<"file">> => LicensePath},
|
||||||
|
emqx_config:put_raw([<<"license">>], RawConfig),
|
||||||
|
ok = meck:new(emqx_license, [non_strict, passthrough, no_history, no_link]),
|
||||||
|
%% meck:expect(emqx_license, read_license, fun() -> {ok, License} end),
|
||||||
|
meck:expect(
|
||||||
|
emqx_license_parser,
|
||||||
|
parse,
|
||||||
|
fun(X) ->
|
||||||
|
emqx_license_parser:parse(
|
||||||
|
X,
|
||||||
|
emqx_license_test_lib:public_key_pem()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
ok;
|
||||||
|
(_) ->
|
||||||
|
ok
|
||||||
|
end}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
Nodes = [emqx_common_test_helpers:start_slave(Name, Opts) || {Name, Opts} <- Cluster],
|
||||||
|
[{nodes, Nodes}, {cluster, Cluster}, {old_license, LicenseKey}];
|
||||||
|
setup_test(_TestCase, _Config) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
teardown_test(TestCase, Config) when
|
||||||
|
TestCase =:= t_update_file_cluster_backup
|
||||||
|
->
|
||||||
|
Nodes = ?config(nodes, Config),
|
||||||
|
lists:foreach(
|
||||||
|
fun(N) ->
|
||||||
|
LicenseDir = erpc:call(N, emqx_license, license_dir, []),
|
||||||
|
{ok, _} = emqx_common_test_helpers:stop_slave(N),
|
||||||
|
ok = file:del_dir_r(LicenseDir),
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
Nodes
|
||||||
|
),
|
||||||
|
ok;
|
||||||
|
teardown_test(_TestCase, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_invalid_license_file(t_read_license_from_invalid_file) ->
|
set_invalid_license_file(t_read_license_from_invalid_file) ->
|
||||||
Config = #{file => "/invalid/file"},
|
Config = #{type => file, file => "/invalid/file"},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_invalid_license_file(_) ->
|
set_invalid_license_file(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
restore_valid_license_file(t_read_license_from_invalid_file) ->
|
restore_valid_license_file(t_read_license_from_invalid_file) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
restore_valid_license_file(_) ->
|
restore_valid_license_file(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config),
|
emqx_config:put([license], Config),
|
||||||
RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
|
RawConfig = #{<<"type">> => file, <<"file">> => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig);
|
emqx_config:put_raw([<<"license">>], RawConfig);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
assert_on_nodes(Nodes, RunFun, CheckFun) ->
|
||||||
|
Res = [{N, erpc:call(N, RunFun)} || N <- Nodes],
|
||||||
|
lists:foreach(CheckFun, Res).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Tests
|
%% Tests
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_update_file(_Config) ->
|
t_update_file(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {invalid_license_file, enoent}},
|
{error, enoent},
|
||||||
emqx_license:update_file("/unknown/path")
|
emqx_license:update_file("/unknown/path")
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -75,6 +226,115 @@ t_update_file(_Config) ->
|
||||||
emqx_license:update_file(emqx_license_test_lib:default_license())
|
emqx_license:update_file(emqx_license_test_lib:default_license())
|
||||||
).
|
).
|
||||||
|
|
||||||
|
t_update_file_cluster_backup(Config) ->
|
||||||
|
OldLicenseKey = ?config(old_license, Config),
|
||||||
|
Nodes = [N1 | _] = ?config(nodes, Config),
|
||||||
|
|
||||||
|
%% update the license file for the cluster
|
||||||
|
{NewLicenseKey, NewDecodedLicense} = mk_license(
|
||||||
|
[
|
||||||
|
%% license format version
|
||||||
|
"220111",
|
||||||
|
%% license type
|
||||||
|
"0",
|
||||||
|
%% customer type
|
||||||
|
"10",
|
||||||
|
%% customer name
|
||||||
|
"Foo",
|
||||||
|
%% customer email
|
||||||
|
"contact@foo.com",
|
||||||
|
%% deplayment name
|
||||||
|
"bar-deployment",
|
||||||
|
%% start date
|
||||||
|
"20220111",
|
||||||
|
%% days
|
||||||
|
"100000",
|
||||||
|
%% max connections
|
||||||
|
"190"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
NewLicensePath = "tmp_new_license.lic",
|
||||||
|
ok = file:write_file(NewLicensePath, NewLicenseKey),
|
||||||
|
{ok, _} = erpc:call(N1, emqx_license, update_file, [NewLicensePath]),
|
||||||
|
|
||||||
|
assert_on_nodes(
|
||||||
|
Nodes,
|
||||||
|
fun() ->
|
||||||
|
Conf = emqx_conf:get([license]),
|
||||||
|
emqx_license:read_license(Conf)
|
||||||
|
end,
|
||||||
|
fun({N, Res}) ->
|
||||||
|
?assertMatch({ok, _}, Res, #{node => N}),
|
||||||
|
{ok, License} = Res,
|
||||||
|
?assertEqual(NewDecodedLicense, License, #{node => N})
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
assert_on_nodes(
|
||||||
|
Nodes,
|
||||||
|
fun() ->
|
||||||
|
LicenseDir = emqx_license:license_dir(),
|
||||||
|
file:list_dir(LicenseDir)
|
||||||
|
end,
|
||||||
|
fun({N, Res}) ->
|
||||||
|
?assertMatch({ok, _}, Res, #{node => N}),
|
||||||
|
{ok, DirContents} = Res,
|
||||||
|
%% the now current license
|
||||||
|
?assert(lists:member("emqx.lic", DirContents), #{node => N, dir_contents => DirContents}),
|
||||||
|
%% the backed up old license
|
||||||
|
?assert(
|
||||||
|
lists:any(
|
||||||
|
fun
|
||||||
|
("emqx.lic." ++ Suffix) -> lists:suffix(".backup", Suffix);
|
||||||
|
(_) -> false
|
||||||
|
end,
|
||||||
|
DirContents
|
||||||
|
),
|
||||||
|
#{node => N, dir_contents => DirContents}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
assert_on_nodes(
|
||||||
|
Nodes,
|
||||||
|
fun() ->
|
||||||
|
LicenseDir = emqx_license:license_dir(),
|
||||||
|
{ok, DirContents} = file:list_dir(LicenseDir),
|
||||||
|
[BackupLicensePath0] = [
|
||||||
|
F
|
||||||
|
|| "emqx.lic." ++ F <- DirContents, lists:suffix(".backup", F)
|
||||||
|
],
|
||||||
|
BackupLicensePath = "emqx.lic." ++ BackupLicensePath0,
|
||||||
|
{ok, BackupLicense} = file:read_file(filename:join(LicenseDir, BackupLicensePath)),
|
||||||
|
{ok, NewLicense} = file:read_file(filename:join(LicenseDir, "emqx.lic")),
|
||||||
|
#{
|
||||||
|
backup => BackupLicense,
|
||||||
|
new => NewLicense
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
fun({N, #{backup := BackupLicense, new := NewLicense}}) ->
|
||||||
|
?assertEqual(OldLicenseKey, BackupLicense, #{node => N}),
|
||||||
|
?assertEqual(NewLicenseKey, NewLicense, #{node => N})
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
%% uploading the same license twice should not generate extra backups.
|
||||||
|
{ok, _} = erpc:call(N1, emqx_license, update_file, [NewLicensePath]),
|
||||||
|
|
||||||
|
assert_on_nodes(
|
||||||
|
Nodes,
|
||||||
|
fun() ->
|
||||||
|
LicenseDir = emqx_license:license_dir(),
|
||||||
|
{ok, DirContents} = file:list_dir(LicenseDir),
|
||||||
|
[F || "emqx.lic." ++ F <- DirContents, lists:suffix(".backup", F)]
|
||||||
|
end,
|
||||||
|
fun({N, Backups}) ->
|
||||||
|
?assertMatch([_], Backups, #{node => N})
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
t_update_value(_Config) ->
|
t_update_value(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, [_ | _]},
|
{error, [_ | _]},
|
||||||
|
@ -95,7 +355,7 @@ t_read_license_from_invalid_file(_Config) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
t_check_exceeded(_Config) ->
|
t_check_exceeded(_Config) ->
|
||||||
License = mk_license(
|
{_, License} = mk_license(
|
||||||
[
|
[
|
||||||
"220111",
|
"220111",
|
||||||
"0",
|
"0",
|
||||||
|
@ -124,7 +384,7 @@ t_check_exceeded(_Config) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
t_check_ok(_Config) ->
|
t_check_ok(_Config) ->
|
||||||
License = mk_license(
|
{_, License} = mk_license(
|
||||||
[
|
[
|
||||||
"220111",
|
"220111",
|
||||||
"0",
|
"0",
|
||||||
|
@ -153,7 +413,7 @@ t_check_ok(_Config) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
t_check_expired(_Config) ->
|
t_check_expired(_Config) ->
|
||||||
License = mk_license(
|
{_, License} = mk_license(
|
||||||
[
|
[
|
||||||
"220111",
|
"220111",
|
||||||
%% Official customer
|
%% Official customer
|
||||||
|
@ -183,6 +443,39 @@ t_check_not_loaded(_Config) ->
|
||||||
emqx_license:check(#{}, #{})
|
emqx_license:check(#{}, #{})
|
||||||
).
|
).
|
||||||
|
|
||||||
|
t_change_from_file_to_key(_Config) ->
|
||||||
|
%% precondition
|
||||||
|
?assertMatch(#{file := _}, emqx_conf:get([license])),
|
||||||
|
|
||||||
|
OldConf = emqx_conf:get_raw([]),
|
||||||
|
|
||||||
|
%% this saves updated config to `{cluster,local}-overrrides.conf'
|
||||||
|
{ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),
|
||||||
|
{ok, _NewConf} = emqx_license:update_key(LicenseValue),
|
||||||
|
|
||||||
|
%% assert that `{cluster,local}-overrides.conf' merge correctly
|
||||||
|
?assertEqual(ok, emqx_config:init_load(emqx_license_schema, OldConf, #{})),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_change_from_key_to_file(_Config) ->
|
||||||
|
Config = #{type => key, key => <<"some key">>},
|
||||||
|
emqx_config:put([license], Config),
|
||||||
|
RawConfig = #{<<"type">> => key, <<"key">> => <<"some key">>},
|
||||||
|
emqx_config:put_raw([<<"license">>], RawConfig),
|
||||||
|
|
||||||
|
%% precondition
|
||||||
|
?assertMatch(#{type := key, key := _}, emqx_conf:get([license])),
|
||||||
|
OldConf = emqx_conf:get_raw([]),
|
||||||
|
|
||||||
|
%% this saves updated config to `{cluster,local}-overrrides.conf'
|
||||||
|
{ok, _NewConf} = emqx_license:update_file(emqx_license_test_lib:default_license()),
|
||||||
|
|
||||||
|
%% assert that `{cluster,local}-overrides.conf' merge correctly
|
||||||
|
?assertEqual(ok, emqx_config:init_load(emqx_license_schema, OldConf, #{})),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -193,4 +486,4 @@ mk_license(Fields) ->
|
||||||
EncodedLicense,
|
EncodedLicense,
|
||||||
emqx_license_test_lib:public_key_pem()
|
emqx_license_test_lib:public_key_pem()
|
||||||
),
|
),
|
||||||
License.
|
{EncodedLicense, License}.
|
||||||
|
|
|
@ -35,7 +35,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -31,9 +31,9 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config),
|
emqx_config:put([license], Config),
|
||||||
RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
|
RawConfig = #{<<"type">> => file, <<"file">> => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig);
|
emqx_config:put_raw([<<"license">>], RawConfig);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -31,7 +31,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -30,7 +30,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -30,7 +30,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -31,7 +31,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{file => emqx_license_test_lib:default_license()},
|
Config = #{type => file, file => emqx_license_test_lib:default_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
@ -59,9 +59,9 @@ t_connection_count(_Config) ->
|
||||||
meck:new(emqx_cm, [passthrough]),
|
meck:new(emqx_cm, [passthrough]),
|
||||||
meck:expect(emqx_cm, get_connected_client_count, fun() -> 10 end),
|
meck:expect(emqx_cm, get_connected_client_count, fun() -> 10 end),
|
||||||
|
|
||||||
meck:new(emqx_license_proto_v1, [passthrough]),
|
meck:new(emqx_license_proto_v2, [passthrough]),
|
||||||
meck:expect(
|
meck:expect(
|
||||||
emqx_license_proto_v1,
|
emqx_license_proto_v2,
|
||||||
remote_connection_counts,
|
remote_connection_counts,
|
||||||
fun(_Nodes) ->
|
fun(_Nodes) ->
|
||||||
[{ok, 5}, {error, some_error}]
|
[{ok, 5}, {error, some_error}]
|
||||||
|
@ -82,8 +82,8 @@ t_connection_count(_Config) ->
|
||||||
end
|
end
|
||||||
),
|
),
|
||||||
|
|
||||||
meck:unload(emqx_license_proto_v1),
|
meck:unload(emqx_license_proto_v2),
|
||||||
meck:unload(emqx_cm).
|
meck:unload(emqx_cm).
|
||||||
|
|
||||||
t_emqx_license_proto(_Config) ->
|
t_emqx_license_proto(_Config) ->
|
||||||
?assert("5.0.0" =< emqx_license_proto_v1:introduced_in()).
|
?assert("5.0.0" =< emqx_license_proto_v2:introduced_in()).
|
||||||
|
|
Loading…
Reference in New Issue