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, } iex> Prymn.Agents.start_connection("127.0.0.1") {:ok, } """ 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 {: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)) 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