defmodule Runbox.Slave do
@moduledoc """
Utilities for starting a slave node with a scenario release and communicating
with the node.
"""
@doc """
Starts a slave node.
The `release_dir` is a directory with scenario release.
If `app_env` is set, given app environment variables are set on slave.
"""
@spec start(String.t(), [{Application.app(), [{Application.key(), Application.value()}]}]) ::
{:ok, node()} | {:error, term()}
def start(release_dir, app_env \\ []) do
host = slave_host() |> to_charlist()
slave_name = UUID.uuid4() |> to_charlist()
args =
[
"-setcookie #{Node.get_cookie()}",
~s(-boot "#{boot_file(release_dir)}"),
~s(-boot_var RELEASE_LIB "#{release_lib(release_dir)}"),
~s(-config "#{config_file(release_dir)}"),
"-mode interactive",
"-runbox mode slave",
erl_opt_toolbox_scenario_config_dir(),
erl_opt_logger_console()
]
|> Enum.join(" ")
|> to_charlist()
link_to = self()
# The progname has to be stated explicitly, otherwise it can be wrongly set
# to the name of the release script.
progname = 'erl'
case :slave.start(host, slave_name, args, link_to, progname) do
{:ok, slave} ->
with :ok <- check_runbox_started(slave),
:ok <- check_runbox_same_version(slave),
:ok <- check_runbox_scenario_app_configured(slave),
:ok <- put_app_env(slave, app_env) do
{:ok, slave}
else
{:error, _} = error ->
:ok = :slave.stop(slave)
error
end
{:error, _} = error ->
error
end
end
@doc """
Spawns a slave and executes the `fun`.
`fun` recives the slave node as a parameter. The slave is terminated after
the `fun` finishes.
"""
@spec with_slave(String.t(), (node() -> result)) :: {:ok, result} | {:error, term}
when result: any()
def with_slave(release_dir, fun) do
with {:ok, slave} <- start(release_dir) do
try do
{:ok, fun.(slave)}
after
:ok = :slave.stop(slave)
end
end
end
@doc """
Calls a function on the slave.
Calls MFA on the `slave`. See `:erpc.call/4` for details.
"""
@spec call(node(), module(), atom(), list()) :: term()
def call(slave, m, f, a) do
:erpc.call(slave, m, f, a)
end
defp check_runbox_started(slave) do
apps = call(slave, Application, :started_applications, [])
if Enum.any?(apps, &match?({:runbox, _, _}, &1)) do
:ok
else
{:error, :runbox_not_running_on_slave}
end
end
defp check_runbox_same_version(slave) do
{:ok, runbox_master} = :application.get_key(:runbox, :vsn)
{:ok, runbox_slave} = call(slave, :application, :get_key, [:runbox, :vsn])
runbox_master_vsn = Version.parse!(to_string(runbox_master))
runbox_slave_vsn = Version.parse!(to_string(runbox_slave))
if runbox_master_vsn.major == runbox_slave_vsn.major do
:ok
else
{:error, {:runbox_version_mismatch_on_slave, %{master: runbox_master, slave: runbox_slave}}}
end
end
defp check_runbox_scenario_app_configured(slave) do
case call(slave, Application, :fetch_env, [:runbox, :scenario_app]) do
{:ok, _} -> :ok
:error -> {:error, {:runbox_misconfigured, "Missing app config key :scenario_app"}}
end
end
defp put_app_env(slave, app_env) do
if app_env == [], do: :ok, else: call(slave, Application, :put_all_env, [app_env])
end
defp release_lib(release_dir) do
Path.join([release_dir, "lib"])
end
defp boot_file(release_dir) do
Path.join([release_dir, "releases", release_version(release_dir), "start"])
end
defp config_file(release_dir) do
Path.join([release_dir, "releases", release_version(release_dir), "sys"])
end
defp release_version(release_dir) do
data_file = Path.join([release_dir, "releases", "start_erl.data"])
[_, release_version] =
data_file
|> File.read!()
|> String.split(" ")
release_version
end
defp slave_host, do: this_host()
# The name of the host has to be derived from the node name. If taken from
# `:inet.gethostname`, it does not work inside docker.
defp this_host do
[_name, host] =
node()
|> Atom.to_string()
|> String.split("@")
host
end
defp erl_opt_toolbox_scenario_config_dir do
scenario_config_dir = Application.fetch_env!(:toolbox, :scenario_config_dir)
~s(-toolbox scenario_config_dir "#{erl_format_term(scenario_config_dir)}")
end
defp erl_opt_logger_console do
case Application.fetch_env(:logger, :console) do
{:ok, conf} -> ~s(-logger console "#{erl_format_term(conf)}")
:error -> ""
end
end
defp erl_format_term(term) do
IO.chardata_to_string(:io_lib.format("~tw", [term]))
end
end