Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export ERL_AFLAGS="-start_epmd false -epmd_module Elixir.ControlNode.Epmd"
# `prevent_overlapping_partition` flag is turned off to prevent the following
# issue,
# [warning] 'global' at node :"node1@host1" requested disconnect from node :"node2@host2" in order to prevent overlapping partitions pid=<0.55.0>
# The above occur because BEAM is trying to work in a clustered mode but for
# control node we start network topology i.e. we don't expect release nodes to
# be connected to one another
export ERL_AFLAGS="-start_epmd false -epmd_module Elixir.ControlNode.Epmd -kernel prevent_overlapping_partitions false"
14 changes: 9 additions & 5 deletions lib/control_node/epmd.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ defmodule ControlNode.Epmd do

def address_please(name, host, _address_family) do
key = {"#{name}", "#{host}"}
[{_, port}] = :ets.lookup(:control_node_epmd, key)
# The distribution protocol version number has been 5 ever since
# Erlang/OTP R6.
version = 5
{:ok, {127, 0, 0, 1}, port, version}
case :ets.lookup(:control_node_epmd, key) do
[{_, port}] ->
# The distribution protocol version number has been 5 ever since
# Erlang/OTP R6.
version = 5
{:ok, {127, 0, 0, 1}, port, version}
_ ->
{:error, :not_found}
end
end

def register_release(name, host, port) do
Expand Down
19 changes: 13 additions & 6 deletions lib/control_node/host.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ defmodule ControlNode.Host do
end
end

@spec init_release(SSH.t(), binary, atom) :: :ok | :failure | {:error, any}
def init_release(%SSH{} = host_spec, init_file, command) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <-
SSH.exec(host_spec, "nohup #{init_file} #{command} &", true) do
@spec init_release(SSH.t(), binary) :: :ok | :failure | {:error, any}
def init_release(%SSH{} = host_spec, exec_binary) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, exec_binary, skip_eof: true) do
:ok
end
end

# TODO : check and remove
@spec stop_release(SSH.t(), binary) :: :ok | :failure | {:error, any}
def stop_release(%SSH{} = host_spec, cmd) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, "nohup #{cmd} stop") do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, "#{cmd} stop") do
:ok
end
end
Expand All @@ -58,7 +58,7 @@ defmodule ControlNode.Host do
@spec hostname(SSH.t()) :: {:ok, binary}
def hostname(%SSH{} = host_spec) do
with {:ok, %SSH.ExecStatus{exit_status: :success, message: [hostname]}} <-
SSH.exec(host_spec, "hostname") do
SSH.exec(host_spec, "hostname", skip_env_vars: true) do
{:ok, %SSH{host_spec | hostname: String.trim(hostname)}}
end
end
Expand All @@ -70,6 +70,13 @@ defmodule ControlNode.Host do
with {:ok, info} <- epmd_list_names(host_spec) do
disconnect(host_spec)
{:ok, info}
else
# No data was received, this usually implies that EPMD may not be running
# on remote host. So, no beam service is running hence we return empty map
{:error, :no_data} ->
{:ok, %Info{services: %{}}}
other ->
other
end
end

Expand Down
34 changes: 22 additions & 12 deletions lib/control_node/host/ssh.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ defmodule ControlNode.Host.SSH do
* `:user` : SSH user name
* `:private_key_dir` : Path to the `.ssh` folder (eg. `/home/user/.ssh`)
* `via_ssh_agent`: Use SSH Agent for authentication (default `false`)
* `env_vars`: Define env vars (key, value) to be passed when running a command on the remote host
* `env_vars`: Define env vars (key, value) to be passed when running a command on the remote host.
NOTE: `value` can be data or function with signature `fn (%ControlNode.Host.SSH{}) -> ... end`
and must return computed data
"""
@type t :: %__MODULE__{
host: binary,
Expand All @@ -35,7 +37,8 @@ defmodule ControlNode.Host.SSH do
private_key_dir: binary,
conn: :ssh.connection_ref(),
hostname: binary,
via_ssh_agent: boolean
via_ssh_agent: boolean,
env_vars: Map.t() | nil
}
@timeout :infinity

Expand Down Expand Up @@ -127,9 +130,12 @@ defmodule ControlNode.Host.SSH do
be set to `true`. This enable `exec` to return `ExecStatus` while the command
is left running on host.
"""
@spec exec(t, list | binary) :: {:ok, ExecStatus.t()} | :failure | {:error, any}
def exec(ssh_config, commands, skip_eof \\ false) do
env_vars = to_shell_env_vars(ssh_config.env_vars, :inline)
@spec exec(t, list | binary, list) :: {:ok, ExecStatus.t()} | :failure | {:error, any}
def exec(ssh_config, commands, opts \\ []) do
skip_eof = Keyword.get(opts, :skip_eof, false)
skip_env_vars = Keyword.get(opts, :skip_env_vars, false)

env_vars = not skip_env_vars && to_shell_env_vars(ssh_config, :inline) || ""
Logger.debug("Processed env var", env_vars: env_vars)

script =
Expand All @@ -143,9 +149,9 @@ defmodule ControlNode.Host.SSH do
end

