%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% %% Tests of traffic between seven Diameter nodes in four realms, %% connected as follows. %% %% ----- SERVER1.REALM2 ----- %% / \ %% / ----- SERVER2.REALM2 ----- \ %% | / \ | %% CLIENT.REALM1 ------ SERVER3.REALM2 ------ CLIENT.REALM4 %% | \ / | %% | \ / | %% \ ---- SERVER1.REALM3 ----- / %% \ / %% ----- SERVER2.REALM3 ----- %% -module(diameter_failover_SUITE). -export([suite/0, all/0]). %% testcases -export([start/1, start_services/1, connect/1, send_ok/1, send_nok/1, send_discard_1/1, send_discard_2/1, stop_services/1, empty/1, stop/1]). %% diameter callbacks -export([pick_peer/4, prepare_request/3, prepare_retransmit/3, handle_error/4, handle_answer/4, handle_request/3]). -include("diameter.hrl"). -include("diameter_gen_base_rfc3588.hrl"). %% =========================================================================== -define(util, diameter_util). -define(ADDR, {127,0,0,1}). -define(CLIENT1, "CLIENT.REALM1"). -define(CLIENT2, "CLIENT.REALM4"). -define(SERVER1, "SERVER1.REALM2"). -define(SERVER2, "SERVER2.REALM2"). -define(SERVER3, "SERVER3.REALM2"). -define(SERVER4, "SERVER1.REALM3"). -define(SERVER5, "SERVER2.REALM3"). -define(IS_CLIENT(Svc), Svc == ?CLIENT1; Svc == ?CLIENT2). -define(CLIENTS, [?CLIENT1, ?CLIENT2]). -define(SERVERS, [?SERVER1, ?SERVER2, ?SERVER3, ?SERVER4, ?SERVER5]). -define(DICT_COMMON, ?DIAMETER_DICT_COMMON). -define(APP_ALIAS, the_app). -define(APP_ID, ?DICT_COMMON:id()). %% Config for diameter:start_service/2. -define(SERVICE(Host), [{'Origin-Host', Host}, {'Origin-Realm', realm(Host)}, {'Host-IP-Address', [?ADDR]}, {'Vendor-Id', 12345}, {'Product-Name', "OTP/diameter"}, {'Acct-Application-Id', [?APP_ID]}, {application, [{alias, ?APP_ALIAS}, {dictionary, ?DICT_COMMON}, {module, #diameter_callback {peer_up = false, peer_down = false, default = ?MODULE}}, {answer_errors, callback}]}]). -define(SUCCESS, 2001). %% Value of Termination-Cause determines client/server behaviour. -define(LOGOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_LOGOUT'). -define(MOVED, ?'DIAMETER_BASE_TERMINATION-CAUSE_USER_MOVED'). -define(TIMEOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_SESSION_TIMEOUT'). %% =========================================================================== suite() -> [{timetrap, {seconds, 60}}]. all() -> [start, start_services, connect, send_ok, send_nok, send_discard_1, send_discard_2, stop_services, empty, stop]. %% =========================================================================== %% start/stop testcases start(_Config) -> ok = diameter:start(). start_services(_Config) -> Servers = [server(N) || N <- ?SERVERS], [] = [T || C <- ?CLIENTS, T <- [diameter:start_service(C, ?SERVICE(C))], T /= ok], {save_config, Servers}. connect(Config) -> {start_services, Servers} = proplists:get_value(saved_config, Config), lists:foreach(fun(C) -> connect(C, Servers) end, ?CLIENTS). stop_services(_Config) -> [] = [{H,T} || H <- ?CLIENTS ++ ?SERVERS, T <- [diameter:stop_service(H)], T /= ok]. %% Ensure transports have been removed from request table. empty(_Config) -> [] = ets:tab2list(diameter_request). stop(_Config) -> ok = diameter:stop(). %% ---------------------------------------- server(Name) -> ok = diameter:start_service(Name, ?SERVICE(Name)), {Name, ?util:listen(Name, tcp)}. connect(Name, Refs) -> [{{Name, ?util:connect(Name, tcp, LRef)}, T} || {_, LRef} = T <- Refs]. %% =========================================================================== %% traffic testcases %% Send an STR and expect success after SERVER3 answers after a couple %% of failovers. send_ok(_Config) -> Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER1), 'Termination-Cause' = ?LOGOUT, 'Auth-Application-Id' = ?APP_ID}, #diameter_base_STA{'Result-Code' = ?SUCCESS, 'Origin-Host' = ?SERVER3} = call(?CLIENT1, Req). %% Send an STR and expect failure when both servers fail. send_nok(_Config) -> Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER4), 'Termination-Cause' = ?LOGOUT, 'Auth-Application-Id' = ?APP_ID}, {failover, ?LOGOUT} = call(?CLIENT1, Req). %% Send an STR and have prepare_retransmit discard it. send_discard_1(_Config) -> Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER1), 'Termination-Cause' = ?TIMEOUT, 'Auth-Application-Id' = ?APP_ID}, {rejected, ?TIMEOUT} = call(?CLIENT2, Req). send_discard_2(_Config) -> Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER4), 'Termination-Cause' = ?MOVED, 'Auth-Application-Id' = ?APP_ID}, {discarded, ?MOVED} = call(?CLIENT2, Req). %% =========================================================================== realm(Host) -> tl(lists:dropwhile(fun(C) -> C /= $. end, Host)). call(Svc, Req) -> diameter:call(Svc, ?APP_ALIAS, Req, [{filter, realm}]). %% =========================================================================== %% diameter callbacks %% pick_peer/4 %% Choose a server other than SERVER3 or SERVER5 if possible. pick_peer(Peers, _, Svc, _State) when ?IS_CLIENT(Svc) -> case lists:partition(fun({_, #diameter_caps{origin_host = {_, OH}}}) -> OH /= ?SERVER3 andalso OH /= ?SERVER5 end, Peers) of {[], [Peer]} -> {ok, Peer}; {[Peer | _], _} -> {ok, Peer} end. %% prepare_request/3 prepare_request(Pkt, Svc, {_Ref, Caps}) when ?IS_CLIENT(Svc) -> {send, prepare(Pkt, Caps)}. prepare(#diameter_packet{msg = Req}, Caps) -> #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}} = Caps, Req#diameter_base_STR{'Origin-Host' = OH, 'Origin-Realm' = OR, 'Session-Id' = diameter:session_id(OH)}. %% prepare_retransmit/3 prepare_retransmit(#diameter_packet{header = H} = P, Svc, {_,_}) when ?IS_CLIENT(Svc) -> #diameter_header{is_retransmitted = true} = H, %% assert prepare(P). prepare(#diameter_packet{msg = M} = P) -> case M#diameter_base_STR.'Termination-Cause' of ?LOGOUT -> {send, P}; ?MOVED -> discard; ?TIMEOUT -> {discard, rejected} end. %% handle_error/4 handle_error(Reason, Req, _, _) -> {Reason, Req#diameter_base_STR.'Termination-Cause'}. %% handle_answer/4 handle_answer(Pkt, _Req, Svc, _Peer) when ?IS_CLIENT(Svc) -> #diameter_packet{msg = Rec, errors = []} = Pkt, Rec. %% handle_request/3 %% Only SERVER3 actually answers. handle_request(Pkt, ?SERVER3, {_, Caps}) -> #diameter_packet{header = #diameter_header{is_retransmitted = true}, msg = #diameter_base_STR{'Session-Id' = SId}} = Pkt, #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}} = Caps, {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS, 'Session-Id' = SId, 'Origin-Host' = OH, 'Origin-Realm' = OR}}; %% Others kill the transport to force failover. handle_request(_, _, {TPid, _}) -> exit(TPid, kill), discard.