install_upgrade.escript 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env escript
  2. %%! -noshell -noinput
  3. %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
  4. %% ex: ft=erlang ts=4 sw=4 et
  5. -define(TIMEOUT, 300000).
  6. -define(INFO(Fmt,Args), io:format(Fmt++"~n",Args)).
  7. main([Command0, DistInfoStr | CommandArgs]) ->
  8. %% convert the distribution info arguments string to an erlang term
  9. {ok, Tokens, _} = erl_scan:string(DistInfoStr ++ "."),
  10. {ok, DistInfo} = erl_parse:parse_term(Tokens),
  11. %% convert arguments into a proplist
  12. Opts = parse_arguments(CommandArgs),
  13. %% invoke the command passed as argument
  14. F = case Command0 of
  15. "install" -> fun(A, B) -> install(A, B) end;
  16. "unpack" -> fun(A, B) -> unpack(A, B) end;
  17. "upgrade" -> fun(A, B) -> upgrade(A, B) end;
  18. "downgrade" -> fun(A, B) -> downgrade(A, B) end;
  19. "uninstall" -> fun(A, B) -> uninstall(A, B) end;
  20. "versions" -> fun(A, B) -> versions(A, B) end
  21. end,
  22. F(DistInfo, Opts);
  23. main(Args) ->
  24. ?INFO("unknown args: ~p", [Args]),
  25. erlang:halt(1).
  26. unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  27. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  28. Version = proplists:get_value(version, Opts),
  29. case unpack_release(RelName, TargetNode, Version) of
  30. {ok, Vsn} ->
  31. ?INFO("Unpacked successfully: ~p", [Vsn]);
  32. old ->
  33. %% no need to unpack, has been installed previously
  34. ?INFO("Release ~s is marked old.",[Version]);
  35. unpacked ->
  36. ?INFO("Release ~s is already unpacked.",[Version]);
  37. current ->
  38. ?INFO("Release ~s is already installed and current.",[Version]);
  39. permanent ->
  40. ?INFO("Release ~s is already installed and set permanent.",[Version]);
  41. {error, Reason} ->
  42. ?INFO("Unpack failed: ~p.",[Reason]),
  43. print_existing_versions(TargetNode),
  44. erlang:halt(2)
  45. end;
  46. unpack(_, Args) ->
  47. ?INFO("unpack: unknown args ~p", [Args]).
  48. install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  49. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  50. Version = proplists:get_value(version, Opts),
  51. case unpack_release(RelName, TargetNode, Version) of
  52. {ok, Vsn} ->
  53. ?INFO("Unpacked successfully: ~p.", [Vsn]),
  54. check_and_install(TargetNode, Vsn),
  55. maybe_permafy(TargetNode, RelName, Vsn, Opts);
  56. old ->
  57. %% no need to unpack, has been installed previously
  58. ?INFO("Release ~s is marked old, switching to it.",[Version]),
  59. check_and_install(TargetNode, Version),
  60. maybe_permafy(TargetNode, RelName, Version, Opts);
  61. unpacked ->
  62. ?INFO("Release ~s is already unpacked, now installing.",[Version]),
  63. check_and_install(TargetNode, Version),
  64. maybe_permafy(TargetNode, RelName, Version, Opts);
  65. current ->
  66. case proplists:get_value(permanent, Opts, true) of
  67. true ->
  68. ?INFO("Release ~s is already installed and current, making permanent.",
  69. [Version]),
  70. permafy(TargetNode, RelName, Version);
  71. false ->
  72. ?INFO("Release ~s is already installed and current.",
  73. [Version])
  74. end;
  75. permanent ->
  76. %% this release is marked permanent, however it might not the
  77. %% one currently running
  78. case current_release_version(TargetNode) of
  79. Version ->
  80. ?INFO("Release ~s is already installed, running and set permanent.",
  81. [Version]);
  82. CurrentVersion ->
  83. ?INFO("Release ~s is the currently running version.",
  84. [CurrentVersion]),
  85. check_and_install(TargetNode, Version),
  86. maybe_permafy(TargetNode, RelName, Version, Opts)
  87. end;
  88. {error, Reason} ->
  89. ?INFO("Unpack failed: ~p",[Reason]),
  90. print_existing_versions(TargetNode),
  91. erlang:halt(2)
  92. end;
  93. install(_, Args) ->
  94. ?INFO("install: unknown args ~p", [Args]).
  95. upgrade(DistInfo, Args) ->
  96. install(DistInfo, Args).
  97. downgrade(DistInfo, Args) ->
  98. install(DistInfo, Args).
  99. uninstall({_RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  100. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  101. WhichReleases = which_releases(TargetNode),
  102. Version = proplists:get_value(version, Opts),
  103. case proplists:get_value(Version, WhichReleases) of
  104. undefined ->
  105. ?INFO("Release ~s is already uninstalled.", [Version]);
  106. old ->
  107. ?INFO("Release ~s is marked old, uninstalling it.", [Version]),
  108. remove_release(TargetNode, Version);
  109. unpacked ->
  110. ?INFO("Release ~s is marked unpacked, uninstalling it",
  111. [Version]),
  112. remove_release(TargetNode, Version);
  113. current ->
  114. ?INFO("Uninstall failed: Release ~s is marked current.", [Version]),
  115. erlang:halt(2);
  116. permanent ->
  117. ?INFO("Uninstall failed: Release ~s is running.", [Version]),
  118. erlang:halt(2)
  119. end;
  120. uninstall(_, Args) ->
  121. ?INFO("uninstall: unknown args ~p", [Args]).
  122. versions({_RelName, NameTypeArg, NodeName, Cookie}, []) ->
  123. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  124. print_existing_versions(TargetNode).
  125. parse_arguments(Args) ->
  126. parse_arguments(Args, []).
  127. parse_arguments([], Acc) -> Acc;
  128. parse_arguments(["--no-permanent"|Rest], Acc) ->
  129. parse_arguments(Rest, [{permanent, false}] ++ Acc);
  130. parse_arguments([VersionStr|Rest], Acc) ->
  131. Version = parse_version(VersionStr),
  132. parse_arguments(Rest, [{version, Version}] ++ Acc).
  133. unpack_release(RelName, TargetNode, Version) ->
  134. WhichReleases = which_releases(TargetNode),
  135. case proplists:get_value(Version, WhichReleases) of
  136. undefined ->
  137. %% not installed, so unpack tarball:
  138. %% look for a release package with the intended version in the following order:
  139. %% releases/<relname>-<version>.tar.gz
  140. %% releases/<version>/<relname>-<version>.tar.gz
  141. %% releases/<version>/<relname>.tar.gz
  142. case find_and_link_release_package(Version, RelName) of
  143. {_, undefined} ->
  144. {error, release_package_not_found};
  145. {ReleasePackage, ReleasePackageLink} ->
  146. ?INFO("Release ~s not found, attempting to unpack ~s",
  147. [Version, ReleasePackage]),
  148. case rpc:call(TargetNode, release_handler, unpack_release,
  149. [ReleasePackageLink], ?TIMEOUT) of
  150. {ok, Vsn} -> {ok, Vsn};
  151. {error, _} = Error -> Error
  152. end
  153. end;
  154. Other -> Other
  155. end.
  156. %% 1. look for a release package tarball with the provided version in the following order:
  157. %% releases/<relname>-<version>.tar.gz
  158. %% releases/<version>/<relname>-<version>.tar.gz
  159. %% releases/<version>/<relname>.tar.gz
  160. %% 2. create a symlink from a fixed location (ie. releases/<version>/<relname>.tar.gz)
  161. %% to the release package tarball found in 1.
  162. %% 3. return a tuple with the paths to the release package and
  163. %% to the symlink that is to be provided to release handler
  164. find_and_link_release_package(Version, RelName) ->
  165. RelNameStr = atom_to_list(RelName),
  166. %% regardless of the location of the release package, we'll
  167. %% always give release handler the same path which is the symlink
  168. %% the path to the package link is relative to "releases/" because
  169. %% that's what release handler is expecting
  170. ReleaseHandlerPackageLink = filename:join(Version, RelNameStr),
  171. %% this is the symlink name we'll create once
  172. %% we've found where the actual release package is located
  173. ReleaseLink = filename:join(["releases", Version,
  174. RelNameStr ++ ".tar.gz"]),
  175. ok = unpack_zipballs(RelNameStr, Version),
  176. TarBalls = [
  177. filename:join(["releases",
  178. RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
  179. filename:join(["releases", Version,
  180. RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
  181. filename:join(["releases", Version,
  182. RelNameStr ++ ".tar.gz"])
  183. ],
  184. case first_value(fun filelib:is_file/1, TarBalls) of
  185. no_value ->
  186. {undefined, undefined};
  187. %% no need to create the link since the release package we
  188. %% found is located in the same place as the link would be
  189. {ok, Filename} when is_list(Filename) andalso
  190. Filename =:= ReleaseLink ->
  191. {Filename, ReleaseHandlerPackageLink};
  192. {ok, Filename} when is_list(Filename) ->
  193. %% we now have the location of the release package, however
  194. %% release handler expects a fixed nomenclature (<relname>.tar.gz)
  195. %% so give it just that by creating a symlink to the tarball
  196. %% we found.
  197. %% make sure that the dir where we're creating the link in exists
  198. ok = filelib:ensure_dir(filename:join([filename:dirname(ReleaseLink), "dummy"])),
  199. %% create the symlink pointing to the full path name of the
  200. %% release package we found
  201. case file:make_symlink(filename:absname(Filename), ReleaseLink) of
  202. ok ->
  203. ok;
  204. {error, eperm} -> % windows!
  205. {ok,_} = file:copy(filename:absname(Filename), ReleaseLink)
  206. end,
  207. {Filename, ReleaseHandlerPackageLink}
  208. end.
  209. unpack_zipballs(RelNameStr, Version) ->
  210. {ok, Cwd} = file:get_cwd(),
  211. GzFile = filename:absname(filename:join(["releases", RelNameStr ++ "-" ++ Version ++ ".tar.gz"])),
  212. ZipFiles = filelib:wildcard(filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.zip"])),
  213. ?INFO("unzip ~p", [ZipFiles]),
  214. [begin
  215. TmdTarD="/tmp/emqx_untar_" ++ integer_to_list(erlang:system_time()),
  216. ok = filelib:ensure_dir(filename:join([TmdTarD, "dummy"])),
  217. {ok, _} = file:copy(Zip, filename:join([TmdTarD, "emqx.zip"])),
  218. ok = file:set_cwd(filename:join([TmdTarD])),
  219. {ok, _FileList} = zip:unzip("emqx.zip"),
  220. ok = file:set_cwd(filename:join([TmdTarD, "emqx"])),
  221. ok = erl_tar:create(GzFile, filelib:wildcard("*"), [compressed])
  222. end || Zip <- ZipFiles],
  223. file:set_cwd(Cwd).
  224. first_value(_Fun, []) -> no_value;
  225. first_value(Fun, [Value | Rest]) ->
  226. case Fun(Value) of
  227. false ->
  228. first_value(Fun, Rest);
  229. true ->
  230. {ok, Value}
  231. end.
  232. parse_version(V) when is_list(V) ->
  233. hd(string:tokens(V,"/")).
  234. check_and_install(TargetNode, Vsn) ->
  235. %% Backup the vm.args. VM args should be unchanged during hot upgrade
  236. %% but we still backup it here
  237. {ok, [[CurrVmArgs]]} = rpc:call(TargetNode, init, get_argument, [vm_args], ?TIMEOUT),
  238. {ok, _} = file:copy(CurrVmArgs, filename:join(["releases", Vsn, "vm.args"])),
  239. %% Backup the sys.config, this will be used when we check and install release
  240. %% NOTE: We cannot backup the old sys.config directly, because the
  241. %% configs for plugins are only in app-envs, not in the old sys.config
  242. Configs0 =
  243. [{AppName, rpc:call(TargetNode, application, get_all_env, [AppName], ?TIMEOUT)}
  244. || {AppName, _, _} <- rpc:call(TargetNode, application, which_applications, [], ?TIMEOUT)],
  245. Configs1 = [{AppName, Conf} || {AppName, Conf} <- Configs0, Conf =/= []],
  246. ok = file:write_file(filename:join(["releases", Vsn, "sys.config"]), io_lib:format("~p.", [Configs1])),
  247. %% check and install release
  248. case rpc:call(TargetNode, release_handler,
  249. check_install_release, [Vsn], ?TIMEOUT) of
  250. {ok, _OtherVsn, _Desc} ->
  251. ok;
  252. {error, Reason} ->
  253. ?INFO("ERROR: release_handler:check_install_release failed: ~p.",[Reason]),
  254. erlang:halt(3)
  255. end,
  256. case rpc:call(TargetNode, release_handler, install_release,
  257. [Vsn, [{update_paths, true}]], ?TIMEOUT) of
  258. {ok, _, _} ->
  259. ?INFO("Installed Release: ~s.", [Vsn]),
  260. ok;
  261. {error, {no_such_release, Vsn}} ->
  262. VerList =
  263. iolist_to_binary(
  264. [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]),
  265. ?INFO("Installed versions:~n~s", [VerList]),
  266. ?INFO("ERROR: Unable to revert to '~s' - not installed.", [Vsn]),
  267. erlang:halt(2);
  268. %% as described in http://erlang.org/doc/man/appup.html, when performing a relup
  269. %% with soft purge:
  270. %% If the value is soft_purge, release_handler:install_release/1
  271. %% returns {error,{old_processes,Mod}}
  272. {error, {old_processes, Mod}} ->
  273. ?INFO("ERROR: unable to install '~s' - old processes still running code from module ~p",
  274. [Vsn, Mod]),
  275. erlang:halt(3);
  276. {error, Reason1} ->
  277. ?INFO("ERROR: release_handler:install_release failed: ~p",[Reason1]),
  278. erlang:halt(4)
  279. end.
  280. maybe_permafy(TargetNode, RelName, Vsn, Opts) ->
  281. case proplists:get_value(permanent, Opts, true) of
  282. true ->
  283. permafy(TargetNode, RelName, Vsn);
  284. false -> ok
  285. end.
  286. permafy(TargetNode, RelName, Vsn) ->
  287. RelNameStr = atom_to_list(RelName),
  288. ok = rpc:call(TargetNode, release_handler,
  289. make_permanent, [Vsn], ?TIMEOUT),
  290. ?INFO("Made release permanent: ~p", [Vsn]),
  291. %% upgrade/downgrade the scripts by replacing them
  292. Scripts = [RelNameStr, RelNameStr++"_ctl", "cuttlefish", "nodetool",
  293. "install_upgrade.escript"],
  294. [{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]),
  295. filename:join(["bin", File]))
  296. || File <- Scripts],
  297. %% update the vars
  298. UpdatedVars = io_lib:format("REL_VSN=\"~s\"~nERTS_VSN=\"~s\"~n", [Vsn, erts_vsn()]),
  299. file:write_file(filename:absname(filename:join(["releases", "emqx_vars"])), UpdatedVars, [append]).
  300. remove_release(TargetNode, Vsn) ->
  301. case rpc:call(TargetNode, release_handler, remove_release, [Vsn], ?TIMEOUT) of
  302. ok ->
  303. ?INFO("Uninstalled Release: ~s", [Vsn]),
  304. ok;
  305. {error, Reason} ->
  306. ?INFO("ERROR: release_handler:remove_release failed: ~p", [Reason]),
  307. erlang:halt(3)
  308. end.
  309. which_releases(TargetNode) ->
  310. R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT),
  311. [ {V, S} || {_,V,_, S} <- R ].
  312. %% the running release version is either the only one marked `current´
  313. %% or, if none exists, the one marked `permanent`
  314. current_release_version(TargetNode) ->
  315. R = rpc:call(TargetNode, release_handler, which_releases,
  316. [], ?TIMEOUT),
  317. Versions = [ {S, V} || {_,V,_, S} <- R ],
  318. %% current version takes priority over the permanent
  319. proplists:get_value(current, Versions,
  320. proplists:get_value(permanent, Versions)).
  321. print_existing_versions(TargetNode) ->
  322. VerList = iolist_to_binary([
  323. io_lib:format("* ~s\t~s~n",[V,S])
  324. || {V,S} <- which_releases(TargetNode) ]),
  325. ?INFO("Installed versions:~n~s", [VerList]).
  326. start_distribution(TargetNode, NameTypeArg, Cookie) ->
  327. MyNode = make_script_node(TargetNode),
  328. {ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]),
  329. erlang:set_cookie(node(), Cookie),
  330. case {net_kernel:connect_node(TargetNode),
  331. net_adm:ping(TargetNode)} of
  332. {true, pong} ->
  333. ok;
  334. {_, pang} ->
  335. ?INFO("Node ~p not responding to pings.", [TargetNode]),
  336. erlang:halt(1)
  337. end,
  338. {ok, Cwd} = file:get_cwd(),
  339. ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT),
  340. TargetNode.
  341. make_script_node(Node) ->
  342. [Name, Host] = string:tokens(atom_to_list(Node), "@"),
  343. list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])).
  344. %% get name type from arg
  345. get_name_type(NameTypeArg) ->
  346. case NameTypeArg of
  347. "-sname" ->
  348. shortnames;
  349. _ ->
  350. longnames
  351. end.
  352. erts_vsn() ->
  353. {ok, Str} = file:read_file(filename:join(["releases", "start_erl.data"])),
  354. [ErtsVsn, _] = string:tokens(binary_to_list(Str), " "),
  355. ErtsVsn.