1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2019-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22%% Tests of traffic between two Diameter nodes, the server being
23%% spread across three Erlang nodes.
24%%
25
26-module(diameter_dist_SUITE).
27
28-export([suite/0,
29         all/0]).
30
31%% testcases
32-export([enslave/1, enslave/0,
33         ping/1,
34         start/1,
35         connect/1,
36         send/1,
37         stop/1, stop/0]).
38
39%% diameter callbacks
40-export([peer_up/3,
41         peer_down/3,
42         pick_peer/4,
43         prepare_request/3,
44         prepare_retransmit/3,
45         handle_answer/4,
46         handle_error/4,
47         handle_request/3]).
48
49-export([call/1]).
50
51-include("diameter.hrl").
52-include("diameter_gen_base_rfc6733.hrl").
53
54%% ===========================================================================
55
56-define(util, diameter_util).
57
58-define(CLIENT, 'CLIENT').
59-define(SERVER, 'SERVER').
60-define(REALM, "erlang.org").
61-define(DICT, diameter_gen_base_rfc6733).
62-define(ADDR, {127,0,0,1}).
63
64%% Config for diameter:start_service/2.
65-define(SERVICE(Host),
66        [{'Origin-Host', Host ++ [$.|?REALM]},
67         {'Origin-Realm', ?REALM},
68         {'Host-IP-Address', [?ADDR]},
69         {'Vendor-Id', 12345},
70         {'Product-Name', "OTP/diameter"},
71         {'Auth-Application-Id', [?DICT:id()]},
72         {'Origin-State-Id', origin()},
73         {spawn_opt, {diameter_dist, route_session, [#{id => []}]}},
74         {sequence, fun sequence/0},
75         {string_decode, false},
76         {application, [{dictionary, ?DICT},
77                        {module, ?MODULE},
78                        {request_errors, callback},
79                        {answer_errors, callback}]}]).
80
81-define(SUCCESS, 2001).
82-define(BUSY,    3004).
83-define(LOGOUT,  ?'DIAMETER_BASE_TERMINATION-CAUSE_LOGOUT').
84-define(MOVED,   ?'DIAMETER_BASE_TERMINATION-CAUSE_USER_MOVED').
85-define(TIMEOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_SESSION_TIMEOUT').
86
87-define(L, atom_to_list).
88-define(A, list_to_atom).
89
90%% The order here is significant and causes the server to listen
91%% before the clients connect. The server listens on the first node,
92%% and distributes requests to the other two.
93-define(NODES, [{server0, ?SERVER},
94                {server1, ?SERVER},
95                {server2, ?SERVER},
96                {client, ?CLIENT}]).
97
98%% Options to ct_slave:start/2.
99-define(TIMEOUTS, [{T, 15000} || T <- [boot_timeout,
100                                       init_timeout,
101                                       start_timeout]]).
102
103%% ===========================================================================
104
105suite() ->
106    [{timetrap, {seconds, 60}}].
107
108all() ->
109    [enslave,
110     ping,
111     start,
112     connect,
113     send,
114     stop].
115
116%% ===========================================================================
117%% start/stop testcases
118
119%% enslave/1
120%%
121%% Start four slave nodes, three to implement a Diameter server,
122%% one to implement a client.
123
124enslave() ->
125    [{timetrap, {seconds, 30*length(?NODES)}}].
126
127enslave(Config) ->
128    Here = filename:dirname(code:which(?MODULE)),
129    Ebin = filename:join([Here, "..", "ebin"]),
130    Dirs = [Here, Ebin],
131    Nodes = [{N,S} || {M,S} <- ?NODES, N <- [slave(M, Dirs)]],
132    ?util:write_priv(Config, nodes, [{N,S} || {{N,ok},S} <- Nodes]),
133    [] = [{T,S} || {{_,E} = T, S} <- Nodes, E /= ok].
134
135slave(Name, Dirs) ->
136    add_pathsa(Dirs, ct_slave:start(Name, ?TIMEOUTS)).
137
138add_pathsa(Dirs, {ok, Node}) ->
139    {Node, rpc:call(Node, code, add_pathsa, [Dirs])};
140add_pathsa(_, No) ->
141    {No, error}.
142
143%% ping/1
144%%
145%% Ensure the server nodes are connected so that diameter_dist can attach.
146
147ping({S, Nodes}) ->
148    ?SERVER = S,
149    [N || {N,_} <- Nodes,
150          node() /= N,
151          pang <- [net_adm:ping(N)]];
152
153ping(Config) ->
154    Nodes = lists:droplast(?util:read_priv(Config, nodes)),
155    [] = [{N,RC} || {N,S} <- Nodes,
156                    RC <- [rpc:call(N, ?MODULE, ping, [{S,Nodes}])],
157                    RC /= []].
158
159%% start/1
160%%
161%% Start diameter services.
162
163%% There's no need to start diameter on a node that only services
164%% diameter_dist as a handler of incoming requests, but the
165%% diameter_dist server must be started since the servers communicate
166%% to determine who services what. The typical case is probably that
167%% handler nodes also want to be able to send Diameter requests, in
168%% which case the application needs to be started and diameter_dist is
169%% started as a part of this, but only start the server here to ensure
170%% everything still works as expected.
171start({_SvcName, [_, {S1, _}, {S2, _}, _]})
172  when node() == S1;    %% server1
173       node() == S2 ->  %% server2
174    Mod = diameter_dist,
175    {ok, _} = gen_server:start({local, Mod}, Mod, _Args = [], _Opts  = []),
176    ok;
177
178start({SvcName, [{S0, _}, _, _, {C, _}]})
179  when node() == S0;    %% server0
180       node() == C ->   %% client
181    ok = diameter:start(),
182    ok = diameter:start_service(SvcName, ?SERVICE((?L(SvcName))));
183
184start(Config)
185  when is_list(Config) ->
186    Nodes = ?util:read_priv(Config, nodes),
187    [] = [{N,RC} || {N,S} <- Nodes,
188                    RC <- [rpc:call(N, ?MODULE, start, [{S, Nodes}])],
189                    RC /= ok].
190
191sequence() ->
192    sequence(sname()).
193
194sequence(client) ->
195    {0,32};
196sequence(Server) ->
197    "server" ++ N = ?L(Server),
198    {list_to_integer(N), 30}.
199
200origin() ->
201    origin(sname()).
202
203origin(client) ->
204    99;
205origin(Server) ->
206    "server" ++ N = ?L(Server),
207    list_to_integer(N).
208
209%% connect/1
210%%
211%% Establish one connection from the client, terminated on the first
212%% server node, the others handling requests.
213
214connect({?SERVER, Config, [{Node, _} | _]})
215  when Node == node() ->  %% server0
216    ok = ?util:write_priv(Config, lref, {Node, ?util:listen(?SERVER, tcp)});
217
218connect({?SERVER, _Config, _}) -> %% server[12]: register to receive requests
219    ok = diameter_dist:attach([?SERVER]);
220
221connect({?CLIENT, Config, _}) ->
222    ?util:connect(?CLIENT, tcp, ?util:read_priv(Config, lref)),
223    ok;
224
225connect(Config) ->
226    Nodes = ?util:read_priv(Config, nodes),
227    [] = [{N,RC} || {N,S} <- Nodes,
228                    RC <- [rpc:call(N, ?MODULE, connect, [{S, Config, Nodes}])],
229                    RC /= ok].
230
231%% stop/1
232%%
233%% Stop the slave nodes.
234
235stop() ->
236    [{timetrap, {seconds, 30*length(?NODES)}}].
237
238stop(_Config) ->
239    [] = [{N,E} || {N,_} <- ?NODES,
240                   {error, _, _} = E <- [ct_slave:stop(N)]].
241
242%% ===========================================================================
243%% traffic testcases
244
245%% send/1
246%%
247%% Send 100 requests and ensure the node name sent as User-Name isn't
248%% the node terminating transport.
249
250send(Config) ->
251    send(Config, 100, dict:new()).
252
253%% send/2
254
255send(Config, 0, Dict) ->
256    [{Server0, _} | _] = ?util:read_priv(Config, nodes) ,
257    Node = atom_to_binary(Server0, utf8),
258    {false, _} = {dict:is_key(Node, Dict), dict:to_list(Dict)},
259    %% Check that counters have been incremented as expected on server0.
260    [Info] = rpc:call(Server0, diameter, service_info, [?SERVER, connections]),
261    {[Stats], _} = {[S || {statistics, S} <- Info], Info},
262    {[{recv, 1, 100}, {send, 0, 100}], _}
263        = {[{D,R,N} || T <- [recv, send],
264                       {{{0,275,R}, D}, N} <- Stats,
265                       D == T],
266           Stats},
267    {[{send, 0, 100, 2001}], _}
268        = {[{D,R,N,C} || {{{0,275,R}, D, {'Result-Code', C}}, N} <- Stats],
269           Stats};
270
271send(Config, N, Dict) ->
272    #diameter_base_STA{'Result-Code' = ?SUCCESS,
273                       'User-Name' = [ServerNode]}
274        = send(Config, str(?LOGOUT)),
275    true = is_binary(ServerNode),
276    send(Config, N-1, dict:update_counter(ServerNode, 1, Dict)).
277
278%% ===========================================================================
279
280str(Cause) ->
281    #diameter_base_STR{'Destination-Realm'   = ?REALM,
282                       'Auth-Application-Id' = ?DICT:id(),
283                       'Termination-Cause'   = Cause}.
284
285%% send/2
286
287send(Config, Req) ->
288    {Node, _} = lists:last(?util:read_priv(Config, nodes)),
289    rpc:call(Node, ?MODULE, call, [Req]).
290
291%% call/1
292
293call(Req) ->
294    diameter:call(?CLIENT, ?DICT, Req, []).
295
296%% sname/0
297
298sname() ->
299    ?A(hd(string:tokens(?L(node()), "@"))).
300
301%% ===========================================================================
302%% diameter callbacks
303
304%% peer_up/3
305
306peer_up(_SvcName, _Peer, State) ->
307    State.
308
309%% peer_down/3
310
311peer_down(_SvcName, _Peer, State) ->
312    State.
313
314%% pick_peer/4
315
316pick_peer([Peer], [], ?CLIENT, _State) ->
317    {ok, Peer}.
318
319%% prepare_request/3
320
321prepare_request(Pkt, ?CLIENT, {_Ref, Caps}) ->
322    #diameter_packet{msg = Req}
323        = Pkt,
324    #diameter_caps{origin_host  = {OH, _},
325                   origin_realm = {OR, _}}
326        = Caps,
327    {send, Req#diameter_base_STR{'Origin-Host' = OH,
328                                 'Origin-Realm' = OR,
329                                 'Session-Id' = diameter:session_id(OH)}}.
330
331%% prepare_retransmit/3
332
333prepare_retransmit(_, ?CLIENT, _) ->
334    discard.
335
336%% handle_answer/5
337
338handle_answer(Pkt, _Req, ?CLIENT, _Peer) ->
339    #diameter_packet{msg = Rec, errors = []} = Pkt,
340    Rec.
341
342%% handle_error/5
343
344handle_error(Reason, _Req, ?CLIENT, _Peer) ->
345    {error, Reason}.
346
347%% handle_request/3
348
349handle_request(Pkt, ?SERVER, {_, Caps}) ->
350    #diameter_packet{msg = #diameter_base_STR{'Session-Id' = SId}}
351        = Pkt,
352    #diameter_caps{origin_host  = {OH, _},
353                   origin_realm = {OR, _}}
354        = Caps,
355    {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS,
356                               'Session-Id' = SId,
357                               'Origin-Host' = OH,
358                               'Origin-Realm' = OR,
359                               'User-Name' = [atom_to_binary(node(), utf8)]}}.
360