%% Copyright (C) 2025 by sysmocom - s.f.m.c. GmbH %% Author: Vadim Yanitskiy %% %% All Rights Reserved %% %% SPDX-License-Identifier: AGPL-3.0-or-later %% %% This program is free software; you can redistribute it and/or modify %% it under the terms of the GNU Affero General Public License as %% published by the Free Software Foundation; either version 3 of the %% License, or (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU Affero General Public License %% along with this program. If not, see . %% %% Additional Permission under GNU AGPL version 3 section 7: %% %% If you modify this Program, or any covered work, by linking or %% combining it with runtime libraries of Erlang/OTP as released by %% Ericsson on https://www.erlang.org (or a modified version of these %% libraries), containing parts covered by the terms of the Erlang Public %% License (https://www.erlang.org/EPLICENSE), the licensors of this %% Program grant you additional permission to convey the resulting work %% without the need to license the runtime libraries of Erlang/OTP under %% the GNU Affero General Public License. Corresponding Source for a %% non-source form of such a combination shall include the source code %% for the parts of the runtime libraries of Erlang/OTP used as well as %% that of the covered work. -module(mme_registry). -behaviour(gen_server). -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2]). -export([start_link/0, mme_register/1, mme_unregister/1, mme_select/1, fetch_mme_info/1, fetch_mme_list/0, shutdown/0]). -include_lib("kernel/include/logger.hrl"). -include("s1gw_metrics.hrl"). -include("s1ap.hrl"). -type mme_name() :: string(). -type mme_info() :: #{name := mme_name(), %% unique identifier of this MME laddr => string() | inet:ip_address(), raddr := string() | inet:ip_address(), rport => inet:port_number(), tac_list => [s1ap_utils:tac()] }. -type mme_list() :: [mme_info()]. -type mme_select_params() :: #{prev_mme => mme_name(), enb_tacs := [s1ap_utils:tac()] }. -export_type([mme_name/0, mme_info/0, mme_select_params/0]). %% ------------------------------------------------------------------ %% public API %% ------------------------------------------------------------------ -spec start_link() -> {ok, pid()} | term(). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec mme_register(mme_info()) -> ok | {error, term()}. mme_register(MmeInfo) -> gen_server:call(?MODULE, {?FUNCTION_NAME, MmeInfo}). -spec mme_unregister(mme_name()) -> ok | {error, term()}. mme_unregister(MmeName) -> gen_server:call(?MODULE, {?FUNCTION_NAME, MmeName}). -spec mme_select(mme_select_params()) -> {ok, mme_info()} | error. mme_select(SelParams) -> gen_server:call(?MODULE, {?FUNCTION_NAME, SelParams}). -spec fetch_mme_info(mme_name()) -> {ok, mme_info()} | error. fetch_mme_info(MmeName) -> gen_server:call(?MODULE, {?FUNCTION_NAME, MmeName}). -spec fetch_mme_list() -> mme_list(). fetch_mme_list() -> gen_server:call(?MODULE, ?FUNCTION_NAME). -spec shutdown() -> ok. shutdown() -> gen_server:stop(?MODULE). %% ------------------------------------------------------------------ %% gen_server API %% ------------------------------------------------------------------ init([]) -> %% parse MMEs from the environment MMEs = mme_list_from_env(), {ok, MMEs}. handle_call({mme_register, MmeInfo}, _From, MMEs0) -> case mme_add(MmeInfo, MMEs0) of {ok, MMEs1} -> {reply, ok, MMEs1}; {error, Error} -> {reply, {error, Error}, MMEs0} end; handle_call({mme_unregister, MmeName}, _From, MMEs0) -> case mme_del(MmeName, MMEs0) of {ok, MMEs1} -> %% TODO: kill all eNB connections? {reply, ok, MMEs1}; {error, Error} -> {reply, {error, Error}, MMEs0} end; handle_call({mme_select, Params}, _From, MMEs0) -> OldMmeName = maps:get(prev_mme, Params, undefined), EnbTACs = maps:get(enb_tacs, Params, []), MMEs1 = lists:filter(fun(E) -> mme_match_by_tac(E, EnbTACs) end, MMEs0), Reply = mme_select(OldMmeName, MMEs1), {reply, Reply, MMEs0}; handle_call({fetch_mme_info, MmeName}, _From, MMEs) -> Reply = mme_find(MmeName, MMEs), {reply, Reply, MMEs}; handle_call(fetch_mme_list, _From, MMEs) -> {reply, MMEs, MMEs}; handle_call(Info, From, MMEs) -> ?LOG_ERROR("unknown ~p() from ~p: ~p", [?FUNCTION_NAME, From, Info]), {reply, {error, not_implemented}, MMEs}. handle_cast(Info, MMEs) -> ?LOG_ERROR("unknown ~p(): ~p", [?FUNCTION_NAME, Info]), {noreply, MMEs}. handle_info(Info, MMEs) -> ?LOG_ERROR("unknown ~p(): ~p", [?FUNCTION_NAME, Info]), {noreply, MMEs}. terminate(Reason, _MMEs) -> ?LOG_NOTICE("Terminating, reason ~p", [Reason]), ok. %% ------------------------------------------------------------------ %% private API %% ------------------------------------------------------------------ %% Match the given MME by name and raddr/rport fields -spec mme_match(mme_info(), map()) -> true | false. mme_match(#{name := Name}, #{name := Name}) -> true; mme_match(#{raddr := Addr, rport := Port}, #{raddr := Addr, rport := Port}) -> true; mme_match(_, _) -> false. %% Match the given MME by eNB's TAC list -spec mme_match_by_tac(MmeInfo, EnbTACs) -> true | false when MmeInfo :: mme_info(), EnbTACs :: [s1ap_utils:tac()]. mme_match_by_tac(#{tac_list := []}, _EnbTACs) -> %% empty MME TAC list => this MME allows all TACs true; mme_match_by_tac(#{tac_list := MmeTACs}, EnbTACs) -> %% eNB's TACs must be a subset of MME's TACs lists:subtract(EnbTACs, MmeTACs) == []. %% Add a new MME if it does not already exist -spec mme_add(mme_info(), mme_list()) -> {ok, mme_list()} | {error, term()}. mme_add(MmeInfo0, MMEs) -> %% assign defaults, parse local/remote addresses MmeName = maps:get(name, MmeInfo0), LAddr = sctp_common:parse_addr(maps:get(laddr, MmeInfo0, "::")), RAddr = sctp_common:parse_addr(maps:get(raddr, MmeInfo0)), RPort = maps:get(rport, MmeInfo0, ?S1AP_PORT), TACs = maps:get(tac_list, MmeInfo0, []), MmeInfo1 = MmeInfo0#{laddr => LAddr, raddr => RAddr, rport => RPort, tac_list => TACs}, %% check for duplicates case lists:any(fun(E) -> mme_match(E, MmeInfo1) end, MMEs) of true -> ?LOG_ERROR("MME (name=~p / ~p:~p) is *already* registered", [MmeName, RAddr, RPort]), {error, already_registered}; false -> ?LOG_INFO("MME (name=~p, ~p:~p) registered", [MmeName, RAddr, RPort]), {ok, MMEs ++ [MmeInfo1]} end. %% Remove an MME by name -spec mme_del(mme_name(), mme_list()) -> {ok, mme_list()} | {error, term()}. mme_del(MmeName, MMEs0) -> Fun = fun(E) -> not mme_match(E, #{name => MmeName}) end, case lists:filter(Fun, MMEs0) of MMEs0 -> %% unchanged list means nothing was filtered out ?LOG_ERROR("MME (name=~p) is *not* registered", [MmeName]), {error, not_registered}; MMEs1 -> ?LOG_INFO("MME (name=~p) unregistered", [MmeName]), {ok, MMEs1} end. %% Select an MME from the pool -spec mme_select(OldMmeName, MMEs) -> Result when OldMmeName :: undefined | mme_name(), MMEs :: mme_list(), Result :: {ok, mme_info()} | error. mme_select(_OldMmeName, []) -> %% empty MME pool error; mme_select(_OldMmeName, [MmeInfo]) -> %% only one MME in the pool {ok, MmeInfo}; mme_select(undefined, MMEs) -> %% old MME unknown => fall back to the first entry {ok, hd(MMEs)}; mme_select(OldMmeName, MMEs) -> Fun = fun(E) -> maps:get(name, E) =/= OldMmeName end, case lists:dropwhile(Fun, MMEs) of [] -> %% old MME is not registered, fall back to the first entry {ok, hd(MMEs)}; [_ | []] -> %% old MME was the last entry, wrap around {ok, hd(MMEs)}; [_ | [MmeInfo | _]] -> %% return MME following the old one {ok, MmeInfo} end. %% Find an MME by name -spec mme_find(mme_name(), mme_list()) -> {ok, mme_info()} | error. mme_find(MmeName, MMEs) -> Fun = fun(E) -> mme_match(E, #{name => MmeName}) end, case lists:filter(Fun, MMEs) of [MmeInfo] -> {ok, MmeInfo}; [] -> error end. -spec mme_list_from_env() -> mme_list(). mme_list_from_env() -> MMEs = osmo_s1gw:get_env(mme_pool, []), lists:foldl(fun mme_add_from_env/2, [], MMEs). -spec mme_add_from_env(mme_info(), mme_list()) -> mme_list(). mme_add_from_env(MmeInfo, MMEs0) -> {ok, MMEs1} = mme_add(MmeInfo, MMEs0), MMEs1. %% vim:set ts=4 sw=4 et: