rabbitmq-server/deps/rabbit/test/peer_discovery_tmp_hidden_n...

286 lines
11 KiB
Erlang

%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2024 Broadcom. All Rights Reserved. The term “Broadcom”
%% refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(peer_discovery_tmp_hidden_node_SUITE).
-include_lib("kernel/include/inet.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("public_key/include/public_key.hrl").
-include_lib("rabbit_common/include/logging.hrl").
-export([suite/0,
all/0,
init_per_suite/1,
end_per_suite/1,
init_per_group/2,
end_per_group/2,
init_per_testcase/2,
end_per_testcase/2,
do_setup_test_node/1,
no_connection_between_peers_is_opened/1,
long_names_work/1,
ipv6_works/1,
inetrc_file_as_atom_works/1,
tls_dist_works/1
]).
suite() ->
[{timetrap, {minutes, 15}}].
all() ->
[no_connection_between_peers_is_opened,
long_names_work,
ipv6_works,
inetrc_file_as_atom_works,
tls_dist_works].
%% -------------------------------------------------------------------
%% Testsuite setup/teardown
%% -------------------------------------------------------------------
init_per_suite(Config) ->
rabbit_ct_helpers:log_environment(),
rabbit_ct_helpers:run_setup_steps(Config).
end_per_suite(Config) ->
rabbit_ct_helpers:run_teardown_steps(Config).
init_per_group(_Group, Config) ->
Config.
end_per_group(_Group, Config) ->
Config.
init_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_started(Config, Testcase),
Config.
end_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase).
%% -------------------------------------------------------------------
%% Testcases.
%% -------------------------------------------------------------------
no_connection_between_peers_is_opened(_Config) ->
PeerOptions = #{longnames => false},
test_query_node_props(?FUNCTION_NAME, 2, PeerOptions).
long_names_work(_Config) ->
PeerOptions = #{longnames => true},
test_query_node_props(?FUNCTION_NAME, 2, PeerOptions).
ipv6_works(Config) ->
PrivDir = ?config(priv_dir, Config),
InetrcFilename = filename:join(PrivDir, "inetrc-ipv6.erl"),
ct:log("Inetrc filename:~n~0p", [InetrcFilename]),
Inetrc = [{inet6, true}],
InetrcContent = [io_lib:format("~p.~n", [Param]) || Param <- Inetrc],
ct:log("Inetrc file content:~n---8<---~n~s---8<---", [InetrcContent]),
ok = file:write_file(InetrcFilename, InetrcContent),
InetrcArg = rabbit_misc:format("~0p", [InetrcFilename]),
PeerOptions = #{host => "::1",
args => ["-proto_dist", "inet6_tcp",
"-kernel", "inetrc", InetrcArg]},
test_query_node_props(?FUNCTION_NAME, 2, PeerOptions).
inetrc_file_as_atom_works(_Config) ->
%% We can't write the inetrc file in `privdir' like we did in
%% `ipv6_works/1' because here we convert the filename to an atom and an
%% atom can't be more than 255 characters. It happens that in the
%% Buildbuddy CI worker, we reach a filename of 340+ characters.
%%
%% Instead, we write the file in the temporary directory.
%%
%% TEMP and TMP are used on Microsoft Windows, TMPDIR on Unix (but TMPDIR
%% might not be defined).
TmpDir = os:getenv("TEMP", os:getenv("TMP", os:getenv("TMPDIR", "/tmp"))),
InetrcFilename = filename:join(TmpDir, "inetrc-ipv6.erl"),
ct:log("Inetrc filename:~n~0p", [InetrcFilename]),
Inetrc = [{inet6, true}],
InetrcContent = [io_lib:format("~p.~n", [Param]) || Param <- Inetrc],
ct:log("Inetrc file content:~n---8<---~n~s---8<---", [InetrcContent]),
ok = file:write_file(InetrcFilename, InetrcContent),
InetrcArg = rabbit_misc:format("~0p", [list_to_atom(InetrcFilename)]),
PeerOptions = #{host => "::1",
args => ["-proto_dist", "inet6_tcp",
"-kernel", "inetrc", InetrcArg]},
test_query_node_props(?FUNCTION_NAME, 2, PeerOptions).
tls_dist_works(Config) ->
CertsDir = ?config(rmq_certsdir, Config),
Password = ?config(rmq_certspwd, Config),
CACert = filename:join([CertsDir, "testca", "cacert.pem"]),
ServerCert = filename:join([CertsDir, "server", "cert.pem"]),
ServerKey = filename:join([CertsDir, "server", "key.pem"]),
SslOptions = [{server,
[{cacertfile, CACert},
{certfile, ServerCert},
{keyfile, ServerKey},
{password, Password},
{secure_renegotiate, true},
{verify, verify_none},
{fail_if_no_peer_cert, false}]},
{client,
[{cacertfile, CACert},
{secure_renegotiate, true}]}],
PrivDir = ?config(priv_dir, Config),
SslOptFilename = filename:join(PrivDir, "ssl-options.erl"),
ct:log("SSL options filename:~n~0p", [SslOptFilename]),
SslOptContent = rabbit_misc:format("~p.~n", [SslOptions]),
ct:log("SSL options file content:~n---8<---~n~s---8<---", [SslOptContent]),
ok = file:write_file(SslOptFilename, SslOptContent),
%% We need to read the certificate's Subject ID to see what hostname is
%% used in the certificate and use the same to start the test Erlang nodes.
%% We also need to pay attention if the name is short or long.
{ok, ServerCertBin} = file:read_file(ServerCert),
ct:log("ServerCertBin = ~p", [ServerCertBin]),
[DecodedCert] = public_key:pem_decode(ServerCertBin),
ct:log("DecodedCert = ~p", [DecodedCert]),
DecodedCert1 = element(2, DecodedCert),
{_SerialNr, {rdnSequence, IssuerAttrs}} = public_key:pkix_subject_id(
DecodedCert1),
ct:log("IssuerAttrs = ~p", [IssuerAttrs]),
[ServerName] = [Value
|| [#'AttributeTypeAndValue'{type = {2, 5, 4, 3},
value = {utf8String, Value}}]
<- IssuerAttrs],
ct:log("ServerName = ~p", [ServerName]),
UseLongnames = re:run(ServerName, "\\.", [{capture, none}]) =:= match,
PeerOptions = #{host => binary_to_list(ServerName),
longnames => UseLongnames,
args => ["-proto_dist", "inet_tls",
"-ssl_dist_optfile", SslOptFilename]},
test_query_node_props(?FUNCTION_NAME, 2, PeerOptions).
test_query_node_props(Testcase, NodeCount, PeerOptions) ->
Peers = start_test_nodes(Testcase, NodeCount, PeerOptions),
try
do_test_query_node_props(Peers)
after
stop_test_nodes(Peers)
end.
do_test_query_node_props(Peers) ->
%% Ensure no connection exists at the beginning.
ensure_no_connections_between_test_nodes(Peers),
%% Query the remote node's properties. The return value should have the
%% properties of the peer node, otherwise it means that we failed to
%% contact it.
[NodeA, NodeB] = lists:sort(maps:keys(Peers)),
NodeAPid = maps:get(NodeA, Peers),
Ret = peer:call(
NodeAPid,
rabbit_peer_discovery, query_node_props, [[NodeB]],
infinity),
ct:log("Discovered nodes properties:~n~p", [Ret]),
?assertMatch([{NodeB, [NodeB], _, false}], Ret),
%% Ensure no connection exists after the query.
ensure_no_connections_between_test_nodes(Peers).
%% -------------------------------------------------------------------
%% Helpers.
%% -------------------------------------------------------------------
start_test_nodes(Testcase, NodeCount, PeerOptions) ->
PeerOptions1 = PeerOptions#{
%% We use an alternative connection channel, not the
%% regular Erlang distribution, because we want to test
%% the behavior of the temporary hidden node and
%% especially that it doesn't rely or create a connection
%% between the two nodes.
connection => standard_io,
wait_boot => infinity},
TestEbin = filename:dirname(code:which(?MODULE)),
Args0 = maps:get(args, PeerOptions1, []),
Args1 = ["-pa", TestEbin | Args0],
Env0 = maps:get(env, PeerOptions1, []),
Env1 = [{"ERL_LIBS", os:getenv("ERL_LIBS")} | Env0],
PeerOptions2 = PeerOptions1#{args => Args1,
env => Env1},
start_test_nodes(Testcase, 1, NodeCount, PeerOptions2, #{}).
start_test_nodes(Testcase, NodeNumber, NodeCount, PeerOptions, Peers)
when NodeNumber =< NodeCount ->
PeerName0 = rabbit_misc:format("~s-~b", [Testcase, NodeNumber]),
PeerOptions1 = PeerOptions#{name => PeerName0},
PeerOptions2 = case PeerOptions1 of
#{host := _} ->
PeerOptions1;
#{longnames := true} ->
%% To simulate Erlang long node names, we use a
%% hard-coded IP address that is likely to exist.
%%
%% We can't rely on the host proper network
%% configuration because it appears that several
%% hosts are half-configured (at least some random
%% GitHub workers and Broadcom-managed OSX laptops
%% in the team).
PeerOptions1#{host => "127.0.0.1"};
_ ->
PeerOptions1
end,
ct:log("Starting peer with options: ~p", [PeerOptions2]),
case catch peer:start(PeerOptions2) of
{ok, PeerPid, PeerName} ->
ct:log("Configuring peer '~ts'", [PeerName]),
setup_test_node(PeerPid, PeerOptions2),
Peers1 = Peers#{PeerName => PeerPid},
start_test_nodes(
Testcase, NodeNumber + 1, NodeCount, PeerOptions, Peers1);
Error ->
ct:log("Failed to started peer node:~n"
"Options: ~p~n"
"Error: ~p", [PeerOptions2, Error]),
stop_test_nodes(Peers),
erlang:throw(Error)
end;
start_test_nodes(_Testcase, _NodeNumber, _Count, _PeerOptions, Peers) ->
ct:log("Peers: ~p", [Peers]),
Peers.
setup_test_node(PeerPid, PeerOptions) ->
peer:call(PeerPid, ?MODULE, do_setup_test_node, [PeerOptions]).
do_setup_test_node(PeerOptions) ->
Context = case maps:get(longnames, PeerOptions, false) of
true -> #{nodename_type => longnames};
false -> #{}
end,
logger:set_primary_config(level, debug),
meck:new(rabbit_prelaunch, [unstick, passthrough, no_link]),
meck:expect(rabbit_prelaunch, get_context, fun() -> Context end),
meck:new(rabbit_nodes, [unstick, passthrough, no_link]),
Nodes = [node()],
meck:expect(rabbit_nodes, all, fun() -> Nodes end),
meck:expect(rabbit_nodes, list_members, fun() -> Nodes end),
ok.
stop_test_nodes(Peers) ->
maps:foreach(
fun(_PeerName, PeerPid) ->
peer:stop(PeerPid)
end, Peers).
ensure_no_connections_between_test_nodes(Peers) ->
maps:foreach(
fun(_PeerName, PeerPid) ->
?assertEqual([], peer:call(PeerPid, erlang, nodes, []))
end, Peers).