defp do_exec(ssh_config, commands, skip_eof) when is_list(commands) do
env_vars = to_shell_env_vars(ssh_config.env_vars, :export)
commands = env_vars <> Enum.join(commands, "; ")
do_exec(ssh_config, Enum.join(commands, "; "), skip_eof)
env_vars = to_shell_env_vars(ssh_config, :export)
commands = env_vars <> Enum.join(commands, " && ")
do_exec(ssh_config, commands, skip_eof)
end

defp do_exec(ssh_config, script, skip_eof) when is_binary(script) do
Expand All @@ -163,18 +169,22 @@ defmodule ControlNode.Host.SSH do
end
end

@spec to_shell_env_vars(Map.t() | nil, :inline | :export) :: String.t()
defp to_shell_env_vars(nil, _), do: ""
@spec to_shell_env_vars(t, :inline | :export) :: String.t()
defp to_shell_env_vars(%__MODULE__{env_vars: nil}, _), do: ""

defp to_shell_env_vars(env_vars, :inline) do
defp to_shell_env_vars(%__MODULE__{env_vars: env_vars} = ssh_config, :inline) do
Enum.map(env_vars, fn {key, value} ->
value = is_function(value) && value.(%__MODULE__{ssh_config | conn: nil}) || value

"#{key}='#{value}'"
end)
|> Enum.join(" ")
end

defp to_shell_env_vars(env_vars, :export) do
defp to_shell_env_vars(%__MODULE__{env_vars: env_vars} = ssh_config, :export) do
Enum.map(env_vars, fn {key, value} ->
value = is_function(value) && value.(%__MODULE__{ssh_config | conn: nil}) || value

"export #{key}=#{value}"
end)
|> Enum.join("; ")
Expand Down
11 changes: 10 additions & 1 deletion lib/control_node/namespace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ defmodule ControlNode.Namespace do
GenServer.call(namespace_pid, :current_version)
end

def update_namespace_spec(%ControlNode.Namespace.Spec{} = spec) do
GenServer.call(spec, {:update_namespace_spec, spec})
end

def start_link(namespace_spec, release_mod) do
name = :"#{namespace_spec.tag}_#{release_mod.release_name}"
name = release_mod.get_namespace_id(namespace_spec)
Logger.debug("Starting namespace with name #{name}")
GenServer.start_link(__MODULE__, [namespace_spec, release_mod], name: name)
end
Expand Down Expand Up @@ -86,6 +90,11 @@ defmodule ControlNode.Namespace do
{:reply, {:ok, version_list}, state}
end

@impl true
def handle_call({:update_namespace_spec, spec}, _from, state) do
{:reply, :ok, %{state | spec: spec}}
end

@impl true
def handle_cast({:deploy, version}, %{spec: namespace_spec, release_mod: release_mod} = state) do
Enum.map(namespace_spec.hosts, fn host_spec ->
Expand Down
2 changes: 1 addition & 1 deletion lib/control_node/namespace/connect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ControlNode.Namespace.Connect do
def callback_mode, do: :handle_event_function

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end
end
4 changes: 2 additions & 2 deletions lib/control_node/namespace/initialize.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule ControlNode.Namespace.Initialize do
:initialize,
%Workflow.Data{deploy_attempts: deploy_attempts} = data
)
when deploy_attempts >= 5 do
when deploy_attempts >= 3 do
Logger.error("Depoyment attempts exhausted, failed to deploy release version #{version}")
{state, actions} = Namespace.Workflow.next(@state_name, :not_running, :ignore)
data = %Workflow.Data{data | deploy_attempts: 0}
Expand Down Expand Up @@ -95,7 +95,7 @@ defmodule ControlNode.Namespace.Initialize do

{:running, 0}
else
Logger.warn("Release state loaded, expected version #{version} found #{current_version}")
Logger.warning("Release state loaded, expected version #{version} found #{current_version || "err_not_deployed"}")

{:partially_running, data.deploy_attempts}
end
Expand Down
4 changes: 2 additions & 2 deletions lib/control_node/namespace/manage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule ControlNode.Namespace.Manage do
data = %Workflow.Data{data | health_check_timer: timer_ref}
{:keep_state, data, []}
else
Logger.warn("Release health check failed")
Logger.warning("Release health check failed")

# TODO: respect max failure count before rebooting the release
if hc_spec.on_failure == :reboot do
Expand Down Expand Up @@ -106,7 +106,7 @@ defmodule ControlNode.Namespace.Manage do
end

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end

Expand Down
2 changes: 1 addition & 1 deletion lib/control_node/namespace/observe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule ControlNode.Namespace.Observe do
end

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end
end
7 changes: 7 additions & 0 deletions lib/control_node/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ defmodule ControlNode.Registry do
|> File.read()
end

@doc """
Retrieves application release tar file location
"""
def location(%Local{} = registry_spec, application, version) do
Path.join(registry_spec.path, "#{application}-#{version}.tar.gz")
end

@doc """
Stores application release tar file in the filesystem
"""
Expand Down
Loading
Loading