towerops/lib/towerops/agents.ex

258 lines
6.5 KiB
Elixir

defmodule Towerops.Agents do
@moduledoc """
The Agents context for managing remote SNMP polling agents.
This context provides functions for creating and managing agent authentication tokens,
assigning equipment to agents, and verifying agent credentials.
"""
import Ecto.Query, warn: false
alias Towerops.Agents.AgentAssignment
alias Towerops.Agents.AgentToken
alias Towerops.Repo
## Token management
@doc """
Creates a new agent token for an organization.
Returns {:ok, agent_token, token_string} where token_string is the plain token
that should be shown to the user ONCE and never stored.
## Examples
iex> create_agent_token(org_id, "Datacenter Agent")
{:ok, %AgentToken{}, "abc123..."}
iex> create_agent_token(nil, "")
{:error, %Ecto.Changeset{}}
"""
def create_agent_token(organization_id, name) do
{token, changeset} = AgentToken.build_token(organization_id, name)
case Repo.insert(changeset) do
{:ok, agent_token} -> {:ok, agent_token, token}
{:error, changeset} -> {:error, changeset}
end
end
@doc """
Lists all agent tokens for an organization.
Returns a list of agent tokens ordered by creation date (newest first).
## Examples
iex> list_organization_agent_tokens(org_id)
[%AgentToken{}, ...]
"""
def list_organization_agent_tokens(organization_id) do
AgentToken
|> where([t], t.organization_id == ^organization_id)
|> order_by([t], desc: t.inserted_at)
|> Repo.all()
end
@doc """
Gets a single agent token by ID.
Raises `Ecto.NoResultsError` if the token does not exist.
## Examples
iex> get_agent_token!(id)
%AgentToken{}
iex> get_agent_token!("nonexistent")
** (Ecto.NoResultsError)
"""
def get_agent_token!(id), do: Repo.get!(AgentToken, id)
@doc """
Updates the heartbeat timestamp and metadata for an agent token.
This is called automatically when an agent makes an API request.
## Examples
iex> update_agent_token_heartbeat(token_id, "192.168.1.1", %{version: "0.1.0"})
{1, nil}
"""
def update_agent_token_heartbeat(agent_token_id, ip_address, metadata \\ nil) do
now = DateTime.truncate(DateTime.utc_now(), :second)
# Only update metadata if explicitly provided (not nil and not empty)
updates =
case metadata do
nil -> [last_seen_at: now, last_ip: ip_address]
meta when map_size(meta) > 0 -> [last_seen_at: now, last_ip: ip_address, metadata: meta]
_ -> [last_seen_at: now, last_ip: ip_address]
end
AgentToken
|> where([t], t.id == ^agent_token_id)
|> Repo.update_all(set: updates)
end
@doc """
Verifies an agent token and returns the agent token record if valid.
Returns {:ok, agent_token} if the token is valid and enabled,
otherwise {:error, :invalid_token}.
## Examples
iex> verify_agent_token("valid_token")
{:ok, %AgentToken{}}
iex> verify_agent_token("invalid_token")
{:error, :invalid_token}
"""
def verify_agent_token(token) do
with {:ok, token_string} <- AgentToken.verify_token(token),
%AgentToken{} = agent_token <-
Repo.get_by(AgentToken, token: token_string, enabled: true) do
{:ok, agent_token}
else
_ -> {:error, :invalid_token}
end
end
@doc """
Revokes an agent token by setting enabled to false.
## Examples
iex> revoke_agent_token(id)
{:ok, %AgentToken{enabled: false}}
"""
def revoke_agent_token(id) do
AgentToken
|> Repo.get!(id)
|> Ecto.Changeset.change(enabled: false)
|> Repo.update()
end
## Assignment management
@doc """
Assigns equipment to an agent.
Creates an agent assignment that links equipment to an agent token.
Equipment can only be assigned to one agent at a time due to unique constraint.
## Examples
iex> assign_equipment_to_agent(agent_id, equipment_id)
{:ok, %AgentAssignment{}}
iex> assign_equipment_to_agent(agent_id, already_assigned_equipment_id)
{:error, %Ecto.Changeset{}}
"""
def assign_equipment_to_agent(agent_token_id, equipment_id) do
%AgentAssignment{}
|> AgentAssignment.changeset(%{
agent_token_id: agent_token_id,
equipment_id: equipment_id
})
|> Repo.insert()
end
@doc """
Removes the agent assignment for a piece of equipment.
## Examples
iex> unassign_equipment(equipment_id)
{1, nil}
"""
def unassign_equipment(equipment_id) do
AgentAssignment
|> where([a], a.equipment_id == ^equipment_id)
|> Repo.delete_all()
end
@doc """
Lists all equipment assigned to an agent.
Returns equipment records with preloaded site and SNMP device associations.
## Examples
iex> list_agent_equipment(agent_token_id)
[%Equipment{site: %Site{}, snmp_device: %Device{sensors: [...], interfaces: [...]}}]
"""
def list_agent_equipment(agent_token_id) do
Repo.all(
from(e in Towerops.Equipment.Equipment,
join: a in AgentAssignment,
on: a.equipment_id == e.id,
where: a.agent_token_id == ^agent_token_id and a.enabled == true,
preload: [:site, snmp_device: [:sensors, :interfaces]]
)
)
end
@doc """
Gets the agent assignment for a piece of equipment.
Returns the assignment record if it exists, nil otherwise.
## Examples
iex> get_equipment_assignment(equipment_id)
%AgentAssignment{}
iex> get_equipment_assignment(unassigned_equipment_id)
nil
"""
def get_equipment_assignment(equipment_id) do
Repo.get_by(AgentAssignment, equipment_id: equipment_id)
end
@doc """
Updates or creates an equipment assignment.
If agent_token_id is nil, removes any existing assignment.
Otherwise, creates or updates the assignment.
## Examples
iex> update_equipment_assignment(equipment_id, agent_token_id)
{:ok, %AgentAssignment{}}
iex> update_equipment_assignment(equipment_id, nil)
{:ok, nil}
"""
def update_equipment_assignment(equipment_id, nil) do
# Remove assignment if setting to nil
unassign_equipment(equipment_id)
{:ok, nil}
end
def update_equipment_assignment(equipment_id, agent_token_id) do
case get_equipment_assignment(equipment_id) do
nil ->
# Create new assignment
assign_equipment_to_agent(agent_token_id, equipment_id)
existing ->
# Update existing assignment
existing
|> AgentAssignment.changeset(%{agent_token_id: agent_token_id})
|> Repo.update()
end
end
end