%%-------------------------------------------------------------------- %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %%---------------------------------------------------------------------- %% File : ct_slave.erl %% Description : CT module for starting nodes for large-scale testing. %% %% Created : 7 April 2010 %%---------------------------------------------------------------------- -module(ct_slave). -export([start/1, start/2, start/3, stop/1, stop/2]). -export([slave_started/2, slave_ready/2, monitor_master/1]). -record(options, {username, password, boot_timeout, init_timeout, startup_timeout, startup_functions, monitor_master, kill_if_fail, erl_flags, env, ssh_port, ssh_opts, stop_timeout}). start(Node) -> start(gethostname(), Node). start(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc when is_list(Opts) -> start(gethostname(), Node, Opts); start(Host, Node) -> start(Host, Node, []). start(Host, Node, Opts) -> ENode = enodename(Host, Node), case erlang:is_alive() of false-> {error, not_alive, node()}; true-> case is_started(ENode) of false-> OptionsRec = fetch_options(Opts), do_start(Host, Node, OptionsRec); {true, not_connected}-> {error, started_not_connected, ENode}; {true, connected}-> {error, already_started, ENode} end end. stop(Node) -> stop(gethostname(), Node). stop(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc when is_list(Opts) -> stop(gethostname(), Node, Opts); stop(Host, Node) -> stop(Host, Node, []). stop(Host, Node, Opts) -> ENode = enodename(Host, Node), case is_started(ENode) of {true, connected}-> OptionsRec = fetch_options(Opts), do_stop(ENode, OptionsRec); {true, not_connected}-> {error, not_connected, ENode}; false-> {error, not_started, ENode} end. %%% fetch an option value from the tagged tuple list with default get_option_value(Key, OptionList, Default) -> case lists:keyfind(Key, 1, OptionList) of false-> Default; {Key, Value}-> Value end. %%% convert option list to the option record, fill all defaults fetch_options(Options) -> UserName = get_option_value(username, Options, []), Password = get_option_value(password, Options, []), BootTimeout = get_option_value(boot_timeout, Options, 3), InitTimeout = get_option_value(init_timeout, Options, 1), StartupTimeout = get_option_value(startup_timeout, Options, 1), StartupFunctions = get_option_value(startup_functions, Options, []), Monitor = get_option_value(monitor_master, Options, false), KillIfFail = get_option_value(kill_if_fail, Options, true), ErlFlags = get_option_value(erl_flags, Options, []), EnvVars = get_option_value(env, Options, []), SSHPort = get_option_value(ssh_port, Options, []), SSHOpts = get_option_value(ssh_opts, Options, []), StopTimeout = get_option_value(stop_timeout, Options, 5), #options{username=UserName, password=Password, boot_timeout=BootTimeout, init_timeout=InitTimeout, startup_timeout=StartupTimeout, startup_functions=StartupFunctions, monitor_master=Monitor, kill_if_fail=KillIfFail, erl_flags=ErlFlags, env=EnvVars, ssh_port=SSHPort, ssh_opts=SSHOpts, stop_timeout=StopTimeout}. % send a message when slave node is started slave_started(ENode, MasterPid) -> MasterPid ! {node_started, ENode}, ok. % send a message when slave node has finished startup slave_ready(ENode, MasterPid) -> MasterPid ! {node_ready, ENode}, ok. % start monitoring of the master node monitor_master(MasterNode) -> spawn(fun() -> monitor_master_int(MasterNode) end). % code of the masterdeath-waiter process monitor_master_int(MasterNode) -> ct_util:mark_process(), erlang:monitor_node(MasterNode, true), receive {nodedown, MasterNode}-> init:stop() end. % check if node is listed in the nodes() is_connected(ENode) -> [N||N<-nodes(), N==ENode] == [ENode]. % check if node is alive (ping and disconnect if pingable) is_started(ENode) -> case is_connected(ENode) of true-> {true, connected}; false-> case net_adm:ping(ENode) of pang-> false; pong-> erlang:disconnect_node(ENode), {true, not_connected} end end. % make a Erlang node name from name and hostname enodename(Host, Node) -> case lists:member($@, atom_to_list(Node)) of true -> Node; false -> list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host)) end. % performs actual start of the "slave" node do_start(Host, Node, Options) -> ENode = enodename(Host, Node), Functions = lists:append([[{ct_slave, slave_started, [ENode, self()]}], Options#options.startup_functions, [{ct_slave, slave_ready, [ENode, self()]}]]), Functions2 = if Options#options.monitor_master-> [{ct_slave, monitor_master, [node()]}|Functions]; true-> Functions end, MasterHost = gethostname(), _ = if MasterHost == Host -> spawn_local_node(Node, Options); true-> spawn_remote_node(Host, Node, Options) end, BootTimeout = Options#options.boot_timeout, InitTimeout = Options#options.init_timeout, StartupTimeout = Options#options.startup_timeout, Result = case wait_for_node_alive(ENode, BootTimeout) of pong-> case test_server:is_cover() of true -> MainCoverNode = cover:get_main_node(), rpc:call(MainCoverNode,cover,start,[ENode]); false -> ok end, call_functions(ENode, Functions2), receive {node_started, ENode}-> receive {node_ready, ENode}-> {ok, ENode} after StartupTimeout*1000-> {error, startup_timeout, ENode} end after InitTimeout*1000 -> {error, init_timeout, ENode} end; pang-> {error, boot_timeout, ENode} end, _ = case Result of {ok, ENode}-> ok; {error, Timeout, ENode} when ((Timeout==init_timeout) or (Timeout==startup_timeout)) and Options#options.kill_if_fail-> do_stop(ENode); _-> ok end, Result. % are we using fully qualified hostnames long_or_short() -> case net_kernel:longnames() of true-> " -name "; false-> " -sname " end. % get the localhost's name, depending on the using name policy gethostname() -> Hostname = case net_kernel:longnames() of true-> net_adm:localhost(); _-> {ok, Name}=inet:gethostname(), Name end, list_to_atom(Hostname). % get cmd for starting Erlang get_cmd(Node, Flags) -> Cookie = erlang:get_cookie(), "erl -detached -noinput -setcookie "++ atom_to_list(Cookie) ++ long_or_short() ++ atom_to_list(Node) ++ " " ++ Flags. % spawn node locally spawn_local_node(Node, Options) -> #options{env=Env,erl_flags=ErlFlags} = Options, Cmd = get_cmd(Node, ErlFlags), open_port({spawn, Cmd}, [stream,{env,Env}]). % spawn node remotely spawn_remote_node(Host, Node, Options) -> #options{username=Username, password=Password, erl_flags=ErlFlags, env=Env, ssh_port=MaybeSSHPort, ssh_opts=SSHOpts} = Options, SSHPort = case MaybeSSHPort of [] -> 22; % Use default SSH port A -> A end, SSHOptions = case {Username, Password} of {[], []}-> []; {_, []}-> [{user, Username}]; {_, _}-> [{user, Username}, {password, Password}] end ++ [{silently_accept_hosts, true}] ++ SSHOpts, {ok, _} = application:ensure_all_started(ssh), {ok, SSHConnRef} = ssh:connect(atom_to_list(Host), SSHPort, SSHOptions), {ok, SSHChannelId} = ssh_connection:session_channel(SSHConnRef, infinity), ssh_setenv(SSHConnRef, SSHChannelId, Env), ssh_connection:exec(SSHConnRef, SSHChannelId, get_cmd(Node, ErlFlags), infinity). ssh_setenv(SSHConnRef, SSHChannelId, [{Var, Value} | Vars]) when is_list(Var), is_list(Value) -> success = ssh_connection:setenv(SSHConnRef, SSHChannelId, Var, Value, infinity), ssh_setenv(SSHConnRef, SSHChannelId, Vars); ssh_setenv(_SSHConnRef, _SSHChannelId, []) -> ok. % call functions on a remote Erlang node call_functions(_Node, []) -> ok; call_functions(Node, [{M, F, A}|Functions]) -> rpc:call(Node, M, F, A), call_functions(Node, Functions). % wait N seconds until node is pingable wait_for_node_alive(_Node, 0) -> pang; wait_for_node_alive(Node, N) -> timer:sleep(1000), case net_adm:ping(Node) of pong-> pong; pang-> wait_for_node_alive(Node, N-1) end. % call init:stop on a remote node do_stop(ENode) -> do_stop(ENode, fetch_options([])). do_stop(ENode, Options) -> {Cover,MainCoverNode} = case test_server:is_cover() of true -> Main = cover:get_main_node(), rpc:call(Main,cover,flush,[ENode]), {true,Main}; false -> {false,undefined} end, spawn(ENode, init, stop, []), StopTimeout = Options#options.stop_timeout, case wait_for_node_dead(ENode, StopTimeout) of {ok,ENode} -> if Cover -> %% To avoid that cover is started again if a node %% with the same name is started later. rpc:call(MainCoverNode,cover,stop,[ENode]); true -> ok end, {ok,ENode}; Error -> Error end. % wait N seconds until node is disconnected wait_for_node_dead(Node, 0) -> {error, stop_timeout, Node}; wait_for_node_dead(Node, N) -> timer:sleep(1000), case lists:member(Node, nodes()) of true-> wait_for_node_dead(Node, N-1); false-> {ok, Node} end.