dgiot/bin/install_upgrade.escript
2022-01-07 16:30:56 +08:00

383 lines
16 KiB
Erlang
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env escript
%%! -noshell -noinput
%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ft=erlang ts=4 sw=4 et
-define(TIMEOUT, 300000).
-define(INFO(Fmt,Args), io:format(Fmt++"~n",Args)).
main([Command0, DistInfoStr | CommandArgs]) ->
%% convert the distribution info arguments string to an erlang term
{ok, Tokens, _} = erl_scan:string(DistInfoStr ++ "."),
{ok, DistInfo} = erl_parse:parse_term(Tokens),
%% convert arguments into a proplist
Opts = parse_arguments(CommandArgs),
%% invoke the command passed as argument
F = case Command0 of
"install" -> fun(A, B) -> install(A, B) end;
"unpack" -> fun(A, B) -> unpack(A, B) end;
"upgrade" -> fun(A, B) -> upgrade(A, B) end;
"downgrade" -> fun(A, B) -> downgrade(A, B) end;
"uninstall" -> fun(A, B) -> uninstall(A, B) end;
"versions" -> fun(A, B) -> versions(A, B) end
end,
F(DistInfo, Opts);
main(Args) ->
?INFO("unknown args: ~p", [Args]),
erlang:halt(1).
unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
Version = proplists:get_value(version, Opts),
case unpack_release(RelName, TargetNode, Version) of
{ok, Vsn} ->
?INFO("Unpacked successfully: ~p", [Vsn]);
old ->
%% no need to unpack, has been installed previously
?INFO("Release ~s is marked old.",[Version]);
unpacked ->
?INFO("Release ~s is already unpacked.",[Version]);
current ->
?INFO("Release ~s is already installed and current.",[Version]);
permanent ->
?INFO("Release ~s is already installed and set permanent.",[Version]);
{error, Reason} ->
?INFO("Unpack failed: ~p.",[Reason]),
print_existing_versions(TargetNode),
erlang:halt(2)
end;
unpack(_, Args) ->
?INFO("unpack: unknown args ~p", [Args]).
install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
Version = proplists:get_value(version, Opts),
case unpack_release(RelName, TargetNode, Version) of
{ok, Vsn} ->
?INFO("Unpacked successfully: ~p.", [Vsn]),
check_and_install(TargetNode, Vsn),
maybe_permafy(TargetNode, RelName, Vsn, Opts);
old ->
%% no need to unpack, has been installed previously
?INFO("Release ~s is marked old, switching to it.",[Version]),
check_and_install(TargetNode, Version),
maybe_permafy(TargetNode, RelName, Version, Opts);
unpacked ->
?INFO("Release ~s is already unpacked, now installing.",[Version]),
check_and_install(TargetNode, Version),
maybe_permafy(TargetNode, RelName, Version, Opts);
current ->
case proplists:get_value(permanent, Opts, true) of
true ->
?INFO("Release ~s is already installed and current, making permanent.",
[Version]),
permafy(TargetNode, RelName, Version);
false ->
?INFO("Release ~s is already installed and current.",
[Version])
end;
permanent ->
%% this release is marked permanent, however it might not the
%% one currently running
case current_release_version(TargetNode) of
Version ->
?INFO("Release ~s is already installed, running and set permanent.",
[Version]);
CurrentVersion ->
?INFO("Release ~s is the currently running version.",
[CurrentVersion]),
check_and_install(TargetNode, Version),
maybe_permafy(TargetNode, RelName, Version, Opts)
end;
{error, Reason} ->
?INFO("Unpack failed: ~p",[Reason]),
print_existing_versions(TargetNode),
erlang:halt(2)
end;
install(_, Args) ->
?INFO("install: unknown args ~p", [Args]).
upgrade(DistInfo, Args) ->
install(DistInfo, Args).
downgrade(DistInfo, Args) ->
install(DistInfo, Args).
uninstall({_RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
WhichReleases = which_releases(TargetNode),
Version = proplists:get_value(version, Opts),
case proplists:get_value(Version, WhichReleases) of
undefined ->
?INFO("Release ~s is already uninstalled.", [Version]);
old ->
?INFO("Release ~s is marked old, uninstalling it.", [Version]),
remove_release(TargetNode, Version);
unpacked ->
?INFO("Release ~s is marked unpacked, uninstalling it",
[Version]),
remove_release(TargetNode, Version);
current ->
?INFO("Uninstall failed: Release ~s is marked current.", [Version]),
erlang:halt(2);
permanent ->
?INFO("Uninstall failed: Release ~s is running.", [Version]),
erlang:halt(2)
end;
uninstall(_, Args) ->
?INFO("uninstall: unknown args ~p", [Args]).
versions({_RelName, NameTypeArg, NodeName, Cookie}, []) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
print_existing_versions(TargetNode).
parse_arguments(Args) ->
parse_arguments(Args, []).
parse_arguments([], Acc) -> Acc;
parse_arguments(["--no-permanent"|Rest], Acc) ->
parse_arguments(Rest, [{permanent, false}] ++ Acc);
parse_arguments([VersionStr|Rest], Acc) ->
Version = parse_version(VersionStr),
parse_arguments(Rest, [{version, Version}] ++ Acc).
unpack_release(RelName, TargetNode, Version) ->
WhichReleases = which_releases(TargetNode),
case proplists:get_value(Version, WhichReleases) of
undefined ->
%% not installed, so unpack tarball:
%% look for a release package with the intended version in the following order:
%% releases/<relname>-<version>.tar.gz
%% releases/<version>/<relname>-<version>.tar.gz
%% releases/<version>/<relname>.tar.gz
case find_and_link_release_package(Version, RelName) of
{_, undefined} ->
{error, release_package_not_found};
{ReleasePackage, ReleasePackageLink} ->
?INFO("Release ~s not found, attempting to unpack ~s",
[Version, ReleasePackage]),
case rpc:call(TargetNode, release_handler, unpack_release,
[ReleasePackageLink], ?TIMEOUT) of
{ok, Vsn} -> {ok, Vsn};
{error, _} = Error -> Error
end
end;
Other -> Other
end.
%% 1. look for a release package tarball with the provided version in the following order:
%% releases/<relname>-<version>.tar.gz
%% releases/<version>/<relname>-<version>.tar.gz
%% releases/<version>/<relname>.tar.gz
%% 2. create a symlink from a fixed location (ie. releases/<version>/<relname>.tar.gz)
%% to the release package tarball found in 1.
%% 3. return a tuple with the paths to the release package and
%% to the symlink that is to be provided to release handler
find_and_link_release_package(Version, RelName) ->
RelNameStr = atom_to_list(RelName),
%% regardless of the location of the release package, we'll
%% always give release handler the same path which is the symlink
%% the path to the package link is relative to "releases/" because
%% that's what release handler is expecting
ReleaseHandlerPackageLink = filename:join(Version, RelNameStr),
%% this is the symlink name we'll create once
%% we've found where the actual release package is located
ReleaseLink = filename:join(["releases", Version,
RelNameStr ++ ".tar.gz"]),
ok = unpack_zipballs(RelNameStr, Version),
TarBalls = [
filename:join(["releases",
RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
filename:join(["releases", Version,
RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
filename:join(["releases", Version,
RelNameStr ++ ".tar.gz"])
],
case first_value(fun filelib:is_file/1, TarBalls) of
no_value ->
{undefined, undefined};
%% no need to create the link since the release package we
%% found is located in the same place as the link would be
{ok, Filename} when is_list(Filename) andalso
Filename =:= ReleaseLink ->
{Filename, ReleaseHandlerPackageLink};
{ok, Filename} when is_list(Filename) ->
%% we now have the location of the release package, however
%% release handler expects a fixed nomenclature (<relname>.tar.gz)
%% so give it just that by creating a symlink to the tarball
%% we found.
%% make sure that the dir where we're creating the link in exists
ok = filelib:ensure_dir(filename:join([filename:dirname(ReleaseLink), "dummy"])),
%% create the symlink pointing to the full path name of the
%% release package we found
case file:make_symlink(filename:absname(Filename), ReleaseLink) of
ok ->
ok;
{error, eperm} -> % windows!
{ok,_} = file:copy(filename:absname(Filename), ReleaseLink)
end,
{Filename, ReleaseHandlerPackageLink}
end.
unpack_zipballs(RelNameStr, Version) ->
{ok, Cwd} = file:get_cwd(),
GzFile = filename:absname(filename:join(["releases", RelNameStr ++ "-" ++ Version ++ ".tar.gz"])),
ZipFiles = filelib:wildcard(filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.zip"])),
?INFO("unzip ~p", [ZipFiles]),
[begin
TmdTarD="/tmp/emqx_untar_" ++ integer_to_list(erlang:system_time()),
ok = filelib:ensure_dir(filename:join([TmdTarD, "dummy"])),
{ok, _} = file:copy(Zip, filename:join([TmdTarD, "emqx.zip"])),
ok = file:set_cwd(filename:join([TmdTarD])),
{ok, _FileList} = zip:unzip("emqx.zip"),
ok = file:set_cwd(filename:join([TmdTarD, "emqx"])),
ok = erl_tar:create(GzFile, filelib:wildcard("*"), [compressed])
end || Zip <- ZipFiles],
file:set_cwd(Cwd).
first_value(_Fun, []) -> no_value;
first_value(Fun, [Value | Rest]) ->
case Fun(Value) of
false ->
first_value(Fun, Rest);
true ->
{ok, Value}
end.
parse_version(V) when is_list(V) ->
hd(string:tokens(V,"/")).
check_and_install(TargetNode, Vsn) ->
%% Backup the vm.args. VM args should be unchanged during hot upgrade
%% but we still backup it here
{ok, [[CurrVmArgs]]} = rpc:call(TargetNode, init, get_argument, [vm_args], ?TIMEOUT),
{ok, _} = file:copy(CurrVmArgs, filename:join(["releases", Vsn, "vm.args"])),
%% Backup the sys.config, this will be used when we check and install release
%% NOTE: We cannot backup the old sys.config directly, because the
%% configs for plugins are only in app-envs, not in the old sys.config
Configs0 =
[{AppName, rpc:call(TargetNode, application, get_all_env, [AppName], ?TIMEOUT)}
|| {AppName, _, _} <- rpc:call(TargetNode, application, which_applications, [], ?TIMEOUT)],
Configs1 = [{AppName, Conf} || {AppName, Conf} <- Configs0, Conf =/= []],
ok = file:write_file(filename:join(["releases", Vsn, "sys.config"]), io_lib:format("~p.", [Configs1])),
%% check and install release
case rpc:call(TargetNode, release_handler,
check_install_release, [Vsn], ?TIMEOUT) of
{ok, _OtherVsn, _Desc} ->
ok;
{error, Reason} ->
?INFO("ERROR: release_handler:check_install_release failed: ~p.",[Reason]),
erlang:halt(3)
end,
case rpc:call(TargetNode, release_handler, install_release,
[Vsn, [{update_paths, true}]], ?TIMEOUT) of
{ok, _, _} ->
?INFO("Installed Release: ~s.", [Vsn]),
ok;
{error, {no_such_release, Vsn}} ->
VerList =
iolist_to_binary(
[io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]),
?INFO("Installed versions:~n~s", [VerList]),
?INFO("ERROR: Unable to revert to '~s' - not installed.", [Vsn]),
erlang:halt(2);
%% as described in http://erlang.org/doc/man/appup.html, when performing a relup
%% with soft purge:
%% If the value is soft_purge, release_handler:install_release/1
%% returns {error,{old_processes,Mod}}
{error, {old_processes, Mod}} ->
?INFO("ERROR: unable to install '~s' - old processes still running code from module ~p",
[Vsn, Mod]),
erlang:halt(3);
{error, Reason1} ->
?INFO("ERROR: release_handler:install_release failed: ~p",[Reason1]),
erlang:halt(4)
end.
maybe_permafy(TargetNode, RelName, Vsn, Opts) ->
case proplists:get_value(permanent, Opts, true) of
true ->
permafy(TargetNode, RelName, Vsn);
false -> ok
end.
permafy(TargetNode, RelName, Vsn) ->
RelNameStr = atom_to_list(RelName),
ok = rpc:call(TargetNode, release_handler,
make_permanent, [Vsn], ?TIMEOUT),
?INFO("Made release permanent: ~p", [Vsn]),
%% upgrade/downgrade the scripts by replacing them
Scripts = [RelNameStr, RelNameStr++"_ctl", "cuttlefish", "nodetool",
"install_upgrade.escript"],
[{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]),
filename:join(["bin", File]))
|| File <- Scripts],
%% update the vars
UpdatedVars = io_lib:format("REL_VSN=\"~s\"~nERTS_VSN=\"~s\"~n", [Vsn, erts_vsn()]),
file:write_file(filename:absname(filename:join(["releases", "emqx_vars"])), UpdatedVars, [append]).
remove_release(TargetNode, Vsn) ->
case rpc:call(TargetNode, release_handler, remove_release, [Vsn], ?TIMEOUT) of
ok ->
?INFO("Uninstalled Release: ~s", [Vsn]),
ok;
{error, Reason} ->
?INFO("ERROR: release_handler:remove_release failed: ~p", [Reason]),
erlang:halt(3)
end.
which_releases(TargetNode) ->
R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT),
[ {V, S} || {_,V,_, S} <- R ].
%% the running release version is either the only one marked `current´
%% or, if none exists, the one marked `permanent`
current_release_version(TargetNode) ->
R = rpc:call(TargetNode, release_handler, which_releases,
[], ?TIMEOUT),
Versions = [ {S, V} || {_,V,_, S} <- R ],
%% current version takes priority over the permanent
proplists:get_value(current, Versions,
proplists:get_value(permanent, Versions)).
print_existing_versions(TargetNode) ->
VerList = iolist_to_binary([
io_lib:format("* ~s\t~s~n",[V,S])
|| {V,S} <- which_releases(TargetNode) ]),
?INFO("Installed versions:~n~s", [VerList]).
start_distribution(TargetNode, NameTypeArg, Cookie) ->
MyNode = make_script_node(TargetNode),
{ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]),
erlang:set_cookie(node(), Cookie),
case {net_kernel:connect_node(TargetNode),
net_adm:ping(TargetNode)} of
{true, pong} ->
ok;
{_, pang} ->
?INFO("Node ~p not responding to pings.", [TargetNode]),
erlang:halt(1)
end,
{ok, Cwd} = file:get_cwd(),
ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT),
TargetNode.
make_script_node(Node) ->
[Name, Host] = string:tokens(atom_to_list(Node), "@"),
list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])).
%% get name type from arg
get_name_type(NameTypeArg) ->
case NameTypeArg of
"-sname" ->
shortnames;
_ ->
longnames
end.
erts_vsn() ->
{ok, Str} = file:read_file(filename:join(["releases", "start_erl.data"])),
[ErtsVsn, _] = string:tokens(binary_to_list(Str), " "),
ErtsVsn.