backend: live connection status!
This commit is contained in:
parent
e12e20eb38
commit
a8699035d8
9 changed files with 135 additions and 45 deletions
|
@ -1,6 +1,9 @@
|
||||||
defmodule Prymn.Agents.Connection do
|
defmodule Prymn.Agents.Connection do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
|
# TODO: Disconnect after a while of idling. Disconnect when the healthcheck
|
||||||
|
# fails too many times.
|
||||||
|
|
||||||
defstruct [:channel, up?: false]
|
defstruct [:channel, up?: false]
|
||||||
|
|
||||||
@ping_interval 20000
|
@ping_interval 20000
|
||||||
|
@ -46,15 +49,27 @@ defmodule Prymn.Agents.Connection do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:gun_up, _pid, _protocol}, state) do
|
def handle_info({:gun_up, _pid, _protocol}, %{channel: channel} = state) do
|
||||||
Logger.info("regained connection (#{inspect(state.channel)})")
|
Logger.info("[Agent] #{state.channel.host} regained connection")
|
||||||
|
|
||||||
|
Phoenix.PubSub.broadcast!(
|
||||||
|
Prymn.PubSub,
|
||||||
|
"agent:#{channel.host}",
|
||||||
|
{:healthcheck, :up, channel.host}
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, %{state | up?: true}}
|
{:noreply, %{state | up?: true}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:gun_down, _pid, _protocol, reason, _killed_streams}, state) do
|
def handle_info({:gun_down, _pid, _proto, reason, _}, %{channel: channel} = state) do
|
||||||
Logger.info("lost connection (reason: #{inspect(reason)}, #{inspect(state.channel)})")
|
Logger.info("[Agent] #{channel.host} lost connection, reason: #{reason}")
|
||||||
|
|
||||||
|
Phoenix.PubSub.broadcast!(
|
||||||
|
Prymn.PubSub,
|
||||||
|
"agent:#{channel.host}",
|
||||||
|
{:healthcheck, :down, channel.host}
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, %{state | up?: false}}
|
{:noreply, %{state | up?: false}}
|
||||||
end
|
end
|
||||||
|
@ -67,8 +82,6 @@ defmodule Prymn.Agents.Connection do
|
||||||
{:ok, _reply} ->
|
{:ok, _reply} ->
|
||||||
:noop
|
:noop
|
||||||
|
|
||||||
# IO.inspect(reply)
|
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.warning("healthcheck error for server #{channel.host}, reason: #{inspect(error)}")
|
Logger.warning("healthcheck error for server #{channel.host}, reason: #{inspect(error)}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,12 @@ defmodule Prymn.Servers do
|
||||||
"""
|
"""
|
||||||
def get_server!(id), do: Repo.get!(Server, id)
|
def get_server!(id), do: Repo.get!(Server, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a single server by its IP.
|
||||||
|
"""
|
||||||
|
@spec get_server_by_ip!(String.t()) :: %Server{}
|
||||||
|
def get_server_by_ip!(ip), do: Repo.get_by!(Server, public_ip: ip)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
||||||
Start a new server connection with the app.
|
Start a new server connection with the app.
|
||||||
|
@ -52,16 +58,19 @@ defmodule Prymn.Servers do
|
||||||
Registers a server using a registration token.
|
Registers a server using a registration token.
|
||||||
"""
|
"""
|
||||||
def register_server(token, public_ip) do
|
def register_server(token, public_ip) do
|
||||||
# TODO: Validate public ip
|
with true <- :inet.is_ip_address(public_ip),
|
||||||
|
{:ok, token} <- Base.decode64(token) do
|
||||||
|
public_ip_string =
|
||||||
|
public_ip
|
||||||
|
|> :inet.ntoa()
|
||||||
|
|> to_string()
|
||||||
|
|
||||||
case Base.decode64(token) do
|
|
||||||
{:ok, token} ->
|
|
||||||
from(s in Server, where: s.registration_token == ^token, select: s)
|
from(s in Server, where: s.registration_token == ^token, select: s)
|
||||||
|> Repo.one()
|
|> Repo.one!()
|
||||||
|> update_server(%{public_ip: public_ip})
|
|> update_server(%{public_ip: public_ip_string, status: :registered})
|
||||||
|
else
|
||||||
:error ->
|
false -> {:error, :invalid_ip}
|
||||||
{:error, "token is not a valid base64 encoded string"}
|
:error -> {:error, :bad_token}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,4 +120,26 @@ defmodule Prymn.Servers do
|
||||||
def change_server(%Server{} = server, attrs \\ %{}) do
|
def change_server(%Server{} = server, attrs \\ %{}) do
|
||||||
Server.changeset(server, attrs)
|
Server.changeset(server, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a string containing the command that needs to be executed to the
|
||||||
|
remote server in order to register it to the backend.
|
||||||
|
"""
|
||||||
|
@spec create_setup_command(%Server{}) :: String.t()
|
||||||
|
def create_setup_command(%Server{registration_token: token}) do
|
||||||
|
build_command = fn token ->
|
||||||
|
if Application.get_env(:prymn, :environment) == :prod do
|
||||||
|
"curl -sSf https://static.prymn.net/get_prymn.sh | sh -s " <> token
|
||||||
|
else
|
||||||
|
# On a dev environment we want to download the local version of the agent
|
||||||
|
agent_path = Path.expand("../agent/target/debug/prymn_agent")
|
||||||
|
get_prymn_path = Path.expand("../agent/get_prymn.sh")
|
||||||
|
"GET_PRYMN_ROOT=file://#{agent_path} #{get_prymn_path} #{token}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
token
|
||||||
|
|> Base.encode64()
|
||||||
|
|> then(build_command)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,9 +8,11 @@ defmodule Prymn.Servers.Server do
|
||||||
field :provider, Ecto.Enum, values: [:Hetzner, :Custom]
|
field :provider, Ecto.Enum, values: [:Hetzner, :Custom]
|
||||||
field :registration_token, :binary, redact: true
|
field :registration_token, :binary, redact: true
|
||||||
|
|
||||||
field :connection_status, Ecto.Enum,
|
field :status, Ecto.Enum,
|
||||||
values: [:awaiting, :connecting, :installing, :connected, :disconnected],
|
values: [:unregistered, :registered],
|
||||||
default: :awaiting
|
default: :unregistered
|
||||||
|
|
||||||
|
field :connection_status, :string, virtual: true
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -18,8 +20,9 @@ defmodule Prymn.Servers.Server do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(server, attrs) do
|
def changeset(server, attrs) do
|
||||||
server
|
server
|
||||||
|> cast(attrs, [:name, :provider, :registration_token])
|
|> cast(attrs, [:name, :public_ip, :provider, :registration_token, :status])
|
||||||
|> validate_required([:name, :provider])
|
|> validate_required([:name, :provider])
|
||||||
|> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)")
|
|> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)")
|
||||||
|
|> unique_constraint([:public_ip])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +1,36 @@
|
||||||
defmodule PrymnWeb.ServerController do
|
defmodule PrymnWeb.ServerController do
|
||||||
use PrymnWeb, :controller
|
|
||||||
|
|
||||||
alias Prymn.Servers
|
alias Prymn.Servers
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
use PrymnWeb, :controller
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Used by clients to request a new server connection to the prymn backend
|
Used by clients to request a new server connection to the prymn backend
|
||||||
validating their registration token.
|
validating their registration token.
|
||||||
"""
|
"""
|
||||||
def register(conn, %{"token" => token, "ip" => ip}) do
|
def register(conn, %{"token" => token}) do
|
||||||
case Servers.register_server(token, ip) do
|
case Servers.register_server(token, conn.remote_ip) do
|
||||||
{:ok, _server} ->
|
{:ok, _server} ->
|
||||||
json(conn, %{"connected" => true})
|
json(conn, %{"connected" => true})
|
||||||
|
|
||||||
|
{:error, :invalid_ip} ->
|
||||||
|
Logger.error("could not register a server because we received an invalid ip")
|
||||||
|
|
||||||
|
put_status(conn, 500)
|
||||||
|
|> json(%{"errors" => ["invalid ip received"]})
|
||||||
|
|
||||||
|
{:error, :bad_token} ->
|
||||||
|
put_status(conn, 400)
|
||||||
|
|> json(%{"errors" => %{"token" => "token is not valid"}})
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _} -> msg end)
|
||||||
|
|
||||||
|
put_status(conn, 400)
|
||||||
|
|> json(%{"errors" => errors})
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
raise inspect(error)
|
raise "An unhandled error was received #{inspect(error)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
defmodule PrymnWeb.ServerLive.Index do
|
defmodule PrymnWeb.ServerLive.Index do
|
||||||
use PrymnWeb, :live_view
|
|
||||||
|
|
||||||
alias Prymn.Servers
|
alias Prymn.Servers
|
||||||
|
|
||||||
|
use PrymnWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
# Run this for every server:
|
servers = Servers.list_servers()
|
||||||
# make sure an agent connection is made (async "cheap" request)
|
pid = self()
|
||||||
# then wait for events
|
|
||||||
# pubsub will eventually send a connected or a disconnected (and anything else) event
|
for %{public_ip: public_ip} <- servers, public_ip != nil do
|
||||||
{:ok, stream(socket, :servers, Servers.list_servers())}
|
:ok = Phoenix.PubSub.subscribe(Prymn.PubSub, "agent:#{public_ip}")
|
||||||
|
|
||||||
|
Task.start_link(fn ->
|
||||||
|
case Prymn.Agents.ensure_connection("#{public_ip}:50012") do
|
||||||
|
:ok -> Process.send(pid, {:healthcheck, :up, public_ip}, [])
|
||||||
|
{:error, _error} -> Process.send(pid, {:healthcheck, :down, public_ip}, [])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, stream(socket, :servers, servers)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -29,12 +39,22 @@ defmodule PrymnWeb.ServerLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:connect, %Servers.Server{} = server}, socket) do
|
def handle_info({:connect, %Servers.Server{} = server}, socket) do
|
||||||
socket =
|
{:noreply, stream_insert(socket, :servers, server)}
|
||||||
if server.provider == :Custom,
|
end
|
||||||
do: push_navigate(socket, to: ~p"/servers/#{server}"),
|
|
||||||
else: stream_insert(socket, :servers, server)
|
|
||||||
|
|
||||||
{:noreply, socket}
|
@impl true
|
||||||
|
def handle_info({:healthcheck, status, ip}, socket) do
|
||||||
|
server = Servers.get_server_by_ip!(ip)
|
||||||
|
|
||||||
|
status =
|
||||||
|
case status do
|
||||||
|
:up -> "Connected"
|
||||||
|
:down -> "Disconnected"
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> stream_insert(:servers, Map.put(server, :connection_status, status))}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -13,19 +13,23 @@
|
||||||
row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
|
row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
|
||||||
row_indicator={
|
row_indicator={
|
||||||
fn
|
fn
|
||||||
{_id, %Servers.Server{connection_status: :awaiting}} ->
|
{_id, %Servers.Server{status: :unregistered}} ->
|
||||||
~H(<span class="text-grey-600">Awaiting connection</span>)
|
~H(<span class="text-grey-600">Awaiting registration</span>)
|
||||||
|
|
||||||
{_id, %Servers.Server{connection_status: :connecting}} ->
|
{_id, %Servers.Server{connection_status: nil, status: :registered}} ->
|
||||||
~H(<span class="text-purple-600">Connecting</span>)
|
~H(<span class="text-yellow-600">Connecting...</span>)
|
||||||
|
|
||||||
{_id, %Servers.Server{connection_status: :connected}} ->
|
{_id, %Servers.Server{connection_status: "Connected"}} ->
|
||||||
~H(<span class="text-green-600">Connected</span>)
|
~H(<span class="text-green-600">Connected</span>)
|
||||||
|
|
||||||
|
{_id, %Servers.Server{connection_status: "Disconnected"}} ->
|
||||||
|
~H(<span class="text-red-600">Disconnected</span>)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
indicator_label="Status"
|
indicator_label="Status"
|
||||||
>
|
>
|
||||||
<:col :let={{_id, server}} label="Name"><%= server.name %></:col>
|
<:col :let={{_id, server}} label="Name"><%= server.name %></:col>
|
||||||
|
<:col :let={{_id, server}} label="IP"><%= server.public_ip || "N/A" %></:col>
|
||||||
<:action :let={{id, server}}>
|
<:action :let={{id, server}}>
|
||||||
<.link
|
<.link
|
||||||
phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")}
|
phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")}
|
||||||
|
|
|
@ -15,6 +15,7 @@ defmodule PrymnWeb.ServerLive.Show do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, server.name)
|
|> assign(:page_title, server.name)
|
||||||
|> assign(:server, server)}
|
|> assign(:server, server)
|
||||||
|
|> assign(:registration_command, Servers.create_setup_command(server))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Server <%= @server.name %>
|
Server <%= @server.name %>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<section :if={@server.connection_status == :awaiting} class="my-10">
|
<section :if={@server.status == :unregistered} class="my-10">
|
||||||
<p class="mb-9">
|
<p class="mb-9">
|
||||||
Connect to your server using root credentials and execute the following command:
|
Connect to your server using root credentials and execute the following command:
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<code class="flex gap-4">
|
<code class="flex gap-4">
|
||||||
<span class="select-none text-gray-500">#</span>
|
<span class="select-none text-gray-500">#</span>
|
||||||
<span class="flex-1">
|
<span class="flex-1">
|
||||||
your command here that contains token: <%= Base.encode64(@server.registration_token) %>
|
<%= @registration_command %>
|
||||||
</span>
|
</span>
|
||||||
</code>
|
</code>
|
||||||
<button type="button" tabindex="-1">
|
<button type="button" tabindex="-1">
|
||||||
|
|
|
@ -6,12 +6,13 @@ defmodule Prymn.Repo.Migrations.CreateServers do
|
||||||
add :name, :string
|
add :name, :string
|
||||||
add :public_ip, :string
|
add :public_ip, :string
|
||||||
add :provider, :string
|
add :provider, :string
|
||||||
add :connection_status, :string
|
add :status, :string
|
||||||
add :registration_token, :binary
|
add :registration_token, :binary
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
create index("servers", [:registration_token], unique: true)
|
create index("servers", [:registration_token], unique: true)
|
||||||
|
create index("servers", [:public_ip], unique: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue