aboutsummaryrefslogtreecommitdiffstats
path: root/priv/templates/install_upgrade_escript
blob: c2aa06cb0308606e2e9626d0728f8402692ea74a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#!/usr/bin/env escript
%%! -noshell -noinput
%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ft=erlang ts=4 sw=4 et
%% the following is so we have access to ?MODULE
-mode(compile).

-export([install/2,
         unpack/2,
         upgrade/2,
         downgrade/2,
         uninstall/2,
         versions/2]).

-define(TIMEOUT, 300000).
-define(INFO(Fmt,Args), io:format(Fmt,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),
    Command = list_to_atom(Command0),
    %% convert arguments into a proplist
    Opts = parse_arguments(CommandArgs),
    %% invoke the command passed as argument
    erlang:apply(?MODULE, Command, [DistInfo, Opts]);
main(Args) ->
    ?INFO("unknown args: ~p\n", [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~n", [Vsn]);
        old ->
            %% no need to unpack, has been installed previously
            ?INFO("Release ~s is marked old.~n",[Version]);
        unpacked ->
            ?INFO("Release ~s is already unpacked.~n",[Version]);
        current ->
            ?INFO("Release ~s is already installed and current.~n",[Version]);
        permanent ->
            ?INFO("Release ~s is already installed and set permanent.~n",[Version]);
        {error, Reason} ->
            ?INFO("Unpack failed: ~p~n",[Reason]),
            print_existing_versions(TargetNode),
            erlang:halt(2)
    end;
unpack(_, Args) ->
    ?INFO("unpack: unknown args ~p\n", [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~n", [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.~n",[Version]),
            check_and_install(TargetNode, Version),
            maybe_permafy(TargetNode, RelName, Version, Opts);
        unpacked ->
            ?INFO("Release ~s is already unpacked, now installing.~n",[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.~n",
                        [Version]),
                    permafy(TargetNode, RelName, Version);
                false ->
                    ?INFO("Release ~s is already installed and current.~n",
                        [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.~n",
                        [Version]);
                CurrentVersion ->
                    ?INFO("Release ~s is the currently running version.~n",
                        [CurrentVersion]),
                    check_and_install(TargetNode, Version),
                    maybe_permafy(TargetNode, RelName, Version, Opts)
            end;
        {error, Reason} ->
            ?INFO("Unpack failed: ~p~n",[Reason]),
            print_existing_versions(TargetNode),
            erlang:halt(2)
    end;
install(_, Args) ->
    ?INFO("install: unknown args ~p\n", [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.~n", [Version]);
        old ->
            ?INFO("Release ~s is marked old, uninstalling it.~n", [Version]),
            remove_release(TargetNode, Version);
        unpacked ->
            ?INFO("Release ~s is marked unpacked, uninstalling it~n",
                [Version]),
            remove_release(TargetNode, Version);
        current ->
            ?INFO("Uninstall failed: Release ~s is marked current.~n", [Version]),
            erlang:halt(2);
        permanent ->
            ?INFO("Uninstall failed: Release ~s is running.~n", [Version]),
            erlang:halt(2)
    end;
uninstall(_, Args) ->
    ?INFO("uninstall: unknown args ~p\n", [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~n",
                        [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"]),
    case first_value(fun filelib:is_file/1,
                     [filename:join(["releases",
                                     RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
                      filename:join(["releases", Version,
                                     RelNameStr ++ "-" ++ Version ++ ".tar.gz"]),
                      filename:join(["releases", Version,
                                     RelNameStr ++ ".tar.gz"])]) 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
            ok = file:make_symlink(filename:absname(Filename), ReleaseLink),
            {Filename, ReleaseHandlerPackageLink}
    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) ->
    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~n",[Reason]),
            erlang:halt(3)
    end,
    case rpc:call(TargetNode, release_handler, install_release,
                  [Vsn, [{update_paths, true}]], ?TIMEOUT) of
        {ok, _, _} ->
            ?INFO("Installed Release: ~s~n", [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.~n", [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~n",
                [Vsn, Mod]),
            erlang:halt(3);
        {error, Reason1} ->
            ?INFO("ERROR: release_handler:install_release failed: ~p~n",[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) ->
    ok = rpc:call(TargetNode, release_handler,
                  make_permanent, [Vsn], ?TIMEOUT),
    file:copy(filename:join(["bin", atom_to_list(RelName)++"-"++Vsn]),
              filename:join(["bin", atom_to_list(RelName)])),
    ?INFO("Made release permanent: ~p~n", [Vsn]),
    ok.

remove_release(TargetNode, Vsn) ->
    case rpc:call(TargetNode, release_handler, remove_release, [Vsn], ?TIMEOUT) of
        ok ->
            ?INFO("Uninstalled Release: ~s~n", [Vsn]),
            ok;
        {error, Reason} ->
            ?INFO("ERROR: release_handler:remove_release failed: ~p~n", [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} ->
            io:format("Node ~p not responding to pings.\n", [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.