defmodule KantanCluster do
@moduledoc """
Form a simple Erlang cluster easily in Elixir.
"""
require Logger
@typedoc """
A node type.
See https://hexdocs.pm/elixir/1.12/Node.html#start/3
"""
@type node_type :: :longnames | :shortnames
@typedoc """
Options for a cluster.
* `:node` - a name of the node that you want to start (default: `{:longnames, :"xxxx@yyyy.local"}` where `xxxx` is a random string, `yyyy` is the hostname of a machine)
* `:cookie` - [Erlang magic cookie] to form a cluster (default: random cookie)
* `:connect_to` - a list of nodes we want our node to be connected with (default: `[]`)
"""
@type option() ::
{:node, {node_type, node}}
| {:cookie, atom}
| {:connect_to, node | [node]}
@doc """
Starts a node and attempts to connect it to specified nodes. Configuration
options can be specified as an argument
KantanCluster.start(
cookie: :hello,
connect_to: [:"nerves@nerves-mn00.local"]
)
or in your `config/config.exs`.
config :kantan_cluster,
cookie: :hello,
connect_to: [:"nerves@nerves-mn00.local"]
"""
@spec start([option]) :: :ok
def start(opts \\ []) when is_list(opts) do
ensure_distribution!(opts)
validate_hostname_resolution!()
set_cookie(opts)
KantanCluster.Config.get_connect_to_option(opts) |> connect()
:ok
end
@doc """
Stops a node.
"""
def stop() do
# KantanCluster.NodeConnector will be stopped when node gets stopped.
Node.stop()
end
@doc """
Connects current node to specified nodes.
"""
@spec connect(node | [node]) :: pid | nil | list
def connect(connect_to) when is_atom(connect_to) do
KantanCluster.NodeConnectorSupervisor.find_or_start_child_process(connect_to)
end
def connect(connect_to) when is_list(connect_to) do
connect_to |> Enum.map(&KantanCluster.NodeConnectorSupervisor.find_or_start_child_process/1)
end
@doc """
Disconnects current node from speficied nodes.
"""
@spec disconnect(node | [node]) :: :ok
def disconnect(node_name) when is_atom(node_name) do
KantanCluster.NodeConnector.disconnect(node_name)
end
def disconnect(node_names) when is_list(node_names) do
node_names |> Enum.each(&KantanCluster.NodeConnector.disconnect/1)
:ok
end
@spec ensure_distribution!(keyword) :: :ok
defp ensure_distribution!(opts) do
if Node.alive?() do
Logger.info("distributed node already started: #{Node.self()}")
else
case System.cmd("epmd", ["-daemon"]) do
{_, 0} -> :ok
_ -> raise("could not start epmd (Erlang Port Mapper Driver).")
end
{type, name} = KantanCluster.Config.get_node_option(opts)
case Node.start(name, type) do
{:ok, _} -> :ok
{:error, reason} -> raise("could not start distributed node: #{inspect(reason)}")
end
end
end
import Record
defrecordp :hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl")
defp validate_hostname_resolution!() do
validate_hostname_resolution!(KantanCluster.Utils.shortnames_mode?())
end
defp validate_hostname_resolution!(_shortnames_mode = true) do
hostname = KantanCluster.Utils.node_hostname() |> to_charlist()
case :inet.gethostbyname(hostname) do
{:error, :nxdomain} ->
invalid_hostname!("your hostname \"#{hostname}\" does not resolve to an IP address")
{:ok, hostent(h_addrtype: :inet, h_addr_list: addresses)} ->
any_loopback? = Enum.any?(addresses, &match?({127, _, _, _}, &1))
unless any_loopback? do
invalid_hostname!(
"your hostname \"#{hostname}\" does not resolve to a loopback address (127.0.0.0/8)"
)
end
_ ->
:ok
end
end
defp validate_hostname_resolution!(_), do: :ok
@spec invalid_hostname!(binary) :: no_return
defp invalid_hostname!(prelude) do
raise("""
#{prelude}, which indicates something wrong in your OS configuration.
Make sure your computer's name resolves locally or start KantanCluster using a long distribution name.
""")
end
@spec set_cookie(keyword) :: true
defp set_cookie(opts) do
KantanCluster.Config.get_cookie_option(opts) |> Node.set_cookie()
end
end