towerops/lib/towerops_web/live/agent_live/index.ex

332 lines
12 KiB
Elixir

defmodule ToweropsWeb.AgentLive.Index do
@moduledoc false
use ToweropsWeb, :live_view
import ToweropsWeb.AgentLive.Helpers
alias Towerops.Accounts.Scope
alias Towerops.Agents
alias Towerops.Agents.Stats
alias Towerops.Settings
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_organization
current_scope = socket.assigns.current_scope
is_superuser = Scope.superuser?(current_scope)
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
# If superadmin (including when impersonating), also load cloud pollers and global default
{cloud_pollers, global_default_cloud_poller_id} =
if is_superuser do
{Agents.list_cloud_pollers(), Settings.get_global_default_cloud_poller()}
else
{[], nil}
end
# device counts for each agent (both direct and total with inheritance)
equipment_counts =
Map.new(agent_tokens, fn token ->
direct = Agents.count_assigned_devices(token.id)
total = length(Agents.list_agent_polling_targets(token.id))
{token.id, %{direct: direct, total: total}}
end)
# Get organization-wide agent health statistics
agent_health_stats = Stats.get_organization_agent_health(organization.id)
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
offline_agents = Stats.get_offline_agents(organization.id)
# Get agent image URL from config or use default
agent_image = Application.get_env(:towerops, :agent_docker_image, "towerops/agent:latest")
{:ok,
socket
|> assign(:page_title, "Remote Agents")
|> assign(:is_superuser, is_superuser)
|> assign(:agent_tokens, agent_tokens)
|> assign(:cloud_pollers, cloud_pollers)
|> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id)
|> assign(:selected_global_default, global_default_cloud_poller_id || "")
|> assign(:device_counts, equipment_counts)
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:offline_agents, offline_agents)
|> assign(:agent_image, agent_image)
|> assign(:new_token, nil)
|> assign(:show_token_modal, false)
|> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false}))}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
@impl true
def handle_event("create_agent", params, socket) do
{name, is_cloud_poller} = parse_agent_params(params)
with :ok <- validate_cloud_poller_permission(socket.assigns.current_scope, is_cloud_poller),
{:ok, agent_token, token} <- create_agent(socket.assigns.current_organization, name, is_cloud_poller) do
handle_agent_creation_success(socket, agent_token, token, is_cloud_poller)
else
{:error, :unauthorized} ->
{:noreply, put_flash(socket, :error, "Only superadmins can create cloud pollers")}
{:error, _changeset} ->
error_message =
if is_cloud_poller, do: "Failed to create cloud poller", else: "Failed to create agent"
{:noreply, put_flash(socket, :error, error_message)}
end
end
@impl true
def handle_event("close_token_modal", _params, socket) do
{:noreply,
socket
|> assign(:show_token_modal, false)
|> assign(:new_token, nil)}
end
@impl true
def handle_event("show_setup", %{"id" => id}, socket) do
agent_token = Agents.get_agent_token!(id)
{:noreply,
socket
|> assign(:new_token, %{agent_token: agent_token, token: agent_token.token})
|> assign(:show_token_modal, true)}
end
@impl true
def handle_event("regenerate_token", %{"id" => id}, socket) do
agent_token = Agents.get_agent_token!(id)
organization = socket.assigns.current_organization
# Revoke the old token and create a new one
case Agents.revoke_agent_token(id) do
{:ok, _} ->
case Agents.create_agent_token(organization.id, agent_token.name) do
{:ok, new_agent_token, token} ->
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
{:noreply,
socket
|> assign(:agent_tokens, agent_tokens)
|> assign(:new_token, %{agent_token: new_agent_token, token: token})
|> assign(:show_token_modal, true)
|> put_flash(:info, "New token generated successfully")}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to generate new token")}
end
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to revoke old token")}
end
end
@impl true
def handle_event("delete_agent", %{"id" => id}, socket) do
case Agents.delete_agent_token(id) do
{:ok, _} ->
organization = socket.assigns.current_organization
# If the deleted agent was the global default, clear it
if Scope.superuser?(socket.assigns.current_scope) &&
socket.assigns.global_default_cloud_poller_id == id do
Settings.set_global_default_cloud_poller(nil)
end
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
# Refresh cloud pollers list if superadmin (in case a cloud poller was deleted)
cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope)
# Reload global default cloud poller ID
global_default_cloud_poller_id =
if Scope.superuser?(socket.assigns.current_scope) do
Settings.get_global_default_cloud_poller()
else
socket.assigns.global_default_cloud_poller_id
end
# Recalculate device counts after deletion
equipment_counts =
Map.new(agent_tokens, fn token ->
direct = Agents.count_assigned_devices(token.id)
total = length(Agents.list_agent_polling_targets(token.id))
{token.id, %{direct: direct, total: total}}
end)
# Refresh health statistics
agent_health_stats = Stats.get_organization_agent_health(organization.id)
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
offline_agents = Stats.get_offline_agents(organization.id)
{:noreply,
socket
|> assign(:agent_tokens, agent_tokens)
|> assign(:cloud_pollers, cloud_pollers)
|> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id)
|> assign(:selected_global_default, global_default_cloud_poller_id || "")
|> assign(:device_counts, equipment_counts)
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:offline_agents, offline_agents)
|> put_flash(:info, "Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling.")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to delete agent")}
end
end
@impl true
def handle_event("update_selected_global_default", %{"agent_token_id" => agent_token_id}, socket) do
# Just update the selected value in the dropdown, don't save yet
{:noreply, assign(socket, :selected_global_default, agent_token_id)}
end
@impl true
def handle_event("save_global_default", _params, socket) do
current_scope = socket.assigns.current_scope
if Scope.superuser?(current_scope) do
agent_token_id = socket.assigns.selected_global_default
# Handle empty string as nil
agent_token_id = if agent_token_id == "", do: nil, else: agent_token_id
# Validate that the agent exists if not nil
case validate_and_save_global_default(agent_token_id) do
{:ok, validated_id} ->
handle_global_default_success(socket, validated_id)
{:error, :agent_not_found} ->
{:noreply, put_flash(socket, :error, "Selected agent no longer exists. Please choose another agent.")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update global default cloud poller")}
end
else
{:noreply, put_flash(socket, :error, "Only superadmins can set the global default cloud poller")}
end
end
defp apply_action(socket, :index, _params) do
socket
end
defp parse_agent_params(params) do
case params do
%{"agent_form" => %{"name" => n, "is_cloud_poller" => cp}} ->
{n, cp in ["true", "on", true]}
%{"agent_form" => %{"name" => n}} ->
{n, false}
%{"name" => n, "is_cloud_poller" => cp} ->
{n, cp in ["true", "on", true]}
%{"name" => n} ->
{n, false}
end
end
defp validate_cloud_poller_permission(scope, is_cloud_poller) do
if is_cloud_poller && !Scope.superuser?(scope) do
{:error, :unauthorized}
else
:ok
end
end
defp create_agent(organization, name, is_cloud_poller) do
if is_cloud_poller do
Agents.create_cloud_poller(name)
else
Agents.create_agent_token(organization.id, name)
end
end
defp handle_agent_creation_success(socket, agent_token, token, is_cloud_poller) do
organization = socket.assigns.current_organization
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope)
equipment_counts = calculate_device_counts(agent_tokens)
agent_health_stats = Stats.get_organization_agent_health(organization.id)
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
offline_agents = Stats.get_offline_agents(organization.id)
success_message = get_success_message(is_cloud_poller)
{:noreply,
socket
|> assign(:agent_tokens, agent_tokens)
|> assign(:cloud_pollers, cloud_pollers)
|> assign(:device_counts, equipment_counts)
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:offline_agents, offline_agents)
|> assign(:new_token, %{agent_token: agent_token, token: token})
|> assign(:show_token_modal, true)
|> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false}))
|> put_flash(:info, success_message)}
end
defp load_cloud_pollers_if_superuser(scope) do
if Scope.superuser?(scope), do: Agents.list_cloud_pollers(), else: []
end
defp calculate_device_counts(agent_tokens) do
Map.new(agent_tokens, fn t ->
direct = Agents.count_assigned_devices(t.id)
total = length(Agents.list_agent_polling_targets(t.id))
{t.id, %{direct: direct, total: total}}
end)
end
defp get_success_message(is_cloud_poller) do
if is_cloud_poller,
do: "Cloud poller created successfully",
else: "Agent created successfully"
end
defp validate_and_save_global_default(nil) do
# Clearing the default is always valid
case Settings.set_global_default_cloud_poller(nil) do
{:ok, _} -> {:ok, nil}
error -> error
end
end
defp validate_and_save_global_default(agent_token_id) do
# Verify the agent exists before saving
%Agents.AgentToken{} = Agents.get_agent_token!(agent_token_id)
case Settings.set_global_default_cloud_poller(agent_token_id) do
{:ok, _} -> {:ok, agent_token_id}
error -> error
end
rescue
Ecto.NoResultsError ->
{:error, :agent_not_found}
end
defp handle_global_default_success(socket, agent_token_id) do
message =
if agent_token_id,
do: "Global default cloud poller set successfully",
else: "Global default cloud poller cleared"
{:noreply,
socket
|> assign(:global_default_cloud_poller_id, agent_token_id)
|> assign(:selected_global_default, agent_token_id || "")
|> put_flash(:info, message)}
end
end