277 lines
9.0 KiB
Erlang
277 lines
9.0 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
%%--------------------------------------------------------------------
|
|
|
|
-module(emqx_license_checker).
|
|
|
|
-include("emqx_license.hrl").
|
|
-include_lib("emqx/include/logger.hrl").
|
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-define(CHECK_INTERVAL, timer:seconds(5)).
|
|
-define(REFRESH_INTERVAL, timer:minutes(2)).
|
|
-define(EXPIRY_ALARM_CHECK_INTERVAL, timer:hours(24)).
|
|
|
|
-define(OK(EXPR),
|
|
try
|
|
_ = begin
|
|
EXPR
|
|
end,
|
|
ok
|
|
catch
|
|
_:_ -> ok
|
|
end
|
|
).
|
|
|
|
-export([
|
|
start_link/1,
|
|
start_link/2,
|
|
update/1,
|
|
dump/0,
|
|
expiry_epoch/0,
|
|
purge/0,
|
|
limits/0,
|
|
print_warnings/1
|
|
]).
|
|
|
|
%% gen_server callbacks
|
|
-export([
|
|
init/1,
|
|
handle_call/3,
|
|
handle_cast/2,
|
|
handle_info/2
|
|
]).
|
|
|
|
-define(LICENSE_TAB, emqx_license).
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% API
|
|
%%------------------------------------------------------------------------------
|
|
|
|
-type limits() :: #{max_connections := non_neg_integer() | ?ERR_EXPIRED}.
|
|
|
|
-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()) -> map().
|
|
update(License) ->
|
|
gen_server:call(?MODULE, {update, License}, infinity).
|
|
|
|
-spec dump() -> [{atom(), term()}].
|
|
dump() ->
|
|
gen_server:call(?MODULE, dump, infinity).
|
|
|
|
-spec expiry_epoch() -> integer().
|
|
expiry_epoch() ->
|
|
gen_server:call(?MODULE, expiry_epoch, infinity).
|
|
|
|
-spec limits() -> {ok, limits()} | {error, any()}.
|
|
limits() ->
|
|
try ets:lookup(?LICENSE_TAB, limits) of
|
|
[{limits, Limits}] -> {ok, Limits};
|
|
_ -> {error, no_license}
|
|
catch
|
|
error:badarg ->
|
|
{error, no_license}
|
|
end.
|
|
|
|
%% @doc Force purge the license table.
|
|
-spec purge() -> ok.
|
|
purge() ->
|
|
gen_server:call(?MODULE, purge, infinity).
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% gen_server callbacks
|
|
%%------------------------------------------------------------------------------
|
|
|
|
init([LicenseFetcher, CheckInterval]) ->
|
|
case LicenseFetcher() of
|
|
{ok, License} ->
|
|
?LICENSE_TAB = ets:new(?LICENSE_TAB, [
|
|
set, protected, named_table, {read_concurrency, true}
|
|
]),
|
|
ok = print_warnings(check_license(License)),
|
|
State0 = ensure_check_license_timer(#{
|
|
check_license_interval => CheckInterval,
|
|
license => License
|
|
}),
|
|
State1 = ensure_refresh_timer(State0),
|
|
State = ensure_check_expiry_timer(State1),
|
|
{ok, State};
|
|
{error, Reason} ->
|
|
{stop, Reason}
|
|
end.
|
|
|
|
handle_call({update, License}, _From, #{license := Old} = State) ->
|
|
ok = expiry_early_alarm(License),
|
|
State1 = ensure_refresh_timer(State),
|
|
ok = log_new_license(Old, License),
|
|
{reply, check_license(License), State1#{license => License}};
|
|
handle_call(dump, _From, #{license := License} = State) ->
|
|
{reply, emqx_license_parser:dump(License), State};
|
|
handle_call(expiry_epoch, _From, #{license := License} = State) ->
|
|
ExpiryEpoch = date_to_expiry_epoch(emqx_license_parser:expiry_date(License)),
|
|
{reply, ExpiryEpoch, State};
|
|
handle_call(purge, _From, State) ->
|
|
_ = ets:delete_all_objects(?LICENSE_TAB),
|
|
{reply, ok, 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_check_license_timer(State),
|
|
?tp(emqx_license_checked, #{}),
|
|
{noreply, NewState};
|
|
handle_info(check_expiry_alarm, #{license := License} = State) ->
|
|
ok = expiry_early_alarm(License),
|
|
NewState = ensure_check_expiry_timer(State),
|
|
{noreply, NewState};
|
|
handle_info(refresh, State0) ->
|
|
State1 = refresh(State0),
|
|
NewState = ensure_refresh_timer(State1),
|
|
{noreply, NewState};
|
|
handle_info(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Private functions
|
|
%%------------------------------------------------------------------------------
|
|
|
|
refresh(#{license := #{source := <<"file://", _/binary>> = Source} = License} = State) ->
|
|
case emqx_license_parser:parse(Source) of
|
|
{ok, License} ->
|
|
?tp(emqx_license_refresh_no_change, #{}),
|
|
%% no change
|
|
State;
|
|
{ok, NewLicense} ->
|
|
ok = log_new_license(License, NewLicense),
|
|
%% ensure alarm is set or cleared
|
|
ok = expiry_early_alarm(NewLicense),
|
|
?tp(emqx_license_refresh_changed, #{new_license => NewLicense}),
|
|
State#{license => NewLicense};
|
|
{error, Reason} ->
|
|
?tp(
|
|
error,
|
|
emqx_license_refresh_failed,
|
|
Reason#{continue_with_license => emqx_license_parser:summary(License)}
|
|
),
|
|
State
|
|
end;
|
|
refresh(State) ->
|
|
State.
|
|
|
|
log_new_license(Old, New) ->
|
|
?SLOG(info, #{
|
|
msg => "new_license_loaded",
|
|
old_license => emqx_license_parser:summary(Old),
|
|
new_license => emqx_license_parser:summary(New)
|
|
}).
|
|
|
|
ensure_check_license_timer(#{check_license_interval := CheckInterval} = State) ->
|
|
ok = cancel_timer(State, check_timer),
|
|
State#{check_timer => erlang:send_after(CheckInterval, self(), check_license)}.
|
|
|
|
ensure_check_expiry_timer(State) ->
|
|
ok = cancel_timer(State, expiry_alarm_timer),
|
|
Ref = erlang:send_after(?EXPIRY_ALARM_CHECK_INTERVAL, self(), check_expiry_alarm),
|
|
State#{expiry_alarm_timer => Ref}.
|
|
|
|
%% refresh is to work with file:// license keys.
|
|
ensure_refresh_timer(State) ->
|
|
ok = cancel_timer(State, refresh_timer),
|
|
Ref = erlang:send_after(?REFRESH_INTERVAL, self(), refresh),
|
|
State#{refresh_timer => Ref}.
|
|
|
|
cancel_timer(State, Key) ->
|
|
case maps:find(Key, State) of
|
|
{ok, Ref} when is_reference(Ref) ->
|
|
_ = erlang:cancel_timer(Ref),
|
|
ok;
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
check_license(License) ->
|
|
DaysLeft = days_left(License),
|
|
IsOverdue = is_overdue(License, DaysLeft),
|
|
NeedRestriction = IsOverdue,
|
|
MaxConn = emqx_license_parser:max_connections(License),
|
|
Limits = limits(License, NeedRestriction),
|
|
true = apply_limits(Limits),
|
|
#{
|
|
warn_evaluation => warn_evaluation(License, NeedRestriction, MaxConn),
|
|
warn_expiry => {(DaysLeft < 0), -DaysLeft}
|
|
}.
|
|
|
|
warn_evaluation(License, false, MaxConn) ->
|
|
{emqx_license_parser:customer_type(License) == ?EVALUATION_CUSTOMER, MaxConn};
|
|
warn_evaluation(_License, _NeedRestrict, _Limits) ->
|
|
false.
|
|
|
|
limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)};
|
|
limits(_License, true) -> #{max_connections => ?ERR_EXPIRED}.
|
|
|
|
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).
|
|
|
|
is_overdue(License, DaysLeft) ->
|
|
CType = emqx_license_parser:customer_type(License),
|
|
Type = emqx_license_parser:license_type(License),
|
|
|
|
small_customer_overdue(CType, DaysLeft) orelse
|
|
non_official_license_overdue(Type, DaysLeft).
|
|
|
|
%% small customers overdue 90 days after license expiry date
|
|
small_customer_overdue(?SMALL_CUSTOMER, DaysLeft) -> DaysLeft < ?EXPIRED_DAY;
|
|
small_customer_overdue(_CType, _DaysLeft) -> false.
|
|
|
|
%% never restrict official license
|
|
non_official_license_overdue(?OFFICIAL, _) -> false;
|
|
non_official_license_overdue(_, DaysLeft) -> DaysLeft < 0.
|
|
|
|
%% 62167219200 =:= calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}).
|
|
-define(EPOCH_START, 62167219200).
|
|
-spec date_to_expiry_epoch(calendar:date()) -> Seconds :: non_neg_integer().
|
|
date_to_expiry_epoch({Y, M, D}) ->
|
|
calendar:datetime_to_gregorian_seconds({{Y, M, D}, {0, 0, 0}}) - ?EPOCH_START.
|
|
|
|
apply_limits(Limits) ->
|
|
ets:insert(?LICENSE_TAB, {limits, Limits}).
|
|
|
|
expiry_early_alarm(License) ->
|
|
case days_left(License) < 30 of
|
|
true ->
|
|
{Y, M, D} = emqx_license_parser:expiry_date(License),
|
|
Date = iolist_to_binary(io_lib:format("~B~2..0B~2..0B", [Y, M, D])),
|
|
?OK(emqx_alarm:activate(license_expiry, #{expiry_at => Date}));
|
|
false ->
|
|
?OK(emqx_alarm:ensure_deactivated(license_expiry))
|
|
end.
|
|
|
|
print_warnings(Warnings) ->
|
|
ok = print_evaluation_warning(Warnings),
|
|
ok = print_expiry_warning(Warnings).
|
|
|
|
print_evaluation_warning(#{warn_evaluation := {true, MaxConn}}) ->
|
|
io:format(?EVALUATION_LOG, [MaxConn]);
|
|
print_evaluation_warning(_) ->
|
|
ok.
|
|
|
|
print_expiry_warning(#{warn_expiry := {true, Days}}) ->
|
|
io:format(?EXPIRY_LOG, [Days]);
|
|
print_expiry_warning(_) ->
|
|
ok.
|