dotfiles/app/lib/prymn/agents.ex

146 lines
4 KiB
Elixir
Raw Normal View History

defmodule Prymn.Agents do
@moduledoc ~S"""
Prymn Agents are programs that manage a remote client machine. Prymn backend
communicates with them using GRPC calls. GRPC connections are started using
the Prymn.Agents.ConnectionSupervisor and are book-kept using the
Prymn.Agents.Registry.
Agents are only valid when a `Prymn.Servers.Server` is considered registered.
"""
require Logger
alias Prymn.Agents.{Connection, Health, Agent}
alias PrymnProto.Prymn.Agent.Stub
alias PrymnProto.Prymn.{ExecRequest, SysUpdateRequest}
@doc """
Establish a connection with a Server if one does not already exist, and
return an Agent that interfaces with the rest of the system.
## Examples
iex> Prymn.Servers.get_server_by_ip!("127.0.0.1") |> Prymn.Agents.from_server()
%Prymn.Agents.Agent{}
"""
def from_server(%Prymn.Servers.Server{status: :registered} = server) do
case start_connection(server.public_ip) do
{:ok, _pid} -> Agent.new(server.public_ip)
{:error, error} -> {:error, error}
end
end
def from_server(%Prymn.Servers.Server{}) do
Logger.error("Tried to establish a connection with an unregistered server.")
{:error, :unauthorized_action}
end
@doc """
Establish a connection with a Server if one does not already exist for a
given App. Returns an [Agent] that interfaces with the rest of the system.
"""
def from_app(%Prymn.Apps.App{} = app) do
app = Prymn.Repo.preload(app, :server)
from_server(app.server)
end
@doc """
Starts a new connection with `host_address` if one does not exist.
## Examples
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
"""
def start_connection(host_address) do
spec = {Connection, host_address}
case DynamicSupervisor.start_child(Prymn.Agents.ConnectionSupervisor, spec) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
{:error, error} -> {:error, error}
end
end
@doc """
Subscribe to the host's Health using Phoenix.PubSub.
Broadcasted messages are the Health struct:
%Prymn.Agents.Health{}
"""
def subscribe_to_health(%Agent{} = agent) do
:ok = Health.subscribe(agent.host_address)
agent
end
def subscribe_to_health(host_address) do
:ok = Health.subscribe(host_address)
end
# TODO
# def alive?(host_address) do
# end
@doc """
Return the last known health status of the Agent, or `nil` if it doesn't
exist.
"""
def get_health(host_address) do
Health.lookup(host_address)
end
@doc """
Get the system's information (CPU, Memory usage, etc.).
"""
def get_sys_info(%Agent{} = agent) do
2024-02-01 15:34:26 +00:00
{:error, :unimplemented}
end
@doc """
Run a command.
"""
def exec(%Agent{} = agent, %ExecRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.exec(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
def exec(%Agent{} = agent, request) when is_map(request),
do: exec(agent, struct(ExecRequest, request))
@doc """
Perform a system update.
"""
def sys_update(%Agent{} = agent, %SysUpdateRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.sys_update(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
def sys_update(%Agent{} = agent, request) when is_map(request),
do: sys_update(agent, struct(SysUpdateRequest, request))
2023-12-16 20:40:57 +00:00
def terminal(%Agent{} = agent) do
# TODO: Find a better solve for bi-directional GRPC stream
with {:ok, channel} <- get_channel(agent),
stream <- Stub.terminal(channel) do
stream
else
{:error, error} -> {:error, error}
end
end
defp get_channel(%Agent{} = agent) do
case start_connection(agent.host_address) do
{:ok, pid} -> {:ok, Connection.get_channel(pid)}
{:error, error} -> {:error, error}
end
end
end