258 lines
6.5 KiB
Elixir
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
|