From 5298e93a180e6db87a33f26eb6a2db06e8065dc7 Mon Sep 17 00:00:00 2001 From: joewilliams Date: Thu, 27 Jan 2011 18:15:25 +0100 Subject: Add 'generate-upgrade' command To support OTP release upgrades I have added support for building upgrade packages. Support for this is included in the rebar_upgrade module, specifically generate_upgrade/2. It requires one variable to be set on the command line 'previous_release' which is the absolute path or relative path from 'rel/' to the previous release one is upgrading from. Running an upgrade will create the needed files, including a relup and result in a tarball containing the upgrade being written to 'rel/'. When done it cleans up the temporary files systools created. Usage: $ rebar generate-upgrade previous_release=/path/to/old/version This also includes a dummy application that can be used to test upgrades as well as an example. Special thanks to Daniel Reverri, Jesper Louis Andersen and Richard Jones for comments and patches. --- test/upgrade_project/README.md | 38 +++++ test/upgrade_project/apps/dummy/ebin/dummy.appup | 8 ++ test/upgrade_project/apps/dummy/src/dummy.app.src | 9 ++ test/upgrade_project/apps/dummy/src/dummy_app.erl | 9 ++ .../apps/dummy/src/dummy_server.erl | 56 ++++++++ test/upgrade_project/apps/dummy/src/dummy_sup.erl | 15 ++ test/upgrade_project/rebar.config | 4 + test/upgrade_project/rel/files/app.config | 10 ++ test/upgrade_project/rel/files/dummy | 155 +++++++++++++++++++++ test/upgrade_project/rel/files/erl | 34 +++++ test/upgrade_project/rel/files/nodetool | 116 +++++++++++++++ test/upgrade_project/rel/files/vm.args | 20 +++ test/upgrade_project/rel/reltool.config | 25 ++++ 13 files changed, 499 insertions(+) create mode 100644 test/upgrade_project/README.md create mode 100644 test/upgrade_project/apps/dummy/ebin/dummy.appup create mode 100644 test/upgrade_project/apps/dummy/src/dummy.app.src create mode 100644 test/upgrade_project/apps/dummy/src/dummy_app.erl create mode 100644 test/upgrade_project/apps/dummy/src/dummy_server.erl create mode 100644 test/upgrade_project/apps/dummy/src/dummy_sup.erl create mode 100644 test/upgrade_project/rebar.config create mode 100644 test/upgrade_project/rel/files/app.config create mode 100755 test/upgrade_project/rel/files/dummy create mode 100755 test/upgrade_project/rel/files/erl create mode 100755 test/upgrade_project/rel/files/nodetool create mode 100644 test/upgrade_project/rel/files/vm.args create mode 100644 test/upgrade_project/rel/reltool.config (limited to 'test') diff --git a/test/upgrade_project/README.md b/test/upgrade_project/README.md new file mode 100644 index 0000000..cf753e8 --- /dev/null +++ b/test/upgrade_project/README.md @@ -0,0 +1,38 @@ +#### Building version 0.1 + rebar compile + rebar generate + mv rel/dummy rel/dummy_0.1 + rebar clean + # start the release: + cd rel/dummy_0.1 + bin/dummy console + + erl> dummy_server:get_state(). + erl> dummy_server:set_state(123). + erl> dummy_server:get_state(). + +#### Building version 0.2 + + # Now, in another terminal we prepare an upgrade.. + + # change release version numbers from 0.1 to 0.2 in + $EDITOR apps/dummy/src/dummy.app.src + $EDITOR rel/reltool.config + + rebar compile + rebar generate + rebar generate-upgrade previous_release=dummy_0.1 + tar -zvtf rel/dummy_0.2.tar.gz + + +#### Deploying with release_handler + mv rel/dummy_0.2.tar.gz rel/dummy_0.1/releases/ + + # Now use release_handler in the running erlang console for the deploy: + + erl> release_handler:unpack_release("dummy_0.2"). + erl> release_handler:install_release("0.2"). + erl> release_handler:make_permanent("0.2"). + + erl> release_handler:which_releases(). + erl> dummy_server:get_state(). diff --git a/test/upgrade_project/apps/dummy/ebin/dummy.appup b/test/upgrade_project/apps/dummy/ebin/dummy.appup new file mode 100644 index 0000000..e77f66f --- /dev/null +++ b/test/upgrade_project/apps/dummy/ebin/dummy.appup @@ -0,0 +1,8 @@ +{"0.2", + [{"0.1",[ + {update, dummy_server, {advanced, [foo]}} + ]}], + [{"0.1",[ + {update, dummy_server, {advanced, [foo]}} + ]}] +}. diff --git a/test/upgrade_project/apps/dummy/src/dummy.app.src b/test/upgrade_project/apps/dummy/src/dummy.app.src new file mode 100644 index 0000000..dd06752 --- /dev/null +++ b/test/upgrade_project/apps/dummy/src/dummy.app.src @@ -0,0 +1,9 @@ +{application, dummy, [ + {description, "a dummy app"}, + {vsn, "0.1"}, + {registered, [ + dummy_app + ]}, + {mod, {dummy_app, []}}, + {applications, [kernel, stdlib, sasl]} +]}. diff --git a/test/upgrade_project/apps/dummy/src/dummy_app.erl b/test/upgrade_project/apps/dummy/src/dummy_app.erl new file mode 100644 index 0000000..51363b3 --- /dev/null +++ b/test/upgrade_project/apps/dummy/src/dummy_app.erl @@ -0,0 +1,9 @@ +-module(dummy_app). +-behaviour(application). + +-export([start/2, stop/1]). + +start(_,_) -> + dummy_sup:start_link(). + +stop(_) -> ok. diff --git a/test/upgrade_project/apps/dummy/src/dummy_server.erl b/test/upgrade_project/apps/dummy/src/dummy_server.erl new file mode 100644 index 0000000..382251e --- /dev/null +++ b/test/upgrade_project/apps/dummy/src/dummy_server.erl @@ -0,0 +1,56 @@ +-module(dummy_server). +-behaviour(gen_server). + +-export([start_link/0, set_state/1, get_state/0]). + +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +set_state(What) -> + gen_server:call(?MODULE, {set_state, What}). + +get_state() -> + gen_server:call(?MODULE, get_state). + + +%% + +init([]) -> + say("init, setting state to 0", []), + {ok, 0}. + + +handle_call({set_state, NewState}, _From, _State) -> + {reply, {ok, NewState}, NewState}; + +handle_call(get_state, _From, State) -> + {reply, State, State}. + +handle_cast('__not_implemented', State) -> + {noreply, State}. + +handle_info(_Info, State) -> + say("info ~p, ~p.", [_Info, State]), + {noreply, State}. + +terminate(_Reason, _State) -> + say("terminate ~p, ~p", [_Reason, _State]), + ok. + +code_change(_OldVsn, State, _Extra) -> + say("code_change ~p, ~p, ~p", [_OldVsn, State, _Extra]), + {ok, State}. + +%% Internal + +say(Format, Data) -> + io:format("~p:~p: ~s~n", [?MODULE, self(), io_lib:format(Format, Data)]). diff --git a/test/upgrade_project/apps/dummy/src/dummy_sup.erl b/test/upgrade_project/apps/dummy/src/dummy_sup.erl new file mode 100644 index 0000000..b5617c7 --- /dev/null +++ b/test/upgrade_project/apps/dummy/src/dummy_sup.erl @@ -0,0 +1,15 @@ +-module(dummy_sup). +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Dummy = {dummy_server, + {dummy_server, start_link, []}, + permanent, 5000, worker, [dummy_server]}, + + {ok, {{one_for_one, 10, 10}, [Dummy]}}. diff --git a/test/upgrade_project/rebar.config b/test/upgrade_project/rebar.config new file mode 100644 index 0000000..11b5b9a --- /dev/null +++ b/test/upgrade_project/rebar.config @@ -0,0 +1,4 @@ +{sub_dirs, [ + "apps/dummy", + "rel" +]}. diff --git a/test/upgrade_project/rel/files/app.config b/test/upgrade_project/rel/files/app.config new file mode 100644 index 0000000..25a3bb3 --- /dev/null +++ b/test/upgrade_project/rel/files/app.config @@ -0,0 +1,10 @@ +[ + %% SASL config + {sasl, [ + {sasl_error_logger, {file, "log/sasl-error.log"}}, + {errlog_type, error}, + {error_logger_mf_dir, "log/sasl"}, % Log directory + {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size + {error_logger_mf_maxfiles, 5} % 5 files max + ]} +]. diff --git a/test/upgrade_project/rel/files/dummy b/test/upgrade_project/rel/files/dummy new file mode 100755 index 0000000..ec6d7ff --- /dev/null +++ b/test/upgrade_project/rel/files/dummy @@ -0,0 +1,155 @@ +#!/bin/bash +# -*- tab-width:4;indent-tabs-mode:nil -*- +# ex: ts=4 sw=4 et + +RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) + +RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} +RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc +RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log +PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ +RUNNER_USER= + +# Make sure this script is running as the appropriate user +if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then + exec sudo -u $RUNNER_USER -i $0 $@ +fi + +# Make sure CWD is set to runner base dir +cd $RUNNER_BASE_DIR + +# Make sure log directory exists +mkdir -p $RUNNER_LOG_DIR + +# Extract the target node name from node.args +NAME_ARG=`grep -e '-[s]*name' $RUNNER_ETC_DIR/vm.args` +if [ -z "$NAME_ARG" ]; then + echo "vm.args needs to have either -name or -sname parameter." + exit 1 +fi + +# Extract the target cookie +COOKIE_ARG=`grep -e '-setcookie' $RUNNER_ETC_DIR/vm.args` +if [ -z "$COOKIE_ARG" ]; then + echo "vm.args needs to have a -setcookie parameter." + exit 1 +fi + +# Identify the script name +SCRIPT=`basename $0` + +# Parse out release and erts info +START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` +ERTS_VSN=${START_ERL% *} +APP_VSN=${START_ERL#* } + +# Add ERTS bin dir to our path +ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin + +# Setup command to control the node +NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" + +# Check the first argument for instructions +case "$1" in + start) + # Make sure there is not already a node running + RES=`$NODETOOL ping` + if [ "$RES" = "pong" ]; then + echo "Node is already running!" + exit 1 + fi + HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start" + export HEART_COMMAND + mkdir -p $PIPE_DIR + # Note the trailing slash on $PIPE_DIR/ + $ERTS_PATH/run_erl -daemon $PIPE_DIR/ $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console" 2>&1 + ;; + + stop) + # Wait for the node to completely stop... + case `uname -s` in + Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) + # PID COMMAND + PID=`ps ax -o pid= -o command=|\ + grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` + ;; + SunOS) + # PID COMMAND + PID=`ps -ef -o pid= -o args=|\ + grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` + ;; + CYGWIN*) + # UID PID PPID TTY STIME COMMAND + PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'` + ;; + esac + $NODETOOL stop + while `kill -0 $PID 2>/dev/null`; + do + sleep 1 + done + ;; + + restart) + ## Restart the VM without exiting the process + $NODETOOL restart + ;; + + reboot) + ## Restart the VM completely (uses heart to restart it) + $NODETOOL reboot + ;; + + ping) + ## See if the VM is alive + $NODETOOL ping + ;; + + attach) + # Make sure a node IS running + RES=`$NODETOOL ping` + if [ "$RES" != "pong" ]; then + echo "Node is not running!" + exit 1 + fi + + shift + $ERTS_PATH/to_erl $PIPE_DIR + ;; + + console|console_clean) + # .boot file typically just $SCRIPT (ie, the app name) + # however, for debugging, sometimes start_clean.boot is useful: + case "$1" in + console) BOOTFILE=$SCRIPT ;; + console_clean) BOOTFILE=start_clean ;; + esac + # Setup beam-required vars + ROOTDIR=$RUNNER_BASE_DIR + BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin + EMU=beam + PROGNAME=`echo $0 | sed 's/.*\\///'` + CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" + export EMU + export ROOTDIR + export BINDIR + export PROGNAME + + # Dump environment info for logging purposes + echo "Exec: $CMD" + echo "Root: $ROOTDIR" + + # Log the startup + logger -t "$SCRIPT[$$]" "Starting up" + + # Start the VM + exec $CMD + ;; + + *) + echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach}" + exit 1 + ;; +esac + +exit 0 diff --git a/test/upgrade_project/rel/files/erl b/test/upgrade_project/rel/files/erl new file mode 100755 index 0000000..b985f23 --- /dev/null +++ b/test/upgrade_project/rel/files/erl @@ -0,0 +1,34 @@ +#!/bin/bash + +## This script replaces the default "erl" in erts-VSN/bin. This is necessary +## as escript depends on erl and in turn, erl depends on having access to a +## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect +## of running escript -- the embedded node bypasses erl and uses erlexec directly +## (as it should). +## +## Note that this script makes the assumption that there is a start_clean.boot +## file available in $ROOTDIR/release/VSN. + +# Determine the abspath of where this script is executing from. +ERTS_BIN_DIR=$(cd ${0%/*} && pwd) + +# Now determine the root directory -- this script runs from erts-VSN/bin, +# so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR +# path. +ROOTDIR=${ERTS_BIN_DIR%/*/*} + +# Parse out release and erts info +START_ERL=`cat $ROOTDIR/releases/start_erl.data` +ERTS_VSN=${START_ERL% *} +APP_VSN=${START_ERL#* } + +BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin +EMU=beam +PROGNAME=`echo $0 | sed 's/.*\\///'` +CMD="$BINDIR/erlexec" +export EMU +export ROOTDIR +export BINDIR +export PROGNAME + +exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} diff --git a/test/upgrade_project/rel/files/nodetool b/test/upgrade_project/rel/files/nodetool new file mode 100755 index 0000000..cb524ce --- /dev/null +++ b/test/upgrade_project/rel/files/nodetool @@ -0,0 +1,116 @@ +%% -*- mode:erlang;tab-width:4;erlang-indent-level:4;indent-tabs-mode:nil -*- +%% ex: ft=erlang ts=4 sw=4 et +%% ------------------------------------------------------------------- +%% +%% nodetool: Helper Script for interacting with live nodes +%% +%% ------------------------------------------------------------------- + +main(Args) -> + %% Extract the args + {RestArgs, TargetNode} = process_args(Args, [], undefined), + + %% See if the node is currently running -- if it's not, we'll bail + case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of + {true, pong} -> + ok; + {_, pang} -> + io:format("Node ~p not responding to pings.\n", [TargetNode]), + halt(1) + end, + + case RestArgs of + ["ping"] -> + %% If we got this far, the node already responsed to a ping, so just dump + %% a "pong" + io:format("pong\n"); + ["stop"] -> + io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); + ["restart"] -> + io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); + ["reboot"] -> + io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); + ["rpc", Module, Function | RpcArgs] -> + case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), + [RpcArgs], 60000) of + ok -> + ok; + {badrpc, Reason} -> + io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), + halt(1); + _ -> + halt(1) + end; + ["rpcterms", Module, Function, ArgsAsString] -> + case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), + consult(ArgsAsString), 60000) of + {badrpc, Reason} -> + io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), + halt(1); + Other -> + io:format("~p\n", [Other]) + end; + Other -> + io:format("Other: ~p\n", [Other]), + io:format("Usage: nodetool {ping|stop|restart|reboot}\n") + end, + net_kernel:stop(). + +process_args([], Acc, TargetNode) -> + {lists:reverse(Acc), TargetNode}; +process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> + erlang:set_cookie(node(), list_to_atom(Cookie)), + process_args(Rest, Acc, TargetNode); +process_args(["-name", TargetName | Rest], Acc, _) -> + ThisNode = append_node_suffix(TargetName, "_maint_"), + {ok, _} = net_kernel:start([ThisNode, longnames]), + process_args(Rest, Acc, nodename(TargetName)); +process_args(["-sname", TargetName | Rest], Acc, _) -> + ThisNode = append_node_suffix(TargetName, "_maint_"), + {ok, _} = net_kernel:start([ThisNode, shortnames]), + process_args(Rest, Acc, nodename(TargetName)); +process_args([Arg | Rest], Acc, Opts) -> + process_args(Rest, [Arg | Acc], Opts). + + +nodename(Name) -> + case string:tokens(Name, "@") of + [_Node, _Host] -> + list_to_atom(Name); + [Node] -> + [_, Host] = string:tokens(atom_to_list(node()), "@"), + list_to_atom(lists:concat([Node, "@", Host])) + end. + +append_node_suffix(Name, Suffix) -> + case string:tokens(Name, "@") of + [Node, Host] -> + list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); + [Node] -> + list_to_atom(lists:concat([Node, Suffix, os:getpid()])) + end. + + +%% +%% Given a string or binary, parse it into a list of terms, ala file:consult/0 +%% +consult(Str) when is_list(Str) -> + consult([], Str, []); +consult(Bin) when is_binary(Bin)-> + consult([], binary_to_list(Bin), []). + +consult(Cont, Str, Acc) -> + case erl_scan:tokens(Cont, Str, 0) of + {done, Result, Remaining} -> + case Result of + {ok, Tokens, _} -> + {ok, Term} = erl_parse:parse_term(Tokens), + consult([], Remaining, [Term | Acc]); + {eof, _Other} -> + lists:reverse(Acc); + {error, Info, _} -> + {error, Info} + end; + {more, Cont1} -> + consult(Cont1, eof, Acc) + end. diff --git a/test/upgrade_project/rel/files/vm.args b/test/upgrade_project/rel/files/vm.args new file mode 100644 index 0000000..7448fb1 --- /dev/null +++ b/test/upgrade_project/rel/files/vm.args @@ -0,0 +1,20 @@ + +## Name of the node +-name dummy@127.0.0.1 + +## Cookie for distributed erlang +-setcookie dummy + +## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive +## (Disabled by default..use with caution!) +##-heart + +## Enable kernel poll and a few async threads ++K true ++A 5 + +## Increase number of concurrent ports/sockets +-env ERL_MAX_PORTS 4096 + +## Tweak GC to run more often +-env ERL_FULLSWEEP_AFTER 10 diff --git a/test/upgrade_project/rel/reltool.config b/test/upgrade_project/rel/reltool.config new file mode 100644 index 0000000..22cec5e --- /dev/null +++ b/test/upgrade_project/rel/reltool.config @@ -0,0 +1,25 @@ +{sys, [ + {lib_dirs, ["../apps"]}, + {rel, "dummy", "0.1", [ + kernel, + stdlib, + sasl, + dummy + ]}, + {rel, "start_clean", "", [kernel, stdlib]}, + {boot_rel, "dummy"}, + {profile, embedded}, + {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)"]}, + {excl_archive_filters, [".*"]}, + + {app, dummy, [{incl_cond, include}]} +]}. + +{overlay, [ + {mkdir, "log/sasl"}, + {copy, "files/erl", "{{erts_vsn}}/bin/erl"}, + {copy, "files/nodetool", "{{erts_vsn}}/bin/nodetool"}, + {copy, "files/dummy", "bin/dummy"}, + {copy, "files/app.config", "etc/app.config"}, + {copy, "files/vm.args", "etc/vm.args"} + ]}. -- cgit v1.1