From b942fde8367be72738cd73651d5b5500f8d442ae Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 24 May 2022 15:05:08 +0800 Subject: [PATCH 1/6] fix: porting the install_upgrade.escript from v4.4 --- bin/install_upgrade.escript | 125 ++++++++++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/bin/install_upgrade.escript b/bin/install_upgrade.escript index 8adced930..14d90156c 100755 --- a/bin/install_upgrade.escript +++ b/bin/install_upgrade.escript @@ -5,6 +5,9 @@ -define(TIMEOUT, 300000). -define(INFO(Fmt,Args), io:format(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 +55,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 +146,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/-.tar.gz @@ -159,10 +164,39 @@ 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/ 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. + +untar_for_unpacked_release(RelName, Vsn) -> + {ok, Root} = file:get_cwd(), + RelDir = filename:join([Root, "releases"]), + %% untar the .tar file, so release/ 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 in the following order: @@ -184,6 +218,7 @@ 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"]), + ok = unpack_zipballs(RelNameStr, Version), TarBalls = [ filename:join(["releases", RelNameStr ++ "-" ++ Version ++ ".tar.gz"]), @@ -209,15 +244,46 @@ find_and_link_release_package(Version, RelName) -> 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, + make_symlink_or_copy(filename:absname(Filename), ReleaseLink), {Filename, ReleaseHandlerPackageLink} 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} -> + ?INFO("create symlink ~p failed", [ReleaseLink]), + error({Reason, ReleaseLink}) + end. + +unpack_zipballs(RelNameStr, Version) -> + {ok, Cwd} = file:get_cwd(), + try + GzFile = filename:absname(filename:join(["releases", RelNameStr ++ "-" ++ Version ++ ".tar.gz"])), + ZipFiles = filelib:wildcard(filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.zip"])), + ?INFO("unzip ~p", [ZipFiles]), + lists:foreach( + fun(Zip) -> + 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, + ZipFiles) + after + % restore cwd + ok = file:set_cwd(Cwd) + end. + first_value(_Fun, []) -> no_value; first_value(Fun, [Value | Rest]) -> case Fun(Value) of @@ -231,6 +297,10 @@ 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 @@ -287,8 +357,8 @@ 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", "cuttlefish", "nodetool", + "install_upgrade.escript"], [{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]), filename:join(["bin", File])) || File <- Scripts], @@ -330,8 +400,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 +413,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 +428,33 @@ 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; + _ -> + ?INFO("Cannot upgrade/downgrade to ~s from ~s~n" + "We only support relup between patch versions", + [TargetVersion, CurrentVersion]), + error({relup_not_allowed, unsupported_target_version}) + 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_semver, Version}) + 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). From f036a946a805ffbc9087be93584192e74b57cb8e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 24 May 2022 22:39:29 +0800 Subject: [PATCH 2/6] fix: only support .tar.gz files when hot upgrade --- bin/install_upgrade.escript | 74 ++++++------------------------------- 1 file changed, 11 insertions(+), 63 deletions(-) diff --git a/bin/install_upgrade.escript b/bin/install_upgrade.escript index 14d90156c..b416533ee 100755 --- a/bin/install_upgrade.escript +++ b/bin/install_upgrade.escript @@ -199,10 +199,8 @@ extract_tar(Cwd, Tar) -> throw({error, {cannot_extract_file, Name, Reason}}) end. -%% 1. look for a release package tarball with the provided version in the following order: -%% releases/-.tar.gz -%% releases//-.tar.gz -%% releases//.tar.gz +%% 1. look for a release package tarball with the provided version: +%% releases/-**.tar.gz %% 2. create a symlink from a fixed location (ie. releases//.tar.gz) %% to the release package tarball found in 1. %% 3. return a tuple with the paths to the release package and @@ -218,26 +216,12 @@ 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"]), - 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 -> + 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 (.tar.gz) + [Filename | _] when is_list(Filename) -> + %% the release handler expects a fixed nomenclature (.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 @@ -262,45 +246,10 @@ make_symlink_or_copy(Filename, ReleaseLink) -> error({Reason, ReleaseLink}) end. -unpack_zipballs(RelNameStr, Version) -> - {ok, Cwd} = file:get_cwd(), - try - GzFile = filename:absname(filename:join(["releases", RelNameStr ++ "-" ++ Version ++ ".tar.gz"])), - ZipFiles = filelib:wildcard(filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.zip"])), - ?INFO("unzip ~p", [ZipFiles]), - lists:foreach( - fun(Zip) -> - 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, - ZipFiles) - after - % restore cwd - ok = file:set_cwd(Cwd) - end. - -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 @@ -357,8 +306,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", "cuttlefish", "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], @@ -434,9 +382,9 @@ validate_target_version(TargetVersion, TargetNode) -> case {get_major_minor_vsn(CurrentVersion), get_major_minor_vsn(TargetVersion)} of {{Major, Minor}, {Major, Minor}} -> ok; _ -> - ?INFO("Cannot upgrade/downgrade to ~s from ~s~n" - "We only support relup between patch versions", - [TargetVersion, CurrentVersion]), + ?INFO("Cannot upgrade/downgrade from ~s to ~s~n" + "Hot upgrade is only supported between patch releases.", + [CurrentVersion, TargetVersion]), error({relup_not_allowed, unsupported_target_version}) end. From eea9bd90b4bbf0432ea7b5f3c014437d5ac0aec3 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 May 2022 01:58:05 +0800 Subject: [PATCH 3/6] fix: remove the extra level of 'emqx' dir from .tar.gz --- .ci/fvt_tests/relup.lux | 3 ++- .github/workflows/build_packages.yaml | 3 ++- .github/workflows/build_slim_packages.yaml | 3 ++- build | 11 ++++++----- scripts/pkg-tests.sh | 6 ++++-- scripts/update_appup.escript | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.ci/fvt_tests/relup.lux b/.ci/fvt_tests/relup.lux index 4a8894476..45065d4bb 100644 --- a/.ci/fvt_tests/relup.lux +++ b/.ci/fvt_tests/relup.lux @@ -22,7 +22,8 @@ [shell emqx] !cd $PACKAGE_PATH - !tar -zxf ${PROFILE}-$(echo $old_vsn | sed -r 's/[v|e]//g')-*-ubuntu20.04-amd64.tar.gz + mkdir -p emqx + !tar -C emqx -zxf ${PROFILE}-$(echo $old_vsn | sed -r 's/[v|e]//g')-*-ubuntu20.04-amd64.tar.gz ?SH-PROMPT !cd emqx diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 9bcfa1ced..09a443848 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -179,7 +179,8 @@ jobs: working-directory: source run: | pkg_name=$(find _packages/${{ matrix.profile }} -mindepth 1 -maxdepth 1 -iname \*.tar.gz) - tar -zxf $pkg_name + mkdir -p emqx + tar -C emqx -zxf $pkg_name # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index e68c99cf4..f62febf0e 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -197,7 +197,8 @@ jobs: - name: test run: | pkg_name=$(find _packages/${{ matrix.profile }} -mindepth 1 -maxdepth 1 -iname \*.tar.gz) - tar -zxf $pkg_name + mkdir -p emqx + tar -C emqx -zxf $pkg_name # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' diff --git a/build b/build index bd60f1c5e..4aee6d5e8 100755 --- a/build +++ b/build @@ -111,12 +111,12 @@ make_relup() { mkdir -p "${rel_dir}/lib" mkdir -p "${rel_dir}/releases" local name_pattern - name_pattern="${PROFILE}-$(./pkg-vsn.sh "$PROFILE" --vsn_matcher)" + name_pattern="${PROFILE}-$(./pkg-vsn.sh "$PROFILE" --vsn_matcher --long)" local releases=() while read -r tgzfile ; do local base_vsn - base_vsn="$(echo "$tgzfile" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9])?(-[0-9a-f]{8})?" | head -1)" - tar -C "$rel_dir" -zxf ---keep-old-files "$tgzfile" emqx/releases emqx/lib + base_vsn="$(echo "$tgzfile" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9])?(-[0-9a-f]{8})?" | head -1)" + tar -C "$rel_dir" -zxf "$tgzfile" --keep-old-files releases lib releases+=( "$base_vsn" ) done < <("$FIND" _upgrade_base -maxdepth 1 -name "${name_pattern}.tar.gz" -type f) if [ ${#releases[@]} -eq 0 ]; then @@ -183,8 +183,9 @@ make_tgz() { ## for DEB and RPM packages the dependencies are resoved by yum and apt cp_dyn_libs "${tard}/emqx" ## create tar after change dir (for windows) - pushd "${tard}" >/dev/null - tar -czf "${target_name}" emqx + ## to avoid creating an extra level of 'emqx' dir in the .tar.gz file + pushd "${tard}/emqx" >/dev/null + tar -zcf "../${target_name}" * popd >/dev/null mv "${tard}/${target_name}" "${target}" case "$SYSTEM" in diff --git a/scripts/pkg-tests.sh b/scripts/pkg-tests.sh index 81800427c..5fce0f71e 100755 --- a/scripts/pkg-tests.sh +++ b/scripts/pkg-tests.sh @@ -88,7 +88,8 @@ emqx_test(){ local packagename="${PACKAGE_FILE_NAME}" case "$PKG_SUFFIX" in "tar.gz") - tar -zxf "${PACKAGE_PATH}/${packagename}" + mkdir -p "${PACKAGE_PATH}/emqx" + tar -C "${PACKAGE_PATH}/emqx" -zxf "${PACKAGE_PATH}/${packagename}" export EMQX_ZONES__DEFAULT__MQTT__SERVER_KEEPALIVE=60 export EMQX_MQTT__MAX_TOPIC_ALIAS=10 export EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug @@ -231,7 +232,8 @@ relup_test(){ pattern="$EMQX_NAME-$("$CODE_PATH"/pkg-vsn.sh "${EMQX_NAME}" --long --vsn_matcher)" while read -r pkg; do packagename=$(basename "${pkg}") - tar -zxf "$packagename" + mkdir -p emqx + tar -C emqx -zxf "$packagename" if ! ./emqx/bin/emqx start; then cat emqx/log/erlang.log.1 || true cat emqx/log/emqx.log.1 || true diff --git a/scripts/update_appup.escript b/scripts/update_appup.escript index f2b6235e9..21fc9bb34 100755 --- a/scripts/update_appup.escript +++ b/scripts/update_appup.escript @@ -173,7 +173,7 @@ download_prev_release(Tag, #{binary_rel_url := {ok, URL0}, clone_url := Repo}) - Filename = filename:join(BaseDir, Dir), Script = "mkdir -p ${OUTFILE} && wget -c -O ${OUTFILE}.tar.gz ${URL} && - tar -zxf ${OUTFILE} ${OUTFILE}.tar.gz", + tar -zxf ${OUTFILE}.tar.gz -C ${OUTFILE}", Env = [{"TAG", Tag}, {"OUTFILE", Filename}, {"URL", URL}], bash(Script, Env), {ok, Filename}. From b9a9da3208dc30c6986abc5d6c45b50162be1438 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 May 2022 10:28:59 +0800 Subject: [PATCH 4/6] fix(relup): improve the error code and log messages --- bin/install_upgrade.escript | 39 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/bin/install_upgrade.escript b/bin/install_upgrade.escript index b416533ee..4ab4947c0 100755 --- a/bin/install_upgrade.escript +++ b/bin/install_upgrade.escript @@ -4,7 +4,8 @@ %% 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). @@ -220,7 +221,7 @@ find_and_link_release_package(Version, RelName) -> case filelib:wildcard(TarBalls) of [] -> {undefined, undefined}; - [Filename | _] when is_list(Filename) -> + [Filename] when is_list(Filename) -> %% the release handler expects a fixed nomenclature (.tar.gz) %% so give it just that by creating a symlink to the tarball %% we found. @@ -229,21 +230,25 @@ find_and_link_release_package(Version, RelName) -> %% 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} + {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]), + ?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} -> - ?INFO("create symlink ~p failed", [ReleaseLink]), - error({Reason, ReleaseLink}) + ?ERROR("Create symlink ~p failed, error: ~p", [ReleaseLink, Reason]), + erlang:halt(47) end. parse_version(V) when is_list(V) -> @@ -265,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, @@ -278,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. @@ -320,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. @@ -382,10 +387,10 @@ validate_target_version(TargetVersion, TargetNode) -> case {get_major_minor_vsn(CurrentVersion), get_major_minor_vsn(TargetVersion)} of {{Major, Minor}, {Major, Minor}} -> ok; _ -> - ?INFO("Cannot upgrade/downgrade from ~s to ~s~n" - "Hot upgrade is only supported between patch releases.", - [CurrentVersion, TargetVersion]), - error({relup_not_allowed, unsupported_target_version}) + ?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) -> @@ -397,7 +402,9 @@ get_major_minor_vsn(Version) -> parse_semver(Version) -> case re:run(Version, ?SEMVER_RE, [{capture, all_but_first, binary}]) of {match, Parts} -> Parts; - nomatch -> error({invalid_semver, Version}) + nomatch -> + ?ERROR("Invalid semantic version: '~s'~n", [Version]), + erlang:halt(22) end. str(A) when is_atom(A) -> From 40ad23ee74f3fc4ebf811a4c1486a9a34e7aa0b9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 May 2022 10:24:02 +0800 Subject: [PATCH 5/6] fix(relup): create tmp dir to preserve current files --- build | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/build b/build index 4aee6d5e8..1dc75e751 100755 --- a/build +++ b/build @@ -108,15 +108,19 @@ make_elixir_rel() { ## extract previous version .tar.gz files to _build/$PROFILE/rel/emqx before making relup make_relup() { local rel_dir="_build/$PROFILE/rel/emqx" - mkdir -p "${rel_dir}/lib" - mkdir -p "${rel_dir}/releases" local name_pattern name_pattern="${PROFILE}-$(./pkg-vsn.sh "$PROFILE" --vsn_matcher --long)" local releases=() while read -r tgzfile ; do local base_vsn base_vsn="$(echo "$tgzfile" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9])?(-[0-9a-f]{8})?" | head -1)" - tar -C "$rel_dir" -zxf "$tgzfile" --keep-old-files releases lib + ## we have to create tmp dir to untar old tgz, as `tar --skip-old-files` is not supported on all plantforms + local tmp_dir + tmp_dir="$(mktemp -d -t emqx.XXXXXXX)" + tar -C "$tmp_dir" -zxf "$tgzfile" + cp -npr "$tmp_dir/releases"/* "${rel_dir}/releases/" || true + cp -npr "$tmp_dir/lib"/* "${rel_dir}/lib/" || true + rm -rf "$tmp_dir" releases+=( "$base_vsn" ) done < <("$FIND" _upgrade_base -maxdepth 1 -name "${name_pattern}.tar.gz" -type f) if [ ${#releases[@]} -eq 0 ]; then @@ -185,7 +189,7 @@ make_tgz() { ## create tar after change dir (for windows) ## to avoid creating an extra level of 'emqx' dir in the .tar.gz file pushd "${tard}/emqx" >/dev/null - tar -zcf "../${target_name}" * + tar -zcf "../${target_name}" -- * popd >/dev/null mv "${tard}/${target_name}" "${target}" case "$SYSTEM" in From 3d7cab4288c160c404fbab66f5385d8b8631a787 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 May 2022 12:09:58 +0800 Subject: [PATCH 6/6] fix: porting emqx_relup.erl from 4.4 --- apps/emqx/src/emqx_relup.erl | 42 ++++++++++++ build | 8 ++- scripts/inject-relup.escript | 121 +++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 apps/emqx/src/emqx_relup.erl create mode 100755 scripts/inject-relup.escript diff --git a/apps/emqx/src/emqx_relup.erl b/apps/emqx/src/emqx_relup.erl new file mode 100644 index 000000000..82ba90b4f --- /dev/null +++ b/apps/emqx/src/emqx_relup.erl @@ -0,0 +1,42 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_relup). + +%% NOTE: DO NOT remove this `-include`. +%% We use this to force this module to be upgraded every release. +-include("emqx_release.hrl"). + +-export([ + post_release_upgrade/2, + post_release_downgrade/2 +]). + +-define(INFO(FORMAT), io:format("[emqx_relup] " ++ FORMAT ++ "~n")). +-define(INFO(FORMAT, ARGS), io:format("[emqx_relup] " ++ FORMAT ++ "~n", ARGS)). + +%% What to do after upgraded from an old release vsn. +post_release_upgrade(FromRelVsn, _) -> + ?INFO("emqx has been upgraded from ~s to ~s!", [FromRelVsn, emqx_release:version()]), + reload_components(). + +%% What to do after downgraded to an old release vsn. +post_release_downgrade(ToRelVsn, _) -> + ?INFO("emqx has been downgraded from ~s to ~s!", [emqx_release:version(), ToRelVsn]), + reload_components(). + +reload_components() -> + ok. diff --git a/build b/build index 1dc75e751..b48b943ee 100755 --- a/build +++ b/build @@ -175,18 +175,20 @@ make_tgz() { target="${pkgpath}/${target_name}" src_tarball="${relpath}/emqx-${PKG_VSN}.tar.gz" - tard="tmp/emqx_untar_${PKG_VSN}" - rm -rf "${tard}" + tard="$(mktemp -d -t emqx.XXXXXXX)" mkdir -p "${tard}/emqx" mkdir -p "${pkgpath}" if [ ! -f "$src_tarball" ]; then log "ERROR: $src_tarball is not found" fi tar zxf "${src_tarball}" -C "${tard}/emqx" + if [ -f "${tard}/emqx/releases/${PKG_VSN}/relup" ]; then + ./scripts/inject-relup.escript "${tard}/emqx/releases/${PKG_VSN}/relup" + fi ## try to be portable for tar.gz packages. ## for DEB and RPM packages the dependencies are resoved by yum and apt cp_dyn_libs "${tard}/emqx" - ## create tar after change dir (for windows) + ## create tar after change dir ## to avoid creating an extra level of 'emqx' dir in the .tar.gz file pushd "${tard}/emqx" >/dev/null tar -zcf "../${target_name}" -- * diff --git a/scripts/inject-relup.escript b/scripts/inject-relup.escript new file mode 100755 index 000000000..b7d905979 --- /dev/null +++ b/scripts/inject-relup.escript @@ -0,0 +1,121 @@ +#!/usr/bin/env escript + +%% This script injects implicit relup instructions for emqx applications. + +-mode(compile). + +-define(ERROR(FORMAT, ARGS), io:format(standard_error, "[inject-relup] " ++ FORMAT ++ "~n", ARGS)). +-define(INFO(FORMAT, ARGS), io:format(user, "[inject-relup] " ++ FORMAT ++ "~n", ARGS)). + +usage() -> + "Usage: " ++ escript:script_name() ++ " ". + +main([RelupFile]) -> + ok = inject_relup_file(RelupFile); +main(_Args) -> + ?ERROR("~s", [usage()]), + erlang:halt(1). + +inject_relup_file(File) -> + case file:script(File) of + {ok, {CurrRelVsn, UpVsnRUs, DnVsnRUs}} -> + ?INFO("Injecting instructions to: ~p", [File]), + UpdatedContent = {CurrRelVsn, + inject_relup_instrs(up, UpVsnRUs), + inject_relup_instrs(down, DnVsnRUs)}, + file:write_file(File, term_to_text(UpdatedContent)); + {ok, _BadFormat} -> + ?ERROR("Bad formatted relup file: ~p", [File]), + error({bad_relup_format, File}); + {error, enoent} -> + ?INFO("Cannot find relup file: ~p", [File]), + ok; + {error, Reason} -> + ?ERROR("Read relup file ~p failed: ~p", [File, Reason]), + error({read_relup_error, Reason}) + end. + +inject_relup_instrs(Type, RUs) -> + lists:map(fun({Vsn, Desc, Instrs}) -> + {Vsn, Desc, append_emqx_relup_instrs(Type, Vsn, Instrs)} + end, RUs). + +append_emqx_relup_instrs(up, FromRelVsn, Instrs0) -> + {{UpExtra, _}, Instrs1} = filter_and_check_instrs(up, Instrs0), + Instrs1 ++ + [ {load, {emqx_release, brutal_purge, soft_purge}} + , {load, {emqx_relup, brutal_purge, soft_purge}} + , {apply, {emqx_relup, post_release_upgrade, [FromRelVsn, UpExtra]}} + ]; + +append_emqx_relup_instrs(down, ToRelVsn, Instrs0) -> + {{_, DnExtra}, Instrs1} = filter_and_check_instrs(down, Instrs0), + %% NOTE: When downgrading, we apply emqx_relup:post_release_downgrade/2 before reloading + %% or removing the emqx_relup module. + Instrs2 = Instrs1 ++ + [ {load, {emqx_release, brutal_purge, soft_purge}} + , {apply, {emqx_relup, post_release_downgrade, [ToRelVsn, DnExtra]}} + , {load, {emqx_relup, brutal_purge, soft_purge}} + ], + Instrs2. + +filter_and_check_instrs(Type, Instrs) -> + case filter_fetch_emqx_mods_and_extra(Instrs) of + {_, DnExtra, _, _} when Type =:= up, DnExtra =/= undefined -> + ?ERROR("Got '{apply,{emqx_relup,post_release_downgrade,[_,Extra]}}'" + " from the upgrade instruction list, should be 'post_release_upgrade'", []), + error({instruction_not_found, load_object_code}); + {UpExtra, _, _, _} when Type =:= down, UpExtra =/= undefined -> + ?ERROR("Got '{apply,{emqx_relup,post_release_upgrade,[_,Extra]}}'" + " from the downgrade instruction list, should be 'post_release_downgrade'", []), + error({instruction_not_found, load_object_code}); + {_, _, [], _} -> + ?ERROR("Cannot find any 'load_object_code' instructions for app emqx", []), + error({instruction_not_found, load_object_code}); + {UpExtra, DnExtra, EmqxMods, RemainInstrs} -> + assert_mandatory_modules(Type, EmqxMods), + {{UpExtra, DnExtra}, RemainInstrs} + end. + +filter_fetch_emqx_mods_and_extra(Instrs) -> + lists:foldl(fun do_filter_and_get/2, {undefined, undefined, [], []}, Instrs). + +%% collect modules for emqx app +do_filter_and_get({load_object_code, {emqx, _AppVsn, Mods}} = Instr, + {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods ++ Mods, RemainInstrs ++ [Instr]}; +%% remove 'load' instrs for emqx_relup and emqx_release +do_filter_and_get({load, {Mod, _, _}}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) + when Mod =:= emqx_relup; Mod =:= emqx_release -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'remove' and 'purge' instrs for emqx_relup +do_filter_and_get({remove, {emqx_relup, _, _}}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +do_filter_and_get({purge, [emqx_relup]}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'apply' instrs for upgrade, and collect the 'Extra' parameter +do_filter_and_get({apply, {emqx_relup, post_release_upgrade, [_, UpExtra0]}}, + {_, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra0, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'apply' instrs for downgrade, and collect the 'Extra' parameter +do_filter_and_get({apply, {emqx_relup, post_release_downgrade, [_, DnExtra0]}}, + {UpExtra, _, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra0, EmqxMods, RemainInstrs}; +%% keep all other instrs unchanged +do_filter_and_get(Instr, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs ++ [Instr]}. + +assert_mandatory_modules(_, Mods) -> + MandInstrs = [{load_module,emqx_release,brutal_purge,soft_purge,[]}, + {load_module,emqx_relup}], + assert(lists:member(emqx_relup, Mods) andalso lists:member(emqx_release, Mods), + "The following instructions are mandatory in every clause of the emqx.appup.src: ~p", [MandInstrs]). + +assert(true, _, _) -> + ok; +assert(false, Msg, Args) -> + ?ERROR(Msg, Args), + error(assert_failed). + +term_to_text(Term) -> + io_lib:format("~p.", [Term]).