install_upgrade.escript 19 KB

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