| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- #!/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(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
- {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),
- validate_target_version(Version, TargetNode),
- 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) ->
- StartScriptExists = filelib:is_dir(filename:join(["releases", Version, "start.boot"])),
- WhichReleases = which_releases(TargetNode),
- case proplists:get_value(Version, WhichReleases) of
- 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
- %% 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, {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
- end.
- 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
- %% 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"]),
- TarBalls = filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.tar.gz"]),
- case filelib:wildcard(TarBalls) of
- [] ->
- {undefined, undefined};
- [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
- 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.
- 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) ->
- hd(string:tokens(V,"/")).
- check_and_install(TargetNode, Vsn) ->
- %% 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} ->
- ?ERROR("Call 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]),
- ?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}} ->
- ?ERROR("Unable to install '~s' - old processes still running code from module ~p",
- [Vsn, Mod]),
- erlang:halt(3);
- {error, Reason1} ->
- ?ERROR("Call 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", "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} ->
- ?ERROR("Call 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:hidden_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(["remsh_", 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.
- 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).
|