nodetool 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. #!/usr/bin/env escript
  2. %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
  3. %% ex: ft=erlang ts=4 sw=4 et
  4. %% -------------------------------------------------------------------
  5. %%
  6. %% nodetool: Helper Script for interacting with live nodes
  7. %%
  8. %% -------------------------------------------------------------------
  9. -mode(compile).
  10. main(Args) ->
  11. case os:type() of
  12. {win32, nt} -> ok;
  13. _nix ->
  14. case init:get_argument(start_epmd) of
  15. {ok, [["true"]]} ->
  16. ok = start_epmd();
  17. _ ->
  18. ok
  19. end
  20. end,
  21. ok = add_libs_dir(),
  22. case Args of
  23. ["hocon" | Rest] ->
  24. %% forward the call to hocon_cli
  25. hocon_cli:main(Rest);
  26. ["check_license_key", Key] ->
  27. check_license(#{key => list_to_binary(Key)});
  28. _ ->
  29. do(Args)
  30. end.
  31. do(Args) ->
  32. ok = do_with_halt(Args, "mnesia_dir", fun create_mnesia_dir/2),
  33. ok = do_with_halt(Args, "chkconfig", fun("-config", X) -> chkconfig(X) end),
  34. ok = do_with_halt(Args, "chkconfig", fun chkconfig/1),
  35. Args1 = do_with_ret(Args, "-name",
  36. fun(TargetName) ->
  37. ThisNode = this_node_name(longnames, TargetName),
  38. {ok, _} = net_kernel:start([ThisNode, longnames]),
  39. put(target_node, nodename(TargetName))
  40. end),
  41. Args2 = do_with_ret(Args1, "-sname",
  42. fun(TargetName) ->
  43. ThisNode = this_node_name(shortnames, TargetName),
  44. {ok, _} = net_kernel:start([ThisNode, shortnames]),
  45. put(target_node, nodename(TargetName))
  46. end),
  47. RestArgs = do_with_ret(Args2, "-setcookie",
  48. fun(Cookie) ->
  49. erlang:set_cookie(node(), list_to_atom(Cookie))
  50. end),
  51. [application:start(App) || App <- [crypto, public_key, ssl]],
  52. TargetNode = get(target_node),
  53. %% See if the node is currently running -- if it's not, we'll bail
  54. case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
  55. {true, pong} ->
  56. ok;
  57. {false, pong} ->
  58. io:format(standard_error, "Failed to connect to node ~p\n", [TargetNode]),
  59. halt(1);
  60. {_, pang} ->
  61. io:format(standard_error, "Node ~p not responding to pings.\n", [TargetNode]),
  62. halt(1)
  63. end,
  64. %% Mute logger from now on.
  65. %% Otherwise Erlang distribution over TLS (inet_tls_dist) warning logs
  66. %% and supervisor reports may contaminate io:format outputs
  67. logger:set_primary_config(level, none),
  68. case RestArgs of
  69. ["getpid"] ->
  70. io:format("~p\n", [list_to_integer(rpc:call(TargetNode, os, getpid, []))]);
  71. ["ping"] ->
  72. %% If we got this far, the node already responded to a ping, so just dump
  73. %% a "pong"
  74. io:format("pong\n");
  75. ["stop"] ->
  76. case rpc:call(TargetNode, emqx_machine, graceful_shutdown, [], 60000) of
  77. ok ->
  78. ok;
  79. {badrpc, nodedown} ->
  80. %% nodetool commands are always executed after a ping
  81. %% which if the code gets here, it's because the target node
  82. %% has shutdown before RPC returns.
  83. ok
  84. end;
  85. ["rpc", Module, Function | RpcArgs] ->
  86. case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
  87. [RpcArgs], 60000) of
  88. ok ->
  89. ok;
  90. {error, cmd_not_found} ->
  91. halt(1);
  92. {error, Reason} ->
  93. io:format("RPC to ~s error: ~p\n", [TargetNode, Reason]),
  94. halt(1);
  95. {badrpc, Reason} ->
  96. io:format("RPC to ~s failed: ~p\n", [TargetNode, Reason]),
  97. halt(1);
  98. _ ->
  99. halt(1)
  100. end;
  101. ["rpc_infinity", Module, Function | RpcArgs] ->
  102. case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), [RpcArgs], infinity) of
  103. ok ->
  104. ok;
  105. {badrpc, Reason} ->
  106. io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
  107. halt(1);
  108. _ ->
  109. halt(1)
  110. end;
  111. ["rpcterms", Module, Function | ArgsAsString] ->
  112. case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
  113. consult(lists:flatten(ArgsAsString)), 60000) of
  114. {badrpc, Reason} ->
  115. io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
  116. halt(1);
  117. Other ->
  118. io:format("~p\n", [Other])
  119. end;
  120. ["eval" | ListOfArgs] ->
  121. Parsed = parse_eval_args(ListOfArgs),
  122. % and evaluate it on the remote node
  123. case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of
  124. {value, Value, _} ->
  125. io:format ("~p~n",[Value]);
  126. {badrpc, Reason} ->
  127. io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]),
  128. halt(1)
  129. end;
  130. Other ->
  131. io:format("Other: ~p~n", [Other]),
  132. io:format("Usage: nodetool chkconfig|getpid|ping|stop|rpc|rpc_infinity|rpcterms|eval|cold_eval [Terms] [RPC]\n")
  133. end,
  134. net_kernel:stop().
  135. parse_eval_args(Args) ->
  136. % shells may process args into more than one, and end up stripping
  137. % spaces, so this converts all of that to a single string to parse
  138. String = binary_to_list(
  139. list_to_binary(
  140. join(Args," ")
  141. )
  142. ),
  143. % then just as a convenience to users, if they forgot a trailing
  144. % '.' add it for them.
  145. Normalized =
  146. case lists:reverse(String) of
  147. [$. | _] -> String;
  148. R -> lists:reverse([$. | R])
  149. end,
  150. % then scan and parse the string
  151. {ok, Scanned, _} = erl_scan:string(Normalized),
  152. {ok, Parsed } = erl_parse:parse_exprs(Scanned),
  153. Parsed.
  154. do_with_ret(Args, Name, Handler) ->
  155. {arity, Arity} = erlang:fun_info(Handler, arity),
  156. case take_args(Args, Name, Arity) of
  157. false ->
  158. Args;
  159. {Args1, Rest} ->
  160. _ = erlang:apply(Handler, Args1),
  161. Rest
  162. end.
  163. do_with_halt(Args, Name, Handler) ->
  164. {arity, Arity} = erlang:fun_info(Handler, arity),
  165. case take_args(Args, Name, Arity) of
  166. false ->
  167. ok;
  168. {Args1, _Rest} ->
  169. erlang:apply(Handler, Args1), %% should halt
  170. io:format(standard_error, "~s handler did not halt", [Name]),
  171. halt(?LINE)
  172. end.
  173. %% Return option args list if found, otherwise 'false'.
  174. take_args(Args, OptName, 0) ->
  175. lists:member(OptName, Args) andalso [];
  176. take_args(Args, OptName, OptArity) ->
  177. take_args(Args, OptName, OptArity, _Scanned = []).
  178. take_args([], _, _, _) -> false; %% no such option
  179. take_args([Name | Rest], Name, Arity, Scanned) ->
  180. length(Rest) >= Arity orelse error({not_enough_args_for, Name}),
  181. {Result, Tail} = lists:split(Arity, Rest),
  182. {Result, lists:reverse(Scanned) ++ Tail};
  183. take_args([Other | Rest], Name, Arity, Scanned) ->
  184. take_args(Rest, Name, Arity, [Other | Scanned]).
  185. start_epmd() ->
  186. [] = os:cmd("\"" ++ epmd_path() ++ "\" -daemon"),
  187. ok.
  188. epmd_path() ->
  189. ErtsBinDir = filename:dirname(escript:script_name()),
  190. Name = "epmd",
  191. case os:find_executable(Name, ErtsBinDir) of
  192. false ->
  193. case os:find_executable(Name) of
  194. false ->
  195. io:format("Could not find epmd.~n"),
  196. halt(1);
  197. GlobalEpmd ->
  198. GlobalEpmd
  199. end;
  200. Epmd ->
  201. Epmd
  202. end.
  203. nodename(Name) ->
  204. case re:split(Name, "@", [{return, list}, unicode]) of
  205. [_Node, _Host] ->
  206. list_to_atom(Name);
  207. [Node] ->
  208. [_, Host] = re:split(atom_to_list(node()), "@", [{return, list}, unicode]),
  209. list_to_atom(lists:concat([Node, "@", Host]))
  210. end.
  211. this_node_name(longnames, Name) ->
  212. [Node, Host] = re:split(Name, "@", [{return, list}, unicode]),
  213. list_to_atom(lists:concat(["remsh_maint_", Node, node_name_suffix_id(), "@", Host]));
  214. this_node_name(shortnames, Name) ->
  215. list_to_atom(lists:concat(["remsh_maint_", Name, node_name_suffix_id()])).
  216. %% use the reversed value that from pid mod 1000 as the node name suffix
  217. node_name_suffix_id() ->
  218. Pid = os:getpid(),
  219. string:slice(string:reverse(Pid), 0, 3).
  220. %% For windows???
  221. create_mnesia_dir(DataDir, NodeName) ->
  222. MnesiaDir = filename:join(DataDir, NodeName),
  223. file:make_dir(MnesiaDir),
  224. io:format("~s", [MnesiaDir]),
  225. halt(0).
  226. chkconfig(File) ->
  227. case file:consult(File) of
  228. {ok, Terms} ->
  229. case validate(Terms) of
  230. ok ->
  231. halt(0);
  232. {error, Problems} ->
  233. lists:foreach(fun print_issue/1, Problems),
  234. %% halt(1) if any problems were errors
  235. halt(case [x || {error, _} <- Problems] of
  236. [] -> 0;
  237. _ -> 1
  238. end)
  239. end;
  240. {error, {Line, Mod, Term}} ->
  241. io:format(standard_error, ["Error on line ", file:format_error({Line, Mod, Term}), "\n"], []),
  242. halt(1);
  243. {error, Error} ->
  244. io:format(standard_error, ["Error reading config file: ", File, " ", file:format_error(Error), "\n"], []),
  245. halt(1)
  246. end.
  247. check_license(Config) ->
  248. ok = application:load(emqx_license),
  249. %% This checks formal license validity to ensure
  250. %% that the node can successfully start with the given license.
  251. %% However, a valid license may be expired. In this case, the node will
  252. %% start but will not be able to receive connections due to connection limits.
  253. %% It may receive license updates from the cluster further.
  254. case emqx_license:read_license(Config) of
  255. {ok, _} -> ok;
  256. {error, Error} ->
  257. io:format(standard_error, "Error reading license: ~p~n", [Error]),
  258. halt(1)
  259. end.
  260. %%
  261. %% Given a string or binary, parse it into a list of terms, ala file:consult/0
  262. %%
  263. consult(Str) when is_list(Str) ->
  264. consult([], Str, []);
  265. consult(Bin) when is_binary(Bin)->
  266. consult([], binary_to_list(Bin), []).
  267. consult(Cont, Str, Acc) ->
  268. case erl_scan:tokens(Cont, Str, 0) of
  269. {done, Result, Remaining} ->
  270. case Result of
  271. {ok, Tokens, _} ->
  272. {ok, Term} = erl_parse:parse_term(Tokens),
  273. consult([], Remaining, [Term | Acc]);
  274. {eof, _Other} ->
  275. lists:reverse(Acc);
  276. {error, Info, _} ->
  277. {error, Info}
  278. end;
  279. {more, Cont1} ->
  280. consult(Cont1, eof, Acc)
  281. end.
  282. %%
  283. %% Validation functions for checking the app.config
  284. %%
  285. validate([Terms]) ->
  286. Results = [ValidateFun(Terms) || ValidateFun <- get_validation_funs()],
  287. Failures = [Res || Res <- Results, Res /= true],
  288. case Failures of
  289. [] ->
  290. ok;
  291. _ ->
  292. {error, Failures}
  293. end.
  294. %% Some initial and basic checks for the app.config file
  295. get_validation_funs() ->
  296. [ ].
  297. print_issue({warning, Warning}) ->
  298. io:format(standard_error, "Warning in app.config: ~s~n", [Warning]);
  299. print_issue({error, Error}) ->
  300. io:format(standard_error, "Error in app.config: ~s~n", [Error]).
  301. %% string:join/2 copy; string:join/2 is getting obsoleted
  302. %% and replaced by lists:join/2, but lists:join/2 is too new
  303. %% for version support (only appeared in 19.0) so it cannot be
  304. %% used. Instead we just adopt join/2 locally and hope it works
  305. %% for most unicode use cases anyway.
  306. join([], Sep) when is_list(Sep) ->
  307. [];
  308. join([H|T], Sep) ->
  309. H ++ lists:append([Sep ++ X || X <- T]).
  310. add_libs_dir() ->
  311. [_ | _] = RootDir = os:getenv("RUNNER_ROOT_DIR"),
  312. CurrentVsn = os:getenv("REL_VSN"),
  313. RelFile = filename:join([RootDir, "releases", "RELEASES"]),
  314. case file:consult(RelFile) of
  315. {ok, [Releases]} ->
  316. Release = lists:keyfind(CurrentVsn, 3, Releases),
  317. {release, _Name, _AppVsn, _ErtsVsn, Libs, _State} = Release,
  318. lists:foreach(
  319. fun({Name, Vsn, _}) ->
  320. add_lib_dir(RootDir, Name, Vsn)
  321. end, Libs);
  322. {error, Reason} ->
  323. %% rel file was been deleted by release handler
  324. error({failed_to_read_RELEASES_file, RelFile, Reason})
  325. end.
  326. add_lib_dir(RootDir, Name, Vsn) ->
  327. LibDir = filename:join([RootDir, lib, atom_to_list(Name) ++ "-" ++ Vsn, ebin]),
  328. case code:add_patha(LibDir) of
  329. true -> ok;
  330. {error, _} -> error(LibDir)
  331. end.