1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2014-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%-----------------------------------------------------------------
21%% EXPERIMENTAL support for testing of upgrade.
22%%
23%% This is a library module containing support for test of release
24%% related activities in one or more applications. Currenty it
25%% supports upgrade only.
26%%
27%% == Configuration ==
28%%
29%% In order to find version numbers of applications to upgrade from,
30%% ct_release_test needs to access and start old OTP
31%% releases. A `common_test' configuration file can be used for
32%% specifying the location of such releases, for example:
33%%
34%% ```
35%% %% old-rels.cfg
36%% {otp_releases,[{r16b,"/path/to/R16B03-1/bin/erl"},
37%% 	       {'17',"/path/to/17.3/bin/erl"}]}.'''
38%%
39%% The configuration file should preferably point out the latest patch
40%% level on each major release.
41%%
42%% If no such configuration file is given, init/1 will return
43%% {skip,Reason} and any attempt at running upgrade/4
44%% will fail.
45%%
46%% == Callback functions ==
47%%
48%% The following functions should be exported from a ct_release_test
49%% callback module.
50%%
51%% All callback functions are called on the node where the upgrade is
52%% executed.
53%%
54%%   Module:upgrade_init(CtData,State) -> NewState
55%%   Types:
56%%
57%%     CtData = ct_data()
58%%     State = NewState = cb_state()
59%%
60%%     Initialyze system before upgrade test starts.
61%%
62%%     This function is called before the upgrade is started. All
63%%     applications given in upgrade/4 are already started by
64%%     the boot script, so this callback is intended for additional
65%%     initialization, if necessary.
66%%
67%%     CtData is an opaque data structure which shall be used
68%%     in any call to ct_release_test inside the callback.
69%%
70%%     Example:
71%%
72%% upgrade_init(CtData,State) ->
73%%     {ok,{FromVsn,ToVsn}} = ct_release_test:get_app_vsns(CtData,myapp),
74%%     open_connection(State).
75%%
76%%   Module:upgrade_upgraded(CtData,State) -> NewState
77%%   Types:
78%%
79%%     CtData = ct_data()
80%%     State = NewState = cb_state()
81%%
82%%     Check that upgrade was successful.
83%%
84%%     This function is called after the release_handler has
85%%     successfully unpacked and installed the new release, and it has
86%%     been made permanent. It allows application specific checks to
87%%     ensure that the upgrade was successful.
88%%
89%%     CtData is an opaque data structure which shall be used
90%%     in any call to ct_release_test inside the callback.
91%%
92%%     Example:
93%%
94%% upgrade_upgraded(CtData,State) ->
95%%     check_connection_still_open(State).
96%%
97%%   Module:upgrade_downgraded(CtData,State) -> NewState
98%%   Types:
99%%
100%%     CtData = ct_data()
101%%     State = NewState = cb_state()
102%%
103%%     Check that downgrade was successful.
104%%
105%%     This function is called after the release_handler has
106%%     successfully re-installed the original release, and it has been
107%%     made permanent. It allows application specific checks to ensure
108%%     that the downgrade was successful.
109%%
110%%     CtData is an opaque data structure which shall be used
111%%     in any call to ct_release_test inside the callback.
112%%
113%%     Example:
114%%
115%% upgrade_downgraded(CtData,State) ->
116%%     check_connection_closed(State).
117%%-----------------------------------------------------------------
118-module(ct_release_test).
119
120-export([init/1, upgrade/4, cleanup/1, get_app_vsns/2, get_appup/2]).
121
122-include_lib("kernel/include/file.hrl").
123
124%%-----------------------------------------------------------------
125-define(testnode, 'ct_release_test-upgrade').
126-define(exclude_apps, [dialyzer]). % never include these apps
127
128%%-----------------------------------------------------------------
129-record(ct_data, {from,to}).
130
131%%-----------------------------------------------------------------
132-type config() :: [{atom(),term()}].
133-type cb_state() :: term().
134-opaque ct_data() :: #ct_data{}.
135-export_type([ct_data/0]).
136
137-callback upgrade_init(ct_data(),cb_state()) -> cb_state().
138-callback upgrade_upgraded(ct_data(),cb_state()) -> cb_state().
139-callback upgrade_downgraded(ct_data(),cb_state()) -> cb_state().
140
141%%-----------------------------------------------------------------
142-spec init(Config) -> Result when
143      Config :: config(),
144      Result :: config() | SkipOrFail,
145      SkipOrFail :: {skip,Reason} | {fail,Reason}.
146%% Initialize ct_release_test.
147%%
148%% This function can be called from any of the
149%% `init_per_*' functions in the test suite. It updates
150%% the given `Config' with data that will be
151%% used by future calls to other functions in this module. The
152%% returned configuration must therefore also be returned from
153%% the calling `init_per_*'.
154%%
155%% If the initialization fails, e.g. if a required release
156%% cannot be found, the function returns `{skip,Reason}'. In
157%% this case the other test support functions in this mudule
158%% cannot be used.
159%%
160%% Example:
161%%
162%% init_per_suite(Config) ->
163%%     ct_release_test:init(Config).
164%%
165init(Config) ->
166    try init_upgrade_test() of
167	{Major,Minor} ->
168	    [{release_test,[{major,Major},{minor,Minor}]} | Config]
169    catch throw:Thrown ->
170	    Thrown
171    end.
172
173%%-----------------------------------------------------------------
174-spec upgrade(App,Level,Callback,Config) -> any() when
175      App :: atom(),
176      Level :: minor | major,
177      Callback :: {module(),InitState},
178      InitState :: cb_state(),
179      Config :: config();
180	     (Apps,Level,Callback,Config) -> any() when
181      Apps :: [App],
182      App :: atom(),
183      Level :: minor | major,
184      Callback :: {module(),InitState},
185      InitState :: cb_state(),
186      Config :: config().
187%% Test upgrade of the given application(s).
188%%
189%% This function can be called from a test case. It requires that
190%% `Config' has been initialized by calling
191%% init/1 prior to this, for example from `init_per_suite/1'.
192%%
193%% Upgrade tests are performed as follows:
194%%
195%%   Figure out which OTP release to test upgrade
196%%     from. Start a node running that release and find the
197%%     application versions on that node. Terminate the
198%%     node.
199%%   Figure out all dependencies for the applications under
200%%     test.
201%%   Create a release containing the core
202%%     applications `kernel', `stdlib' and `sasl'
203%%     in addition to the application(s) under test and all
204%%     dependencies of these. The versions of the applications
205%%     under test will be the ones found on the OTP release to
206%%     upgrade from. The versions of all other applications will
207%%     be those found on the current node, i.e. the common_test
208%%     node. This is the "From"-release.
209%%   Create another release containing the same
210%%     applications as in the previous step, but with all
211%%     application versions taken from the current node. This is
212%%     the "To"-release.
213%%   Install the "From"-release and start a new node
214%%     running this release.
215%%   Perform the upgrade test and allow customized
216%%     control by using callbacks:
217%%       Callback: `upgrade_init/2'
218%%       Unpack the new release
219%%       Install the new release
220%%       Callback: `upgrade_upgraded/2'
221%%       Install the original release
222%%       Callback: `upgrade_downgraded/2'
223%%
224%% `App' or `Apps'
225%% specifies the applications under test, i.e. the applications
226%% which shall be upgraded. All other applications that are
227%% included have the same releases in the "From"- and
228%% "To"-releases and will therefore not be upgraded.
229%%
230%% `Level' specifies which OTP release to
231%% pick the "From" versions from.
232%%   major
233%%   From verions are picked from the previous major
234%%     release. For example, if the test is run on an OTP-17
235%%     node, ct_release_test will pick the application
236%%     "From" versions from an OTP installation running OTP
237%%     R16B.
238%%
239%%   minor
240%%   From verions are picked from the current major
241%%     release. For example, if the test is run on an OTP-17
242%%     node, ct_release_test will pick the application
243%%     "From" versions from an OTP installation running an
244%%     earlier patch level of OTP-17.
245%%
246%% The application "To" versions are allways picked from the
247%% current node, i.e. the common_test node.
248%%
249%% `Callback' specifies the module (normally the
250%% test suite) which implements the Callback functions, and
251%% the initial value of the `State' variable used in these
252%% functions.
253%%
254%% `Config' is the input argument received
255%% in the test case function.
256%%
257%% Example:
258%%
259%% minor_upgrade(Config) ->
260%%     ct_release_test:upgrade(ssl,minor,{?MODULE,[]},Config).
261%%
262upgrade(App,Level,Callback,Config) when is_atom(App) ->
263    upgrade([App],Level,Callback,Config);
264upgrade(Apps,Level,Callback,Config) ->
265    Dir = proplists:get_value(priv_dir,Config),
266    CreateDir = filename:join([Dir,Level,create]),
267    InstallDir = filename:join([Dir,Level,install]),
268    ok = filelib:ensure_dir(filename:join(CreateDir,"*")),
269    ok = filelib:ensure_dir(filename:join(InstallDir,"*")),
270    try upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) of
271	ok ->
272	    %%rm_rf(CreateDir),
273	    Tars = filelib:wildcard(filename:join(CreateDir,"*.tar.gz")),
274	    _ = [file:delete(Tar) || Tar <- Tars],
275	    rm_rf(InstallDir),
276	    ok
277    catch throw:{fail,Reason} ->
278	    ct:fail(Reason);
279	  throw:{skip,Reason} ->
280	    rm_rf(CreateDir),
281	    rm_rf(InstallDir),
282	    {skip,Reason}
283    after
284	%% Brutally kill all nodes that erroneously survived the test.
285	%% Note, we will not reach this if the test fails with a
286	%% timetrap timeout in the test suite! Thus we can have
287	%% hanging nodes...
288	Nodes = lists:filter(fun(Node) ->
289				     case atom_to_list(Node) of
290					 "ct_release_test-" ++_ -> true;
291					 _ -> false
292				     end
293			     end,
294			     nodes()),
295	[rpc:call(Node,erlang,halt,[]) || Node <- Nodes]
296    end.
297
298%%-----------------------------------------------------------------
299-spec cleanup(Config) -> Result when
300      Config :: config(),
301      Result :: config().
302%% Clean up after tests.
303%%
304%% This function shall be called from the `end_per_*' function
305%% complementing the `init_per_*' function where init/1
306%% is called.
307%%
308%% It cleans up after the test, for example kills hanging
309%% nodes.
310%%
311%% Example:
312%%
313%% end_per_suite(Config) ->
314%%     ct_release_test:cleanup(Config).
315%%
316cleanup(Config) ->
317    AllNodes = [node_name(?testnode)|nodes()],
318    Nodes = lists:filter(fun(Node) ->
319				 case atom_to_list(Node) of
320				     "ct_release_test-" ++_ -> true;
321				     _ -> false
322				 end
323			 end,
324			 AllNodes),
325    _ = [rpc:call(Node,erlang,halt,[]) || Node <- Nodes],
326    Config.
327
328%%-----------------------------------------------------------------
329-spec get_app_vsns(CtData,App) -> {ok,{From,To}} | {error,Reason} when
330      CtData :: ct_data(),
331      App :: atom(),
332      From :: string(),
333      To :: string(),
334      Reason :: {app_not_found,App}.
335%% Get versions involved in this upgrade for the given application.
336%%
337%% This function can be called from inside any of the callback
338%% functions. It returns the old (From) and new (To) versions involved
339%% in the upgrade/downgrade test for the given application.
340%%
341%% CtData must be the first argument received in the
342%% calling callback function - an opaque data structure set by
343%% ct_release_tests.
344get_app_vsns(#ct_data{from=FromApps,to=ToApps},App) ->
345    case {lists:keyfind(App,1,FromApps),lists:keyfind(App,1,ToApps)} of
346	{{App,FromVsn,_},{App,ToVsn,_}} ->
347	    {ok,{FromVsn,ToVsn}};
348	_ ->
349	    {error,{app_not_found,App}}
350    end.
351
352%%-----------------------------------------------------------------
353-spec get_appup(CtData,App) -> {ok,Appup} | {error,Reason} when
354      CtData :: ct_data(),
355      App :: atom(),
356      Appup :: {From,To,Up,Down},
357      From :: string(),
358      To :: string(),
359      Up :: [Instr],
360      Down :: [Instr],
361      Instr :: term(),
362      Reason :: {app_not_found,App} | {vsn_not_found,{App,From}}.
363%% Get appup instructions for the given application.
364%%
365%% This function can be called from inside any of the callback
366%% functions. It reads the appup file for the given application and
367%% returns the instructions for upgrade and downgrade for the versions
368%% in the test.
369%%
370%% CtData must be the first argument received in the
371%% calling callback function - an opaque data structure set by
372%% ct_release_tests.
373%%
374%% See reference manual for appup files for types definitions for the
375%% instructions.
376get_appup(#ct_data{from=FromApps,to=ToApps},App) ->
377    case lists:keyfind(App,1,ToApps) of
378	{App,ToVsn,ToDir} ->
379	    Appup = filename:join([ToDir, "ebin", atom_to_list(App)++".appup"]),
380	    {ok, [{ToVsn, Ups, Downs}]} = file:consult(Appup),
381	    {App,FromVsn,_} = lists:keyfind(App,1,FromApps),
382	    case {systools_relup:appup_search_for_version(FromVsn,Ups),
383		  systools_relup:appup_search_for_version(FromVsn,Downs)} of
384		{{ok,Up},{ok,Down}} ->
385		    {ok,{FromVsn,ToVsn,Up,Down}};
386		_ ->
387		    {error,{vsn_not_found,{App,FromVsn}}}
388	    end;
389	false ->
390	    {error,{app_not_found,App}}
391    end.
392
393%%-----------------------------------------------------------------
394init_upgrade_test() ->
395    %% Check that a real release is running, not e.g. cerl
396    ok = application:ensure_started(sasl),
397    case release_handler:which_releases() of
398	[{_,_,[],_}] ->
399	    %% Fake release, no applications
400	    throw({skip, "Need a real release running to create other releases"});
401	_ ->
402	    Major = init_upgrade_test(major),
403	    Minor = init_upgrade_test(minor),
404	    {Major,Minor}
405    end.
406
407init_upgrade_test(Level) ->
408    {FromVsn,ToVsn} = get_rels(Level),
409    OldRel =
410	case test_server:is_release_available(FromVsn) of
411	    true ->
412		{release,FromVsn};
413	    false ->
414		case ct:get_config({otp_releases,list_to_atom(FromVsn)}) of
415		    undefined ->
416			false;
417		    Prog0 ->
418			case os:find_executable(Prog0) of
419			    false ->
420				false;
421			    Prog ->
422				{prog,Prog}
423			end
424		end
425	end,
426    case OldRel of
427	false ->
428	    ct:log("Release ~tp is not available."
429		   " Upgrade on '~p' level cannot be tested.",
430		   [FromVsn,Level]),
431	    undefined;
432	_ ->
433	    init_upgrade_test(FromVsn,ToVsn,OldRel)
434    end.
435
436get_rels(major) ->
437    %% Given that the current major release is X, then this is an
438    %% upgrade from major release X-1 to the current release.
439    Current = erlang:system_info(otp_release),
440    PreviousMajor = previous_major(Current),
441    {PreviousMajor,Current};
442get_rels(minor) ->
443    %% Given that this is a (possibly) patched version of major
444    %% release X, then this is an upgrade from major release X to the
445    %% current release.
446    CurrentMajor = erlang:system_info(otp_release),
447    Current = CurrentMajor++"_patched",
448    {CurrentMajor,Current}.
449
450init_upgrade_test(FromVsn,ToVsn,OldRel) ->
451    Name = list_to_atom("ct_release_test-otp-"++FromVsn),
452    ct:log("Starting node to fetch application versions to upgrade from"),
453    {ok,Node} = test_server:start_node(Name,peer,[{erl,[OldRel]}]),
454    {Apps,Path} = fetch_all_apps(Node),
455    test_server:stop_node(Node),
456    {FromVsn,ToVsn,Apps,Path}.
457
458fetch_all_apps(Node) ->
459    Paths = rpc:call(Node,code,get_path,[]),
460    %% Find all possible applications in the path
461    AppFiles =
462	lists:flatmap(
463	  fun(P) ->
464		  filelib:wildcard(filename:join(P,"*.app"))
465	  end,
466	  Paths),
467    %% Figure out which version of each application is running on this
468    %% node. Using application:load and application:get_key instead of
469    %% reading the .app files since there might be multiple versions
470    %% of a .app file and we only want the one that is actually
471    %% running.
472    AppVsns =
473	lists:flatmap(
474	  fun(F) ->
475		  A = list_to_atom(filename:basename(filename:rootname(F))),
476		  _ = rpc:call(Node,application,load,[A]),
477		  case rpc:call(Node,application,get_key,[A,vsn]) of
478		      {ok,V} -> [{A,V,rpc:call(Node,code,lib_dir,[A])}];
479		      _ -> []
480		  end
481	  end,
482	  AppFiles),
483    ErtsVsn = rpc:call(Node, erlang, system_info, [version]),
484    {[{erts,ErtsVsn}|AppVsns], Paths}.
485
486
487%%-----------------------------------------------------------------
488upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) ->
489    ct:log("Test upgrade of the following applications: ~p",[Apps]),
490    ct:log(".rel files and start scripts are created in:~n~ts",[CreateDir]),
491    ct:log("The release is installed in:~n~ts",[InstallDir]),
492    case proplists:get_value(release_test,Config) of
493	undefined ->
494	    throw({fail,"ct_release_test:init/1 not run"});
495	RTConfig ->
496	    case proplists:get_value(Level,RTConfig) of
497		undefined ->
498		    throw({skip,"Old release not available"});
499		Data ->
500		    {FromVsn,FromRel,FromAppsVsns} =
501			target_system(Apps, CreateDir, InstallDir, Data),
502		    {ToVsn,ToRel,ToAppsVsns} =
503			upgrade_system(Apps, FromRel, CreateDir,
504				       InstallDir, Data),
505		    ct:log("Upgrade from: OTP-~ts, ~tp",[FromVsn, FromAppsVsns]),
506		    ct:log("Upgrade to: OTP-~ts, ~tp",[ToVsn, ToAppsVsns]),
507		    do_upgrade(Callback, FromVsn, FromAppsVsns, ToRel,
508			       ToAppsVsns, InstallDir)
509	    end
510    end.
511
512%%% This is similar to sasl/examples/src/target_system.erl, but with
513%%% the following adjustments:
514%%% - add a log directory
515%%% - use an own 'start' script
516%%% - chmod 'start' and 'start_erl'
517target_system(Apps,CreateDir,InstallDir,{FromVsn,_,AllAppsVsns,Path}) ->
518    RelName0 = "otp-"++FromVsn,
519
520    AppsVsns = [{A,V,D} || {A,V,D} <- AllAppsVsns, lists:member(A,Apps)],
521    {RelName,ErtsVsn} = create_relfile(AppsVsns,CreateDir,RelName0,FromVsn),
522
523    %% Create .script and .boot
524    ok = systools(make_script,[RelName,[{path,Path}]]),
525
526    %% Create base tar file - i.e. erts and all apps
527    ok = systools(make_tar,[RelName,[{erts,code:root_dir()},
528				     {path,Path}]]),
529
530    %% Unpack the tar to complete the installation
531    ok = erl_tar:extract(RelName ++ ".tar.gz", [{cwd, InstallDir}, compressed]),
532
533    %% Add bin and log dirs
534    BinDir = filename:join([InstallDir, "bin"]),
535    ok = make_dir(BinDir),
536    ok = make_dir(filename:join(InstallDir,"log")),
537
538    %% Delete start scripts - they will be added later
539    ErtsBinDir = filename:join([InstallDir, "erts-" ++ ErtsVsn, "bin"]),
540    ok = delete_file(filename:join([ErtsBinDir, "erl"])),
541    ok = delete_file(filename:join([ErtsBinDir, "start"])),
542    ok = delete_file(filename:join([ErtsBinDir, "start_erl"])),
543
544    %% Copy .boot to bin/start.boot
545    copy_file(RelName++".boot",filename:join([BinDir, "start.boot"])),
546
547    %% Copy scripts from erts-xxx/bin to bin
548    copy_file(filename:join([ErtsBinDir, "epmd"]),
549              filename:join([BinDir, "epmd"]), [preserve]),
550    copy_file(filename:join([ErtsBinDir, "run_erl"]),
551              filename:join([BinDir, "run_erl"]), [preserve]),
552    copy_file(filename:join([ErtsBinDir, "to_erl"]),
553              filename:join([BinDir, "to_erl"]), [preserve]),
554
555    %% create start_erl.data, sys.config and start.src
556    StartErlData = filename:join([InstallDir, "releases", "start_erl.data"]),
557    ok = write_file(StartErlData, io_lib:fwrite("~s ~s~n", [ErtsVsn, FromVsn])),
558    SysConfig = filename:join([InstallDir, "releases", FromVsn, "sys.config"]),
559    ok = write_file(SysConfig, "[]."),
560    StartSrc = filename:join(ErtsBinDir,"start.src"),
561    ok = write_file(StartSrc,start_script()),
562    ok = file:change_mode(StartSrc,8#0755),
563
564    %% Make start_erl executable
565    %% (this has been fixed in OTP 17 - it is now installed with
566    %% $INSTALL_SCRIPT instead of $INSTALL_DATA and should therefore
567    %% be executable from the start)
568    ok = file:change_mode(filename:join(ErtsBinDir,"start_erl.src"),8#0755),
569
570    %% Substitute variables in erl.src, start.src and start_erl.src
571    %% (.src found in erts-xxx/bin - result stored in bin)
572    subst_src_scripts(["erl", "start", "start_erl"], ErtsBinDir, BinDir,
573                      [{"FINAL_ROOTDIR", InstallDir}, {"EMU", "beam"}],
574                      [preserve]),
575
576    %% Create RELEASES
577    RelFile = filename:join([InstallDir, "releases",
578			     filename:basename(RelName) ++ ".rel"]),
579    release_handler:create_RELEASES(InstallDir, RelFile),
580
581    {FromVsn, RelName,AppsVsns}.
582
583systools(Func,Args) ->
584    case apply(systools,Func,Args) of
585	ok ->
586	    ok;
587	error ->
588	    throw({fail,{systools,Func,Args}})
589    end.
590
591%%% This is a copy of $ROOT/erts-xxx/bin/start.src, modified to add
592%%% sname and heart
593start_script() ->
594    ["#!/bin/sh\n"
595     "ROOTDIR=%FINAL_ROOTDIR%\n"
596     "\n"
597     "if [ -z \"$RELDIR\" ]\n"
598     "then\n"
599     "   RELDIR=$ROOTDIR/releases\n"
600     "fi\n"
601     "\n"
602     "START_ERL_DATA=${1:-$RELDIR/start_erl.data}\n"
603     "\n"
604     "$ROOTDIR/bin/run_erl -daemon /tmp/ $ROOTDIR/log \"exec $ROOTDIR/bin/start_erl $ROOTDIR $RELDIR $START_ERL_DATA -sname ",atom_to_list(?testnode)," -heart\"\n"].
605
606%%% Create a release containing the current (the test node) OTP
607%%% release, including relup to allow upgrade from an earlier OTP
608%%% release.
609upgrade_system(Apps, FromRel, CreateDir, InstallDir, {_,ToVsn,_,_}) ->
610    ct:log("Generating release to upgrade to."),
611
612    RelName0 = "otp-"++ToVsn,
613
614    AppsVsns = get_vsns(Apps),
615    {RelName,_} = create_relfile(AppsVsns,CreateDir,RelName0,ToVsn),
616    FromPath = filename:join([InstallDir,lib,"*",ebin]),
617
618    ok = systools(make_script,[RelName]),
619    ok = systools(make_relup,[RelName,[FromRel],[FromRel],
620			      [{path,[FromPath]},
621			       {outdir,CreateDir}]]),
622    SysConfig = filename:join([CreateDir, "sys.config"]),
623    ok = write_file(SysConfig, "[]."),
624
625    ok = systools(make_tar,[RelName,[{erts,code:root_dir()}]]),
626
627    {ToVsn, RelName,AppsVsns}.
628
629%%% Start a new node running the release from target_system/6
630%%% above. Then upgrade to the system from upgrade_system/6.
631do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) ->
632    ct:log("Upgrade test attempting to start node.~n"
633	   "If test fails, logs can be found in:~n~ts",
634	   [filename:join(InstallDir,log)]),
635    Start = filename:join([InstallDir,bin,start]),
636    {ok,Node} = start_node(Start,FromVsn,FromAppsVsns),
637
638    ct:log("Node started: ~p",[Node]),
639    CtData = #ct_data{from = FromAppsVsns,to=ToAppsVsns},
640    State1 = do_callback(Node,Cb,upgrade_init,[CtData,InitState]),
641
642    [{"OTP upgrade test",FromVsn,_,permanent}] =
643	rpc:call(Node,release_handler,which_releases,[]),
644    ToRelName = filename:basename(ToRel),
645    copy_file(ToRel++".tar.gz",
646	      filename:join([InstallDir,releases,ToRelName++".tar.gz"])),
647    ct:log("Unpacking new release"),
648    {ok,ToVsn} = rpc:call(Node,release_handler,unpack_release,[ToRelName]),
649    [{"OTP upgrade test",ToVsn,_,unpacked},
650     {"OTP upgrade test",FromVsn,_,permanent}] =
651	rpc:call(Node,release_handler,which_releases,[]),
652    ct:log("Installing new release"),
653    case rpc:call(Node,release_handler,install_release,[ToVsn]) of
654	{ok,FromVsn,_} ->
655	    ok;
656	{continue_after_restart,FromVsn,_} ->
657	    ct:log("Waiting for node restart")
658    end,
659    %% even if install_release returned {ok,...} there might be an
660    %% emulator restart (instruction restart_emulator), so we must
661    %% always make sure the node is running.
662    {ok, _} = wait_node_up(current,ToVsn,ToAppsVsns),
663
664    [{"OTP upgrade test",ToVsn,_,current},
665     {"OTP upgrade test",FromVsn,_,permanent}] =
666	rpc:call(Node,release_handler,which_releases,[]),
667    ct:log("Permanenting new release"),
668    ok = rpc:call(Node,release_handler,make_permanent,[ToVsn]),
669    [{"OTP upgrade test",ToVsn,_,permanent},
670     {"OTP upgrade test",FromVsn,_,old}] =
671	rpc:call(Node,release_handler,which_releases,[]),
672
673    State2 = do_callback(Node,Cb,upgrade_upgraded,[CtData,State1]),
674
675    ct:log("Re-installing old release"),
676    case rpc:call(Node,release_handler,install_release,[FromVsn]) of
677	{ok,FromVsn,_} ->
678	    ok;
679	{continue_after_restart,FromVsn,_} ->
680	    ct:log("Waiting for node restart")
681    end,
682    %% even if install_release returned {ok,...} there might be an
683    %% emulator restart (instruction restart_emulator), so we must
684    %% always make sure the node is running.
685    {ok, _} = wait_node_up(current,FromVsn,FromAppsVsns),
686
687    [{"OTP upgrade test",ToVsn,_,permanent},
688     {"OTP upgrade test",FromVsn,_,current}] =
689	rpc:call(Node,release_handler,which_releases,[]),
690    ct:log("Permanenting old release"),
691    ok = rpc:call(Node,release_handler,make_permanent,[FromVsn]),
692    [{"OTP upgrade test",ToVsn,_,old},
693     {"OTP upgrade test",FromVsn,_,permanent}] =
694	rpc:call(Node,release_handler,which_releases,[]),
695
696    _State3 = do_callback(Node,Cb,upgrade_downgraded,[CtData,State2]),
697
698    ct:log("Terminating node ~p",[Node]),
699    erlang:monitor_node(Node,true),
700    _ = rpc:call(Node,init,stop,[]),
701    receive {nodedown,Node} -> ok end,
702    ct:log("Node terminated"),
703
704    ok.
705
706do_callback(Node,Mod,Func,Args) ->
707    Dir = filename:dirname(code:which(Mod)),
708    _ = rpc:call(Node,code,add_path,[Dir]),
709    ct:log("Calling ~p:~tp/1",[Mod,Func]),
710    R = rpc:call(Node,Mod,Func,Args),
711    ct:log("~p:~tp/~w returned: ~tp",[Mod,Func,length(Args),R]),
712    case R of
713	{badrpc,Error} ->
714	    throw({fail,{test_upgrade_callback,Mod,Func,Args,Error}});
715	NewState ->
716	    NewState
717    end.
718
719%%% Library functions
720previous_major("17") ->
721    "r16b";
722previous_major(Rel) ->
723    integer_to_list(list_to_integer(Rel)-1).
724
725create_relfile(AppsVsns,CreateDir,RelName0,RelVsn) ->
726    UpgradeAppsVsns = [{A,V,restart_type(A)} || {A,V,_D} <- AppsVsns],
727
728    CoreAppVsns0 = get_vsns([kernel,stdlib,sasl]),
729    CoreAppVsns =
730	[{A,V,restart_type(A)} || {A,V,_D} <- CoreAppVsns0,
731				  false == lists:keymember(A,1,AppsVsns)],
732
733    Apps = [App || {App,_,_} <- AppsVsns],
734    StartDepsVsns = get_start_deps(Apps,CoreAppVsns),
735    StartApps = [StartApp || {StartApp,_,_} <- StartDepsVsns] ++ Apps,
736
737    {RuntimeDepsVsns,_} = get_runtime_deps(StartApps,StartApps,[],[]),
738
739    AllAppsVsns0 = StartDepsVsns ++ UpgradeAppsVsns ++ RuntimeDepsVsns,
740
741    %% Should test tools really be included? Some library functions
742    %% here could be used by callback, but not everything since
743    %% processes of these applications will not be running.
744    TestToolAppsVsns0 = get_vsns([common_test]),
745    TestToolAppsVsns =
746	[{A,V,none} || {A,V,_D} <- TestToolAppsVsns0,
747		       false == lists:keymember(A,1,AllAppsVsns0)],
748
749    AllAppsVsns1 = AllAppsVsns0 ++ TestToolAppsVsns,
750    AllAppsVsns = [AV || AV={A,_,_} <- AllAppsVsns1,
751			 false == lists:member(A,?exclude_apps)],
752
753    ErtsVsn = erlang:system_info(version),
754
755    %% Create the .rel file
756    RelContent = {release,{"OTP upgrade test",RelVsn},{erts,ErtsVsn},AllAppsVsns},
757    RelName = filename:join(CreateDir,RelName0),
758    RelFile = RelName++".rel",
759    {ok,Fd} = file:open(RelFile,[write,{encoding,utf8}]),
760    io:format(Fd,"~tp.~n",[RelContent]),
761    ok = file:close(Fd),
762    {RelName,ErtsVsn}.
763
764get_vsns(Apps) ->
765    [begin
766	 _ = application:load(A),
767	 {ok,V} = application:get_key(A,vsn),
768	 {A,V,code:lib_dir(A)}
769     end || A <- Apps].
770
771get_start_deps([App|Apps],Acc) ->
772    _ = application:load(App),
773    {ok,StartDeps} = application:get_key(App,applications),
774    StartDepsVsns =
775	[begin
776	     _ = application:load(StartApp),
777	     {ok,StartVsn} = application:get_key(StartApp,vsn),
778	     {StartApp,StartVsn,restart_type(StartApp)}
779	 end || StartApp <- StartDeps,
780		false == lists:keymember(StartApp,1,Acc)],
781    DepsStartDeps = get_start_deps(StartDeps,Acc ++ StartDepsVsns),
782    get_start_deps(Apps,DepsStartDeps);
783get_start_deps([],Acc) ->
784    Acc.
785
786get_runtime_deps([App|Apps],StartApps,Acc,Visited) ->
787    case lists:member(App,Visited) of
788	true ->
789	    get_runtime_deps(Apps,StartApps,Acc,Visited);
790	false ->
791	    %% runtime_dependencies should be possible to read with
792	    %% application:get_key/2, but still isn't so we need to
793	    %% read the .app file...
794	    AppFile = code:where_is_file(atom_to_list(App) ++ ".app"),
795	    {ok,[{application,App,Attrs}]} = file:consult(AppFile),
796	    RuntimeDeps =
797		lists:flatmap(
798		  fun(Str) ->
799			  [RuntimeAppStr,_] = string:lexemes(Str,"-"),
800			  RuntimeApp = list_to_atom(RuntimeAppStr),
801			  case {lists:keymember(RuntimeApp,1,Acc),
802				lists:member(RuntimeApp,StartApps)} of
803			      {false,false} when RuntimeApp=/=erts ->
804				  [RuntimeApp];
805			      _ ->
806				  []
807			  end
808		  end,
809		  proplists:get_value(runtime_dependencies,Attrs,[])),
810	    RuntimeDepsVsns =
811		[begin
812		     _ = application:load(RuntimeApp),
813		     {ok,RuntimeVsn} = application:get_key(RuntimeApp,vsn),
814		     {RuntimeApp,RuntimeVsn,none}
815		 end || RuntimeApp <- RuntimeDeps],
816	    {DepsRuntimeDeps,NewVisited} =
817		get_runtime_deps(RuntimeDeps,StartApps,Acc++RuntimeDepsVsns,[App|Visited]),
818	    get_runtime_deps(Apps,StartApps,DepsRuntimeDeps,NewVisited)
819    end;
820get_runtime_deps([],_,Acc,Visited) ->
821    {Acc,Visited}.
822
823restart_type(App) when App==kernel; App==stdlib; App==sasl ->
824    permanent;
825restart_type(_) ->
826    temporary.
827
828copy_file(Src, Dest) ->
829    copy_file(Src, Dest, []).
830
831copy_file(Src, Dest, Opts) ->
832    {ok,_} = file:copy(Src, Dest),
833    case lists:member(preserve, Opts) of
834        true ->
835            {ok, FileInfo} = file:read_file_info(Src),
836            ok = file:write_file_info(Dest, FileInfo);
837        false ->
838            ok
839    end.
840
841write_file(FName, Conts) ->
842    file:write_file(FName, unicode:characters_to_binary(Conts)).
843
844%% Substitute all occurrences of %Var% for Val in the given scripts
845subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) ->
846    lists:foreach(fun(Script) ->
847                          subst_src_script(Script, SrcDir, DestDir,
848                                           Vars, Opts)
849                  end, Scripts).
850
851subst_src_script(Script, SrcDir, DestDir, Vars, Opts) ->
852    subst_file(filename:join([SrcDir, Script ++ ".src"]),
853               filename:join([DestDir, Script]),
854               Vars, Opts).
855
856subst_file(Src, Dest, Vars, Opts) ->
857    {ok, Bin} = file:read_file(Src),
858    Conts = unicode:characters_to_list(Bin),
859    NConts = subst(Conts, Vars),
860    ok = write_file(Dest, NConts),
861    case lists:member(preserve, Opts) of
862        true ->
863            {ok, FileInfo} = file:read_file_info(Src),
864            file:write_file_info(Dest, FileInfo);
865        false ->
866            ok
867    end.
868
869subst(Str, [{Var,Val}|Vars]) ->
870    subst(re:replace(Str,"%"++Var++"%",Val,[{return,list},unicode]),Vars);
871subst(Str, []) ->
872    Str.
873
874%%% Start a node by executing the given start command. This node will
875%%% be used for upgrade.
876start_node(Start,ExpVsn,ExpAppsVsns) ->
877    Port = open_port({spawn_executable, Start}, []),
878    unlink(Port),
879    erlang:port_close(Port),
880    wait_node_up(permanent,ExpVsn,ExpAppsVsns).
881
882wait_node_up(ExpStatus,ExpVsn,ExpAppsVsns0) ->
883    Node = node_name(?testnode),
884    ExpAppsVsns = [{A,V} || {A,V,_D} <- ExpAppsVsns0],
885    wait_node_up(Node,ExpStatus,ExpVsn,lists:keysort(1,ExpAppsVsns),60).
886
887wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,0) ->
888    ct:fail({node_not_started,app_check_failed,ExpVsn,ExpAppsVsns,
889		      rpc:call(Node,release_handler,which_releases,[ExpStatus]),
890		      rpc:call(Node,application,which_applications,[])});
891wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N) ->
892    case {rpc:call(Node,release_handler,which_releases,[ExpStatus]),
893	  rpc:call(Node, application, which_applications, [])} of
894	{[{_,ExpVsn,_,_}],Apps} when is_list(Apps) ->
895	    case [{A,V} || {A,_,V} <- lists:keysort(1,Apps),
896                           lists:keymember(A,1,ExpAppsVsns)] of
897		ExpAppsVsns ->
898		    {ok,Node};
899		_ ->
900		    timer:sleep(2000),
901		    wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
902	    end;
903	_ ->
904	    timer:sleep(2000),
905	    wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
906    end.
907
908node_name(Sname) ->
909    {ok,Host} = inet:gethostname(),
910    list_to_atom(atom_to_list(Sname) ++ "@" ++ Host).
911
912rm_rf(Dir) ->
913    case file:read_file_info(Dir) of
914	{ok, #file_info{type = directory}} ->
915	    {ok, Content} = file:list_dir_all(Dir),
916	    [rm_rf(filename:join(Dir,C)) || C <- Content],
917	    ok=file:del_dir(Dir),
918	    ok;
919	{ok, #file_info{}} ->
920	    ok=file:delete(Dir);
921	_ ->
922	    ok
923    end.
924
925delete_file(FileName) ->
926    case file:delete(FileName) of
927        {error, enoent} ->
928            ok;
929        Else ->
930            Else
931    end.
932
933make_dir(Dir) ->
934    case file:make_dir(Dir) of
935        {error, eexist} ->
936            ok;
937        Else ->
938            Else
939    end.
940