rabbit_ct_helpers: Use arbitrary node name and TCP ports

Furthermore, if the node fails to start, try again with another name and
set of TCP ports until we find a setup which boots fine.

This allows to run the testsuite while there is another unrelated
RabbitMQ node already running. Moreover, this avoids unrelated AMQP
clients to mess with the testsuite node.
This commit is contained in:
Jean-Sébastien Pédron 2016-04-01 16:30:18 +02:00
parent c39f47028b
commit 0ce5eb1bbb
2 changed files with 174 additions and 80 deletions

View File

@ -32,6 +32,12 @@
-define(DEFAULT_USER, "guest").
-define(UNAUTHORIZED_USER, "test_user_no_perm").
-define(SSL_CERT_PASSWORD, "test").
-define(TCP_PORTS_LIST, [
tcp_port_amqp,
tcp_port_amqp_tls,
tcp_port_mgmt,
tcp_port_erlang_dist
]).
%% -------------------------------------------------------------------
%% Testsuite internal helpers.
@ -43,32 +49,30 @@ log_environment() ->
[io_lib:format(" ~s~n", [V]) || V <- Vars]]).
run_setup_steps(Config) ->
Checks = [
Steps = [
fun ensure_amqp_client_srcdir/1,
fun ensure_amqp_client_depsdir/1,
fun ensure_make_cmd/1,
fun ensure_rabbitmqctl_cmd/1,
fun ensure_nodename/1,
fun ensure_ssl_certs/1,
fun write_config_file/1,
fun start_rabbitmq_node/1,
fun create_unauthorized_user/1
],
run_steps(Checks, Config).
run_steps(Config, Steps).
run_teardown_steps(Config) ->
Checks = [
Steps = [
fun delete_unauthorized_user/1,
fun stop_rabbitmq_node/1
],
run_steps(Checks, Config).
run_steps(Config, Steps).
run_steps([Check | Rest], Config) ->
case Check(Config) of
run_steps(Config, [Step | Rest]) ->
case Step(Config) of
{skip, _} = Error -> Error;
Config1 -> run_steps(Rest, Config1)
Config1 -> run_steps(Config1, Rest)
end;
run_steps([], Config) ->
run_steps(Config, []) ->
Config.
ensure_amqp_client_srcdir(Config) ->
@ -173,47 +177,6 @@ ensure_rabbitmqctl_cmd(Config) ->
end
end.
ensure_nodename(Config) ->
Nodename = case get_config(Config, rmq_nodename) of
undefined ->
case os:getenv("RABBITMQ_NODENAME") of
false ->
Rabbitmqctl = ?config(rabbitmqctl_cmd, Config),
Cmd = Rabbitmqctl ++ " status 2>/dev/null |" ++
" awk '/^Status of node/ { print $4; exit; }'",
case run_cmd_and_capture_output(Cmd) of
{ok, Output} ->
list_to_atom(
string:strip(Output, both, $\n));
{error, _} ->
false
end;
N ->
list_to_atom(string:strip(N))
end;
N ->
N
end,
case Nodename of
false ->
{skip,
"RabbitMQ nodename required, " ++
"please set RABBITMQ_NODENAME or 'rmq_nodename' " ++
"in ct config"};
_ ->
%% We test if the nodename is available. This test is
%% incomplete because a node with that name could be running
%% but with another cookie (so the ping will fail). But
%% that's ok, it covers the most common situation. RabbitMQ
%% will fail to start later for the other situation.
case net_adm:ping(Nodename) of
pang -> set_config(Config, {rmq_nodename, Nodename});
pong -> {skip,
"A node with the name '" ++ atom_to_list(Nodename) ++
"' is already running"}
end
end.
ensure_ssl_certs(Config) ->
Make = ?config(make_cmd, Config),
DepsDir = ?config(amqp_client_depsdir, Config),
@ -227,7 +190,6 @@ ensure_ssl_certs(Config) ->
true ->
%% Add SSL certs to the broker configuration.
Config1 = merge_app_env(Config, rabbit, [
{ssl_listeners, [5671]},
{ssl_options, [
{cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])},
{certfile, filename:join([CertsDir, "server", "cert.pem"])},
@ -240,29 +202,54 @@ ensure_ssl_certs(Config) ->
{skip, "Failed to create SSL certificates"}
end.
write_config_file(Config) ->
%% Prepare a RabbitMQ configuration.
PrivDir = ?config(priv_dir, Config),
ConfigFile = filename:join(PrivDir, "rabbitmq"),
ErlangConfig = ?config(erlang_node_config, Config),
Ret = file:write_file(ConfigFile ++ ".config",
io_lib:format("% vim:ft=erlang:~n~n~p.~n",
[ErlangConfig])),
case Ret of
ok ->
set_config(Config, {erlang_node_config_filename, ConfigFile});
{error, Reason} ->
{skip, "Failed to create Erlang node config file \"" ++
ConfigFile ++ "\": " ++ file:format_error(Reason)}
end.
%% To start a RabbitMQ node, we need to:
%% 1. Pick TCP port numbers
%% 2. Generate a node name
%% 3. Write a configuration file
%% 4. Start the node
%%
%% If this fails (usually because the node name is taken or a TCP port
%% is already in use), we start again with another set of TCP ports. The
%% node name is derived from the AMQP TCP port so a new node name is
%% generated.
start_rabbitmq_node(Config) ->
Attempts = case get_config(Config, rmq_failed_boot_attempts) of
undefined -> 0;
N -> N
end,
Config1 = init_tcp_port_numbers(Config),
Config2 = init_nodename(Config1),
Config3 = init_config_filename(Config2),
Steps = [
fun write_config_file/1,
fun do_start_rabbitmq_node/1
],
case run_steps(Config3, Steps) of
{skip, _} = Error when Attempts >= 50 ->
%% It's unlikely we'll ever succeed to start RabbitMQ.
Error;
{skip, _} ->
%% Try again with another TCP port numbers base.
Config4 = move_nonworking_nodedir_away(Config3),
Config5 = set_config(Config4,
{rmq_failed_boot_attempts, Attempts + 1}),
start_rabbitmq_node(Config5);
Config4 ->
Config4
end.
do_start_rabbitmq_node(Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
PrivDir = ?config(priv_dir, Config),
Nodename = ?config(rmq_nodename, Config),
DistPort = ?config(tcp_port_erlang_dist, Config),
ConfigFile = ?config(erlang_node_config_filename, Config),
Cmd = Make ++ " -C " ++ SrcDir ++ make_verbosity() ++
" start-background-node start-rabbit-on-node" ++
" start-background-broker" ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" RABBITMQ_DIST_PORT='" ++ integer_to_list(DistPort) ++ "'" ++
" RABBITMQ_CONFIG_FILE='" ++ ConfigFile ++ "'" ++
" TEST_TMPDIR='" ++ PrivDir ++ "'",
case run_cmd(Cmd) of
@ -275,10 +262,101 @@ start_rabbitmq_node(Config) ->
false -> {skip, "Failed to initialize RabbitMQ"}
end.
init_tcp_port_numbers(Config) ->
%% If there is no TCP port numbers base previously calculated,
%% use the TCP port 21000. If a base was previously calculated,
%% increment it by the number of TCP ports we may open.
%%
%% Port 21000 is an arbitrary choice. We don't want to use the
%% default AMQP port of 5672 so other AMQP clients on the same host
%% do not accidentally use the testsuite broker. There seems to be
%% no registered service around this port in /etc/services. And it
%% should be far enough away from the default ephemeral TCP ports
%% range.
Base = case get_config(Config, tcp_ports_base) of
undefined -> 21000;
P -> P + length(?TCP_PORTS_LIST)
end,
Config1 = set_config(Config, {tcp_ports_base, Base}),
%% Now, compute all TCP port numbers from this base.
{Config2, _} = lists:foldl(
fun(PortName, {NewConfig, NextPort}) ->
{
set_config(NewConfig, {PortName, NextPort}),
NextPort + 1
}
end,
{Config1, Base}, ?TCP_PORTS_LIST),
%% Finally, update the RabbitMQ configuration with the computed TCP
%% port numbers.
update_tcp_ports_in_rmq_config(Config2, ?TCP_PORTS_LIST).
update_tcp_ports_in_rmq_config(Config, [tcp_port_amqp = Key | Rest]) ->
Config1 = merge_app_env(Config, rabbit,
[{tcp_listeners, [?config(Key, Config)]}]),
update_tcp_ports_in_rmq_config(Config1, Rest);
update_tcp_ports_in_rmq_config(Config, [tcp_port_amqp_tls = Key | Rest]) ->
Config1 = merge_app_env(Config, rabbit,
[{ssl_listeners, [?config(Key, Config)]}]),
update_tcp_ports_in_rmq_config(Config1, Rest);
update_tcp_ports_in_rmq_config(Config, [tcp_port_mgmt = Key | Rest]) ->
Config1 = merge_app_env(Config, rabbitmq_management,
[{listener, [{port, ?config(Key, Config)}]}]),
update_tcp_ports_in_rmq_config(Config1, Rest);
update_tcp_ports_in_rmq_config(Config, [tcp_port_erlang_dist | Rest]) ->
%% The Erlang distribution port doesn't appear in the configuration file.
update_tcp_ports_in_rmq_config(Config, Rest);
update_tcp_ports_in_rmq_config(Config, []) ->
Config.
init_nodename(Config) ->
Base = ?config(tcp_ports_base, Config),
Nodename = list_to_atom(rabbit_misc:format("rmq-ct-~b@localhost", [Base])),
set_config(Config, {rmq_nodename, Nodename}).
init_config_filename(Config) ->
PrivDir = ?config(priv_dir, Config),
Nodename = ?config(rmq_nodename, Config),
ConfigDir = filename:join(PrivDir, Nodename),
ConfigFile = filename:join(ConfigDir, Nodename),
set_config(Config, {erlang_node_config_filename, ConfigFile}).
write_config_file(Config) ->
%% Prepare a RabbitMQ configuration.
ErlangConfig = ?config(erlang_node_config, Config),
ConfigFile = ?config(erlang_node_config_filename, Config),
ConfigDir = filename:dirname(ConfigFile),
Ret1 = file:make_dir(ConfigDir),
Ret2 = file:write_file(ConfigFile ++ ".config",
io_lib:format("% vim:ft=erlang:~n~n~p.~n",
[ErlangConfig])),
case {Ret1, Ret2} of
{ok, ok} ->
Config;
{{error, eexist}, ok} ->
Config;
{{error, Reason}, _} when Reason =/= eexist ->
{skip, "Failed to create Erlang node config directory \"" ++
ConfigDir ++ "\": " ++ file:format_error(Reason)};
{_, {error, Reason}} ->
{skip, "Failed to create Erlang node config file \"" ++
ConfigFile ++ "\": " ++ file:format_error(Reason)}
end.
move_nonworking_nodedir_away(Config) ->
ConfigFile = ?config(erlang_node_config_filename, Config),
ConfigDir = filename:dirname(ConfigFile),
NewName = filename:join(
filename:dirname(ConfigDir),
"_unused_nodedir_" ++ filename:basename(ConfigDir)),
file:rename(ConfigDir, NewName),
lists:keydelete(erlang_node_config_filename, 1, Config).
create_unauthorized_user(Config) ->
Rabbitmqctl = ?config(rabbitmqctl_cmd, Config),
Cmd = Rabbitmqctl ++ " add_user " ++
?UNAUTHORIZED_USER ++ " " ++ ?UNAUTHORIZED_USER,
Nodename = ?config(rmq_nodename, Config),
Cmd = Rabbitmqctl ++ " -n " ++ atom_to_list(Nodename) ++
" add_user " ++ ?UNAUTHORIZED_USER ++ " " ++ ?UNAUTHORIZED_USER,
case run_cmd(Cmd) of
true -> set_config(Config,
[{rmq_unauthorized_username,
@ -290,7 +368,9 @@ create_unauthorized_user(Config) ->
delete_unauthorized_user(Config) ->
Rabbitmqctl = ?config(rabbitmqctl_cmd, Config),
Cmd = Rabbitmqctl ++ " delete_user " ++ ?UNAUTHORIZED_USER,
Nodename = ?config(rmq_nodename, Config),
Cmd = Rabbitmqctl ++ " -n " ++ atom_to_list(Nodename) ++
" delete_user " ++ ?UNAUTHORIZED_USER,
run_cmd(Cmd),
Config.
@ -298,8 +378,10 @@ stop_rabbitmq_node(Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
PrivDir = ?config(priv_dir, Config),
Nodename = ?config(rmq_nodename, Config),
Cmd = Make ++ " -C " ++ SrcDir ++ make_verbosity() ++
" stop-rabbit-on-node stop-node" ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" TEST_TMPDIR='" ++ PrivDir ++ "'",
run_cmd(Cmd),
Config.

View File

@ -179,19 +179,22 @@ init_per_testcase(Test, Config) ->
basic_get_ipv6_ssl -> "::1";
_ -> ?config(rmq_hostname, Config)
end,
SSLOpts = if
{Port, SSLOpts} = if
Test =:= basic_get_ipv4_ssl orelse
Test =:= basic_get_ipv6_ssl ->
CertsDir = ?config(amqp_client_certsdir, Config),
[
{cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])},
{certfile, filename:join([CertsDir, "client", "cert.pem"])},
{keyfile, filename:join([CertsDir, "client", "key.pem"])},
{verify, verify_peer},
{fail_if_no_peer_cert, true}
];
{
?config(tcp_port_amqp_tls, Config),
[
{cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])},
{certfile, filename:join([CertsDir, "client", "cert.pem"])},
{keyfile, filename:join([CertsDir, "client", "key.pem"])},
{verify, verify_peer},
{fail_if_no_peer_cert, true}
]
};
true ->
none
{?config(tcp_port_amqp, Config), none}
end,
ChannelMax = case Test of
channel_tune_negotiation -> 10;
@ -209,6 +212,7 @@ init_per_testcase(Test, Config) ->
username = Username,
password = Password,
host = Hostname,
port = Port,
virtual_host = VHost,
channel_max = ChannelMax,
ssl_options = SSLOpts}
@ -1374,27 +1378,35 @@ assert_down_with_error(MonitorRef, CodeAtom) ->
set_resource_alarm(memory, Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
Nodename = ?config(rmq_nodename, Config),
true = rabbit_ct_helpers:run_cmd(
Make ++ " -C " ++ SrcDir ++ rabbit_ct_helpers:make_verbosity() ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" set-resource-alarm SOURCE=memory");
set_resource_alarm(disk, Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
Nodename = ?config(rmq_nodename, Config),
true = rabbit_ct_helpers:run_cmd(
Make ++ " -C " ++ SrcDir ++ rabbit_ct_helpers:make_verbosity() ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" set-resource-alarm SOURCE=disk").
clear_resource_alarm(memory, Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
Nodename = ?config(rmq_nodename, Config),
true = rabbit_ct_helpers:run_cmd(
Make ++ " -C " ++ SrcDir ++ rabbit_ct_helpers:make_verbosity() ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" clear-resource-alarm SOURCE=memory");
clear_resource_alarm(disk, Config) ->
Make = ?config(make_cmd, Config),
SrcDir = ?config(amqp_client_srcdir, Config),
Nodename = ?config(rmq_nodename, Config),
true = rabbit_ct_helpers:run_cmd(
Make ++ " -C " ++ SrcDir ++ rabbit_ct_helpers:make_verbosity() ++
" RABBITMQ_NODENAME='" ++ atom_to_list(Nodename) ++ "'" ++
" clear-resource-alarm SOURCE=disk").
fmt(Fmt, Args) -> list_to_binary(rabbit_misc:format(Fmt, Args)).