install_upgrade.escript 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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(standard_io, Fmt++"~n",Args)).
  7. -define(ERROR(Fmt,Args), io:format(standard_error, "ERROR: "++Fmt++"~n",Args)).
  8. -define(SEMVER_RE, <<"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[a-zA-Z\\d][-a-zA-Z.\\d]*)?(\\+[a-zA-Z\\d][-a-zA-Z.\\d]*)?$">>).
  9. -mode(compile).
  10. main([Command0, DistInfoStr | CommandArgs]) ->
  11. %% convert the distribution info arguments string to an erlang term
  12. {ok, Tokens, _} = erl_scan:string(DistInfoStr ++ "."),
  13. {ok, DistInfo} = erl_parse:parse_term(Tokens),
  14. %% convert arguments into a proplist
  15. Opts = parse_arguments(CommandArgs),
  16. %% invoke the command passed as argument
  17. F = case Command0 of
  18. %% "install" -> fun(A, B) -> install(A, B) end;
  19. %% "unpack" -> fun(A, B) -> unpack(A, B) end;
  20. %% "upgrade" -> fun(A, B) -> upgrade(A, B) end;
  21. %% "downgrade" -> fun(A, B) -> downgrade(A, B) end;
  22. %% "uninstall" -> fun(A, B) -> uninstall(A, B) end;
  23. "versions" -> fun(A, B) -> versions(A, B) end;
  24. _ -> fun fail_upgrade/2
  25. end,
  26. F(DistInfo, Opts);
  27. main(Args) ->
  28. ?INFO("unknown args: ~p", [Args]),
  29. erlang:halt(1).
  30. %% temporary block for hot-upgrades; next release will just remove
  31. %% this and the new script version shall be used instead of this
  32. %% current version.
  33. %% TODO: always deny relup for macos (unsupported)
  34. fail_upgrade(_DistInfo, _Opts) ->
  35. ?ERROR("Unsupported upgrade path", []),
  36. erlang:halt(1).
  37. unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  38. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  39. Version = proplists:get_value(version, Opts),
  40. case unpack_release(RelName, TargetNode, Version, Opts) of
  41. {ok, Vsn} ->
  42. ?INFO("Unpacked successfully: ~p", [Vsn]);
  43. old ->
  44. %% no need to unpack, has been installed previously
  45. ?INFO("Release ~s is marked old.",[Version]);
  46. unpacked ->
  47. ?INFO("Release ~s is already unpacked.",[Version]);
  48. current ->
  49. ?INFO("Release ~s is already installed and current.",[Version]);
  50. permanent ->
  51. ?INFO("Release ~s is already installed and set permanent.",[Version]);
  52. {error, Reason} ->
  53. ?INFO("Unpack failed: ~p.",[Reason]),
  54. print_existing_versions(TargetNode),
  55. erlang:halt(2)
  56. end;
  57. unpack(_, Args) ->
  58. ?INFO("unpack: unknown args ~p", [Args]).
  59. install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  60. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  61. Version = proplists:get_value(version, Opts),
  62. validate_target_version(Version, TargetNode),
  63. case unpack_release(RelName, TargetNode, Version, Opts) of
  64. {ok, Vsn} ->
  65. ?INFO("Unpacked successfully: ~p.", [Vsn]),
  66. check_and_install(TargetNode, Vsn),
  67. maybe_permafy(TargetNode, RelName, Vsn, Opts);
  68. old ->
  69. %% no need to unpack, has been installed previously
  70. ?INFO("Release ~s is marked old, switching to it.",[Version]),
  71. check_and_install(TargetNode, Version),
  72. maybe_permafy(TargetNode, RelName, Version, Opts);
  73. unpacked ->
  74. ?INFO("Release ~s is already unpacked, now installing.",[Version]),
  75. check_and_install(TargetNode, Version),
  76. maybe_permafy(TargetNode, RelName, Version, Opts);
  77. current ->
  78. case proplists:get_value(permanent, Opts, true) of
  79. true ->
  80. ?INFO("Release ~s is already installed and current, making permanent.",
  81. [Version]),
  82. permafy(TargetNode, RelName, Version);
  83. false ->
  84. ?INFO("Release ~s is already installed and current.",
  85. [Version])
  86. end;
  87. permanent ->
  88. %% this release is marked permanent, however it might not the
  89. %% one currently running
  90. case current_release_version(TargetNode) of
  91. Version ->
  92. ?INFO("Release ~s is already installed, running and set permanent.",
  93. [Version]);
  94. CurrentVersion ->
  95. ?INFO("Release ~s is the currently running version.",
  96. [CurrentVersion]),
  97. check_and_install(TargetNode, Version),
  98. maybe_permafy(TargetNode, RelName, Version, Opts)
  99. end;
  100. {error, Reason} ->
  101. ?INFO("Unpack failed: ~p",[Reason]),
  102. print_existing_versions(TargetNode),
  103. erlang:halt(2)
  104. end;
  105. install(_, Args) ->
  106. ?INFO("install: unknown args ~p", [Args]).
  107. upgrade(DistInfo, Args) ->
  108. install(DistInfo, Args).
  109. downgrade(DistInfo, Args) ->
  110. install(DistInfo, Args).
  111. uninstall({_RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
  112. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  113. WhichReleases = which_releases(TargetNode),
  114. Version = proplists:get_value(version, Opts),
  115. case proplists:get_value(Version, WhichReleases) of
  116. undefined ->
  117. ?INFO("Release ~s is already uninstalled.", [Version]);
  118. old ->
  119. ?INFO("Release ~s is marked old, uninstalling it.", [Version]),
  120. remove_release(TargetNode, Version);
  121. unpacked ->
  122. ?INFO("Release ~s is marked unpacked, uninstalling it",
  123. [Version]),
  124. remove_release(TargetNode, Version);
  125. current ->
  126. ?INFO("Uninstall failed: Release ~s is marked current.", [Version]),
  127. erlang:halt(2);
  128. permanent ->
  129. ?INFO("Uninstall failed: Release ~s is running.", [Version]),
  130. erlang:halt(2)
  131. end;
  132. uninstall(_, Args) ->
  133. ?INFO("uninstall: unknown args ~p", [Args]).
  134. versions({_RelName, NameTypeArg, NodeName, Cookie}, _Opts) ->
  135. TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
  136. print_existing_versions(TargetNode).
  137. parse_arguments(Args) ->
  138. IsEnterprise = os:getenv("IS_ENTERPRISE") == "yes",
  139. parse_arguments(Args, [{is_enterprise, IsEnterprise}]).
  140. parse_arguments([], Acc) -> Acc;
  141. parse_arguments(["--no-permanent"|Rest], Acc) ->
  142. parse_arguments(Rest, [{permanent, false}] ++ Acc);
  143. parse_arguments([VersionStr|Rest], Acc) ->
  144. Version = parse_version(VersionStr),
  145. parse_arguments(Rest, [{version, Version}] ++ Acc).
  146. unpack_release(RelName, TargetNode, Version, Opts) ->
  147. StartScriptExists = filelib:is_regular(filename:join(["releases", Version, "start.boot"])),
  148. WhichReleases = which_releases(TargetNode),
  149. IsEnterprise = proplists:get_value(is_enterprise, Opts),
  150. case proplists:get_value(Version, WhichReleases) of
  151. Res when Res =:= undefined; (Res =:= unpacked andalso not StartScriptExists) ->
  152. %% not installed, so unpack tarball:
  153. %% look for a release package with the intended version in the following order:
  154. %% releases/<relname>-<version>.tar.gz
  155. %% releases/<version>/<relname>-<version>.tar.gz
  156. %% releases/<version>/<relname>.tar.gz
  157. case find_and_link_release_package(Version, RelName, IsEnterprise) of
  158. {_, undefined} ->
  159. {error, release_package_not_found};
  160. {ReleasePackage, ReleasePackageLink} ->
  161. ?INFO("Release ~s not found, attempting to unpack ~s",
  162. [Version, ReleasePackage]),
  163. case rpc:call(TargetNode, release_handler, unpack_release,
  164. [ReleasePackageLink], ?TIMEOUT) of
  165. {ok, Vsn} -> {ok, Vsn};
  166. {error, {existing_release, Vsn}} ->
  167. %% sometimes the user may have removed the release/<vsn> dir
  168. %% for an `unpacked` release, then we need to re-unpack it from
  169. %% the .tar ball
  170. untar_for_unpacked_release(str(RelName), Vsn),
  171. {ok, Vsn};
  172. {error, _} = Error -> Error
  173. end
  174. end;
  175. Other ->
  176. Other
  177. end.
  178. untar_for_unpacked_release(RelName, Vsn) ->
  179. {ok, Root} = file:get_cwd(),
  180. RelDir = filename:join([Root, "releases"]),
  181. %% untar the .tar file, so release/<vsn> will be created
  182. Tar = filename:join([RelDir, Vsn, RelName ++ ".tar.gz"]),
  183. extract_tar(Root, Tar),
  184. %% create RELEASE file
  185. RelFile = filename:join([RelDir, Vsn, RelName ++ ".rel"]),
  186. release_handler:create_RELEASES(Root, RelFile),
  187. %% Clean release
  188. _ = file:delete(Tar),
  189. _ = file:delete(RelFile).
  190. extract_tar(Cwd, Tar) ->
  191. case erl_tar:extract(Tar, [keep_old_files, {cwd, Cwd}, compressed]) of
  192. ok -> ok;
  193. {error, {Name, Reason}} -> % New erl_tar (R3A).
  194. throw({error, {cannot_extract_file, Name, Reason}})
  195. end.
  196. %% 1. look for a release package tarball with the provided version:
  197. %% releases/<relname>-*<version>*.tar.gz
  198. %% 2. create a symlink from a fixed location (ie. releases/<version>/<relname>.tar.gz)
  199. %% to the release package tarball found in 1.
  200. %% 3. return a tuple with the paths to the release package and
  201. %% to the symlink that is to be provided to release handler
  202. find_and_link_release_package(Version, RelName, IsEnterprise) ->
  203. RelNameStr = atom_to_list(RelName),
  204. %% regardless of the location of the release package, we'll
  205. %% always give release handler the same path which is the symlink
  206. %% the path to the package link is relative to "releases/" because
  207. %% that's what release handler is expecting
  208. ReleaseHandlerPackageLink = filename:join(Version, RelNameStr),
  209. %% this is the symlink name we'll create once
  210. %% we've found where the actual release package is located
  211. ReleaseLink = filename:join(["releases", Version,
  212. RelNameStr ++ ".tar.gz"]),
  213. ReleaseNamePattern =
  214. case IsEnterprise of
  215. false -> RelNameStr;
  216. true -> RelNameStr ++ "-enterprise"
  217. end,
  218. FilePattern = lists:flatten([ReleaseNamePattern, "-", Version, "*.tar.gz"]),
  219. TarBalls = filename:join(["releases", FilePattern]),
  220. case filelib:wildcard(TarBalls) of
  221. [] ->
  222. {undefined, undefined};
  223. [Filename] when is_list(Filename) ->
  224. %% the release handler expects a fixed nomenclature (<relname>.tar.gz)
  225. %% so give it just that by creating a symlink to the tarball
  226. %% we found.
  227. %% make sure that the dir where we're creating the link in exists
  228. ok = filelib:ensure_dir(filename:join([filename:dirname(ReleaseLink), "dummy"])),
  229. %% create the symlink pointing to the full path name of the
  230. %% release package we found
  231. make_symlink_or_copy(filename:absname(Filename), ReleaseLink),
  232. {Filename, ReleaseHandlerPackageLink};
  233. Files ->
  234. ?ERROR("Found more than one package for version: '~s', "
  235. "files: ~p", [Version, Files]),
  236. erlang:halt(47)
  237. end.
  238. make_symlink_or_copy(Filename, ReleaseLink) ->
  239. case file:make_symlink(Filename, ReleaseLink) of
  240. ok -> ok;
  241. {error, eexist} ->
  242. ?INFO("Symlink ~p already exists, recreate it", [ReleaseLink]),
  243. ok = file:delete(ReleaseLink),
  244. make_symlink_or_copy(Filename, ReleaseLink);
  245. {error, Reason} when Reason =:= eperm; Reason =:= enotsup ->
  246. {ok, _} = file:copy(Filename, ReleaseLink);
  247. {error, Reason} ->
  248. ?ERROR("Create symlink ~p failed, error: ~p", [ReleaseLink, Reason]),
  249. erlang:halt(47)
  250. end.
  251. parse_version(V) when is_list(V) ->
  252. hd(string:tokens(V,"/")).
  253. check_and_install(TargetNode, Vsn) ->
  254. %% Backup the sys.config, this will be used when we check and install release
  255. %% NOTE: We cannot backup the old sys.config directly, because the
  256. %% configs for plugins are only in app-envs, not in the old sys.config
  257. Configs0 =
  258. [{AppName, rpc:call(TargetNode, application, get_all_env, [AppName], ?TIMEOUT)}
  259. || {AppName, _, _} <- rpc:call(TargetNode, application, which_applications, [], ?TIMEOUT)],
  260. Configs1 = [{AppName, Conf} || {AppName, Conf} <- Configs0, Conf =/= []],
  261. ok = file:write_file(filename:join(["releases", Vsn, "sys.config"]), io_lib:format("~p.", [Configs1])),
  262. %% check and install release
  263. case rpc:call(TargetNode, release_handler,
  264. check_install_release, [Vsn], ?TIMEOUT) of
  265. {ok, _OtherVsn, _Desc} ->
  266. ok;
  267. {error, Reason} ->
  268. ?ERROR("Call release_handler:check_install_release failed: ~p.", [Reason]),
  269. erlang:halt(3)
  270. end,
  271. case rpc:call(TargetNode, release_handler, install_release,
  272. [Vsn, [{update_paths, true}]], ?TIMEOUT) of
  273. {ok, _, _} ->
  274. ?INFO("Installed Release: ~s.", [Vsn]),
  275. ok;
  276. {error, {no_such_release, Vsn}} ->
  277. VerList =
  278. iolist_to_binary(
  279. [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]),
  280. ?INFO("Installed versions:~n~s", [VerList]),
  281. ?ERROR("Unable to revert to '~s' - not installed.", [Vsn]),
  282. erlang:halt(2);
  283. %% as described in http://erlang.org/doc/man/appup.html, when performing a relup
  284. %% with soft purge:
  285. %% If the value is soft_purge, release_handler:install_release/1
  286. %% returns {error,{old_processes,Mod}}
  287. {error, {old_processes, Mod}} ->
  288. ?ERROR("Unable to install '~s' - old processes still running code from module ~p",
  289. [Vsn, Mod]),
  290. erlang:halt(3);
  291. {error, Reason1} ->
  292. ?ERROR("Call release_handler:install_release failed: ~p",[Reason1]),
  293. erlang:halt(4)
  294. end.
  295. maybe_permafy(TargetNode, RelName, Vsn, Opts) ->
  296. case proplists:get_value(permanent, Opts, true) of
  297. true ->
  298. permafy(TargetNode, RelName, Vsn);
  299. false -> ok
  300. end.
  301. permafy(TargetNode, RelName, Vsn) ->
  302. RelNameStr = atom_to_list(RelName),
  303. ok = rpc:call(TargetNode, release_handler,
  304. make_permanent, [Vsn], ?TIMEOUT),
  305. ?INFO("Made release permanent: ~p", [Vsn]),
  306. %% upgrade/downgrade the scripts by replacing them
  307. Scripts = [RelNameStr, RelNameStr++"_ctl", "nodetool", "install_upgrade.escript"],
  308. [{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]),
  309. filename:join(["bin", File]))
  310. || File <- Scripts],
  311. %% update the vars
  312. UpdatedVars = io_lib:format("REL_VSN=\"~s\"~nERTS_VSN=\"~s\"~n", [Vsn, erts_vsn()]),
  313. file:write_file(filename:absname(filename:join(["releases", "emqx_vars"])), UpdatedVars, [append]).
  314. remove_release(TargetNode, Vsn) ->
  315. case rpc:call(TargetNode, release_handler, remove_release, [Vsn], ?TIMEOUT) of
  316. ok ->
  317. ?INFO("Uninstalled Release: ~s", [Vsn]),
  318. ok;
  319. {error, Reason} ->
  320. ?ERROR("Call release_handler:remove_release failed: ~p", [Reason]),
  321. erlang:halt(3)
  322. end.
  323. which_releases(TargetNode) ->
  324. R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT),
  325. [ {V, S} || {_,V,_, S} <- R ].
  326. %% the running release version is either the only one marked `current´
  327. %% or, if none exists, the one marked `permanent`
  328. current_release_version(TargetNode) ->
  329. R = rpc:call(TargetNode, release_handler, which_releases,
  330. [], ?TIMEOUT),
  331. Versions = [ {S, V} || {_,V,_, S} <- R ],
  332. %% current version takes priority over the permanent
  333. proplists:get_value(current, Versions,
  334. proplists:get_value(permanent, Versions)).
  335. print_existing_versions(TargetNode) ->
  336. VerList = iolist_to_binary([
  337. io_lib:format("* ~s\t~s~n",[V,S])
  338. || {V,S} <- which_releases(TargetNode) ]),
  339. ?INFO("Installed versions:~n~s", [VerList]).
  340. start_distribution(TargetNode, NameTypeArg, Cookie) ->
  341. MyNode = make_script_node(TargetNode),
  342. {ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]),
  343. erlang:set_cookie(node(), Cookie),
  344. case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
  345. {true, pong} ->
  346. ok;
  347. {_, pang} ->
  348. ?INFO("Node ~p not responding to pings.", [TargetNode]),
  349. erlang:halt(1)
  350. end,
  351. {ok, Cwd} = file:get_cwd(),
  352. ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT),
  353. TargetNode.
  354. make_script_node(Node) ->
  355. [Name, Host] = string:tokens(atom_to_list(Node), "@"),
  356. list_to_atom(lists:concat(["remsh_", Name, "_upgrader_", os:getpid(), "@", Host])).
  357. %% get name type from arg
  358. get_name_type(NameTypeArg) ->
  359. case NameTypeArg of
  360. "-sname" ->
  361. shortnames;
  362. _ ->
  363. longnames
  364. end.
  365. erts_vsn() ->
  366. {ok, Str} = file:read_file(filename:join(["releases", "start_erl.data"])),
  367. [ErtsVsn, _] = string:tokens(binary_to_list(Str), " "),
  368. ErtsVsn.
  369. validate_target_version(TargetVersion, TargetNode) ->
  370. CurrentVersion = current_release_version(TargetNode),
  371. case {get_major_minor_vsn(CurrentVersion), get_major_minor_vsn(TargetVersion)} of
  372. {{Major, Minor}, {Major, Minor}} -> ok;
  373. _ ->
  374. ?ERROR("Cannot upgrade/downgrade from '~s' to '~s'~n"
  375. "Hot upgrade is only supported between patch releases.",
  376. [CurrentVersion, TargetVersion]),
  377. erlang:halt(48)
  378. end.
  379. get_major_minor_vsn(Version) ->
  380. Parts = parse_semver(Version),
  381. [Major | Rem0] = Parts,
  382. [Minor | _Rem1] = Rem0,
  383. {Major, Minor}.
  384. parse_semver(Version) ->
  385. case re:run(Version, ?SEMVER_RE, [{capture, all_but_first, binary}]) of
  386. {match, Parts} -> Parts;
  387. nomatch ->
  388. ?ERROR("Invalid semantic version: '~s'~n", [Version]),
  389. erlang:halt(22)
  390. end.
  391. str(A) when is_atom(A) ->
  392. atom_to_list(A);
  393. str(A) when is_binary(A) ->
  394. binary_to_list(A);
  395. str(A) when is_list(A) ->
  396. (A).