1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2013-2019. 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 client being
23%% spread across three Erlang nodes.
24%%
25
26-module(diameter_distribution_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_local/1,
37         send_remote/1,
38         send_timeout/1,
39         send_failover/1,
40         stop/1, stop/0]).
41
42%% diameter callbacks
43-export([peer_up/3,
44         peer_down/3,
45         pick_peer/5,
46         prepare_request/4,
47         prepare_retransmit/4,
48         handle_answer/5,
49         handle_error/5,
50         handle_request/3]).
51
52-export([call/1]).
53
54-include("diameter.hrl").
55-include("diameter_gen_base_rfc6733.hrl").
56
57%% ===========================================================================
58
59-define(util, diameter_util).
60
61-define(CLIENT, 'CLIENT').
62-define(SERVER, 'SERVER').
63-define(REALM, "erlang.org").
64-define(DICT, diameter_gen_base_rfc6733).
65-define(ADDR, {127,0,0,1}).
66
67%% Config for diameter:start_service/2.
68-define(SERVICE(Host),
69        [{'Origin-Host', Host ++ [$.|?REALM]},
70         {'Origin-Realm', ?REALM},
71         {'Host-IP-Address', [?ADDR]},
72         {'Vendor-Id', 12345},
73         {'Product-Name', "OTP/diameter"},
74         {'Auth-Application-Id', [?DICT:id()]},
75         {'Origin-State-Id', origin()},
76         {share_peers, peers()},
77         {use_shared_peers, peers()},
78         {restrict_connections, false},
79         {spawn_opt, {diameter_dist, spawn_local, []}},
80         {sequence, fun sequence/0},
81         {application, [{dictionary, ?DICT},
82                        {module, ?MODULE},
83                        {request_errors, callback},
84                        {answer_errors, callback}]}]).
85
86-define(SUCCESS, 2001).
87-define(BUSY,    3004).
88-define(LOGOUT,  ?'DIAMETER_BASE_TERMINATION-CAUSE_LOGOUT').
89-define(MOVED,   ?'DIAMETER_BASE_TERMINATION-CAUSE_USER_MOVED').
90-define(TIMEOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_SESSION_TIMEOUT').
91
92-define(L, atom_to_list).
93-define(A, list_to_atom).
94
95%% The order here is significant and causes the server to listen
96%% before the clients connect.
97-define(NODES, [{server, ?SERVER},
98                {client0, ?CLIENT},
99                {client1, ?CLIENT},
100                {client2, ?CLIENT}]).
101
102%% Options to ct_slave:start/2.
103-define(TIMEOUTS, [{T, 15000} || T <- [boot_timeout,
104                                       init_timeout,
105                                       start_timeout]]).
106
107%% ===========================================================================
108
109suite() ->
110    [{timetrap, {seconds, 60}}].
111
112all() ->
113    [enslave,
114     ping,
115     start,
116     connect,
117     send_local,
118     send_remote,
119     send_timeout,
120     send_failover,
121     stop].
122
123%% ===========================================================================
124%% start/stop testcases
125
126%% enslave/1
127%%
128%% Start four slave nodes, one to implement a Diameter server,
129%% three to implement a client.
130
131enslave() ->
132    [{timetrap, {seconds, 30*length(?NODES)}}].
133
134enslave(Config) ->
135    Here = filename:dirname(code:which(?MODULE)),
136    Ebin = filename:join([Here, "..", "ebin"]),
137    Dirs = [Here, Ebin],
138    Nodes = [{N,S} || {M,S} <- ?NODES, N <- [slave(M, Dirs)]],
139    ?util:write_priv(Config, nodes, [{N,S} || {{N,ok},S} <- Nodes]),
140    [] = [{T,S} || {{_,E} = T, S} <- Nodes, E /= ok].
141
142slave(Name, Dirs) ->
143    add_pathsa(Dirs, ct_slave:start(Name, ?TIMEOUTS)).
144
145add_pathsa(Dirs, {ok, Node}) ->
146    {Node, rpc:call(Node, code, add_pathsa, [Dirs])};
147add_pathsa(_, No) ->
148    {No, error}.
149
150%% ping/1
151%%
152%% Ensure the client nodes are connected since the sharing of
153%% transports is only between connected nodes.
154
155ping({?SERVER, _Nodes}) ->
156    [];
157
158ping({?CLIENT, Nodes}) ->
159    [N || {N,_} <- Nodes,
160          node() /= N,
161          pang <- [net_adm:ping(N)]];
162
163ping(Config) ->
164    Nodes = ?util:read_priv(Config, nodes),
165    [] = [{N,RC} || {N,S} <- Nodes,
166                    RC <- [rpc:call(N, ?MODULE, ping, [{S, Nodes}])],
167                    RC /= []].
168
169%% start/1
170%%
171%% Start diameter services.
172
173start(SvcName)
174  when is_atom(SvcName) ->
175    ok = diameter:start(),
176    ok = diameter:start_service(SvcName, ?SERVICE((?L(SvcName))));
177
178start(Config) ->
179    Nodes = ?util:read_priv(Config, nodes),
180    [] = [{N,RC} || {N,S} <- Nodes,
181                    RC <- [rpc:call(N, ?MODULE, start, [S])],
182                    RC /= ok].
183
184sequence() ->
185    sequence(sname()).
186
187sequence(server) ->
188    {0,32};
189sequence(Client) ->
190    "client" ++ N = ?L(Client),
191    {list_to_integer(N), 30}.
192
193origin() ->
194    origin(sname()).
195
196origin(server) ->
197    99;
198origin(Client) ->
199    "client" ++ N = ?L(Client),
200    list_to_integer(N).
201
202peers() ->
203    peers(sname()).
204
205peers(server)  -> true;
206peers(client0) -> [node() | nodes()];
207peers(client1) -> fun erlang:nodes/0;
208peers(client2) -> nodes().
209
210%% connect/1
211%%
212%% Establish one connection to the server from each of the client
213%% nodes.
214
215connect({?SERVER, Config}) ->
216    ?util:write_priv(Config, lref, {node(), ?util:listen(?SERVER, tcp)}),
217    ok;
218
219connect({?CLIENT, Config}) ->
220    ?util:connect(?CLIENT, tcp, ?util:read_priv(Config, lref)),
221    ok;
222
223connect(Config) ->
224    Nodes = ?util:read_priv(Config, nodes),
225    [] = [{N,RC} || {N,S} <- Nodes,
226                    RC <- [rpc:call(N, ?MODULE, connect, [{S,Config}])],
227                    RC /= ok].
228
229%% stop/1
230%%
231%% Stop the slave nodes.
232
233stop() ->
234    [{timetrap, {seconds, 30*length(?NODES)}}].
235
236stop(_Config) ->
237    [] = [{N,E} || {N,_} <- ?NODES,
238                   {error, _, _} = E <- [ct_slave:stop(N)]].
239
240%% ===========================================================================
241%% traffic testcases
242
243%% send_local/1
244%%
245%% Send a request from the first client node, using a the local
246%% transport.
247
248send_local(Config) ->
249    #diameter_base_STA{'Result-Code' = ?SUCCESS}
250        = send(Config, local, str(?LOGOUT)).
251
252%% send_remote/1
253%%
254%% Send a request from the first client node, using a transport on the
255%% another node.
256
257send_remote(Config) ->
258    #diameter_base_STA{'Result-Code' = ?SUCCESS}
259        = send(Config, remote, str(?LOGOUT)).
260
261%% send_timeout/1
262%%
263%% Send a request that the server discards.
264
265send_timeout(Config) ->
266    {error, timeout} = send(Config, remote, str(?TIMEOUT)).
267
268%% send_failover/1
269%%
270%% Send a request that causes the server to remote transports down.
271
272send_failover(Config) ->
273    #'diameter_base_answer-message'{'Result-Code' = ?BUSY}
274        = send(Config, remote, str(?MOVED)).
275
276%% ===========================================================================
277
278str(Cause) ->
279    #diameter_base_STR{'Destination-Realm'   = ?REALM,
280                       'Auth-Application-Id' = ?DICT:id(),
281                       'Termination-Cause'   = Cause}.
282
283%% send/2
284
285send(Config, Where, Req) ->
286    [_, {Node, _} | _] = ?util:read_priv(Config, nodes) ,
287    rpc:call(Node, ?MODULE, call, [{Where, Req}]).
288
289%% call/1
290
291call({Where, Req}) ->
292    diameter:call(?CLIENT, ?DICT, Req, [{extra, [{Where, sname()}]}]).
293
294%% sname/0
295
296sname() ->
297    ?A(hd(string:tokens(?L(node()), "@"))).
298
299%% ===========================================================================
300%% diameter callbacks
301
302%% peer_up/3
303
304peer_up(_SvcName, _Peer, State) ->
305    State.
306
307%% peer_down/3
308
309peer_down(_SvcName, _Peer, State) ->
310    State.
311
312%% pick_peer/4
313
314pick_peer([LP], [_, _], ?CLIENT, _State, {local, client0}) ->
315    {ok, LP};
316
317pick_peer([_], [RP | _], ?CLIENT, _State, {remote, client0}) ->
318    {ok, RP};
319
320pick_peer([LP], [], ?CLIENT, _State, {remote, client0}) ->
321    {ok, LP}.
322
323%% prepare_request/4
324
325prepare_request(Pkt, ?CLIENT, {_Ref, Caps}, {_, client0}) ->
326    #diameter_packet{msg = Req}
327        = Pkt,
328    #diameter_caps{origin_host  = {OH, _},
329                   origin_realm = {OR, _}}
330        = Caps,
331    {send, Req#diameter_base_STR{'Origin-Host' = OH,
332                                 'Origin-Realm' = OR,
333                                 'Session-Id' = diameter:session_id(OH)}}.
334
335%% prepare_retransmit/4
336
337prepare_retransmit(Pkt, ?CLIENT, _, {_, client0}) ->
338    #diameter_packet{msg = #diameter_base_STR{'Termination-Cause' = ?MOVED}}
339        = Pkt,  %% assert
340    {send, Pkt}.
341
342%% handle_answer/5
343
344handle_answer(Pkt, _Req, ?CLIENT, _Peer, {_, client0}) ->
345    #diameter_packet{msg = Rec, errors = []} = Pkt,
346    Rec.
347
348%% handle_error/5
349
350handle_error(Reason, _Req, ?CLIENT, _Peer, {_, client0}) ->
351    {error, Reason}.
352
353%% handle_request/3
354
355handle_request(Pkt, ?SERVER, Peer) ->
356    server = sname(),  %% assert
357    #diameter_packet{msg = Req}
358        = Pkt,
359    request(Req, Peer).
360
361request(#diameter_base_STR{'Termination-Cause' = ?TIMEOUT}, _) ->
362    discard;
363
364request(#diameter_base_STR{'Termination-Cause' = ?MOVED}, Peer) ->
365    {TPid, #diameter_caps{origin_state_id = {_, [N]}}} = Peer,
366    fail(N, TPid);
367
368request(#diameter_base_STR{'Session-Id' = SId}, {_, Caps}) ->
369    #diameter_caps{origin_host  = {OH, _},
370                   origin_realm = {OR, _}}
371        = Caps,
372    {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS,
373                               'Session-Id' = SId,
374                               'Origin-Host' = OH,
375                               'Origin-Realm' = OR}}.
376
377fail(0, _) ->     %% sent from the originating node ...
378    {protocol_error, ?BUSY};
379
380fail(_, TPid) ->  %% ... or through a remote node: force failover
381    exit(TPid, kill),
382    discard.
383