1-module(rabbit_prelaunch_dist).
2
3-include_lib("eunit/include/eunit.hrl").
4-include_lib("kernel/include/logger.hrl").
5
6-include_lib("rabbit_common/include/logging.hrl").
7
8-export([setup/1]).
9
10setup(#{nodename := Node, nodename_type := NameType} = Context) ->
11    ?LOG_DEBUG(
12       "~n== Erlang distribution ==", [],
13       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
14    ?LOG_DEBUG(
15       "Rqeuested node name: ~s (type: ~s)",
16       [Node, NameType],
17       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
18    case node() of
19        nonode@nohost ->
20            ok = rabbit_nodes_common:ensure_epmd(),
21            ok = dist_port_range_check(Context),
22            ok = dist_port_use_check(Context),
23            ok = duplicate_node_check(Context),
24
25            ok = do_setup(Context);
26        Node ->
27            ?LOG_DEBUG(
28              "Erlang distribution already running", [],
29              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
30            ok;
31        Unexpected ->
32            throw({error, {erlang_dist_running_with_unexpected_nodename,
33                           Unexpected, Node}})
34    end,
35    ok.
36
37do_setup(#{nodename := Node,
38           nodename_type := NameType,
39           var_origins := Origins} = Config) ->
40    ?LOG_DEBUG(
41       "Starting Erlang distribution",
42       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
43    case application:get_env(kernel, net_ticktime) of
44        {ok, Ticktime} when is_integer(Ticktime) andalso Ticktime >= 1 ->
45            %% The value passed to net_kernel:start/1 is the
46            %% "minimum transition traffic interval" as defined in
47            %% net_kernel:set_net_ticktime/1.
48            MTTI = Ticktime * 1000 div 4,
49            {ok, _} = net_kernel:start([Node, NameType, MTTI]),
50            ok;
51        _ ->
52            {ok, _} = net_kernel:start([Node, NameType]),
53            ok
54    end,
55
56    %% Override the Erlang cookie if one was set in the environment.
57    case maps:get(erlang_cookie, Origins, default) of
58        environment ->
59            ?LOG_WARNING(
60               "Overriding Erlang cookie using the value set in the environment",
61               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
62            Cookie = maps:get(erlang_cookie, Config),
63            ?assert(is_atom(Cookie)),
64            true = erlang:set_cookie(node(), Cookie);
65        _ ->
66            ok
67    end,
68    ok.
69
70%% Check whether a node with the same name is already running
71duplicate_node_check(#{split_nodename := {NodeName, NodeHost}}) ->
72    ?LOG_DEBUG(
73      "Checking if node name ~s is already used", [NodeName],
74      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
75    PrelaunchName = rabbit_nodes_common:make(
76                      {NodeName ++ "_prelaunch_" ++ os:getpid(),
77                       "localhost"}),
78    {ok, _} = net_kernel:start([PrelaunchName, shortnames]),
79    case rabbit_nodes_common:names(NodeHost) of
80        {ok, NamePorts}  ->
81            case proplists:is_defined(NodeName, NamePorts) of
82                true ->
83                    throw({error, {duplicate_node_name, NodeName, NodeHost}});
84                false ->
85                    ok = net_kernel:stop(),
86                    ok
87            end;
88        {error, EpmdReason} ->
89            throw({error, {epmd_error, NodeHost, EpmdReason}})
90    end.
91
92dist_port_range_check(#{erlang_dist_tcp_port := DistTcpPort}) ->
93    ?LOG_DEBUG(
94      "Checking if TCP port ~b is valid", [DistTcpPort],
95      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
96    case DistTcpPort of
97        _ when DistTcpPort < 1 orelse DistTcpPort > 65535 ->
98            throw({error, {invalid_dist_port_range, DistTcpPort}});
99        _ ->
100            ok
101    end.
102
103dist_port_use_check(#{split_nodename := {_, NodeHost},
104                      erlang_dist_tcp_port := DistTcpPort}) ->
105    ?LOG_DEBUG(
106       "Checking if TCP port ~b is available", [DistTcpPort],
107       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
108    dist_port_use_check_ipv4(NodeHost, DistTcpPort).
109
110dist_port_use_check_ipv4(NodeHost, Port) ->
111    case gen_tcp:listen(Port, [inet, {reuseaddr, true}]) of
112        {ok, Sock} -> gen_tcp:close(Sock);
113        {error, einval} -> dist_port_use_check_ipv6(NodeHost, Port);
114        {error, _} -> dist_port_use_check_fail(Port, NodeHost)
115    end.
116
117dist_port_use_check_ipv6(NodeHost, Port) ->
118    case gen_tcp:listen(Port, [inet6, {reuseaddr, true}]) of
119        {ok, Sock} -> gen_tcp:close(Sock);
120        {error, _} -> dist_port_use_check_fail(Port, NodeHost)
121    end.
122
123-spec dist_port_use_check_fail(non_neg_integer(), string()) ->
124                                         no_return().
125
126dist_port_use_check_fail(Port, Host) ->
127    {ok, Names} = rabbit_nodes_common:names(Host),
128    case [N || {N, P} <- Names, P =:= Port] of
129        [] ->
130            throw({error, {dist_port_already_used, Port, not_erlang, Host}});
131        [Name] ->
132            throw({error, {dist_port_already_used, Port, Name, Host}})
133    end.
134