feat(license): add license application
This commit is contained in:
parent
bede3443a3
commit
17599432d1
|
@ -40,6 +40,8 @@ jobs:
|
||||||
run: ./scripts/ensure-rebar3.sh 3.16.1-emqx-1
|
run: ./scripts/ensure-rebar3.sh 3.16.1-emqx-1
|
||||||
- name: check applications
|
- name: check applications
|
||||||
run: ./scripts/check-elixir-applications.exs
|
run: ./scripts/check-elixir-applications.exs
|
||||||
|
- name: check applications started with emqx_machine
|
||||||
|
run: ./scripts/check-elixir-emqx-machine-boot-discrepancies.exs
|
||||||
env:
|
env:
|
||||||
EMQX_RELEASE_TYPE: ${{ matrix.release_type }}
|
EMQX_RELEASE_TYPE: ${{ matrix.release_type }}
|
||||||
EMQX_PACKAGE_TYPE: ${{ matrix.package_type }}
|
EMQX_PACKAGE_TYPE: ${{ matrix.package_type }}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
{emqx_dashboard,1}.
|
{emqx_dashboard,1}.
|
||||||
{emqx_exhook,1}.
|
{emqx_exhook,1}.
|
||||||
{emqx_gateway_cm,1}.
|
{emqx_gateway_cm,1}.
|
||||||
|
{emqx_license,1}.
|
||||||
{emqx_management,1}.
|
{emqx_management,1}.
|
||||||
{emqx_mgmt_trace,1}.
|
{emqx_mgmt_trace,1}.
|
||||||
{emqx_persistent_session,1}.
|
{emqx_persistent_session,1}.
|
||||||
|
|
|
@ -32,12 +32,17 @@ end_per_suite(_Config) ->
|
||||||
"If this test suite failed, and you are unsure why, read this:~n"
|
"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", []).
|
"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(_) ->
|
t_run_check(_) ->
|
||||||
try
|
try
|
||||||
{ok, OldData} = file:consult(emqx_bpapi_static_checks:versions_file()),
|
{ok, OldData} = file:consult(emqx_bpapi_static_checks:versions_file()),
|
||||||
?assert(emqx_bpapi_static_checks:run()),
|
?assert(emqx_bpapi_static_checks:run()),
|
||||||
{ok, NewData} = file:consult(emqx_bpapi_static_checks:versions_file()),
|
{ok, NewData} = file:consult(emqx_bpapi_static_checks:versions_file()),
|
||||||
OldData =:= NewData orelse
|
check_if_versions_consistent(OldData, NewData) orelse
|
||||||
begin
|
begin
|
||||||
logger:critical(
|
logger:critical(
|
||||||
"BPAPI versions were changed, but not committed to the repo.\n"
|
"BPAPI versions were changed, but not committed to the repo.\n"
|
||||||
|
|
|
@ -113,6 +113,14 @@ node {
|
||||||
## Default: 23
|
## Default: 23
|
||||||
backtrace_depth = 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 {
|
cluster_call {
|
||||||
retry_interval = 1s
|
retry_interval = 1s
|
||||||
max_history = 100
|
max_history = 100
|
||||||
|
|
|
@ -35,9 +35,15 @@ stop(_State) ->
|
||||||
init_conf() ->
|
init_conf() ->
|
||||||
{ok, TnxId} = copy_override_conf_from_core_node(),
|
{ok, TnxId} = copy_override_conf_from_core_node(),
|
||||||
emqx_app:set_init_tnx_id(TnxId),
|
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().
|
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() ->
|
copy_override_conf_from_core_node() ->
|
||||||
case nodes() of
|
case nodes() of
|
||||||
[] -> %% The first core nodes is self.
|
[] -> %% The first core nodes is self.
|
||||||
|
|
|
@ -306,6 +306,11 @@ a crash dump
|
||||||
#{ mapping => "emqx_machine.backtrace_depth"
|
#{ mapping => "emqx_machine.backtrace_depth"
|
||||||
, default => 23
|
, default => 23
|
||||||
})}
|
})}
|
||||||
|
, {"applications",
|
||||||
|
sc(emqx_schema:comma_separated_atoms(),
|
||||||
|
#{ mapping => "emqx_machine.applications"
|
||||||
|
, default => []
|
||||||
|
})}
|
||||||
, {"etc_dir",
|
, {"etc_dir",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{ desc => "`etc` dir for the node"
|
#{ desc => "`etc` dir for the node"
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
-export([sorted_reboot_apps/1]).
|
-export([sorted_reboot_apps/1]).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
|
%% these apps are always (re)started by emqx_machine
|
||||||
|
-define(BASIC_REBOOT_APPS, [gproc, esockd, ranch, cowboy, emqx]).
|
||||||
|
|
||||||
post_boot() ->
|
post_boot() ->
|
||||||
ok = ensure_apps_started(),
|
ok = ensure_apps_started(),
|
||||||
ok = print_vsn(),
|
ok = print_vsn(),
|
||||||
|
@ -80,29 +83,12 @@ start_one_app(App) ->
|
||||||
%% list of app names which should be rebooted when:
|
%% list of app names which should be rebooted when:
|
||||||
%% 1. due to static config change
|
%% 1. due to static config change
|
||||||
%% 2. after join a cluster
|
%% 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() ->
|
reboot_apps() ->
|
||||||
[ gproc
|
{ok, Apps} = application:get_env(emqx_machine, applications),
|
||||||
, esockd
|
?BASIC_REBOOT_APPS ++ Apps.
|
||||||
, 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
|
|
||||||
].
|
|
||||||
|
|
||||||
sorted_reboot_apps() ->
|
sorted_reboot_apps() ->
|
||||||
Apps = [{App, app_deps(App)} || App <- reboot_apps()],
|
Apps = [{App, app_deps(App)} || App <- reboot_apps()],
|
||||||
|
|
|
@ -42,8 +42,24 @@ init_per_suite(Config) ->
|
||||||
%% Unload emqx_authz to avoid reboot this application
|
%% Unload emqx_authz to avoid reboot this application
|
||||||
%%
|
%%
|
||||||
application:unload(emqx_authz),
|
application:unload(emqx_authz),
|
||||||
|
|
||||||
emqx_common_test_helpers:start_apps([emqx_conf]),
|
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.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
|
|
8
bin/emqx
8
bin/emqx
|
@ -17,11 +17,11 @@ ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)"
|
||||||
export RUNNER_ROOT_DIR
|
export RUNNER_ROOT_DIR
|
||||||
export RUNNER_ETC_DIR
|
export RUNNER_ETC_DIR
|
||||||
export REL_VSN
|
export REL_VSN
|
||||||
|
export SCHEMA_MOD
|
||||||
|
|
||||||
RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME"
|
RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME"
|
||||||
CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"
|
CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"
|
||||||
REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN"
|
REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN"
|
||||||
SCHEMA_MOD=emqx_conf_schema
|
|
||||||
|
|
||||||
WHOAMI=$(whoami)
|
WHOAMI=$(whoami)
|
||||||
|
|
||||||
|
@ -389,7 +389,7 @@ generate_config() {
|
||||||
## meaning, certain overrides will not be mapped to app.<time>.config file
|
## meaning, certain overrides will not be mapped to app.<time>.config file
|
||||||
## disable SC2086 to allow EMQX_LICENSE_CONF_OPTION to split
|
## disable SC2086 to allow EMQX_LICENSE_CONF_OPTION to split
|
||||||
# shellcheck disable=SC2086
|
# 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
|
## filenames are per-hocon convention
|
||||||
local CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
|
local CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
|
||||||
|
@ -539,7 +539,7 @@ NAME="${EMQX_NODE__NAME:-}"
|
||||||
if [ -z "$NAME" ]; then
|
if [ -z "$NAME" ]; then
|
||||||
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
|
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
|
||||||
# for boot commands, inspect emqx.conf for node name
|
# 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
|
else
|
||||||
vm_args_file="$(latest_vm_args 'EMQX_NODE__NAME')"
|
vm_args_file="$(latest_vm_args 'EMQX_NODE__NAME')"
|
||||||
NAME="$(grep -E '^-s?name' "${vm_args_file}" | awk '{print $2}')"
|
NAME="$(grep -E '^-s?name' "${vm_args_file}" | awk '{print $2}')"
|
||||||
|
@ -570,7 +570,7 @@ fi
|
||||||
COOKIE="${EMQX_NODE__COOKIE:-}"
|
COOKIE="${EMQX_NODE__COOKIE:-}"
|
||||||
if [ -z "$COOKIE" ]; then
|
if [ -z "$COOKIE" ]; then
|
||||||
if [ "$IS_BOOT_COMMAND" = 'yes' ]; 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
|
else
|
||||||
vm_args_file="$(latest_vm_args 'EMQX_NODE__COOKIE')"
|
vm_args_file="$(latest_vm_args 'EMQX_NODE__COOKIE')"
|
||||||
COOKIE="$(grep -E '^-setcookie' "${vm_args_file}" | awk '{print $2}')"
|
COOKIE="$(grep -E '^-setcookie' "${vm_args_file}" | awk '{print $2}')"
|
||||||
|
|
|
@ -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
|
||||||
|
*~
|
|
@ -0,0 +1,3 @@
|
||||||
|
# emqx_enterprise_conf
|
||||||
|
|
||||||
|
EMQ X Enterprise configuration schema
|
|
@ -0,0 +1,2 @@
|
||||||
|
{erl_opts, [debug_info]}.
|
||||||
|
{deps, []}.
|
|
@ -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, []}
|
||||||
|
]}.
|
|
@ -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).
|
|
@ -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)).
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
# emqx_license
|
||||||
|
|
||||||
|
EMQ X 5.0 License Manager.
|
|
@ -0,0 +1,3 @@
|
||||||
|
license {
|
||||||
|
key = "MjIwMTExCjAKMTAKRm9vCmNvbnRhY3RAZm9vLmNvbQoyMDIyMDExMQoxMDAwMDAKMTAK.Iyle9eMrXSAZwJczR8MEI2dtpxLuL2OKRikTwYvFK/SgxfwZQLR7JJM2rKfkuT5eP4cxh0Y1+84hOoB7fj/MWA=="
|
||||||
|
}
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
{deps, [{emqx, {path, "../../apps/emqx"}}]}.
|
|
@ -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,[]}}]}.
|
|
@ -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}.
|
|
@ -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.
|
|
@ -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}).
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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">>.
|
|
@ -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).
|
|
@ -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()}
|
||||||
|
].
|
|
@ -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]}
|
||||||
|
]}}.
|
|
@ -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).
|
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKHM3qUJR23ixg9CZYFgDdvxmTzsl+T7
|
||||||
|
UDXNxN36UJTRN+9eFIeARbPXw2jiUva/Wf7vQKhilzZ/N+E0mK2CdX0CAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -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-----
|
|
@ -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=">>.
|
|
@ -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])).
|
|
@ -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=">>.
|
|
@ -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)).
|
|
@ -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"]).
|
|
@ -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()).
|
|
@ -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
128
mix.exs
|
@ -132,7 +132,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
end
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
applications: applications(release_type),
|
applications: applications(release_type, edition_type),
|
||||||
skip_mode_validation_for: [
|
skip_mode_validation_for: [
|
||||||
:emqx_gateway,
|
:emqx_gateway,
|
||||||
:emqx_dashboard,
|
:emqx_dashboard,
|
||||||
|
@ -154,7 +154,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def applications(release_type) do
|
def applications(release_type, edition_type) do
|
||||||
[
|
[
|
||||||
crypto: :permanent,
|
crypto: :permanent,
|
||||||
public_key: :permanent,
|
public_key: :permanent,
|
||||||
|
@ -200,12 +200,52 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
] ++
|
] ++
|
||||||
if(enable_quicer?(), do: [quicer: :permanent], else: []) ++
|
if(enable_quicer?(), do: [quicer: :permanent], else: []) ++
|
||||||
if(enable_bcrypt?(), do: [bcrypt: :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,
|
if(release_type == :cloud,
|
||||||
do: [xmerl: :permanent, observer: :load],
|
do: [xmerl: :permanent, observer: :load],
|
||||||
else: []
|
else: []
|
||||||
)
|
)
|
||||||
end
|
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
|
def check_profile!() do
|
||||||
valid_envs = [
|
valid_envs = [
|
||||||
:dev,
|
:dev,
|
||||||
|
@ -314,24 +354,24 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
# This is generated by `scripts/merge-config.escript` or `make
|
# This is generated by `scripts/merge-config.escript` or `make
|
||||||
# conf-segs`. So, this should be run before the release.
|
# conf-segs`. So, this should be run before the release.
|
||||||
# TODO: run as a "compiler" step???
|
# TODO: run as a "compiler" step???
|
||||||
conf_rendered =
|
render_template(
|
||||||
File.read!("apps/emqx_conf/etc/emqx.conf.all")
|
"apps/emqx_conf/etc/emqx.conf.all",
|
||||||
|> from_rebar_to_eex_template()
|
assigns,
|
||||||
|> EEx.eval_string(assigns)
|
Path.join(etc, "emqx.conf")
|
||||||
|
|
||||||
File.write!(
|
|
||||||
Path.join(etc, "emqx.conf"),
|
|
||||||
conf_rendered
|
|
||||||
)
|
)
|
||||||
|
|
||||||
vars_rendered =
|
if edition_type == :enterprise do
|
||||||
File.read!("rel/emqx_vars")
|
render_template(
|
||||||
|> from_rebar_to_eex_template()
|
"apps/emqx_conf/etc/emqx_enterprise.conf.all",
|
||||||
|> EEx.eval_string(assigns)
|
assigns,
|
||||||
|
Path.join(etc, "emqx_enterprise.conf")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
File.write!(
|
render_template(
|
||||||
Path.join([release.path, "releases", "emqx_vars"]),
|
"rel/emqx_vars",
|
||||||
vars_rendered
|
assigns,
|
||||||
|
Path.join([release.path, "releases", "emqx_vars"])
|
||||||
)
|
)
|
||||||
|
|
||||||
vm_args_template_path =
|
vm_args_template_path =
|
||||||
|
@ -343,19 +383,13 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
"apps/emqx/etc/emqx_edge/vm.args"
|
"apps/emqx/etc/emqx_edge/vm.args"
|
||||||
end
|
end
|
||||||
|
|
||||||
vm_args_rendered =
|
render_template(
|
||||||
File.read!(vm_args_template_path)
|
vm_args_template_path,
|
||||||
|> from_rebar_to_eex_template()
|
assigns,
|
||||||
|> EEx.eval_string(assigns)
|
[
|
||||||
|
Path.join(etc, "vm.args"),
|
||||||
File.write!(
|
Path.join(release.version_path, "vm.args")
|
||||||
Path.join(etc, "vm.args"),
|
]
|
||||||
vm_args_rendered
|
|
||||||
)
|
|
||||||
|
|
||||||
File.write!(
|
|
||||||
Path.join(release.version_path, "vm.args"),
|
|
||||||
vm_args_rendered
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for name <- [
|
for name <- [
|
||||||
|
@ -383,19 +417,30 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
File.chmod!(Path.join(bin, name), 0o755)
|
File.chmod!(Path.join(bin, name), 0o755)
|
||||||
end
|
end
|
||||||
|
|
||||||
built_on_rendered =
|
render_template(
|
||||||
File.read!("rel/BUILT_ON")
|
"rel/BUILT_ON",
|
||||||
|> from_rebar_to_eex_template()
|
assigns,
|
||||||
|> EEx.eval_string(assigns)
|
Path.join(release.version_path, "BUILT_ON")
|
||||||
|
|
||||||
File.write!(
|
|
||||||
Path.join([release.version_path, "BUILT_ON"]),
|
|
||||||
built_on_rendered
|
|
||||||
)
|
)
|
||||||
|
|
||||||
release
|
release
|
||||||
end
|
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
|
# needed by nodetool and by release_handler
|
||||||
defp create_RELEASES(release) do
|
defp create_RELEASES(release) do
|
||||||
apps =
|
apps =
|
||||||
|
@ -487,6 +532,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
# FIXME: this is empty in `make emqx` ???
|
# FIXME: this is empty in `make emqx` ???
|
||||||
erl_opts: "",
|
erl_opts: "",
|
||||||
emqx_description: emqx_description(release_type, edition_type),
|
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(),
|
built_on_arch: built_on(),
|
||||||
is_elixir: "yes"
|
is_elixir: "yes"
|
||||||
]
|
]
|
||||||
|
@ -513,6 +560,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
erl_opts: "",
|
erl_opts: "",
|
||||||
emqx_description: emqx_description(release_type, edition_type),
|
emqx_description: emqx_description(release_type, edition_type),
|
||||||
built_on_arch: built_on(),
|
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"
|
is_elixir: "yes"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
@ -530,6 +579,9 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
end
|
end
|
||||||
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
|
defp bcrypt_dep() do
|
||||||
if enable_bcrypt?(),
|
if enable_bcrypt?(),
|
||||||
do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],
|
do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],
|
||||||
|
|
|
@ -222,8 +222,10 @@ emqx_description(cloud, ee) -> "EMQ X Enterprise Edition";
|
||||||
emqx_description(cloud, ce) -> "EMQ X Community Edition";
|
emqx_description(cloud, ce) -> "EMQ X Community Edition";
|
||||||
emqx_description(edge, ce) -> "EMQ X Edge Edition".
|
emqx_description(edge, ce) -> "EMQ X Edge Edition".
|
||||||
|
|
||||||
overlay_vars(RelType, PkgType, _Edition) ->
|
overlay_vars(RelType, PkgType, Edition) ->
|
||||||
overlay_vars_rel(RelType) ++ overlay_vars_pkg(PkgType).
|
overlay_vars_rel(RelType)
|
||||||
|
++ overlay_vars_pkg(PkgType)
|
||||||
|
++ overlay_vars_edition(Edition).
|
||||||
|
|
||||||
%% vars per release type, cloud or edge
|
%% vars per release type, cloud or edge
|
||||||
overlay_vars_rel(RelType) ->
|
overlay_vars_rel(RelType) ->
|
||||||
|
@ -235,6 +237,15 @@ overlay_vars_rel(RelType) ->
|
||||||
[ {vm_args_file, VmArgs}
|
[ {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)
|
%% vars per packaging type, bin(zip/tar.gz/docker) or pkg(rpm/deb)
|
||||||
overlay_vars_pkg(bin) ->
|
overlay_vars_pkg(bin) ->
|
||||||
[ {platform_bin_dir, "bin"}
|
[ {platform_bin_dir, "bin"}
|
||||||
|
@ -316,10 +327,7 @@ relx_apps(ReleaseType, Edition) ->
|
||||||
%++ [emqx_license || is_enterprise(Edition)]
|
%++ [emqx_license || is_enterprise(Edition)]
|
||||||
++ [bcrypt || provide_bcrypt_release(ReleaseType)]
|
++ [bcrypt || provide_bcrypt_release(ReleaseType)]
|
||||||
++ relx_apps_per_rel(ReleaseType)
|
++ relx_apps_per_rel(ReleaseType)
|
||||||
%% NOTE: applications below are only loaded after node start/restart
|
++ relx_additional_apps(ReleaseType, Edition).
|
||||||
%% TODO: Add loaded/unloaded state to plugin apps
|
|
||||||
%% then we can always start plugin apps
|
|
||||||
++ [{N, load} || N <- relx_plugin_apps(ReleaseType, Edition)].
|
|
||||||
|
|
||||||
relx_apps_per_rel(cloud) ->
|
relx_apps_per_rel(cloud) ->
|
||||||
[ xmerl
|
[ xmerl
|
||||||
|
@ -335,19 +343,49 @@ is_app(Name) ->
|
||||||
_ -> false
|
_ -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
relx_plugin_apps(ReleaseType, Edition) ->
|
relx_additional_apps(ReleaseType, Edition) ->
|
||||||
relx_plugin_apps_per_rel(ReleaseType)
|
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(cloud) ->
|
||||||
[];
|
[];
|
||||||
relx_plugin_apps_per_rel(edge) ->
|
relx_plugin_apps_per_rel(edge) ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
relx_plugin_apps_enterprise(ee) ->
|
relx_apps_per_edition(ee) ->
|
||||||
[list_to_atom(A) || A <- filelib:wildcard("*", "lib-ee"),
|
[ emqx_license
|
||||||
filelib:is_dir(filename:join(["lib-ee", A]))];
|
, {emqx_enterprise_conf, load}
|
||||||
relx_plugin_apps_enterprise(ce) -> [].
|
];
|
||||||
|
|
||||||
|
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) ->
|
relx_overlay(ReleaseType, Edition) ->
|
||||||
[ {mkdir, "log/"}
|
[ {mkdir, "log/"}
|
||||||
|
@ -374,34 +412,38 @@ relx_overlay(ReleaseType, Edition) ->
|
||||||
, {copy, "bin/nodetool", "bin/nodetool-{{release_version}}"}
|
, {copy, "bin/nodetool", "bin/nodetool-{{release_version}}"}
|
||||||
] ++ etc_overlay(ReleaseType, Edition).
|
] ++ etc_overlay(ReleaseType, Edition).
|
||||||
|
|
||||||
etc_overlay(ReleaseType, _Edition) ->
|
etc_overlay(ReleaseType, Edition) ->
|
||||||
Templates = emqx_etc_overlay(ReleaseType),
|
Templates = emqx_etc_overlay(ReleaseType, Edition),
|
||||||
[ {mkdir, "etc/"}
|
[ {mkdir, "etc/"}
|
||||||
, {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"}
|
, {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"}
|
||||||
] ++
|
] ++
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({From, To}) -> {template, From, To};
|
fun({From, To}) -> {template, From, To};
|
||||||
(FromTo) -> {template, FromTo, FromTo}
|
(FromTo) -> {template, FromTo, FromTo}
|
||||||
end, Templates)
|
end, Templates).
|
||||||
++ extra_overlay(ReleaseType).
|
|
||||||
|
|
||||||
extra_overlay(cloud) ->
|
emqx_etc_overlay(ReleaseType, Edition) ->
|
||||||
[
|
emqx_etc_overlay_per_rel(ReleaseType)
|
||||||
];
|
++ emqx_etc_overlay_per_edition(Edition)
|
||||||
extra_overlay(edge) ->
|
++ emqx_etc_overlay_common().
|
||||||
[].
|
|
||||||
emqx_etc_overlay(cloud) ->
|
emqx_etc_overlay_per_rel(cloud) ->
|
||||||
emqx_etc_overlay_common() ++
|
|
||||||
[ {"{{base_dir}}/lib/emqx/etc/emqx_cloud/vm.args","etc/vm.args"}
|
[ {"{{base_dir}}/lib/emqx/etc/emqx_cloud/vm.args","etc/vm.args"}
|
||||||
];
|
];
|
||||||
emqx_etc_overlay(edge) ->
|
emqx_etc_overlay_per_rel(edge) ->
|
||||||
emqx_etc_overlay_common() ++
|
|
||||||
[ {"{{base_dir}}/lib/emqx/etc/emqx_edge/vm.args","etc/vm.args"}
|
[ {"{{base_dir}}/lib/emqx/etc/emqx_edge/vm.args","etc/vm.args"}
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_etc_overlay_common() ->
|
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_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) ->
|
get_vsn(Profile) ->
|
||||||
|
|
|
@ -14,8 +14,8 @@ RUNNER_ETC_DIR="{{ runner_etc_dir }}"
|
||||||
RUNNER_DATA_DIR="{{ runner_data_dir }}"
|
RUNNER_DATA_DIR="{{ runner_data_dir }}"
|
||||||
RUNNER_USER="{{ runner_user }}"
|
RUNNER_USER="{{ runner_user }}"
|
||||||
IS_ELIXIR="{{ is_elixir }}"
|
IS_ELIXIR="{{ is_elixir }}"
|
||||||
|
SCHEMA_MOD="{{ emqx_schema_mod }}"
|
||||||
|
|
||||||
EMQX_LICENSE_CONF=''
|
|
||||||
export EMQX_DESCRIPTION='{{ emqx_description }}'
|
export EMQX_DESCRIPTION='{{ emqx_description }}'
|
||||||
|
|
||||||
## computed vars
|
## computed vars
|
||||||
|
|
|
@ -36,6 +36,9 @@ collect_deps([File | Files], Acc) ->
|
||||||
collect_deps(Files, do_collect_deps(Deps, File, Acc)).
|
collect_deps(Files, do_collect_deps(Deps, File, Acc)).
|
||||||
|
|
||||||
do_collect_deps([], _File, Acc) -> 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) ->
|
do_collect_deps([{Name, Ref} | Deps], File, Acc) ->
|
||||||
Refs = maps:get(Name, Acc, []),
|
Refs = maps:get(Name, Acc, []),
|
||||||
do_collect_deps(Deps, File, Acc#{Name => [{Ref, File} | Refs]}).
|
do_collect_deps(Deps, File, Acc#{Name => [{Ref, File} | Refs]}).
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule CheckElixirApplications do
|
||||||
env: [{"DEBUG", "1"}]
|
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)
|
rebar_apps = rebar_applications(profile)
|
||||||
results = diff_apps(mix_apps, rebar_apps)
|
results = diff_apps(mix_apps, rebar_apps)
|
||||||
|
|
||||||
|
@ -70,8 +70,8 @@ defmodule CheckElixirApplications do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mix_applications(release_type) do
|
defp mix_applications(release_type, edition_type) do
|
||||||
EMQXUmbrella.MixProject.applications(release_type)
|
EMQXUmbrella.MixProject.applications(release_type, edition_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp rebar_applications(profile) do
|
defp rebar_applications(profile) do
|
||||||
|
|
|
@ -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()
|
|
@ -12,16 +12,29 @@
|
||||||
|
|
||||||
main(_) ->
|
main(_) ->
|
||||||
{ok, BaseConf} = file:read_file("apps/emqx_conf/etc/emqx_conf.conf"),
|
{ok, BaseConf} = file:read_file("apps/emqx_conf/etc/emqx_conf.conf"),
|
||||||
|
|
||||||
Cfgs = get_all_cfgs("apps/"),
|
Cfgs = get_all_cfgs("apps/"),
|
||||||
Conf = lists:foldl(fun(CfgFile, Acc) ->
|
Conf = [merge(BaseConf, Cfgs),
|
||||||
case filelib:is_regular(CfgFile) of
|
io_lib:nl(),
|
||||||
true ->
|
"include emqx_enterprise.conf",
|
||||||
{ok, Bin1} = file:read_file(CfgFile),
|
io_lib:nl()],
|
||||||
[Acc, io_lib:nl(), Bin1];
|
ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf),
|
||||||
false -> Acc
|
|
||||||
end
|
EnterpriseCfgs = get_all_cfgs("lib-ee/"),
|
||||||
end, BaseConf, Cfgs),
|
EnterpriseConf = merge("", EnterpriseCfgs),
|
||||||
ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf).
|
|
||||||
|
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) ->
|
get_all_cfgs(Root) ->
|
||||||
Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_conf"],
|
Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_conf"],
|
||||||
|
|
Loading…
Reference in New Issue