|
|
@@ -4,7 +4,11 @@
|
|
|
%% ex: ft=erlang ts=4 sw=4 et
|
|
|
|
|
|
-define(TIMEOUT, 300000).
|
|
|
--define(INFO(Fmt,Args), io:format(Fmt++"~n",Args)).
|
|
|
+-define(INFO(Fmt,Args), io:format(standard_io, Fmt++"~n",Args)).
|
|
|
+-define(ERROR(Fmt,Args), io:format(standard_error, "ERROR: "++Fmt++"~n",Args)).
|
|
|
+-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]*)?$">>).
|
|
|
+
|
|
|
+-mode(compile).
|
|
|
|
|
|
main([Command0, DistInfoStr | CommandArgs]) ->
|
|
|
%% convert the distribution info arguments string to an erlang term
|
|
|
@@ -52,6 +56,7 @@ unpack(_, Args) ->
|
|
|
install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
|
|
|
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
|
|
|
Version = proplists:get_value(version, Opts),
|
|
|
+ validate_target_version(Version, TargetNode),
|
|
|
case unpack_release(RelName, TargetNode, Version) of
|
|
|
{ok, Vsn} ->
|
|
|
?INFO("Unpacked successfully: ~p.", [Vsn]),
|
|
|
@@ -142,9 +147,10 @@ parse_arguments([VersionStr|Rest], Acc) ->
|
|
|
parse_arguments(Rest, [{version, Version}] ++ Acc).
|
|
|
|
|
|
unpack_release(RelName, TargetNode, Version) ->
|
|
|
+ StartScriptExists = filelib:is_dir(filename:join(["releases", Version, "start.boot"])),
|
|
|
WhichReleases = which_releases(TargetNode),
|
|
|
case proplists:get_value(Version, WhichReleases) of
|
|
|
- undefined ->
|
|
|
+ Res when Res =:= undefined; (Res =:= unpacked andalso not StartScriptExists) ->
|
|
|
%% not installed, so unpack tarball:
|
|
|
%% look for a release package with the intended version in the following order:
|
|
|
%% releases/<relname>-<version>.tar.gz
|
|
|
@@ -159,16 +165,43 @@ unpack_release(RelName, TargetNode, Version) ->
|
|
|
case rpc:call(TargetNode, release_handler, unpack_release,
|
|
|
[ReleasePackageLink], ?TIMEOUT) of
|
|
|
{ok, Vsn} -> {ok, Vsn};
|
|
|
+ {error, {existing_release, Vsn}} ->
|
|
|
+ %% sometimes the user may have removed the release/<vsn> dir
|
|
|
+ %% for an `unpacked` release, then we need to re-unpack it from
|
|
|
+ %% the .tar ball
|
|
|
+ untar_for_unpacked_release(str(RelName), Vsn),
|
|
|
+ {ok, Vsn};
|
|
|
{error, _} = Error -> Error
|
|
|
end
|
|
|
end;
|
|
|
- Other -> Other
|
|
|
+ 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
|
|
|
+untar_for_unpacked_release(RelName, Vsn) ->
|
|
|
+ {ok, Root} = file:get_cwd(),
|
|
|
+ RelDir = filename:join([Root, "releases"]),
|
|
|
+ %% untar the .tar file, so release/<vsn> will be created
|
|
|
+ Tar = filename:join([RelDir, Vsn, RelName ++ ".tar.gz"]),
|
|
|
+ extract_tar(Root, Tar),
|
|
|
+
|
|
|
+ %% create RELEASE file
|
|
|
+ RelFile = filename:join([RelDir, Vsn, RelName ++ ".rel"]),
|
|
|
+ release_handler:create_RELEASES(Root, RelFile),
|
|
|
+
|
|
|
+ %% Clean release
|
|
|
+ _ = file:delete(Tar),
|
|
|
+ _ = file:delete(RelFile).
|
|
|
+
|
|
|
+extract_tar(Cwd, Tar) ->
|
|
|
+ case erl_tar:extract(Tar, [keep_old_files, {cwd, Cwd}, compressed]) of
|
|
|
+ ok -> ok;
|
|
|
+ {error, {Name, Reason}} -> % New erl_tar (R3A).
|
|
|
+ throw({error, {cannot_extract_file, Name, Reason}})
|
|
|
+ end.
|
|
|
+
|
|
|
+%% 1. look for a release package tarball with the provided version:
|
|
|
+%% releases/<relname>-*<version>*.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
|
|
|
@@ -184,47 +217,38 @@ find_and_link_release_package(Version, RelName) ->
|
|
|
%% we've found where the actual release package is located
|
|
|
ReleaseLink = filename:join(["releases", Version,
|
|
|
RelNameStr ++ ".tar.gz"]),
|
|
|
- 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 ->
|
|
|
+ TarBalls = filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.tar.gz"]),
|
|
|
+ case filelib:wildcard(TarBalls) of
|
|
|
+ [] ->
|
|
|
{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)
|
|
|
+ [Filename] when is_list(Filename) ->
|
|
|
+ %% the 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}
|
|
|
+ make_symlink_or_copy(filename:absname(Filename), ReleaseLink),
|
|
|
+ {Filename, ReleaseHandlerPackageLink};
|
|
|
+ Files ->
|
|
|
+ ?ERROR("Found more than one package for version: '~s', "
|
|
|
+ "files: ~p", [Version, Files]),
|
|
|
+ erlang:halt(47)
|
|
|
end.
|
|
|
|
|
|
-first_value(_Fun, []) -> no_value;
|
|
|
-first_value(Fun, [Value | Rest]) ->
|
|
|
- case Fun(Value) of
|
|
|
- false ->
|
|
|
- first_value(Fun, Rest);
|
|
|
- true ->
|
|
|
- {ok, Value}
|
|
|
+make_symlink_or_copy(Filename, ReleaseLink) ->
|
|
|
+ case file:make_symlink(Filename, ReleaseLink) of
|
|
|
+ ok -> ok;
|
|
|
+ {error, eexist} ->
|
|
|
+ ?INFO("Symlink ~p already exists, recreate it", [ReleaseLink]),
|
|
|
+ ok = file:delete(ReleaseLink),
|
|
|
+ make_symlink_or_copy(Filename, ReleaseLink);
|
|
|
+ {error, Reason} when Reason =:= eperm; Reason =:= enotsup ->
|
|
|
+ {ok, _} = file:copy(Filename, ReleaseLink);
|
|
|
+ {error, Reason} ->
|
|
|
+ ?ERROR("Create symlink ~p failed, error: ~p", [ReleaseLink, Reason]),
|
|
|
+ erlang:halt(47)
|
|
|
end.
|
|
|
|
|
|
parse_version(V) when is_list(V) ->
|
|
|
@@ -246,7 +270,7 @@ check_and_install(TargetNode, Vsn) ->
|
|
|
{ok, _OtherVsn, _Desc} ->
|
|
|
ok;
|
|
|
{error, Reason} ->
|
|
|
- ?INFO("ERROR: release_handler:check_install_release failed: ~p.",[Reason]),
|
|
|
+ ?ERROR("Call release_handler:check_install_release failed: ~p.", [Reason]),
|
|
|
erlang:halt(3)
|
|
|
end,
|
|
|
case rpc:call(TargetNode, release_handler, install_release,
|
|
|
@@ -259,18 +283,18 @@ check_and_install(TargetNode, Vsn) ->
|
|
|
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]),
|
|
|
+ ?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",
|
|
|
+ ?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]),
|
|
|
+ ?ERROR("Call release_handler:install_release failed: ~p",[Reason1]),
|
|
|
erlang:halt(4)
|
|
|
end.
|
|
|
|
|
|
@@ -287,8 +311,7 @@ permafy(TargetNode, RelName, Vsn) ->
|
|
|
make_permanent, [Vsn], ?TIMEOUT),
|
|
|
?INFO("Made release permanent: ~p", [Vsn]),
|
|
|
%% upgrade/downgrade the scripts by replacing them
|
|
|
- Scripts = [RelNameStr, RelNameStr ++ "_ctl",
|
|
|
- "nodetool", "install_upgrade.escript"],
|
|
|
+ Scripts = [RelNameStr, RelNameStr++"_ctl", "nodetool", "install_upgrade.escript"],
|
|
|
[{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]),
|
|
|
filename:join(["bin", File]))
|
|
|
|| File <- Scripts],
|
|
|
@@ -302,7 +325,7 @@ remove_release(TargetNode, Vsn) ->
|
|
|
?INFO("Uninstalled Release: ~s", [Vsn]),
|
|
|
ok;
|
|
|
{error, Reason} ->
|
|
|
- ?INFO("ERROR: release_handler:remove_release failed: ~p", [Reason]),
|
|
|
+ ?ERROR("Call release_handler:remove_release failed: ~p", [Reason]),
|
|
|
erlang:halt(3)
|
|
|
end.
|
|
|
|
|
|
@@ -330,8 +353,7 @@ 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
|
|
|
+ case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
|
|
|
{true, pong} ->
|
|
|
ok;
|
|
|
{_, pang} ->
|
|
|
@@ -344,7 +366,7 @@ start_distribution(TargetNode, NameTypeArg, Cookie) ->
|
|
|
|
|
|
make_script_node(Node) ->
|
|
|
[Name, Host] = string:tokens(atom_to_list(Node), "@"),
|
|
|
- list_to_atom(lists:concat(["remsh_" ++ Name, "_upgrader_", os:getpid(), "@", Host])).
|
|
|
+ list_to_atom(lists:concat(["remsh_", Name, "_upgrader_", os:getpid(), "@", Host])).
|
|
|
|
|
|
%% get name type from arg
|
|
|
get_name_type(NameTypeArg) ->
|
|
|
@@ -359,3 +381,35 @@ erts_vsn() ->
|
|
|
{ok, Str} = file:read_file(filename:join(["releases", "start_erl.data"])),
|
|
|
[ErtsVsn, _] = string:tokens(binary_to_list(Str), " "),
|
|
|
ErtsVsn.
|
|
|
+
|
|
|
+validate_target_version(TargetVersion, TargetNode) ->
|
|
|
+ CurrentVersion = current_release_version(TargetNode),
|
|
|
+ case {get_major_minor_vsn(CurrentVersion), get_major_minor_vsn(TargetVersion)} of
|
|
|
+ {{Major, Minor}, {Major, Minor}} -> ok;
|
|
|
+ _ ->
|
|
|
+ ?ERROR("Cannot upgrade/downgrade from '~s' to '~s'~n"
|
|
|
+ "Hot upgrade is only supported between patch releases.",
|
|
|
+ [CurrentVersion, TargetVersion]),
|
|
|
+ erlang:halt(48)
|
|
|
+ end.
|
|
|
+
|
|
|
+get_major_minor_vsn(Version) ->
|
|
|
+ Parts = parse_semver(Version),
|
|
|
+ [Major | Rem0] = Parts,
|
|
|
+ [Minor | _Rem1] = Rem0,
|
|
|
+ {Major, Minor}.
|
|
|
+
|
|
|
+parse_semver(Version) ->
|
|
|
+ case re:run(Version, ?SEMVER_RE, [{capture, all_but_first, binary}]) of
|
|
|
+ {match, Parts} -> Parts;
|
|
|
+ nomatch ->
|
|
|
+ ?ERROR("Invalid semantic version: '~s'~n", [Version]),
|
|
|
+ erlang:halt(22)
|
|
|
+ end.
|
|
|
+
|
|
|
+str(A) when is_atom(A) ->
|
|
|
+ atom_to_list(A);
|
|
|
+str(A) when is_binary(A) ->
|
|
|
+ binary_to_list(A);
|
|
|
+str(A) when is_list(A) ->
|
|
|
+ (A).
|