rename equipment to device
This commit is contained in:
parent
c88f2bbf96
commit
a810e75fc4
95 changed files with 3062 additions and 2950 deletions
|
|
@ -148,7 +148,7 @@ defmodule Towerops.Admin do
|
|||
ip_address: ip_address
|
||||
})
|
||||
|
||||
# Delete organization (cascades to memberships, sites, equipment)
|
||||
# Delete organization (cascades to memberships, sites, device)
|
||||
Repo.delete!(org)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ defmodule Towerops.Agents do
|
|||
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.
|
||||
assigning devices to agents, and verifying agent credentials.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
|
||||
## Token management
|
||||
|
|
@ -58,18 +58,18 @@ defmodule Towerops.Agents do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Counts the number of equipment directly assigned to an agent.
|
||||
Counts the number of devices directly assigned to an agent.
|
||||
|
||||
This only counts equipment with explicit assignments, not equipment
|
||||
This only counts devices with explicit assignments, not devices
|
||||
that inherit the agent from site or organization defaults.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> count_assigned_equipment(agent_token_id)
|
||||
iex> count_assigned_devices(agent_token_id)
|
||||
5
|
||||
|
||||
"""
|
||||
def count_assigned_equipment(agent_token_id) do
|
||||
def count_assigned_devices(agent_token_id) do
|
||||
AgentAssignment
|
||||
|> where([a], a.agent_token_id == ^agent_token_id and a.enabled == true)
|
||||
|> Repo.aggregate(:count)
|
||||
|
|
@ -165,60 +165,60 @@ defmodule Towerops.Agents do
|
|||
## Assignment management
|
||||
|
||||
@doc """
|
||||
Assigns equipment to an agent.
|
||||
Assigns device 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.
|
||||
Creates an agent assignment that links device to an agent token.
|
||||
Devices can only be assigned to one agent at a time due to unique constraint.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> assign_equipment_to_agent(agent_id, equipment_id)
|
||||
iex> assign_device_to_agent(agent_id, device_id)
|
||||
{:ok, %AgentAssignment{}}
|
||||
|
||||
iex> assign_equipment_to_agent(agent_id, already_assigned_equipment_id)
|
||||
iex> assign_device_to_agent(agent_id, already_assigned_device_id)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def assign_equipment_to_agent(agent_token_id, equipment_id) do
|
||||
def assign_device_to_agent(agent_token_id, device_id) do
|
||||
%AgentAssignment{}
|
||||
|> AgentAssignment.changeset(%{
|
||||
agent_token_id: agent_token_id,
|
||||
equipment_id: equipment_id
|
||||
device_id: device_id
|
||||
})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the agent assignment for a piece of equipment.
|
||||
Removes the agent assignment for a piece of device.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> unassign_equipment(equipment_id)
|
||||
iex> unassign_device(device_id)
|
||||
{1, nil}
|
||||
|
||||
"""
|
||||
def unassign_equipment(equipment_id) do
|
||||
def unassign_device(device_id) do
|
||||
AgentAssignment
|
||||
|> where([a], a.equipment_id == ^equipment_id)
|
||||
|> where([a], a.device_id == ^device_id)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all equipment assigned to an agent.
|
||||
Lists all devices assigned to an agent.
|
||||
|
||||
Returns equipment records with preloaded site and SNMP device associations.
|
||||
Returns device records with preloaded site and SNMP device associations.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_agent_equipment(agent_token_id)
|
||||
[%Equipment{site: %Site{}, snmp_device: %Device{sensors: [...], interfaces: [...]}}]
|
||||
iex> list_agent_devices(agent_token_id)
|
||||
[%Device{site: %Site{}, snmp_device: %Device{sensors: [...], interfaces: [...]}}]
|
||||
|
||||
"""
|
||||
def list_agent_equipment(agent_token_id) do
|
||||
def list_agent_devices(agent_token_id) do
|
||||
Repo.all(
|
||||
from(e in Equipment,
|
||||
from(e in Device,
|
||||
join: a in AgentAssignment,
|
||||
on: a.equipment_id == e.id,
|
||||
on: a.device_id == e.id,
|
||||
where: a.agent_token_id == ^agent_token_id and a.enabled == true,
|
||||
preload: [:site, snmp_device: [:sensors, :interfaces]]
|
||||
)
|
||||
|
|
@ -226,31 +226,31 @@ defmodule Towerops.Agents do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Lists all equipment that should be polled by an agent.
|
||||
Lists all devices that should be polled by an agent.
|
||||
|
||||
This includes equipment that is:
|
||||
This includes devices that is:
|
||||
1. Directly assigned to this agent
|
||||
2. At a site that has this agent as default
|
||||
3. In an organization that has this agent as default (and not overridden by site or equipment)
|
||||
3. In an organization that has this agent as default (and not overridden by site or device)
|
||||
|
||||
Returns equipment records with preloaded associations, filtered to only SNMP-enabled equipment.
|
||||
Returns device records with preloaded associations, filtered to only SNMP-enabled devices.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_agent_polling_targets(agent_token_id)
|
||||
[%Equipment{snmp_enabled: true, site: %Site{}, snmp_device: %Device{sensors: [...], interfaces: [...]}}]
|
||||
[%Device{snmp_enabled: true, site: %Site{}, snmp_device: %Device{sensors: [...], interfaces: [...]}}]
|
||||
|
||||
"""
|
||||
@spec list_agent_polling_targets(Ecto.UUID.t()) :: [Equipment.t()]
|
||||
@spec list_agent_polling_targets(Ecto.UUID.t()) :: [Device.t()]
|
||||
def list_agent_polling_targets(agent_token_id) do
|
||||
# Use SQL to filter equipment by effective agent assignment
|
||||
# This avoids loading all equipment into memory and filtering in application code
|
||||
# Use SQL to filter devices by effective agent assignment
|
||||
# This avoids loading all devices into memory and filtering in application code
|
||||
Repo.all(
|
||||
from(e in Equipment,
|
||||
from(e in Device,
|
||||
join: s in assoc(e, :site),
|
||||
join: o in assoc(s, :organization),
|
||||
left_join: aa in AgentAssignment,
|
||||
on: aa.equipment_id == e.id and aa.enabled == true,
|
||||
on: aa.device_id == e.id and aa.enabled == true,
|
||||
where:
|
||||
e.snmp_enabled == true and
|
||||
(aa.agent_token_id == ^agent_token_id or
|
||||
|
|
@ -267,49 +267,49 @@ defmodule Towerops.Agents do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Gets the agent assignment for a piece of equipment.
|
||||
Gets the agent assignment for a piece of device.
|
||||
|
||||
Returns the assignment record if it exists, nil otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_equipment_assignment(equipment_id)
|
||||
iex> get_device_assignment(device_id)
|
||||
%AgentAssignment{}
|
||||
|
||||
iex> get_equipment_assignment(unassigned_equipment_id)
|
||||
iex> get_device_assignment(unassigned_device_id)
|
||||
nil
|
||||
|
||||
"""
|
||||
def get_equipment_assignment(equipment_id) do
|
||||
Repo.get_by(AgentAssignment, equipment_id: equipment_id)
|
||||
def get_device_assignment(device_id) do
|
||||
Repo.get_by(AgentAssignment, device_id: device_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates or creates an equipment assignment.
|
||||
Updates or creates an device 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)
|
||||
iex> update_device_assignment(device_id, agent_token_id)
|
||||
{:ok, %AgentAssignment{}}
|
||||
|
||||
iex> update_equipment_assignment(equipment_id, nil)
|
||||
iex> update_device_assignment(device_id, nil)
|
||||
{:ok, nil}
|
||||
|
||||
"""
|
||||
def update_equipment_assignment(equipment_id, nil) do
|
||||
def update_device_assignment(device_id, nil) do
|
||||
# Remove assignment if setting to nil
|
||||
unassign_equipment(equipment_id)
|
||||
unassign_device(device_id)
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
def update_equipment_assignment(equipment_id, agent_token_id) do
|
||||
case get_equipment_assignment(equipment_id) do
|
||||
def update_device_assignment(device_id, agent_token_id) do
|
||||
case get_device_assignment(device_id) do
|
||||
nil ->
|
||||
# Create new assignment
|
||||
assign_equipment_to_agent(agent_token_id, equipment_id)
|
||||
assign_device_to_agent(agent_token_id, device_id)
|
||||
|
||||
existing ->
|
||||
# Update existing assignment
|
||||
|
|
@ -320,36 +320,36 @@ defmodule Towerops.Agents do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Gets the effective agent token for a piece of equipment.
|
||||
Gets the effective agent token for a piece of device.
|
||||
|
||||
Resolves the agent token using the following hierarchy:
|
||||
1. Equipment's direct agent assignment (highest priority)
|
||||
1. Device's direct agent assignment (highest priority)
|
||||
2. Site's agent_token_id
|
||||
3. Organization's default_agent_token_id (lowest priority)
|
||||
|
||||
Returns the agent_token_id if found, nil otherwise.
|
||||
|
||||
The equipment must be preloaded with site: [organization: :default_agent_token].
|
||||
The device must be preloaded with site: [organization: :default_agent_token].
|
||||
|
||||
## Examples
|
||||
|
||||
iex> equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
iex> get_effective_agent_token(equipment)
|
||||
iex> device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
iex> get_effective_agent_token(device)
|
||||
"agent-token-uuid"
|
||||
|
||||
iex> get_effective_agent_token(equipment_without_any_assignment)
|
||||
nil
|
||||
|
||||
"""
|
||||
def get_effective_agent_token(%Equipment{} = equipment) do
|
||||
# 1. Check for direct equipment assignment
|
||||
case get_equipment_assignment(equipment.id) do
|
||||
def get_effective_agent_token(%Device{} = device) do
|
||||
# device assignment
|
||||
case get_device_assignment(device.id) do
|
||||
%AgentAssignment{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
|
||||
agent_token_id
|
||||
|
||||
_ ->
|
||||
# 2. Check site's agent token
|
||||
site = equipment.site
|
||||
site = device.site
|
||||
|
||||
case site do
|
||||
%{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
|
||||
|
|
@ -370,29 +370,29 @@ defmodule Towerops.Agents do
|
|||
Gets information about where the effective agent token comes from.
|
||||
|
||||
Returns a tuple of {agent_token_id, source} where source is one of:
|
||||
- :equipment - Direct equipment assignment
|
||||
- :device - Direct device assignment
|
||||
- :site - Inherited from site
|
||||
- :organization - Inherited from organization
|
||||
- :none - No agent assigned
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_effective_agent_token_with_source(equipment)
|
||||
{"token-id", :equipment}
|
||||
iex> get_effective_agent_token_with_source(device)
|
||||
{"token-id", :device}
|
||||
|
||||
iex> get_effective_agent_token_with_source(equipment_no_assignment)
|
||||
{nil, :none}
|
||||
|
||||
"""
|
||||
def get_effective_agent_token_with_source(%Equipment{} = equipment) do
|
||||
# 1. Check for direct equipment assignment
|
||||
case get_equipment_assignment(equipment.id) do
|
||||
def get_effective_agent_token_with_source(%Device{} = device) do
|
||||
# device assignment
|
||||
case get_device_assignment(device.id) do
|
||||
%AgentAssignment{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
|
||||
{agent_token_id, :equipment}
|
||||
{agent_token_id, :device}
|
||||
|
||||
_ ->
|
||||
# 2. Check site's agent token
|
||||
site = equipment.site
|
||||
site = device.site
|
||||
|
||||
case site do
|
||||
%{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule Towerops.Agents.AgentAssignment do
|
||||
@moduledoc """
|
||||
Schema for assigning equipment to remote agents.
|
||||
Schema for assigning devices to remote agents.
|
||||
|
||||
Each equipment can only be assigned to one agent at a time (unique constraint on equipment_id).
|
||||
Each device can only be assigned to one agent at a time (unique constraint on device_id).
|
||||
This ensures that polling is not duplicated across multiple agents.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
|
@ -11,7 +11,7 @@ defmodule Towerops.Agents.AgentAssignment do
|
|||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
|
@ -19,7 +19,7 @@ defmodule Towerops.Agents.AgentAssignment do
|
|||
field :enabled, :boolean, default: true
|
||||
|
||||
belongs_to :agent_token, AgentToken
|
||||
belongs_to :equipment, Equipment
|
||||
belongs_to :device, Device
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
|
@ -29,8 +29,8 @@ defmodule Towerops.Agents.AgentAssignment do
|
|||
enabled: boolean(),
|
||||
agent_token_id: Ecto.UUID.t(),
|
||||
agent_token: NotLoaded.t() | AgentToken.t(),
|
||||
equipment_id: Ecto.UUID.t(),
|
||||
equipment: NotLoaded.t() | Equipment.t(),
|
||||
device_id: Ecto.UUID.t(),
|
||||
device: NotLoaded.t() | Device.t(),
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
|
@ -40,16 +40,16 @@ defmodule Towerops.Agents.AgentAssignment do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> changeset(%AgentAssignment{}, %{agent_token_id: token_id, equipment_id: equip_id})
|
||||
iex> changeset(%AgentAssignment{}, %{agent_token_id: token_id, device_id: equip_id})
|
||||
%Ecto.Changeset{valid?: true}
|
||||
|
||||
"""
|
||||
def changeset(assignment, attrs) do
|
||||
assignment
|
||||
|> cast(attrs, [:agent_token_id, :equipment_id, :enabled])
|
||||
|> validate_required([:agent_token_id, :equipment_id])
|
||||
|> unique_constraint([:equipment_id])
|
||||
|> cast(attrs, [:agent_token_id, :device_id, :enabled])
|
||||
|> validate_required([:agent_token_id, :device_id])
|
||||
|> unique_constraint([:device_id])
|
||||
|> foreign_key_constraint(:agent_token_id)
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> foreign_key_constraint(:device_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Towerops.Agents.Stats do
|
|||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Snmp.SensorReading
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ defmodule Towerops.Agents.Stats do
|
|||
|
||||
Returns a list of agents with their health metrics:
|
||||
- Online status (last_seen_at within 5 minutes)
|
||||
- Number of assigned equipment
|
||||
- Number of assigned devices
|
||||
- Last metric submission time
|
||||
- Version information
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ defmodule Towerops.Agents.Stats do
|
|||
name: "DC1 Agent",
|
||||
online: true,
|
||||
last_seen_at: ~U[2026-01-14 12:00:00Z],
|
||||
equipment_count: 15,
|
||||
device_count: 15,
|
||||
last_metric_at: ~U[2026-01-14 12:00:30Z],
|
||||
version: "0.1.0",
|
||||
hostname: "agent-dc1"
|
||||
|
|
@ -57,8 +57,8 @@ defmodule Towerops.Agents.Stats do
|
|||
|
||||
agents = Repo.all(query)
|
||||
|
||||
# Get equipment counts for all agents in a single query
|
||||
equipment_counts = get_batch_equipment_counts(Enum.map(agents, & &1.id))
|
||||
# device counts for all agents in a single query
|
||||
device_counts = get_batch_device_counts(Enum.map(agents, & &1.id))
|
||||
|
||||
# Get last metric times for all agents in a single query
|
||||
last_metrics = get_batch_last_metric_times(Enum.map(agents, & &1.id))
|
||||
|
|
@ -66,15 +66,15 @@ defmodule Towerops.Agents.Stats do
|
|||
# Combine data for each agent
|
||||
Enum.map(agents, fn agent ->
|
||||
agent
|
||||
|> Map.put(:equipment_count, Map.get(equipment_counts, agent.id, 0))
|
||||
|> Map.put(:device_count, Map.get(device_counts, agent.id, 0))
|
||||
|> Map.put(:last_metric_at, Map.get(last_metrics, agent.id))
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get equipment count breakdown by agent assignment source.
|
||||
Get devices count breakdown by agent assignment source.
|
||||
|
||||
Returns counts for equipment assigned via:
|
||||
Returns counts for device assigned via:
|
||||
- Direct assignment
|
||||
- Site default
|
||||
- Organization default
|
||||
|
|
@ -92,21 +92,21 @@ defmodule Towerops.Agents.Stats do
|
|||
"""
|
||||
def get_equipment_assignment_breakdown(organization_id) do
|
||||
# Use a single SQL query with CASE to determine assignment source
|
||||
# This avoids loading all equipment into memory and calling get_effective_agent_token_with_source
|
||||
# device into memory and calling get_effective_agent_token_with_source
|
||||
results =
|
||||
Repo.all(
|
||||
from(e in Equipment,
|
||||
from(e in Device,
|
||||
join: s in assoc(e, :site),
|
||||
join: o in assoc(s, :organization),
|
||||
left_join: aa in AgentAssignment,
|
||||
on: aa.equipment_id == e.id and aa.enabled == true,
|
||||
on: aa.device_id == e.id and aa.enabled == true,
|
||||
where: s.organization_id == ^organization_id and e.snmp_enabled == true,
|
||||
select: %{
|
||||
source:
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? IS NOT NULL THEN 'equipment'
|
||||
WHEN ? IS NOT NULL THEN 'device'
|
||||
WHEN ? IS NOT NULL THEN 'site'
|
||||
WHEN ? IS NOT NULL THEN 'organization'
|
||||
ELSE 'cloud'
|
||||
|
|
@ -123,7 +123,7 @@ defmodule Towerops.Agents.Stats do
|
|||
# Count by assignment source
|
||||
Enum.reduce(results, %{direct: 0, site: 0, organization: 0, cloud: 0}, fn %{source: source}, acc ->
|
||||
case source do
|
||||
"equipment" -> %{acc | direct: acc.direct + 1}
|
||||
"device" -> %{acc | direct: acc.direct + 1}
|
||||
"site" -> %{acc | site: acc.site + 1}
|
||||
"organization" -> %{acc | organization: acc.organization + 1}
|
||||
"cloud" -> %{acc | cloud: acc.cloud + 1}
|
||||
|
|
@ -161,7 +161,7 @@ defmodule Towerops.Agents.Stats do
|
|||
from(sr in SensorReading,
|
||||
join: s in assoc(sr, :sensor),
|
||||
join: d in assoc(s, :snmp_device),
|
||||
join: e in assoc(d, :equipment),
|
||||
join: e in assoc(d, :device),
|
||||
where: sr.checked_at > ^twenty_four_hours_ago and e.id in subquery(polling_targets_subquery(agent_token_id)),
|
||||
select: count(sr.id)
|
||||
)
|
||||
|
|
@ -173,7 +173,7 @@ defmodule Towerops.Agents.Stats do
|
|||
from(is in Towerops.Snmp.InterfaceStat,
|
||||
join: i in assoc(is, :interface),
|
||||
join: d in assoc(i, :snmp_device),
|
||||
join: e in assoc(d, :equipment),
|
||||
join: e in assoc(d, :device),
|
||||
where: is.checked_at > ^twenty_four_hours_ago and e.id in subquery(polling_targets_subquery(agent_token_id)),
|
||||
select: count(is.id)
|
||||
)
|
||||
|
|
@ -187,7 +187,7 @@ defmodule Towerops.Agents.Stats do
|
|||
from(sr in SensorReading,
|
||||
join: s in assoc(sr, :sensor),
|
||||
join: d in assoc(s, :snmp_device),
|
||||
join: e in assoc(d, :equipment),
|
||||
join: e in assoc(d, :device),
|
||||
where: e.id in subquery(polling_targets_subquery(agent_token_id)),
|
||||
select: max(sr.checked_at)
|
||||
)
|
||||
|
|
@ -231,13 +231,13 @@ defmodule Towerops.Agents.Stats do
|
|||
@doc """
|
||||
Get agents with high load (>50 devices).
|
||||
|
||||
Identifies agents that may need to be split or have their equipment redistributed.
|
||||
Identifies agents that may need to be split or have their devices redistributed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_high_load_agents(org_id)
|
||||
[
|
||||
%{id: "uuid", name: "DC1 Agent", equipment_count: 75}
|
||||
%{id: "uuid", name: "DC1 Agent", device_count: 75}
|
||||
]
|
||||
"""
|
||||
def get_high_load_agents(organization_id, threshold \\ 50) do
|
||||
|
|
@ -249,20 +249,20 @@ defmodule Towerops.Agents.Stats do
|
|||
|
||||
agents = Repo.all(query)
|
||||
|
||||
# Get equipment count for each agent, filter by threshold, and sort
|
||||
# device count for each agent, filter by threshold, and sort
|
||||
agents
|
||||
|> Enum.map(fn agent ->
|
||||
equipment_count = get_agent_equipment_count(agent.id)
|
||||
Map.put(agent, :equipment_count, equipment_count)
|
||||
device_count = get_agent_device_count(agent.id)
|
||||
Map.put(agent, :device_count, device_count)
|
||||
end)
|
||||
|> Enum.filter(fn agent -> agent.equipment_count > threshold end)
|
||||
|> Enum.sort_by(& &1.equipment_count, :desc)
|
||||
|> Enum.filter(fn agent -> agent.device_count > threshold end)
|
||||
|> Enum.sort_by(& &1.device_count, :desc)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get equipment that have no agent assigned and are not polling.
|
||||
Get devices that have no agent assigned and are not polling.
|
||||
|
||||
Identifies orphaned equipment that may need agent assignment.
|
||||
Identifies orphaned devices that may need agent assignment.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -272,15 +272,15 @@ defmodule Towerops.Agents.Stats do
|
|||
]
|
||||
"""
|
||||
def get_unmonitored_equipment(organization_id) do
|
||||
# Use SQL to determine which equipment has no agent assigned
|
||||
# This avoids loading all equipment into memory
|
||||
# device has no agent assigned
|
||||
# device into memory
|
||||
Repo.all(
|
||||
from(e in Equipment,
|
||||
from(e in Device,
|
||||
join: s in assoc(e, :site),
|
||||
join: o in assoc(s, :organization),
|
||||
left_join: aa in AgentAssignment,
|
||||
on: aa.equipment_id == e.id and aa.enabled == true,
|
||||
# Equipment has no agent via any method
|
||||
on: aa.device_id == e.id and aa.enabled == true,
|
||||
# Device has no agent via any method
|
||||
where:
|
||||
s.organization_id == ^organization_id and
|
||||
e.snmp_enabled == true and
|
||||
|
|
@ -303,40 +303,40 @@ defmodule Towerops.Agents.Stats do
|
|||
defp polling_targets_subquery(agent_token_id) do
|
||||
# This is a simplified version - in production you'd want to use
|
||||
# Agents.list_agent_polling_targets/1 logic here
|
||||
from(e in Equipment,
|
||||
from(e in Device,
|
||||
join: a in AgentAssignment,
|
||||
on: a.equipment_id == e.id,
|
||||
on: a.device_id == e.id,
|
||||
where: a.agent_token_id == ^agent_token_id and a.enabled == true and e.snmp_enabled == true,
|
||||
select: e.id
|
||||
)
|
||||
end
|
||||
|
||||
defp get_agent_equipment_count(agent_token_id) do
|
||||
# Count equipment by using list_agent_polling_targets
|
||||
defp get_agent_device_count(agent_token_id) do
|
||||
# device by using list_agent_polling_targets
|
||||
# This properly handles hierarchical assignment
|
||||
agent_token_id
|
||||
|> Towerops.Agents.list_agent_polling_targets()
|
||||
|> length()
|
||||
end
|
||||
|
||||
defp get_batch_equipment_counts(agent_token_ids) do
|
||||
defp get_batch_device_counts(agent_token_ids) do
|
||||
# For now, use the existing function per agent since list_agent_polling_targets
|
||||
# handles complex hierarchical logic in application code
|
||||
# TODO: Optimize this with a pure SQL solution if it becomes a bottleneck
|
||||
Map.new(agent_token_ids, fn id -> {id, get_agent_equipment_count(id)} end)
|
||||
Map.new(agent_token_ids, fn id -> {id, get_agent_device_count(id)} end)
|
||||
end
|
||||
|
||||
defp get_batch_last_metric_times(agent_token_ids) do
|
||||
# Get last metric time for all agents in a single query
|
||||
# Group by equipment ID, then map to agent via assignments
|
||||
# device ID, then map to agent via assignments
|
||||
results =
|
||||
Repo.all(
|
||||
from(sr in SensorReading,
|
||||
join: s in assoc(sr, :sensor),
|
||||
join: d in assoc(s, :snmp_device),
|
||||
join: e in assoc(d, :equipment),
|
||||
join: e in assoc(d, :device),
|
||||
join: aa in AgentAssignment,
|
||||
on: aa.equipment_id == e.id and aa.enabled == true,
|
||||
on: aa.device_id == e.id and aa.enabled == true,
|
||||
where: aa.agent_token_id in ^agent_token_ids,
|
||||
group_by: aa.agent_token_id,
|
||||
select: {aa.agent_token_id, max(sr.checked_at)}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Towerops.Alerts do
|
|||
alias Towerops.Repo
|
||||
|
||||
@doc """
|
||||
Creates an alert for equipment status change.
|
||||
Creates an alert for device status change.
|
||||
"""
|
||||
def create_alert(attrs) do
|
||||
%Alert{}
|
||||
|
|
@ -18,15 +18,15 @@ defmodule Towerops.Alerts do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of alerts for an equipment.
|
||||
Returns the list of alerts for an device.
|
||||
"""
|
||||
def list_equipment_alerts(equipment_id, limit \\ 100) do
|
||||
def list_devices_alerts(device_id, limit \\ 100) do
|
||||
Repo.all(
|
||||
from(a in Alert,
|
||||
where: a.equipment_id == ^equipment_id,
|
||||
where: a.device_id == ^device_id,
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [:equipment, :acknowledged_by]
|
||||
preload: [:device, :acknowledged_by]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
@ -38,13 +38,13 @@ defmodule Towerops.Alerts do
|
|||
def list_organization_active_alerts(organization_id) do
|
||||
Repo.all(
|
||||
from(a in Alert,
|
||||
join: e in assoc(a, :equipment),
|
||||
join: e in assoc(a, :device),
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
where: a.alert_type == :equipment_down,
|
||||
where: a.alert_type == :device_down,
|
||||
where: is_nil(a.resolved_at),
|
||||
order_by: [desc: a.triggered_at],
|
||||
preload: [equipment: {e, site: s}, acknowledged_by: :acknowledged_by]
|
||||
preload: [:acknowledged_by, device: {e, site: s}]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
@ -55,10 +55,10 @@ defmodule Towerops.Alerts do
|
|||
def count_active_alerts(organization_id) do
|
||||
Repo.aggregate(
|
||||
from(a in Alert,
|
||||
join: e in assoc(a, :equipment),
|
||||
join: e in assoc(a, :device),
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
where: a.alert_type == :equipment_down,
|
||||
where: a.alert_type == :device_down,
|
||||
where: is_nil(a.resolved_at)
|
||||
),
|
||||
:count
|
||||
|
|
@ -84,12 +84,12 @@ defmodule Towerops.Alerts do
|
|||
|
||||
query =
|
||||
from(a in Alert,
|
||||
join: e in assoc(a, :equipment),
|
||||
join: e in assoc(a, :device),
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [equipment: {e, site: s}, acknowledged_by: []]
|
||||
preload: [device: {e, site: s}, acknowledged_by: []]
|
||||
)
|
||||
|
||||
query =
|
||||
|
|
@ -115,7 +115,7 @@ defmodule Towerops.Alerts do
|
|||
where: s.organization_id == ^organization_id,
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [equipment: {e, site: s}, acknowledged_by: []]
|
||||
preload: [device: {e, site: s}, acknowledged_by: []]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
@ -126,7 +126,7 @@ defmodule Towerops.Alerts do
|
|||
def get_alert!(id) do
|
||||
Alert
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload([:equipment, :acknowledged_by])
|
||||
|> Repo.preload([:device, :acknowledged_by])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -164,12 +164,12 @@ defmodule Towerops.Alerts do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Checks if there's an active alert of the same type for the equipment.
|
||||
Checks if there's an active alert of the same type for the device.
|
||||
"""
|
||||
def has_active_alert?(equipment_id, alert_type) do
|
||||
def has_active_alert?(device_id, alert_type) do
|
||||
Repo.exists?(
|
||||
from(a in Alert,
|
||||
where: a.equipment_id == ^equipment_id,
|
||||
where: a.device_id == ^device_id,
|
||||
where: a.alert_type == ^alert_type,
|
||||
where: is_nil(a.resolved_at)
|
||||
)
|
||||
|
|
@ -177,12 +177,12 @@ defmodule Towerops.Alerts do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Gets the latest active alert of a specific type for equipment.
|
||||
Gets the latest active alert of a specific type for device.
|
||||
"""
|
||||
def get_active_alert(equipment_id, alert_type) do
|
||||
def get_active_alert(device_id, alert_type) do
|
||||
Repo.one(
|
||||
from(a in Alert,
|
||||
where: a.equipment_id == ^equipment_id,
|
||||
where: a.device_id == ^device_id,
|
||||
where: a.alert_type == ^alert_type,
|
||||
where: is_nil(a.resolved_at),
|
||||
order_by: [desc: a.triggered_at],
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@ defmodule Towerops.Alerts.Alert do
|
|||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Accounts.User
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "alerts" do
|
||||
field :alert_type, Ecto.Enum, values: [:equipment_down, :equipment_up]
|
||||
field :alert_type, Ecto.Enum, values: [:device_down, :device_up]
|
||||
field :triggered_at, :utc_datetime
|
||||
field :acknowledged_at, :utc_datetime
|
||||
field :resolved_at, :utc_datetime
|
||||
field :email_sent_at, :utc_datetime
|
||||
field :message, :string
|
||||
|
||||
belongs_to :equipment, Equipment
|
||||
belongs_to :device, Device
|
||||
belongs_to :acknowledged_by, User
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
|
|
@ -26,14 +26,14 @@ defmodule Towerops.Alerts.Alert do
|
|||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
alert_type: :equipment_down | :equipment_up,
|
||||
alert_type: :device_down | :device_up,
|
||||
triggered_at: DateTime.t(),
|
||||
acknowledged_at: DateTime.t() | nil,
|
||||
resolved_at: DateTime.t() | nil,
|
||||
email_sent_at: DateTime.t() | nil,
|
||||
message: String.t() | nil,
|
||||
equipment_id: Ecto.UUID.t(),
|
||||
equipment: NotLoaded.t() | Equipment.t(),
|
||||
device_id: Ecto.UUID.t(),
|
||||
device: NotLoaded.t() | Devices.t(),
|
||||
acknowledged_by_id: Ecto.UUID.t() | nil,
|
||||
acknowledged_by: NotLoaded.t() | User.t() | nil,
|
||||
inserted_at: DateTime.t(),
|
||||
|
|
@ -44,7 +44,7 @@ defmodule Towerops.Alerts.Alert do
|
|||
def changeset(alert, attrs) do
|
||||
alert
|
||||
|> cast(attrs, [
|
||||
:equipment_id,
|
||||
:device_id,
|
||||
:alert_type,
|
||||
:triggered_at,
|
||||
:acknowledged_at,
|
||||
|
|
@ -53,8 +53,8 @@ defmodule Towerops.Alerts.Alert do
|
|||
:email_sent_at,
|
||||
:message
|
||||
])
|
||||
|> validate_required([:equipment_id, :alert_type, :triggered_at])
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> validate_required([:device_id, :alert_type, :triggered_at])
|
||||
|> foreign_key_constraint(:device_id)
|
||||
|> foreign_key_constraint(:acknowledged_by_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.Alerts.AlertNotifier do
|
|||
import Swoosh.Email
|
||||
|
||||
alias Towerops.Alerts.Alert
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Mailer
|
||||
alias Towerops.Organizations
|
||||
|
||||
|
|
@ -20,12 +20,12 @@ defmodule Towerops.Alerts.AlertNotifier do
|
|||
@spec deliver_alert_notification(Alert.t()) ::
|
||||
{:ok, [{:ok, Swoosh.Email.t()} | {:error, term()}]}
|
||||
def deliver_alert_notification(%Alert{} = alert) do
|
||||
equipment =
|
||||
alert.equipment_id
|
||||
|> Equipment.get_equipment!()
|
||||
device =
|
||||
alert.device_id
|
||||
|> Devices.get_device!()
|
||||
|> Towerops.Repo.preload(site: :organization)
|
||||
|
||||
site = equipment.site
|
||||
site = device.site
|
||||
organization = site.organization
|
||||
|
||||
# Get all owners and admins who should receive alerts
|
||||
|
|
@ -34,31 +34,31 @@ defmodule Towerops.Alerts.AlertNotifier do
|
|||
results =
|
||||
Enum.map(recipients, fn user ->
|
||||
case alert.alert_type do
|
||||
:equipment_down -> deliver_equipment_down_alert(user.email, equipment, organization)
|
||||
:equipment_up -> deliver_equipment_up_alert(user.email, equipment, organization)
|
||||
:device_down -> deliver_equipment_down_alert(user.email, device, organization)
|
||||
:device_up -> deliver_equipment_up_alert(user.email, device, organization)
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, results}
|
||||
end
|
||||
|
||||
defp deliver_equipment_down_alert(recipient_email, equipment, organization) do
|
||||
subject = "[#{organization.name}] Equipment Down: #{equipment.name}"
|
||||
defp deliver_equipment_down_alert(recipient_email, device, organization) do
|
||||
subject = "[#{organization.name}] Device Down: #{device.name}"
|
||||
|
||||
check_method = if equipment.snmp_enabled, do: "SNMP", else: "ping"
|
||||
check_method = if device.snmp_enabled, do: "SNMP", else: "ping"
|
||||
|
||||
body = """
|
||||
|
||||
==============================
|
||||
|
||||
ALERT: Equipment Down
|
||||
ALERT: Device Down
|
||||
|
||||
Organization: #{organization.name}
|
||||
Equipment: #{equipment.name}
|
||||
IP Address: #{equipment.ip_address}
|
||||
Site: #{equipment.site.name}
|
||||
Device: #{device.name}
|
||||
IP Address: #{device.ip_address}
|
||||
Site: #{device.site.name}
|
||||
|
||||
The equipment is not responding to #{check_method} checks.
|
||||
The device is not responding to #{check_method} checks.
|
||||
|
||||
Please investigate as soon as possible.
|
||||
|
||||
|
|
@ -68,23 +68,23 @@ defmodule Towerops.Alerts.AlertNotifier do
|
|||
deliver(recipient_email, subject, body)
|
||||
end
|
||||
|
||||
defp deliver_equipment_up_alert(recipient_email, equipment, organization) do
|
||||
subject = "[#{organization.name}] Equipment Recovered: #{equipment.name}"
|
||||
defp deliver_equipment_up_alert(recipient_email, device, organization) do
|
||||
subject = "[#{organization.name}] Device Recovered: #{device.name}"
|
||||
|
||||
check_method = if equipment.snmp_enabled, do: "SNMP", else: "ping"
|
||||
check_method = if device.snmp_enabled, do: "SNMP", else: "ping"
|
||||
|
||||
body = """
|
||||
|
||||
==============================
|
||||
|
||||
RESOLVED: Equipment Recovered
|
||||
RESOLVED: Device Recovered
|
||||
|
||||
Organization: #{organization.name}
|
||||
Equipment: #{equipment.name}
|
||||
IP Address: #{equipment.ip_address}
|
||||
Site: #{equipment.site.name}
|
||||
Device: #{device.name}
|
||||
IP Address: #{device.ip_address}
|
||||
Site: #{device.site.name}
|
||||
|
||||
The equipment is now responding to #{check_method} checks.
|
||||
The device is now responding to #{check_method} checks.
|
||||
|
||||
==============================
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ defmodule Towerops.Application do
|
|||
{DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Towerops.PubSub},
|
||||
# Start event logger (subscribes to PubSub)
|
||||
Towerops.Equipment.EventLogger,
|
||||
Towerops.Devices.EventLogger,
|
||||
# Start monitoring supervisor
|
||||
Towerops.Monitoring.Supervisor,
|
||||
# Start a worker by calling: Towerops.Worker.start_link(arg)
|
||||
|
|
|
|||
300
lib/towerops/devices.ex
Normal file
300
lib/towerops/devices.ex
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
defmodule Towerops.Devices do
|
||||
@moduledoc """
|
||||
The Devices context.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
alias Towerops.Devices.Event
|
||||
alias Towerops.Repo
|
||||
|
||||
@doc """
|
||||
Returns the list of devices for a site.
|
||||
"""
|
||||
def list_site_devices(site_id) do
|
||||
Repo.all(from(e in DeviceSchema, where: e.site_id == ^site_id, order_by: [asc: e.name]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all devices for an organization (via sites).
|
||||
|
||||
Supports filtering by:
|
||||
- site_id: Filter by specific site
|
||||
- status: Filter by status ("up", "down", "unknown")
|
||||
"""
|
||||
def list_organization_devices(organization_id, filters \\ %{}) do
|
||||
query =
|
||||
from(e in DeviceSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
order_by: [asc: e.name],
|
||||
preload: [site: s]
|
||||
)
|
||||
|
||||
query =
|
||||
if site_id = filters["site_id"] do
|
||||
where(query, [e], e.site_id == ^site_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query =
|
||||
if status = filters["status"] do
|
||||
where(query, [e], e.status == ^status)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of devices for an organization.
|
||||
"""
|
||||
def count_organization_devices(organization_id) do
|
||||
Repo.aggregate(
|
||||
from(e in DeviceSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id
|
||||
),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of devices for a site.
|
||||
"""
|
||||
def count_site_devices(site_id) do
|
||||
Repo.aggregate(
|
||||
from(e in DeviceSchema, where: e.site_id == ^site_id),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of devices that is down for a site.
|
||||
"""
|
||||
def count_site_devices_down(site_id) do
|
||||
Repo.aggregate(
|
||||
from(e in DeviceSchema, where: e.site_id == ^site_id and e.status == "down"),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all devices with monitoring enabled.
|
||||
"""
|
||||
def list_monitored_devices do
|
||||
Repo.all(
|
||||
from(e in DeviceSchema,
|
||||
where: e.monitoring_enabled == true,
|
||||
order_by: [asc: e.name]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all devices with SNMP enabled.
|
||||
"""
|
||||
def list_snmp_enabled_devices do
|
||||
Repo.all(
|
||||
from(e in DeviceSchema,
|
||||
where: e.snmp_enabled == true,
|
||||
order_by: [asc: e.name]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single device.
|
||||
"""
|
||||
def get_device!(id) do
|
||||
DeviceSchema
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload([:site])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets device with full details including SNMP device, interfaces, and sensors.
|
||||
"""
|
||||
def get_device_with_details(id) do
|
||||
DeviceSchema
|
||||
|> Repo.get(id)
|
||||
|> case do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
device ->
|
||||
Repo.preload(device, [:site, snmp_device: [:interfaces, :sensors]])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single device belonging to a specific site.
|
||||
"""
|
||||
def get_site_device!(site_id, device_id) do
|
||||
Repo.one!(from(e in DeviceSchema, where: e.id == ^device_id, where: e.site_id == ^site_id, preload: [:site]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets SNMP configuration for device with hierarchical fallback.
|
||||
|
||||
Falls back in this order:
|
||||
1. Device-level configuration
|
||||
2. Site-level configuration
|
||||
3. Organization-level configuration
|
||||
|
||||
Returns a map with:
|
||||
- version: SNMP version ("1", "2c", or "3")
|
||||
- community: SNMP community string
|
||||
- source: Where the config came from (:device, :site, or :organization)
|
||||
"""
|
||||
def get_snmp_config(device_id) when is_binary(device_id) do
|
||||
device =
|
||||
DeviceSchema
|
||||
|> Repo.get!(device_id)
|
||||
|> Repo.preload(site: :organization)
|
||||
|
||||
get_snmp_config(device)
|
||||
end
|
||||
|
||||
def get_snmp_config(%DeviceSchema{} = device) do
|
||||
# Ensure associations are loaded
|
||||
device =
|
||||
if Ecto.assoc_loaded?(device.site) do
|
||||
if Ecto.assoc_loaded?(device.site.organization) do
|
||||
device
|
||||
else
|
||||
Repo.preload(device, site: :organization)
|
||||
end
|
||||
else
|
||||
Repo.preload(device, site: :organization)
|
||||
end
|
||||
|
||||
cond do
|
||||
# Device-level override
|
||||
device.snmp_community != nil ->
|
||||
%{
|
||||
version: device.snmp_version || "2c",
|
||||
community: device.snmp_community,
|
||||
source: :device
|
||||
}
|
||||
|
||||
# Site-level override
|
||||
device.site.snmp_community != nil ->
|
||||
%{
|
||||
version: device.site.snmp_version || "2c",
|
||||
community: device.site.snmp_community,
|
||||
source: :site
|
||||
}
|
||||
|
||||
# Organization-level default
|
||||
device.site.organization.snmp_community != nil ->
|
||||
%{
|
||||
version: device.site.organization.snmp_version || "2c",
|
||||
community: device.site.organization.snmp_community,
|
||||
source: :organization
|
||||
}
|
||||
|
||||
# No SNMP config set at any level
|
||||
true ->
|
||||
%{
|
||||
version: "2c",
|
||||
community: nil,
|
||||
source: :default
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates device.
|
||||
"""
|
||||
def create_device(attrs) do
|
||||
%DeviceSchema{}
|
||||
|> DeviceSchema.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates device.
|
||||
"""
|
||||
def update_device(%DeviceSchema{} = device, attrs) do
|
||||
device
|
||||
|> DeviceSchema.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes device.
|
||||
"""
|
||||
def delete_device(%DeviceSchema{} = device) do
|
||||
Repo.delete(device)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking device changes.
|
||||
"""
|
||||
def change_device(%DeviceSchema{} = device, attrs \\ %{}) do
|
||||
DeviceSchema.changeset(device, attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the status of device.
|
||||
"""
|
||||
def update_device_status(%DeviceSchema{} = device, new_status) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
changes = %{
|
||||
status: new_status,
|
||||
last_checked_at: now
|
||||
}
|
||||
|
||||
changes =
|
||||
if device.status == new_status do
|
||||
changes
|
||||
else
|
||||
Map.put(changes, :last_status_change_at, now)
|
||||
end
|
||||
|
||||
device
|
||||
|> Ecto.Changeset.change(changes)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the last SNMP poll timestamp for device.
|
||||
Used for distributed coordination to prevent duplicate polling across pods.
|
||||
"""
|
||||
def update_snmp_poll_time(%DeviceSchema{} = device) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
device
|
||||
|> Ecto.Changeset.change(%{last_snmp_poll_at: now})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
## Events
|
||||
|
||||
@doc """
|
||||
Creates an device event.
|
||||
"""
|
||||
def create_event(attrs) do
|
||||
%Event{}
|
||||
|> Event.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of events for device, ordered by most recent first.
|
||||
"""
|
||||
def list_devices_events(device_id, limit \\ 100) do
|
||||
Repo.all(
|
||||
from(e in Event,
|
||||
where: e.device_id == ^device_id,
|
||||
order_by: [desc: e.occurred_at],
|
||||
limit: ^limit
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Towerops.Equipment.Equipment do
|
||||
defmodule Towerops.Devices.Device do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -7,11 +7,11 @@ defmodule Towerops.Equipment.Equipment do
|
|||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Sites.Site
|
||||
alias Towerops.Snmp.Device
|
||||
alias Towerops.Snmp.Device, as: SnmpDevice
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "equipment" do
|
||||
schema "devices" do
|
||||
field :name, :string
|
||||
field :ip_address, :string
|
||||
field :description, :string
|
||||
|
|
@ -31,7 +31,7 @@ defmodule Towerops.Equipment.Equipment do
|
|||
|
||||
belongs_to :site, Site
|
||||
|
||||
has_one :snmp_device, Device
|
||||
has_one :snmp_device, SnmpDevice
|
||||
has_many :agent_assignments, AgentAssignment
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
|
|
@ -55,15 +55,15 @@ defmodule Towerops.Equipment.Equipment do
|
|||
last_snmp_poll_at: DateTime.t() | nil,
|
||||
site_id: Ecto.UUID.t(),
|
||||
site: NotLoaded.t() | Site.t(),
|
||||
snmp_device: NotLoaded.t() | Device.t() | nil,
|
||||
snmp_device: NotLoaded.t() | SnmpDevice.t() | nil,
|
||||
agent_assignments: NotLoaded.t() | [AgentAssignment.t()],
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
def changeset(equipment, attrs) do
|
||||
equipment
|
||||
def changeset(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [
|
||||
:name,
|
||||
:ip_address,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Towerops.Equipment.Event do
|
||||
defmodule Towerops.Devices.Event do
|
||||
@moduledoc """
|
||||
Equipment event schema for tracking changes and incidents.
|
||||
Device event schema for tracking changes and incidents.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -26,14 +26,14 @@ defmodule Towerops.Equipment.Event do
|
|||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "equipment_events" do
|
||||
schema "device_events" do
|
||||
field :event_type, :string
|
||||
field :severity, :string
|
||||
field :message, :string
|
||||
field :metadata, :map, default: %{}
|
||||
field :occurred_at, :utc_datetime
|
||||
|
||||
belongs_to :equipment, Towerops.Equipment.Equipment
|
||||
belongs_to :device, Towerops.Devices.Device
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
|
@ -41,8 +41,8 @@ defmodule Towerops.Equipment.Event do
|
|||
@doc false
|
||||
def changeset(event, attrs) do
|
||||
event
|
||||
|> cast(attrs, [:equipment_id, :event_type, :severity, :message, :metadata, :occurred_at])
|
||||
|> validate_required([:equipment_id, :event_type, :severity, :message, :occurred_at])
|
||||
|> cast(attrs, [:device_id, :event_type, :severity, :message, :metadata, :occurred_at])
|
||||
|> validate_required([:device_id, :event_type, :severity, :message, :occurred_at])
|
||||
|> validate_inclusion(:event_type, [
|
||||
"interface_up",
|
||||
"interface_down",
|
||||
|
|
@ -59,6 +59,6 @@ defmodule Towerops.Equipment.Event do
|
|||
"sensor_value_drop"
|
||||
])
|
||||
|> validate_inclusion(:severity, ["info", "warning", "critical"])
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> foreign_key_constraint(:device_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Towerops.Equipment.EventLogger do
|
||||
defmodule Towerops.Devices.EventLogger do
|
||||
@moduledoc """
|
||||
GenServer that subscribes to equipment events via PubSub and logs them to the database.
|
||||
GenServer that subscribes to device events via PubSub and logs them to the database.
|
||||
|
||||
This decouples event detection from event storage, allowing:
|
||||
- Faster polling without database write blocking
|
||||
|
|
@ -9,7 +9,7 @@ defmodule Towerops.Equipment.EventLogger do
|
|||
"""
|
||||
use GenServer
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -23,15 +23,15 @@ defmodule Towerops.Equipment.EventLogger do
|
|||
|
||||
@impl true
|
||||
def init(state) do
|
||||
# Subscribe to equipment events topic
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:events")
|
||||
Logger.info("EventLogger started and subscribed to equipment events")
|
||||
# device events topic
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:events")
|
||||
Logger.info("EventLogger started and subscribed to device events")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:equipment_event, event_attrs}, state) do
|
||||
case Equipment.create_event(event_attrs) do
|
||||
def handle_info({:device_event, event_attrs}, state) do
|
||||
case Devices.create_event(event_attrs) do
|
||||
{:ok, event} ->
|
||||
Logger.debug("Logged event: #{event.message}")
|
||||
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
defmodule Towerops.Equipment do
|
||||
@moduledoc """
|
||||
The Equipment context.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Equipment.Equipment, as: EquipmentSchema
|
||||
alias Towerops.Equipment.Event
|
||||
alias Towerops.Repo
|
||||
|
||||
@doc """
|
||||
Returns the list of equipment for a site.
|
||||
"""
|
||||
def list_site_equipment(site_id) do
|
||||
Repo.all(from(e in EquipmentSchema, where: e.site_id == ^site_id, order_by: [asc: e.name]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all equipment for an organization (via sites).
|
||||
|
||||
Supports filtering by:
|
||||
- site_id: Filter by specific site
|
||||
- status: Filter by status ("up", "down", "unknown")
|
||||
"""
|
||||
def list_organization_equipment(organization_id, filters \\ %{}) do
|
||||
query =
|
||||
from(e in EquipmentSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
order_by: [asc: e.name],
|
||||
preload: [site: s]
|
||||
)
|
||||
|
||||
query =
|
||||
if site_id = filters["site_id"] do
|
||||
where(query, [e], e.site_id == ^site_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query =
|
||||
if status = filters["status"] do
|
||||
where(query, [e], e.status == ^status)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of equipment for an organization.
|
||||
"""
|
||||
def count_organization_equipment(organization_id) do
|
||||
Repo.aggregate(
|
||||
from(e in EquipmentSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id
|
||||
),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of equipment for a site.
|
||||
"""
|
||||
def count_site_equipment(site_id) do
|
||||
Repo.aggregate(
|
||||
from(e in EquipmentSchema, where: e.site_id == ^site_id),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the count of equipment that is down for a site.
|
||||
"""
|
||||
def count_site_equipment_down(site_id) do
|
||||
Repo.aggregate(
|
||||
from(e in EquipmentSchema, where: e.site_id == ^site_id and e.status == "down"),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all equipment with monitoring enabled.
|
||||
"""
|
||||
def list_monitored_equipment do
|
||||
Repo.all(
|
||||
from(e in EquipmentSchema,
|
||||
where: e.monitoring_enabled == true,
|
||||
order_by: [asc: e.name]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all equipment with SNMP enabled.
|
||||
"""
|
||||
def list_snmp_enabled_equipment do
|
||||
Repo.all(
|
||||
from(e in EquipmentSchema,
|
||||
where: e.snmp_enabled == true,
|
||||
order_by: [asc: e.name]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single equipment.
|
||||
"""
|
||||
def get_equipment!(id) do
|
||||
EquipmentSchema
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload([:site])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets equipment with full details including SNMP device, interfaces, and sensors.
|
||||
"""
|
||||
def get_equipment_with_details(id) do
|
||||
EquipmentSchema
|
||||
|> Repo.get(id)
|
||||
|> case do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
equipment ->
|
||||
Repo.preload(equipment, [:site, snmp_device: [:interfaces, :sensors]])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single equipment belonging to a specific site.
|
||||
"""
|
||||
def get_site_equipment!(site_id, equipment_id) do
|
||||
Repo.one!(from(e in EquipmentSchema, where: e.id == ^equipment_id, where: e.site_id == ^site_id, preload: [:site]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets SNMP configuration for equipment with hierarchical fallback.
|
||||
|
||||
Falls back in this order:
|
||||
1. Equipment-level configuration
|
||||
2. Site-level configuration
|
||||
3. Organization-level configuration
|
||||
|
||||
Returns a map with:
|
||||
- version: SNMP version ("1", "2c", or "3")
|
||||
- community: SNMP community string
|
||||
- source: Where the config came from (:equipment, :site, or :organization)
|
||||
"""
|
||||
def get_snmp_config(equipment_id) when is_binary(equipment_id) do
|
||||
equipment =
|
||||
EquipmentSchema
|
||||
|> Repo.get!(equipment_id)
|
||||
|> Repo.preload(site: :organization)
|
||||
|
||||
get_snmp_config(equipment)
|
||||
end
|
||||
|
||||
def get_snmp_config(%EquipmentSchema{} = equipment) do
|
||||
# Ensure associations are loaded
|
||||
equipment =
|
||||
if Ecto.assoc_loaded?(equipment.site) do
|
||||
if Ecto.assoc_loaded?(equipment.site.organization) do
|
||||
equipment
|
||||
else
|
||||
Repo.preload(equipment, site: :organization)
|
||||
end
|
||||
else
|
||||
Repo.preload(equipment, site: :organization)
|
||||
end
|
||||
|
||||
cond do
|
||||
# Equipment-level override
|
||||
equipment.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.snmp_version || "2c",
|
||||
community: equipment.snmp_community,
|
||||
source: :equipment
|
||||
}
|
||||
|
||||
# Site-level override
|
||||
equipment.site.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.site.snmp_version || "2c",
|
||||
community: equipment.site.snmp_community,
|
||||
source: :site
|
||||
}
|
||||
|
||||
# Organization-level default
|
||||
equipment.site.organization.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.site.organization.snmp_version || "2c",
|
||||
community: equipment.site.organization.snmp_community,
|
||||
source: :organization
|
||||
}
|
||||
|
||||
# No SNMP config set at any level
|
||||
true ->
|
||||
%{
|
||||
version: "2c",
|
||||
community: nil,
|
||||
source: :default
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates equipment.
|
||||
"""
|
||||
def create_equipment(attrs) do
|
||||
%EquipmentSchema{}
|
||||
|> EquipmentSchema.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates equipment.
|
||||
"""
|
||||
def update_equipment(%EquipmentSchema{} = equipment, attrs) do
|
||||
equipment
|
||||
|> EquipmentSchema.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes equipment.
|
||||
"""
|
||||
def delete_equipment(%EquipmentSchema{} = equipment) do
|
||||
Repo.delete(equipment)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking equipment changes.
|
||||
"""
|
||||
def change_equipment(%EquipmentSchema{} = equipment, attrs \\ %{}) do
|
||||
EquipmentSchema.changeset(equipment, attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the status of equipment.
|
||||
"""
|
||||
def update_equipment_status(%EquipmentSchema{} = equipment, new_status) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
changes = %{
|
||||
status: new_status,
|
||||
last_checked_at: now
|
||||
}
|
||||
|
||||
changes =
|
||||
if equipment.status == new_status do
|
||||
changes
|
||||
else
|
||||
Map.put(changes, :last_status_change_at, now)
|
||||
end
|
||||
|
||||
equipment
|
||||
|> Ecto.Changeset.change(changes)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the last SNMP poll timestamp for equipment.
|
||||
Used for distributed coordination to prevent duplicate polling across pods.
|
||||
"""
|
||||
def update_snmp_poll_time(%EquipmentSchema{} = equipment) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
equipment
|
||||
|> Ecto.Changeset.change(%{last_snmp_poll_at: now})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
## Events
|
||||
|
||||
@doc """
|
||||
Creates an equipment event.
|
||||
"""
|
||||
def create_event(attrs) do
|
||||
%Event{}
|
||||
|> Event.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of events for equipment, ordered by most recent first.
|
||||
"""
|
||||
def list_equipment_events(equipment_id, limit \\ 100) do
|
||||
Repo.all(
|
||||
from(e in Event,
|
||||
where: e.equipment_id == ^equipment_id,
|
||||
order_by: [desc: e.occurred_at],
|
||||
limit: ^limit
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -19,17 +19,17 @@ defmodule Towerops.Monitoring do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of checks for an equipment.
|
||||
Returns the list of checks for an device.
|
||||
"""
|
||||
def list_equipment_checks(equipment_id, limit \\ 100) do
|
||||
Repo.all(from(c in Check, where: c.equipment_id == ^equipment_id, order_by: [desc: c.checked_at], limit: ^limit))
|
||||
def list_devices_checks(device_id, limit \\ 100) do
|
||||
Repo.all(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: ^limit))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the latest check for an equipment.
|
||||
Returns the latest check for an device.
|
||||
"""
|
||||
def get_latest_check(equipment_id) do
|
||||
Repo.one(from(c in Check, where: c.equipment_id == ^equipment_id, order_by: [desc: c.checked_at], limit: 1))
|
||||
def get_latest_check(device_id) do
|
||||
Repo.one(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: 1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -40,10 +40,10 @@ defmodule Towerops.Monitoring do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Gets hourly statistics for equipment over a time range.
|
||||
Gets hourly statistics for device over a time range.
|
||||
Calculates aggregates on-demand from monitoring_checks.
|
||||
"""
|
||||
def get_hourly_stats(equipment_id, start_time, end_time) do
|
||||
def get_hourly_stats(device_id, start_time, end_time) do
|
||||
query = """
|
||||
SELECT
|
||||
date_trunc('hour', checked_at) as bucket,
|
||||
|
|
@ -55,7 +55,7 @@ defmodule Towerops.Monitoring do
|
|||
MAX(response_time_ms) as max_response_time_ms,
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
|
||||
FROM monitoring_checks
|
||||
WHERE equipment_id = $1::uuid
|
||||
WHERE device_id = $1::uuid
|
||||
AND checked_at >= $2
|
||||
AND checked_at <= $3
|
||||
GROUP BY bucket
|
||||
|
|
@ -65,15 +65,15 @@ defmodule Towerops.Monitoring do
|
|||
SQL.query!(
|
||||
Repo,
|
||||
query,
|
||||
[Ecto.UUID.dump!(equipment_id), start_time, end_time]
|
||||
[Ecto.UUID.dump!(device_id), start_time, end_time]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets daily statistics for equipment over a time range.
|
||||
Gets daily statistics for device over a time range.
|
||||
Calculates aggregates on-demand from monitoring_checks.
|
||||
"""
|
||||
def get_daily_stats(equipment_id, start_time, end_time) do
|
||||
def get_daily_stats(device_id, start_time, end_time) do
|
||||
query = """
|
||||
SELECT
|
||||
date_trunc('day', checked_at) as bucket,
|
||||
|
|
@ -85,7 +85,7 @@ defmodule Towerops.Monitoring do
|
|||
MAX(response_time_ms) as max_response_time_ms,
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
|
||||
FROM monitoring_checks
|
||||
WHERE equipment_id = $1::uuid
|
||||
WHERE device_id = $1::uuid
|
||||
AND checked_at >= $2
|
||||
AND checked_at <= $3
|
||||
GROUP BY bucket
|
||||
|
|
@ -95,26 +95,26 @@ defmodule Towerops.Monitoring do
|
|||
SQL.query!(
|
||||
Repo,
|
||||
query,
|
||||
[Ecto.UUID.dump!(equipment_id), start_time, end_time]
|
||||
[Ecto.UUID.dump!(device_id), start_time, end_time]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets uptime percentage for equipment over the last N days.
|
||||
Gets uptime percentage for device over the last N days.
|
||||
Calculates on-demand from monitoring_checks.
|
||||
"""
|
||||
def get_uptime_percentage(equipment_id, days_ago \\ 30) do
|
||||
def get_uptime_percentage(device_id, days_ago \\ 30) do
|
||||
start_time = DateTime.add(DateTime.utc_now(), -days_ago * 24 * 60 * 60, :second)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
|
||||
FROM monitoring_checks
|
||||
WHERE equipment_id = $1::uuid
|
||||
WHERE device_id = $1::uuid
|
||||
AND checked_at >= $2
|
||||
"""
|
||||
|
||||
case SQL.query!(Repo, query, [Ecto.UUID.dump!(equipment_id), start_time]) do
|
||||
case SQL.query!(Repo, query, [Ecto.UUID.dump!(device_id), start_time]) do
|
||||
%{rows: [[%Decimal{} = uptime]]} ->
|
||||
uptime |> Decimal.to_float() |> Float.round(2)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Towerops.Monitoring.Check do
|
|||
field :response_time_ms, :integer
|
||||
field :checked_at, :utc_datetime
|
||||
|
||||
belongs_to :equipment, Towerops.Equipment.Equipment
|
||||
belongs_to :device, Towerops.Devices.Device
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
|
@ -19,8 +19,8 @@ defmodule Towerops.Monitoring.Check do
|
|||
@doc false
|
||||
def changeset(check, attrs) do
|
||||
check
|
||||
|> cast(attrs, [:equipment_id, :status, :response_time_ms, :checked_at])
|
||||
|> validate_required([:equipment_id, :status, :checked_at])
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> cast(attrs, [:device_id, :status, :response_time_ms, :checked_at])
|
||||
|> validate_required([:device_id, :status, :checked_at])
|
||||
|> foreign_key_constraint(:device_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
221
lib/towerops/monitoring/device_monitor.ex
Normal file
221
lib/towerops/monitoring/device_monitor.ex
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
defmodule Towerops.Monitoring.DeviceMonitor do
|
||||
@moduledoc """
|
||||
GenServer that monitors a single piece of device by periodically pinging it.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Monitoring
|
||||
|
||||
require Logger
|
||||
|
||||
# Allow dependency injection for testing
|
||||
@ping_module Application.compile_env(:towerops, :ping_module, Towerops.Monitoring.Ping)
|
||||
@poller_module Application.compile_env(:towerops, :poller_module, Towerops.Snmp.Poller)
|
||||
|
||||
# Suppress warnings for Mox modules that are defined at runtime during tests
|
||||
@compile {:no_warn_undefined, [Towerops.Monitoring.PingMock, Towerops.Snmp.PollerMock]}
|
||||
|
||||
# Client API
|
||||
|
||||
@doc """
|
||||
Starts a monitor for the given device ID.
|
||||
"""
|
||||
def start_link(device_id) do
|
||||
GenServer.start_link(__MODULE__, device_id, name: via_tuple(device_id))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Triggers an immediate check for the device.
|
||||
"""
|
||||
def trigger_check(device_id) do
|
||||
# Check if monitor process is running
|
||||
case Registry.lookup(Towerops.Monitoring.Registry, device_id) do
|
||||
[{_pid, _}] ->
|
||||
# Process exists, send cast
|
||||
GenServer.cast(via_tuple(device_id), :check_now)
|
||||
|
||||
[] ->
|
||||
# Process doesn't exist (monitoring disabled), perform check directly
|
||||
Task.start(fn -> perform_check(device_id) end)
|
||||
end
|
||||
end
|
||||
|
||||
# Server Callbacks
|
||||
|
||||
@impl true
|
||||
def init(device_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
if device.monitoring_enabled do
|
||||
# Perform immediate check when monitoring starts
|
||||
send(self(), :check_device)
|
||||
end
|
||||
|
||||
{:ok, %{device_id: device_id}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_device, state) do
|
||||
_ = perform_check(state.device_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:check_now, state) do
|
||||
_ = perform_check(state.device_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# Private Functions
|
||||
|
||||
defp perform_check(device_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
# Use SNMP if enabled, otherwise fallback to ping
|
||||
check_result =
|
||||
if device.snmp_enabled do
|
||||
client_opts = @poller_module.build_client_opts(device)
|
||||
@poller_module.check_device(client_opts)
|
||||
else
|
||||
@ping_module.ping(device.ip_address)
|
||||
end
|
||||
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
status =
|
||||
case check_result do
|
||||
{:ok, _time} -> :success
|
||||
{:error, _reason} -> :failure
|
||||
end
|
||||
|
||||
# Save the check result
|
||||
case Monitoring.create_check(%{
|
||||
device_id: device_id,
|
||||
status: status,
|
||||
response_time_ms: nil,
|
||||
checked_at: now
|
||||
}) do
|
||||
{:ok, _check} ->
|
||||
:ok
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Failed to create monitoring check for device #{device_id}: #{inspect(changeset.errors)}")
|
||||
end
|
||||
|
||||
# device status if it changed
|
||||
new_status = if status == :success, do: :up, else: :down
|
||||
old_status = device.status
|
||||
|
||||
_ = Devices.update_device_status(device, new_status)
|
||||
|
||||
# Create alerts if status changed
|
||||
_ =
|
||||
if old_status != new_status do
|
||||
handle_status_change(device, old_status, new_status)
|
||||
end
|
||||
|
||||
# Broadcast status change via PubSub
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"device:#{device_id}",
|
||||
{:device_status_changed, device_id, new_status, nil}
|
||||
)
|
||||
|
||||
# Only schedule next check if monitoring is enabled
|
||||
if device.monitoring_enabled do
|
||||
schedule_next_check(device.check_interval_seconds)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_status_change(device, old_status, new_status) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
case {old_status, new_status} do
|
||||
{_, :down} ->
|
||||
handle_equipment_down(device, now)
|
||||
|
||||
{_, :up} ->
|
||||
handle_equipment_up(device, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_down(device, now) do
|
||||
if Alerts.has_active_alert?(device.id, :device_down) do
|
||||
:ok
|
||||
else
|
||||
create_device_down_alert(device, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_device_down_alert(device, now) do
|
||||
alert_message = get_down_alert_message(device)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: now,
|
||||
message: alert_message
|
||||
})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, device.id, :device_down}
|
||||
)
|
||||
end
|
||||
|
||||
defp get_down_alert_message(device) do
|
||||
if device.snmp_enabled do
|
||||
"Device is not responding to SNMP"
|
||||
else
|
||||
"Device is not responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_up(device, now) do
|
||||
recovery_message = get_recovery_message(device)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_up,
|
||||
triggered_at: now,
|
||||
message: recovery_message
|
||||
})
|
||||
|
||||
resolve_down_alert(device)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, device.id, :device_down}
|
||||
)
|
||||
end
|
||||
|
||||
defp get_recovery_message(device) do
|
||||
if device.snmp_enabled do
|
||||
"Device is now responding to SNMP"
|
||||
else
|
||||
"Device is now responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_down_alert(device) do
|
||||
case Alerts.get_active_alert(device.id, :device_down) do
|
||||
nil -> :ok
|
||||
alert -> Alerts.resolve_alert(alert)
|
||||
end
|
||||
end
|
||||
|
||||
defp schedule_next_check(interval_seconds) do
|
||||
Process.send_after(self(), :check_device, interval_seconds * 1000)
|
||||
end
|
||||
|
||||
defp via_tuple(device_id) do
|
||||
{:via, Registry, {Towerops.Monitoring.Registry, device_id}}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
defmodule Towerops.Monitoring.EquipmentMonitor do
|
||||
@moduledoc """
|
||||
GenServer that monitors a single piece of equipment by periodically pinging it.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Monitoring
|
||||
|
||||
require Logger
|
||||
|
||||
# Allow dependency injection for testing
|
||||
@ping_module Application.compile_env(:towerops, :ping_module, Towerops.Monitoring.Ping)
|
||||
@poller_module Application.compile_env(:towerops, :poller_module, Towerops.Snmp.Poller)
|
||||
|
||||
# Suppress warnings for Mox modules that are defined at runtime during tests
|
||||
@compile {:no_warn_undefined, [Towerops.Monitoring.PingMock, Towerops.Snmp.PollerMock]}
|
||||
|
||||
# Client API
|
||||
|
||||
@doc """
|
||||
Starts a monitor for the given equipment ID.
|
||||
"""
|
||||
def start_link(equipment_id) do
|
||||
GenServer.start_link(__MODULE__, equipment_id, name: via_tuple(equipment_id))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Triggers an immediate check for the equipment.
|
||||
"""
|
||||
def trigger_check(equipment_id) do
|
||||
# Check if monitor process is running
|
||||
case Registry.lookup(Towerops.Monitoring.Registry, equipment_id) do
|
||||
[{_pid, _}] ->
|
||||
# Process exists, send cast
|
||||
GenServer.cast(via_tuple(equipment_id), :check_now)
|
||||
|
||||
[] ->
|
||||
# Process doesn't exist (monitoring disabled), perform check directly
|
||||
Task.start(fn -> perform_check(equipment_id) end)
|
||||
end
|
||||
end
|
||||
|
||||
# Server Callbacks
|
||||
|
||||
@impl true
|
||||
def init(equipment_id) do
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
|
||||
if equipment.monitoring_enabled do
|
||||
# Perform immediate check when monitoring starts
|
||||
send(self(), :check_equipment)
|
||||
end
|
||||
|
||||
{:ok, %{equipment_id: equipment_id}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_equipment, state) do
|
||||
_ = perform_check(state.equipment_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:check_now, state) do
|
||||
_ = perform_check(state.equipment_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# Private Functions
|
||||
|
||||
defp perform_check(equipment_id) do
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
|
||||
# Use SNMP if enabled, otherwise fallback to ping
|
||||
check_result =
|
||||
if equipment.snmp_enabled do
|
||||
client_opts = @poller_module.build_client_opts(equipment)
|
||||
@poller_module.check_device(client_opts)
|
||||
else
|
||||
@ping_module.ping(equipment.ip_address)
|
||||
end
|
||||
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
status =
|
||||
case check_result do
|
||||
{:ok, _time} -> :success
|
||||
{:error, _reason} -> :failure
|
||||
end
|
||||
|
||||
# Save the check result
|
||||
case Monitoring.create_check(%{
|
||||
equipment_id: equipment_id,
|
||||
status: status,
|
||||
response_time_ms: nil,
|
||||
checked_at: now
|
||||
}) do
|
||||
{:ok, _check} ->
|
||||
:ok
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Failed to create monitoring check for equipment #{equipment_id}: #{inspect(changeset.errors)}")
|
||||
end
|
||||
|
||||
# Update equipment status if it changed
|
||||
new_status = if status == :success, do: :up, else: :down
|
||||
old_status = equipment.status
|
||||
|
||||
_ = Equipment.update_equipment_status(equipment, new_status)
|
||||
|
||||
# Create alerts if status changed
|
||||
_ =
|
||||
if old_status != new_status do
|
||||
handle_status_change(equipment, old_status, new_status)
|
||||
end
|
||||
|
||||
# Broadcast status change via PubSub
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:#{equipment_id}",
|
||||
{:equipment_status_changed, equipment_id, new_status, nil}
|
||||
)
|
||||
|
||||
# Only schedule next check if monitoring is enabled
|
||||
if equipment.monitoring_enabled do
|
||||
schedule_next_check(equipment.check_interval_seconds)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_status_change(equipment, old_status, new_status) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
case {old_status, new_status} do
|
||||
{_, :down} ->
|
||||
handle_equipment_down(equipment, now)
|
||||
|
||||
{_, :up} ->
|
||||
handle_equipment_up(equipment, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_down(equipment, now) do
|
||||
if Alerts.has_active_alert?(equipment.id, :equipment_down) do
|
||||
:ok
|
||||
else
|
||||
create_equipment_down_alert(equipment, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_equipment_down_alert(equipment, now) do
|
||||
alert_message = get_down_alert_message(equipment)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: now,
|
||||
message: alert_message
|
||||
})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
)
|
||||
end
|
||||
|
||||
defp get_down_alert_message(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is not responding to SNMP"
|
||||
else
|
||||
"Equipment is not responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_up(equipment, now) do
|
||||
recovery_message = get_recovery_message(equipment)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: now,
|
||||
message: recovery_message
|
||||
})
|
||||
|
||||
resolve_down_alert(equipment)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, equipment.id, :equipment_down}
|
||||
)
|
||||
end
|
||||
|
||||
defp get_recovery_message(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is now responding to SNMP"
|
||||
else
|
||||
"Equipment is now responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_down_alert(equipment) do
|
||||
case Alerts.get_active_alert(equipment.id, :equipment_down) do
|
||||
nil -> :ok
|
||||
alert -> Alerts.resolve_alert(alert)
|
||||
end
|
||||
end
|
||||
|
||||
defp schedule_next_check(interval_seconds) do
|
||||
Process.send_after(self(), :check_equipment, interval_seconds * 1000)
|
||||
end
|
||||
|
||||
defp via_tuple(equipment_id) do
|
||||
{:via, Registry, {Towerops.Monitoring.Registry, equipment_id}}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Towerops.Monitoring.Ping do
|
||||
@moduledoc """
|
||||
Handles ping operations for equipment monitoring.
|
||||
Handles ping operations for device monitoring.
|
||||
"""
|
||||
|
||||
@behaviour Towerops.Monitoring.PingBehaviour
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Towerops.Monitoring.Supervisor do
|
||||
@moduledoc """
|
||||
Supervisor for managing equipment monitoring workers.
|
||||
Supervisor for managing device monitoring workers.
|
||||
"""
|
||||
use Supervisor
|
||||
|
||||
alias Towerops.Monitoring.EquipmentMonitor
|
||||
alias Towerops.Monitoring.DeviceMonitor
|
||||
alias Towerops.Snmp.NeighborCleanupWorker
|
||||
alias Towerops.Snmp.PollerRegistry
|
||||
alias Towerops.Snmp.PollerSupervisor
|
||||
|
|
@ -47,7 +47,7 @@ defmodule Towerops.Monitoring.Supervisor do
|
|||
|
||||
try do
|
||||
start_all_monitors()
|
||||
Logger.info("Started all equipment monitors")
|
||||
Logger.info("Started all devices monitors")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Failed to start monitors: #{inspect(error)}")
|
||||
|
|
@ -67,19 +67,19 @@ defmodule Towerops.Monitoring.Supervisor do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Starts monitoring for a specific equipment.
|
||||
Starts monitoring for a specific device.
|
||||
"""
|
||||
def start_monitor(equipment_id) do
|
||||
spec = {EquipmentMonitor, equipment_id}
|
||||
def start_monitor(device_id) do
|
||||
spec = {DeviceMonitor, device_id}
|
||||
|
||||
DynamicSupervisor.start_child(Towerops.Monitoring.DynamicSupervisor, spec)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops monitoring for a specific equipment.
|
||||
Stops monitoring for a specific device.
|
||||
"""
|
||||
def stop_monitor(equipment_id) do
|
||||
case Registry.lookup(Towerops.Monitoring.Registry, equipment_id) do
|
||||
def stop_monitor(device_id) do
|
||||
case Registry.lookup(Towerops.Monitoring.Registry, device_id) do
|
||||
[{pid, _}] ->
|
||||
DynamicSupervisor.terminate_child(Towerops.Monitoring.DynamicSupervisor, pid)
|
||||
|
||||
|
|
@ -89,27 +89,27 @@ defmodule Towerops.Monitoring.Supervisor do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Starts monitors for all equipment that has monitoring enabled.
|
||||
Starts monitors for all devices that has monitoring enabled.
|
||||
"""
|
||||
def start_all_monitors do
|
||||
Enum.each(Towerops.Equipment.list_monitored_equipment(), fn equipment ->
|
||||
start_monitor(equipment.id)
|
||||
Enum.each(Towerops.Devices.list_monitored_devices(), fn device ->
|
||||
start_monitor(device.id)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts an SNMP poller for a specific equipment.
|
||||
Starts an SNMP poller for a specific device.
|
||||
"""
|
||||
def start_snmp_poller(equipment_id) do
|
||||
spec = {PollerWorker, equipment_id}
|
||||
def start_snmp_poller(device_id) do
|
||||
spec = {PollerWorker, device_id}
|
||||
DynamicSupervisor.start_child(PollerSupervisor, spec)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops an SNMP poller for a specific equipment.
|
||||
Stops an SNMP poller for a specific device.
|
||||
"""
|
||||
def stop_snmp_poller(equipment_id) do
|
||||
case Registry.lookup(PollerRegistry, equipment_id) do
|
||||
def stop_snmp_poller(device_id) do
|
||||
case Registry.lookup(PollerRegistry, device_id) do
|
||||
[{pid, _}] ->
|
||||
DynamicSupervisor.terminate_child(PollerSupervisor, pid)
|
||||
|
||||
|
|
@ -119,11 +119,11 @@ defmodule Towerops.Monitoring.Supervisor do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Starts SNMP pollers for all equipment that has SNMP enabled.
|
||||
Starts SNMP pollers for all devices that has SNMP enabled.
|
||||
"""
|
||||
def start_all_snmp_pollers do
|
||||
Enum.each(Towerops.Equipment.list_snmp_enabled_equipment(), fn equipment ->
|
||||
start_snmp_poller(equipment.id)
|
||||
Enum.each(Towerops.Devices.list_snmp_enabled_devices(), fn device ->
|
||||
start_snmp_poller(device.id)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.Organizations do
|
|||
import Ecto.Query
|
||||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Organizations.Invitation
|
||||
alias Towerops.Organizations.Membership
|
||||
alias Towerops.Organizations.Organization
|
||||
|
|
@ -253,17 +253,17 @@ defmodule Towerops.Organizations do
|
|||
## Bulk Configuration Updates
|
||||
|
||||
@doc """
|
||||
Applies organization's SNMP configuration to all equipment in the organization.
|
||||
Applies organization's SNMP configuration to all devices in the organization.
|
||||
|
||||
This updates all equipment records to use the organization's SNMP version and community string,
|
||||
overwriting any existing equipment-level or site-level configurations.
|
||||
This updates all devices records to use the organization's SNMP version and community string,
|
||||
overwriting any existing device-level or site-level configurations.
|
||||
|
||||
Returns the number of equipment records updated.
|
||||
Returns the number of device records updated.
|
||||
"""
|
||||
def apply_snmp_config_to_all_equipment(organization_id) do
|
||||
organization = get_organization!(organization_id)
|
||||
|
||||
Repo.update_all(from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id),
|
||||
Repo.update_all(from(e in Device, join: s in assoc(e, :site), where: s.organization_id == ^organization_id),
|
||||
set: [
|
||||
snmp_version: organization.snmp_version,
|
||||
snmp_community: organization.snmp_community,
|
||||
|
|
@ -273,33 +273,33 @@ defmodule Towerops.Organizations do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Applies organization's default agent to all equipment in the organization.
|
||||
Applies organization's default agent to all devices in the organization.
|
||||
|
||||
This creates/updates agent assignments for all equipment to use the organization's
|
||||
default agent, overwriting any existing equipment-level or site-level assignments.
|
||||
This creates/updates agent assignments for all devices to use the organization's
|
||||
default agent, overwriting any existing device-level or site-level assignments.
|
||||
|
||||
Returns the number of equipment records assigned.
|
||||
Returns the number of device records assigned.
|
||||
"""
|
||||
def apply_agent_to_all_equipment(organization_id) do
|
||||
organization = Repo.get!(Organization, organization_id)
|
||||
|
||||
if organization.default_agent_token_id do
|
||||
# Get all equipment IDs for this organization
|
||||
equipment_ids =
|
||||
# device IDs for this organization
|
||||
device_ids =
|
||||
Repo.all(
|
||||
from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
|
||||
from(e in Device, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
|
||||
)
|
||||
|
||||
# Delete existing assignments
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
|
||||
# Create new assignments
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
assignments =
|
||||
Enum.map(equipment_ids, fn equipment_id ->
|
||||
Enum.map(device_ids, fn device_id ->
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
agent_token_id: organization.default_agent_token_id,
|
||||
inserted_at: now,
|
||||
updated_at: now
|
||||
|
|
@ -310,12 +310,12 @@ defmodule Towerops.Organizations do
|
|||
{count, nil}
|
||||
else
|
||||
# No default agent set, delete all assignments
|
||||
equipment_ids =
|
||||
device_ids =
|
||||
Repo.all(
|
||||
from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
|
||||
from(e in Device, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
|
||||
)
|
||||
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ defmodule Towerops.Organizations do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> can?(membership, :edit, :equipment)
|
||||
iex> can?(membership, :edit, :device)
|
||||
true
|
||||
|
||||
iex> can?(membership, :delete, :organization)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Towerops.Organizations.Organization do
|
|||
field :name, :string
|
||||
field :slug, :string
|
||||
|
||||
# SNMP configuration (global default for all sites/equipment)
|
||||
# device)
|
||||
field :snmp_version, :string, default: "2c"
|
||||
field :snmp_community, :string
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Towerops.Organizations.Policy do
|
|||
iex> can?(%Membership{role: :owner}, :delete, :organization)
|
||||
true
|
||||
|
||||
iex> can?(%Membership{role: :viewer}, :edit, :equipment)
|
||||
iex> can?(%Membership{role: :viewer}, :edit, :device)
|
||||
false
|
||||
"""
|
||||
def can?(%Membership{role: role}, action, resource) do
|
||||
|
|
@ -37,7 +37,7 @@ defmodule Towerops.Organizations.Policy do
|
|||
defp check_permission(:member, action, :invitation) when action in [:create, :delete], do: false
|
||||
|
||||
defp check_permission(:member, action, resource)
|
||||
when action in [:view, :list, :create, :edit] and resource in [:site, :equipment, :alert], do: true
|
||||
when action in [:view, :list, :create, :edit] and resource in [:site, :device, :alert], do: true
|
||||
|
||||
# Viewer permissions - read-only
|
||||
defp check_permission(:viewer, action, _resource) when action in [:view, :list], do: true
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ defmodule Towerops.Agent.AgentConfig do
|
|||
|
||||
field :version, 1, type: :string
|
||||
field :poll_interval_seconds, 2, type: :uint32, json_name: "pollIntervalSeconds"
|
||||
field :equipment, 3, repeated: true, type: Towerops.Agent.Equipment
|
||||
field :devices, 3, repeated: true, type: Towerops.Agent.Device
|
||||
end
|
||||
|
||||
defmodule Towerops.Agent.Equipment do
|
||||
defmodule Towerops.Agent.Device do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "towerops.agent.Equipment",
|
||||
full_name: "towerops.agent.Device",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
|
|
@ -234,8 +234,8 @@ defmodule Towerops.Agent.AgentJob do
|
|||
|
||||
field :job_id, 1, type: :string, json_name: "jobId"
|
||||
field :job_type, 2, type: Towerops.Agent.JobType, json_name: "jobType", enum: true
|
||||
field :equipment_id, 3, type: :string, json_name: "equipmentId"
|
||||
field :device, 4, type: Towerops.Agent.SnmpDevice
|
||||
field :device_id, 3, type: :string, json_name: "deviceId"
|
||||
field :snmp_device, 4, type: Towerops.Agent.SnmpDevice, json_name: "snmpDevice"
|
||||
field :queries, 5, repeated: true, type: Towerops.Agent.SnmpQuery
|
||||
end
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ defmodule Towerops.Agent.SnmpResult do
|
|||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :equipment_id, 1, type: :string, json_name: "equipmentId"
|
||||
field :device_id, 1, type: :string, json_name: "deviceId"
|
||||
field :job_type, 2, type: Towerops.Agent.JobType, json_name: "jobType", enum: true
|
||||
field :oid_values, 3, repeated: true, type: Towerops.Agent.SnmpResult.OidValuesEntry, json_name: "oidValues", map: true
|
||||
field :timestamp, 4, type: :int64
|
||||
|
|
@ -325,7 +325,7 @@ defmodule Towerops.Agent.AgentError do
|
|||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :equipment_id, 1, type: :string, json_name: "equipmentId"
|
||||
field :device_id, 1, type: :string, json_name: "deviceId"
|
||||
field :job_id, 2, type: :string, json_name: "jobId"
|
||||
field :message, 3, type: :string
|
||||
field :timestamp, 4, type: :int64
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.Sites do
|
|||
import Ecto.Query
|
||||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites.Site
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ defmodule Towerops.Sites do
|
|||
def get_site!(id) do
|
||||
Site
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload([:parent_site, :child_sites, :equipment])
|
||||
|> Repo.preload([:parent_site, :child_sites, :device])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -66,7 +66,7 @@ defmodule Towerops.Sites do
|
|||
from(s in Site,
|
||||
where: s.id == ^site_id,
|
||||
where: s.organization_id == ^organization_id,
|
||||
preload: [:parent_site, :child_sites, :equipment]
|
||||
preload: [:parent_site, :child_sites, :device]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
@ -127,17 +127,17 @@ defmodule Towerops.Sites do
|
|||
## Bulk Configuration Updates
|
||||
|
||||
@doc """
|
||||
Applies site's SNMP configuration to all equipment at the site.
|
||||
Applies site's SNMP configuration to all devices at the site.
|
||||
|
||||
This updates all equipment records to use the site's SNMP version and community string,
|
||||
overwriting any existing equipment-level configurations.
|
||||
This updates all devices records to use the site's SNMP version and community string,
|
||||
overwriting any existing device-level configurations.
|
||||
|
||||
Returns the number of equipment records updated.
|
||||
Returns the number of device records updated.
|
||||
"""
|
||||
def apply_snmp_config_to_all_equipment(site_id) do
|
||||
site = get_site!(site_id)
|
||||
|
||||
Repo.update_all(from(e in Equipment, where: e.site_id == ^site_id),
|
||||
Repo.update_all(from(e in Device, where: e.site_id == ^site_id),
|
||||
set: [
|
||||
snmp_version: site.snmp_version,
|
||||
snmp_community: site.snmp_community,
|
||||
|
|
@ -147,30 +147,30 @@ defmodule Towerops.Sites do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Applies site's agent to all equipment at the site.
|
||||
Applies site's agent to all devices at the site.
|
||||
|
||||
This creates/updates agent assignments for all equipment at the site to use the site's
|
||||
agent, overwriting any existing equipment-level assignments.
|
||||
This creates/updates agent assignments for all devices at the site to use the site's
|
||||
agent, overwriting any existing device-level assignments.
|
||||
|
||||
Returns the number of equipment records assigned.
|
||||
Returns the number of device records assigned.
|
||||
"""
|
||||
def apply_agent_to_all_equipment(site_id) do
|
||||
site = Repo.get!(Site, site_id)
|
||||
|
||||
# Get all equipment IDs for this site
|
||||
equipment_ids = Repo.all(from(e in Equipment, where: e.site_id == ^site_id, select: e.id))
|
||||
# device IDs for this site
|
||||
device_ids = Repo.all(from(e in Device, where: e.site_id == ^site_id, select: e.id))
|
||||
|
||||
if site.agent_token_id do
|
||||
# Delete existing assignments
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
|
||||
# Create new assignments
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
assignments =
|
||||
Enum.map(equipment_ids, fn equipment_id ->
|
||||
Enum.map(device_ids, fn device_id ->
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
agent_token_id: site.agent_token_id,
|
||||
inserted_at: now,
|
||||
updated_at: now
|
||||
|
|
@ -181,7 +181,7 @@ defmodule Towerops.Sites do
|
|||
{count, nil}
|
||||
else
|
||||
# No agent set, delete all assignments (they will inherit from org or use cloud)
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.Sites.Site do
|
|||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Sites.Site
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ defmodule Towerops.Sites.Site do
|
|||
belongs_to :agent_token, AgentToken
|
||||
belongs_to :parent_site, Site
|
||||
has_many :child_sites, Site, foreign_key: :parent_site_id
|
||||
has_many :equipment, Equipment
|
||||
has_many :device, Device
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
|
@ -44,7 +44,7 @@ defmodule Towerops.Sites.Site do
|
|||
parent_site_id: Ecto.UUID.t() | nil,
|
||||
parent_site: NotLoaded.t() | t() | nil,
|
||||
child_sites: NotLoaded.t() | [t()],
|
||||
equipment: NotLoaded.t() | [Equipment.t()],
|
||||
device: NotLoaded.t() | [Device.t()],
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Towerops.Snmp do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Device
|
||||
|
|
@ -39,7 +39,7 @@ defmodule Towerops.Snmp do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Runs SNMP discovery for a piece of equipment.
|
||||
Runs SNMP discovery for a piece of device.
|
||||
|
||||
Discovers:
|
||||
- Device information (manufacturer, model, firmware)
|
||||
|
|
@ -50,15 +50,15 @@ defmodule Towerops.Snmp do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> discover_equipment(equipment)
|
||||
iex> discover_equipment(device)
|
||||
{:ok, %Device{}}
|
||||
"""
|
||||
def discover_equipment(%Equipment{} = equipment) do
|
||||
Discovery.discover_equipment(equipment)
|
||||
def discover_equipment(%DeviceSchema{} = device) do
|
||||
Discovery.discover_equipment(device)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs SNMP discovery for all SNMP-enabled equipment in an organization.
|
||||
Runs SNMP discovery for all SNMP-enabled devices in an organization.
|
||||
|
||||
Returns a summary of successful and failed discoveries.
|
||||
|
||||
|
|
@ -74,26 +74,26 @@ defmodule Towerops.Snmp do
|
|||
# Device queries
|
||||
|
||||
@doc """
|
||||
Gets the SNMP device for a piece of equipment.
|
||||
Gets the SNMP device for a piece of device.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_device(equipment_id)
|
||||
iex> get_device(device_id)
|
||||
%Device{}
|
||||
|
||||
iex> get_device(nonexistent_id)
|
||||
nil
|
||||
"""
|
||||
def get_device(equipment_id) do
|
||||
Repo.get_by(Device, equipment_id: equipment_id)
|
||||
def get_device(device_id) do
|
||||
Repo.get_by(Device, device_id: device_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the SNMP device for a piece of equipment with preloaded associations.
|
||||
Gets the SNMP device for a device with preloaded associations.
|
||||
"""
|
||||
def get_device_with_associations(equipment_id) do
|
||||
def get_device_with_associations(device_id) do
|
||||
Device
|
||||
|> where([d], d.equipment_id == ^equipment_id)
|
||||
|> where([d], d.device_id == ^device_id)
|
||||
|> preload([:interfaces, :sensors])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
|
@ -348,52 +348,52 @@ defmodule Towerops.Snmp do
|
|||
# Neighbor queries
|
||||
|
||||
@doc """
|
||||
Lists all neighbors for a piece of equipment.
|
||||
Lists all neighbors for a piece of device.
|
||||
"""
|
||||
def list_neighbors(equipment_id) do
|
||||
def list_neighbors(device_id) do
|
||||
Neighbor
|
||||
|> where([n], n.equipment_id == ^equipment_id)
|
||||
|> where([n], n.device_id == ^device_id)
|
||||
|> preload(:interface)
|
||||
|> order_by([n], [n.protocol, n.remote_system_name])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all neighbors for equipment with enriched data linking to known equipment.
|
||||
Lists all neighbors for device with enriched data linking to known device.
|
||||
Adds a `matched_equipment` field to each neighbor if we can identify the remote device.
|
||||
"""
|
||||
def list_neighbors_with_equipment(equipment_id) do
|
||||
neighbors = list_neighbors(equipment_id)
|
||||
def list_neighbors_with_equipment(device_id) do
|
||||
neighbors = list_neighbors(device_id)
|
||||
|
||||
# Get the organization_id from the equipment to search within the same org
|
||||
equipment = Towerops.Equipment.get_equipment!(equipment_id)
|
||||
# device to search within the same org
|
||||
device = Towerops.Devices.get_device!(device_id)
|
||||
|
||||
Enum.map(neighbors, fn neighbor ->
|
||||
matched_equipment = find_matching_equipment(neighbor, equipment.site.organization_id)
|
||||
Map.put(neighbor, :matched_equipment, matched_equipment)
|
||||
matched_device = find_matching_equipment(neighbor, device.site.organization_id)
|
||||
Map.put(neighbor, :matched_device, matched_device)
|
||||
end)
|
||||
end
|
||||
|
||||
# Try to match a neighbor to known equipment in the organization
|
||||
# device in the organization
|
||||
defp find_matching_equipment(neighbor, organization_id) do
|
||||
# Try matching by chassis ID (MAC address) first
|
||||
equipment_by_chassis =
|
||||
if neighbor.remote_chassis_id && is_mac_address?(neighbor.remote_chassis_id) do
|
||||
find_equipment_by_mac(neighbor.remote_chassis_id, organization_id)
|
||||
find_device_by_mac(neighbor.remote_chassis_id, organization_id)
|
||||
end
|
||||
|
||||
# Try matching by port ID (often contains MAC address in LLDP)
|
||||
equipment_by_port =
|
||||
if is_nil(equipment_by_chassis) && neighbor.remote_port_id &&
|
||||
is_mac_address?(neighbor.remote_port_id) do
|
||||
find_equipment_by_mac(neighbor.remote_port_id, organization_id)
|
||||
find_device_by_mac(neighbor.remote_port_id, organization_id)
|
||||
end
|
||||
|
||||
# Try matching by system name if MAC matches didn't work
|
||||
equipment_by_name =
|
||||
if is_nil(equipment_by_chassis) && is_nil(equipment_by_port) &&
|
||||
neighbor.remote_system_name do
|
||||
find_equipment_by_name(neighbor.remote_system_name, organization_id)
|
||||
find_device_by_name(neighbor.remote_system_name, organization_id)
|
||||
end
|
||||
|
||||
equipment_by_chassis || equipment_by_port || equipment_by_name
|
||||
|
|
@ -407,7 +407,7 @@ defmodule Towerops.Snmp do
|
|||
|
||||
defp is_mac_address?(_), do: false
|
||||
|
||||
defp find_equipment_by_mac(mac_address, organization_id) do
|
||||
defp find_device_by_mac(mac_address, organization_id) do
|
||||
# MAC address might be in format "aa:bb:cc:dd:ee:ff", "aa-bb-cc-dd-ee-ff", or "aabbccddeeff"
|
||||
# Normalize to just the hex digits for comparison
|
||||
normalized_mac = mac_address |> String.downcase() |> String.replace(~r/[:-]/, "")
|
||||
|
|
@ -416,8 +416,8 @@ defmodule Towerops.Snmp do
|
|||
from(i in Interface,
|
||||
join: d in Device,
|
||||
on: i.snmp_device_id == d.id,
|
||||
join: e in Equipment,
|
||||
on: d.equipment_id == e.id,
|
||||
join: e in DeviceSchema,
|
||||
on: d.device_id == e.id,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
where:
|
||||
|
|
@ -429,11 +429,11 @@ defmodule Towerops.Snmp do
|
|||
)
|
||||
end
|
||||
|
||||
defp find_equipment_by_name(system_name, organization_id) do
|
||||
defp find_device_by_name(system_name, organization_id) do
|
||||
# Try exact match first, then partial match
|
||||
normalized_name = String.downcase(system_name)
|
||||
|
||||
from(e in Equipment,
|
||||
from(e in DeviceSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
where: fragment("LOWER(?)", e.name) == ^normalized_name,
|
||||
|
|
@ -445,7 +445,7 @@ defmodule Towerops.Snmp do
|
|||
nil ->
|
||||
# Try partial match as fallback
|
||||
Repo.one(
|
||||
from(e in Equipment,
|
||||
from(e in DeviceSchema,
|
||||
join: s in assoc(e, :site),
|
||||
where: s.organization_id == ^organization_id,
|
||||
where: fragment("LOWER(?) LIKE ?", e.name, ^"%#{normalized_name}%"),
|
||||
|
|
@ -454,8 +454,8 @@ defmodule Towerops.Snmp do
|
|||
)
|
||||
)
|
||||
|
||||
equipment ->
|
||||
equipment
|
||||
device ->
|
||||
device
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -490,9 +490,9 @@ defmodule Towerops.Snmp do
|
|||
@doc """
|
||||
Deletes stale neighbors that haven't been seen since the given datetime.
|
||||
"""
|
||||
def delete_stale_neighbors(equipment_id, cutoff_datetime) do
|
||||
def delete_stale_neighbors(device_id, cutoff_datetime) do
|
||||
Neighbor
|
||||
|> where([n], n.equipment_id == ^equipment_id)
|
||||
|> where([n], n.device_id == ^device_id)
|
||||
|> where([n], n.last_discovered_at < ^cutoff_datetime)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule Towerops.Snmp.Device do
|
|||
import Ecto.Changeset
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
alias Towerops.Snmp.Interface
|
||||
alias Towerops.Snmp.Sensor
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ defmodule Towerops.Snmp.Device do
|
|||
field :model, :string
|
||||
field :firmware_version, :string
|
||||
|
||||
belongs_to :equipment, Equipment
|
||||
belongs_to :device, DeviceSchema
|
||||
|
||||
has_many :interfaces, Interface, foreign_key: :snmp_device_id
|
||||
has_many :sensors, Sensor, foreign_key: :snmp_device_id
|
||||
|
|
@ -41,8 +41,8 @@ defmodule Towerops.Snmp.Device do
|
|||
manufacturer: String.t() | nil,
|
||||
model: String.t() | nil,
|
||||
firmware_version: String.t() | nil,
|
||||
equipment_id: Ecto.UUID.t(),
|
||||
equipment: NotLoaded.t() | Equipment.t(),
|
||||
device_id: Ecto.UUID.t(),
|
||||
device: NotLoaded.t() | Devices.t(),
|
||||
interfaces: NotLoaded.t() | [Interface.t()],
|
||||
sensors: NotLoaded.t() | [Sensor.t()],
|
||||
inserted_at: DateTime.t(),
|
||||
|
|
@ -53,7 +53,7 @@ defmodule Towerops.Snmp.Device do
|
|||
def changeset(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [
|
||||
:equipment_id,
|
||||
:device_id,
|
||||
:sys_descr,
|
||||
:sys_object_id,
|
||||
:sys_name,
|
||||
|
|
@ -64,8 +64,8 @@ defmodule Towerops.Snmp.Device do
|
|||
:model,
|
||||
:firmware_version
|
||||
])
|
||||
|> validate_required([:equipment_id])
|
||||
|> unique_constraint(:equipment_id)
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> validate_required([:device_id])
|
||||
|> unique_constraint(:device_id)
|
||||
|> foreign_key_constraint(:device_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Equipment.Equipment, as: EquipmentSchema
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Device
|
||||
|
|
@ -83,23 +83,23 @@ defmodule Towerops.Snmp.Discovery do
|
|||
}
|
||||
|
||||
@doc """
|
||||
Runs discovery for a single piece of equipment.
|
||||
Runs discovery for a single piece of device.
|
||||
Returns {:ok, device} or {:error, reason}.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> discover_equipment(equipment)
|
||||
iex> discover_equipment(device)
|
||||
{:ok, %Device{manufacturer: "Cisco", model: "C2960"}}
|
||||
|
||||
iex> discover_equipment(equipment_without_snmp)
|
||||
iex> discover_equipment(device_without_snmp)
|
||||
{:error, :snmp_not_enabled}
|
||||
"""
|
||||
@spec discover_equipment(EquipmentSchema.t()) :: {:ok, Device.t()} | {:error, term()}
|
||||
def discover_equipment(%EquipmentSchema{} = equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
Logger.info("Starting SNMP discovery for equipment: #{equipment.name} (#{equipment.ip_address})")
|
||||
@spec discover_equipment(DeviceSchema.t()) :: {:ok, Device.t()} | {:error, term()}
|
||||
def discover_equipment(%DeviceSchema{} = device) do
|
||||
if device.snmp_enabled do
|
||||
Logger.info("Starting SNMP discovery for device: #{device.name} (#{device.ip_address})")
|
||||
|
||||
client_opts = build_client_opts(equipment)
|
||||
client_opts = build_client_opts(device)
|
||||
|
||||
with {:ok, _} <- Client.test_connection(client_opts),
|
||||
{:ok, system_info} <- discover_system(client_opts),
|
||||
|
|
@ -107,24 +107,24 @@ defmodule Towerops.Snmp.Discovery do
|
|||
{:ok, device_info} <- build_device_info(client_opts, system_info, profile),
|
||||
{:ok, interfaces} <- discover_interfaces(client_opts, profile),
|
||||
{:ok, sensors} <- discover_sensors(client_opts, profile),
|
||||
{:ok, device} <- save_discovery_results(equipment, device_info, interfaces, sensors),
|
||||
{:ok, neighbors} <- NeighborDiscovery.discover_neighbors(client_opts, device.interfaces),
|
||||
:ok <- save_neighbors(equipment.id, neighbors) do
|
||||
_ = update_equipment_discovery_time(equipment)
|
||||
Logger.info("SNMP discovery completed successfully for: #{equipment.name}")
|
||||
{:ok, discovered_device} <- save_discovery_results(device, device_info, interfaces, sensors),
|
||||
{:ok, neighbors} <- NeighborDiscovery.discover_neighbors(client_opts, discovered_device.interfaces),
|
||||
:ok <- save_neighbors(discovered_device.device_id, neighbors) do
|
||||
_ = update_device_discovery_time(device)
|
||||
Logger.info("SNMP discovery completed successfully for: #{device.name}")
|
||||
|
||||
# Broadcast discovery completion for real-time updates
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:#{equipment.id}",
|
||||
{:discovery_completed, equipment.id}
|
||||
"device:#{device.id}",
|
||||
{:discovery_completed, device.id}
|
||||
)
|
||||
|
||||
{:ok, device}
|
||||
{:ok, discovered_device}
|
||||
else
|
||||
{:error, reason} = error ->
|
||||
Logger.error("SNMP discovery failed for #{equipment.name}: #{inspect(reason)}")
|
||||
Logger.error("SNMP discovery failed for #{device.name}: #{inspect(reason)}")
|
||||
error
|
||||
end
|
||||
else
|
||||
|
|
@ -133,21 +133,21 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Runs discovery for all SNMP-enabled equipment in an organization.
|
||||
Runs discovery for all SNMP-enabled devices in an organization.
|
||||
Returns a summary of successful and failed discoveries.
|
||||
"""
|
||||
@spec discover_all(String.t()) :: {:ok, discovery_summary()}
|
||||
def discover_all(org_id) do
|
||||
equipment_list =
|
||||
EquipmentSchema
|
||||
device_list =
|
||||
DeviceSchema
|
||||
|> join(:inner, [e], s in assoc(e, :site))
|
||||
|> where([e, s], s.organization_id == ^org_id and e.snmp_enabled == true)
|
||||
|> Repo.all()
|
||||
|
||||
Logger.info("Starting SNMP discovery for #{length(equipment_list)} devices in org #{org_id}")
|
||||
Logger.info("Starting SNMP discovery for #{length(device_list)} devices in org #{org_id}")
|
||||
|
||||
results =
|
||||
equipment_list
|
||||
device_list
|
||||
|> Task.async_stream(
|
||||
&discover_equipment/1,
|
||||
max_concurrency: 5,
|
||||
|
|
@ -171,16 +171,16 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
# Private functions
|
||||
|
||||
@spec build_client_opts(EquipmentSchema.t()) :: Client.connection_opts()
|
||||
defp build_client_opts(equipment) do
|
||||
# Get SNMP config with hierarchical fallback (equipment -> site -> organization)
|
||||
snmp_config = Equipment.get_snmp_config(equipment)
|
||||
@spec build_client_opts(DeviceSchema.t()) :: Client.connection_opts()
|
||||
defp build_client_opts(device) do
|
||||
# Get SNMP config with hierarchical fallback (device -> site -> organization)
|
||||
snmp_config = Devices.get_snmp_config(device)
|
||||
|
||||
[
|
||||
ip: equipment.ip_address,
|
||||
ip: device.ip_address,
|
||||
community: snmp_config.community,
|
||||
version: snmp_config.version,
|
||||
port: equipment.snmp_port || 161,
|
||||
port: device.snmp_port || 161,
|
||||
timeout: 5000
|
||||
]
|
||||
end
|
||||
|
|
@ -272,32 +272,32 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end
|
||||
|
||||
@spec save_discovery_results(
|
||||
EquipmentSchema.t(),
|
||||
DeviceSchema.t(),
|
||||
device_info(),
|
||||
[interface_data()],
|
||||
[sensor_data()]
|
||||
) :: {:ok, Device.t()} | {:error, term()}
|
||||
defp save_discovery_results(equipment, device_info, interfaces, sensors) do
|
||||
defp save_discovery_results(device, device_info, interfaces, sensors) do
|
||||
Repo.transaction(fn ->
|
||||
# Upsert Device
|
||||
device = upsert_device(equipment, device_info)
|
||||
device = upsert_device(device, device_info)
|
||||
|
||||
# Delete old interfaces/sensors and insert new ones
|
||||
_ = delete_old_data(device)
|
||||
new_interfaces = insert_interfaces(device, interfaces)
|
||||
_ = insert_sensors(device, sensors)
|
||||
|
||||
# Return device with interfaces loaded and equipment_id added
|
||||
%{device | interfaces: Enum.map(new_interfaces, &Map.put(&1, :equipment_id, equipment.id))}
|
||||
# Return device with interfaces loaded and device_id added
|
||||
%{device | interfaces: Enum.map(new_interfaces, &Map.put(&1, :device_id, device.id))}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec upsert_device(EquipmentSchema.t(), device_info()) :: Device.t()
|
||||
defp upsert_device(equipment, device_info) do
|
||||
case Repo.get_by(Device, equipment_id: equipment.id) do
|
||||
@spec upsert_device(DeviceSchema.t(), device_info()) :: Device.t()
|
||||
defp upsert_device(device, device_info) do
|
||||
case Repo.get_by(Device, device_id: device.id) do
|
||||
nil ->
|
||||
%Device{}
|
||||
|> Device.changeset(Map.put(device_info, :equipment_id, equipment.id))
|
||||
|> Device.changeset(Map.put(device_info, :device_id, device.id))
|
||||
|> Repo.insert!()
|
||||
|
||||
existing_device ->
|
||||
|
|
@ -332,26 +332,26 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec update_equipment_discovery_time(EquipmentSchema.t()) ::
|
||||
{:ok, EquipmentSchema.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp update_equipment_discovery_time(equipment) do
|
||||
equipment
|
||||
@spec update_device_discovery_time(DeviceSchema.t()) ::
|
||||
{:ok, DeviceSchema.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp update_device_discovery_time(device) do
|
||||
device
|
||||
|> Ecto.Changeset.change(last_discovery_at: DateTime.truncate(DateTime.utc_now(), :second))
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@spec save_neighbors(binary(), [map()]) :: :ok
|
||||
defp save_neighbors(equipment_id, neighbors) do
|
||||
defp save_neighbors(device_id, neighbors) do
|
||||
# Delete stale neighbors (not seen in last 5 minutes)
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -5, :minute)
|
||||
Towerops.Snmp.delete_stale_neighbors(equipment_id, cutoff)
|
||||
Towerops.Snmp.delete_stale_neighbors(device_id, cutoff)
|
||||
|
||||
# Upsert each discovered neighbor
|
||||
Enum.each(neighbors, fn neighbor_data ->
|
||||
Towerops.Snmp.upsert_neighbor(neighbor_data)
|
||||
end)
|
||||
|
||||
Logger.info("Saved #{length(neighbors)} neighbors for equipment #{equipment_id}")
|
||||
Logger.info("Saved #{length(neighbors)} neighbors for device #{device_id}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Towerops.Snmp.Neighbor do
|
|||
field :remote_capabilities, {:array, :string}, default: []
|
||||
field :last_discovered_at, :utc_datetime
|
||||
|
||||
belongs_to :equipment, Towerops.Equipment.Equipment
|
||||
belongs_to :device, Towerops.Devices.Device
|
||||
belongs_to :interface, Towerops.Snmp.Interface
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
|
|
@ -29,12 +29,12 @@ defmodule Towerops.Snmp.Neighbor do
|
|||
# Ensure UUIDs are properly formatted strings before casting
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.update(:equipment_id, nil, &normalize_uuid/1)
|
||||
|> Map.update(:device_id, nil, &normalize_uuid/1)
|
||||
|> Map.update(:interface_id, nil, &normalize_uuid/1)
|
||||
|
||||
neighbor
|
||||
|> cast(attrs, [
|
||||
:equipment_id,
|
||||
:device_id,
|
||||
:interface_id,
|
||||
:protocol,
|
||||
:remote_chassis_id,
|
||||
|
|
@ -48,13 +48,13 @@ defmodule Towerops.Snmp.Neighbor do
|
|||
:last_discovered_at
|
||||
])
|
||||
|> validate_required([
|
||||
:equipment_id,
|
||||
:device_id,
|
||||
:interface_id,
|
||||
:protocol,
|
||||
:last_discovered_at
|
||||
])
|
||||
|> validate_inclusion(:protocol, ["lldp", "cdp"])
|
||||
|> foreign_key_constraint(:equipment_id)
|
||||
|> foreign_key_constraint(:device_id)
|
||||
|> foreign_key_constraint(:interface_id)
|
||||
|> unique_constraint([:interface_id, :remote_chassis_id, :protocol])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorker do
|
|||
"""
|
||||
use GenServer
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Snmp
|
||||
|
||||
require Logger
|
||||
|
|
@ -41,14 +41,14 @@ defmodule Towerops.Snmp.NeighborCleanupWorker do
|
|||
|
||||
Logger.info("Starting neighbor cleanup for records older than #{cutoff}")
|
||||
|
||||
# Get all equipment IDs with SNMP enabled
|
||||
equipment_list = Equipment.list_snmp_enabled_equipment()
|
||||
# device IDs with SNMP enabled
|
||||
equipment_list = Devices.list_snmp_enabled_devices()
|
||||
|
||||
total_deleted =
|
||||
Enum.reduce(equipment_list, 0, fn equipment, acc ->
|
||||
case Snmp.delete_stale_neighbors(equipment.id, cutoff) do
|
||||
Enum.reduce(equipment_list, 0, fn device, acc ->
|
||||
case Snmp.delete_stale_neighbors(device.id, cutoff) do
|
||||
{count, _} when count > 0 ->
|
||||
Logger.info("Deleted #{count} stale neighbors for equipment #{equipment.name} (#{equipment.id})")
|
||||
Logger.info("Deleted #{count} stale neighbors for device #{device.name} (#{device.id})")
|
||||
|
||||
acc + count
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Towerops.Snmp.NeighborDiscovery do
|
|||
@type neighbor_data :: %{
|
||||
required(:protocol) => String.t(),
|
||||
required(:interface_id) => binary(),
|
||||
required(:equipment_id) => binary(),
|
||||
required(:device_id) => binary(),
|
||||
optional(:remote_chassis_id) => String.t(),
|
||||
optional(:remote_system_name) => String.t(),
|
||||
optional(:remote_system_description) => String.t(),
|
||||
|
|
@ -124,7 +124,7 @@ defmodule Towerops.Snmp.NeighborDiscovery do
|
|||
%{
|
||||
protocol: protocol,
|
||||
interface_id: interface.id,
|
||||
equipment_id: interface.equipment_id,
|
||||
device_id: interface.device_id,
|
||||
remote_chassis_id: sanitize_string_field(neighbor_map[:remote_chassis_id]),
|
||||
remote_system_name: sanitize_string_field(neighbor_map[:remote_system_name]),
|
||||
remote_system_description: sanitize_string_field(neighbor_map[:remote_system_description]),
|
||||
|
|
|
|||
|
|
@ -45,16 +45,16 @@ defmodule Towerops.Snmp.Poller do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Builds client options from equipment record.
|
||||
Builds client options from device record.
|
||||
"""
|
||||
@impl true
|
||||
@spec build_client_opts(map()) :: Client.connection_opts()
|
||||
def build_client_opts(equipment) do
|
||||
def build_client_opts(device) do
|
||||
[
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port || 161,
|
||||
ip: device.ip_address,
|
||||
community: device.snmp_community,
|
||||
version: device.snmp_version,
|
||||
port: device.snmp_port || 161,
|
||||
timeout: 5000
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
- Interface statistics (bandwidth, errors, discards, etc.)
|
||||
- Neighbor discovery (LLDP/CDP topology information)
|
||||
|
||||
Runs independently of the connectivity monitoring in EquipmentMonitor.
|
||||
Poll interval is configurable per equipment (default: 60 seconds).
|
||||
Runs independently of the connectivity monitoring in DeviceMonitor.
|
||||
Poll interval is configurable per device (default: 60 seconds).
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.NeighborDiscovery
|
||||
|
|
@ -26,38 +26,38 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
# Client API
|
||||
|
||||
@doc """
|
||||
Starts a poller for the given equipment ID.
|
||||
Starts a poller for the given device ID.
|
||||
"""
|
||||
def start_link(equipment_id) do
|
||||
GenServer.start_link(__MODULE__, equipment_id, name: via_tuple(equipment_id))
|
||||
def start_link(device_id) do
|
||||
GenServer.start_link(__MODULE__, device_id, name: via_tuple(device_id))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Triggers an immediate poll for the equipment.
|
||||
Triggers an immediate poll for the device.
|
||||
"""
|
||||
def trigger_poll(equipment_id) do
|
||||
case Registry.lookup(PollerRegistry, equipment_id) do
|
||||
def trigger_poll(device_id) do
|
||||
case Registry.lookup(PollerRegistry, device_id) do
|
||||
[{_pid, _}] ->
|
||||
GenServer.cast(via_tuple(equipment_id), :poll_now)
|
||||
GenServer.cast(via_tuple(device_id), :poll_now)
|
||||
|
||||
[] ->
|
||||
# Process doesn't exist, perform poll directly in background
|
||||
Task.start(fn -> perform_poll(equipment_id) end)
|
||||
Task.start(fn -> perform_poll(device_id) end)
|
||||
end
|
||||
end
|
||||
|
||||
# Server Callbacks
|
||||
|
||||
@impl true
|
||||
def init(equipment_id) do
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
def init(device_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
# Subscribe to discovery completion events for this equipment
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{equipment_id}")
|
||||
# device
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}")
|
||||
|
||||
if equipment.snmp_enabled do
|
||||
if device.snmp_enabled do
|
||||
# Get the device to check if it has sensors/interfaces
|
||||
device = Snmp.get_device_with_associations(equipment_id)
|
||||
device = Snmp.get_device_with_associations(device_id)
|
||||
|
||||
if device && (device.sensors != [] || device.interfaces != []) do
|
||||
# Perform immediate poll when starting
|
||||
|
|
@ -65,17 +65,17 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end
|
||||
end
|
||||
|
||||
{:ok, %{equipment_id: equipment_id}}
|
||||
{:ok, %{device_id: device_id}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:poll_data, state) do
|
||||
_ = perform_poll(state.equipment_id)
|
||||
_ = perform_poll(state.device_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:discovery_completed, _equipment_id}, state) do
|
||||
def handle_info({:discovery_completed, _device_id}, state) do
|
||||
Logger.info("Discovery completed, triggering immediate poll")
|
||||
# Trigger immediate poll after discovery
|
||||
send(self(), :poll_data)
|
||||
|
|
@ -84,100 +84,100 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
|
||||
@impl true
|
||||
def handle_info(_msg, state) do
|
||||
# Ignore other messages (like equipment status changes)
|
||||
# device status changes)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:poll_now, state) do
|
||||
_ = perform_poll(state.equipment_id)
|
||||
_ = perform_poll(state.device_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# Private Functions
|
||||
|
||||
defp perform_poll(equipment_id) do
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
defp perform_poll(device_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
if equipment.snmp_enabled do
|
||||
poll_equipment_device(equipment_id, equipment)
|
||||
if device.snmp_enabled do
|
||||
poll_equipment_device(device_id, device)
|
||||
end
|
||||
end
|
||||
|
||||
defp poll_equipment_device(equipment_id, equipment) do
|
||||
device = Snmp.get_device_with_associations(equipment_id)
|
||||
defp poll_equipment_device(device_id, device) do
|
||||
snmp_device = Snmp.get_device_with_associations(device_id)
|
||||
|
||||
if device do
|
||||
poll_interval = get_poll_interval(equipment)
|
||||
execute_device_poll(device, equipment, poll_interval)
|
||||
if snmp_device do
|
||||
poll_interval = get_poll_interval(device)
|
||||
execute_device_poll(device, snmp_device, poll_interval)
|
||||
schedule_next_poll(poll_interval)
|
||||
end
|
||||
end
|
||||
|
||||
defp execute_device_poll(device, equipment, poll_interval) do
|
||||
if should_skip_poll?(equipment, poll_interval, 5) do
|
||||
Logger.debug("Skipping poll for #{equipment.name} - recently polled by another process")
|
||||
defp execute_device_poll(device, snmp_device, poll_interval) do
|
||||
if should_skip_poll?(device, poll_interval, 5) do
|
||||
Logger.debug("Skipping poll for #{device.name} - recently polled by another process")
|
||||
else
|
||||
perform_device_data_collection(device, equipment)
|
||||
perform_device_data_collection(device, snmp_device)
|
||||
end
|
||||
end
|
||||
|
||||
defp perform_device_data_collection(device, equipment) do
|
||||
Equipment.update_snmp_poll_time(equipment)
|
||||
defp perform_device_data_collection(device, snmp_device) do
|
||||
Devices.update_snmp_poll_time(device)
|
||||
|
||||
client_opts = build_client_opts(equipment)
|
||||
client_opts = build_client_opts(device)
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
poll_device_sensors(device, equipment, client_opts, now)
|
||||
poll_device_interfaces(device, equipment, client_opts, now)
|
||||
check_device_interface_changes(device, equipment, client_opts, now)
|
||||
poll_device_neighbors(device, equipment, client_opts)
|
||||
poll_device_sensors(device, snmp_device, client_opts, now)
|
||||
poll_device_interfaces(device, snmp_device, client_opts, now)
|
||||
check_device_interface_changes(device, snmp_device, client_opts, now)
|
||||
poll_device_neighbors(device, snmp_device, client_opts)
|
||||
end
|
||||
|
||||
defp poll_device_sensors(device, equipment, client_opts, now) do
|
||||
poll_sensors(device.sensors, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.sensors)} sensors for #{equipment.name}")
|
||||
defp poll_device_sensors(device, snmp_device, client_opts, now) do
|
||||
poll_sensors(snmp_device.sensors, client_opts, now)
|
||||
Logger.debug("Polled #{length(snmp_device.sensors)} sensors for #{device.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Error polling sensors for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
Logger.error("Error polling sensors for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
end
|
||||
|
||||
defp poll_device_interfaces(device, equipment, client_opts, now) do
|
||||
poll_interfaces(device.interfaces, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.interfaces)} interfaces for #{equipment.name}")
|
||||
defp poll_device_interfaces(device, snmp_device, client_opts, now) do
|
||||
poll_interfaces(snmp_device.interfaces, client_opts, now)
|
||||
Logger.debug("Polled #{length(snmp_device.interfaces)} interfaces for #{device.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Error polling interfaces for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
Logger.error("Error polling interfaces for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
end
|
||||
|
||||
defp check_device_interface_changes(device, equipment, client_opts, now) do
|
||||
check_interface_changes(device.interfaces, equipment, client_opts, now)
|
||||
defp check_device_interface_changes(device, snmp_device, client_opts, now) do
|
||||
check_interface_changes(snmp_device.interfaces, device, client_opts, now)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error(
|
||||
"Error checking interface changes for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
"Error checking interface changes for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
)
|
||||
end
|
||||
|
||||
defp poll_device_neighbors(device, equipment, client_opts) do
|
||||
# Add equipment_id to interfaces for neighbor discovery
|
||||
interfaces_with_equipment = Enum.map(device.interfaces, &Map.put(&1, :equipment_id, equipment.id))
|
||||
defp poll_device_neighbors(device, snmp_device, client_opts) do
|
||||
# Add device_id to interfaces for neighbor discovery
|
||||
interfaces_with_device = Enum.map(snmp_device.interfaces, &Map.put(&1, :device_id, snmp_device.id))
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces_with_equipment)
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces_with_device)
|
||||
|
||||
# Delete stale neighbors (not seen in last 5 minutes)
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -5, :minute)
|
||||
Snmp.delete_stale_neighbors(equipment.id, cutoff)
|
||||
Snmp.delete_stale_neighbors(device.id, cutoff)
|
||||
|
||||
# Upsert each discovered neighbor
|
||||
Enum.each(neighbors, fn neighbor_data ->
|
||||
Snmp.upsert_neighbor(neighbor_data)
|
||||
end)
|
||||
|
||||
Logger.debug("Polled and saved #{length(neighbors)} neighbors for #{equipment.name}")
|
||||
Logger.debug("Polled and saved #{length(neighbors)} neighbors for #{device.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Error polling neighbors for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
Logger.error("Error polling neighbors for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
end
|
||||
|
||||
defp poll_sensors(sensors, client_opts, timestamp) do
|
||||
|
|
@ -319,11 +319,11 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end)
|
||||
end
|
||||
|
||||
defp check_interface_changes(interfaces, equipment, client_opts, timestamp) do
|
||||
defp check_interface_changes(interfaces, device, client_opts, timestamp) do
|
||||
Enum.each(interfaces, fn interface ->
|
||||
case get_interface_attributes(client_opts, interface.if_index) do
|
||||
{:ok, current_attrs} ->
|
||||
detect_and_log_changes(interface, current_attrs, equipment.id, timestamp)
|
||||
detect_and_log_changes(interface, current_attrs, device.id, timestamp)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.debug("Failed to get interface attributes for #{interface.if_name}: #{inspect(reason)}")
|
||||
|
|
@ -365,13 +365,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
DateTime.t()
|
||||
) :: :ok | nil
|
||||
defp detect_and_log_changes(interface, current_attrs, equipment_id, timestamp) do
|
||||
defp detect_and_log_changes(interface, current_attrs, device_id, timestamp) do
|
||||
events =
|
||||
[]
|
||||
|> maybe_add_oper_status_event(interface, current_attrs, equipment_id, timestamp)
|
||||
|> maybe_add_admin_status_event(interface, current_attrs, equipment_id, timestamp)
|
||||
|> maybe_add_speed_change_event(interface, current_attrs, equipment_id, timestamp)
|
||||
|> maybe_add_mac_change_event(interface, current_attrs, equipment_id, timestamp)
|
||||
|> maybe_add_oper_status_event(interface, current_attrs, device_id, timestamp)
|
||||
|> maybe_add_admin_status_event(interface, current_attrs, device_id, timestamp)
|
||||
|> maybe_add_speed_change_event(interface, current_attrs, device_id, timestamp)
|
||||
|> maybe_add_mac_change_event(interface, current_attrs, device_id, timestamp)
|
||||
|
||||
if events != [] do
|
||||
broadcast_interface_events(events)
|
||||
|
|
@ -379,16 +379,16 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_add_oper_status_event(events, interface, current_attrs, equipment_id, timestamp) do
|
||||
defp maybe_add_oper_status_event(events, interface, current_attrs, device_id, timestamp) do
|
||||
if interface.if_oper_status == current_attrs.if_oper_status do
|
||||
events
|
||||
else
|
||||
event = build_oper_status_event(interface, current_attrs, equipment_id, timestamp)
|
||||
event = build_oper_status_event(interface, current_attrs, device_id, timestamp)
|
||||
[{:event, event} | events]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_oper_status_event(interface, current_attrs, equipment_id, timestamp) do
|
||||
defp build_oper_status_event(interface, current_attrs, device_id, timestamp) do
|
||||
message =
|
||||
if interface.if_oper_status do
|
||||
"Interface #{interface.if_name} changed from #{String.upcase(interface.if_oper_status)} to #{String.upcase(current_attrs.if_oper_status)}"
|
||||
|
|
@ -397,7 +397,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end
|
||||
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
event_type: if(current_attrs.if_oper_status == "up", do: "interface_up", else: "interface_down"),
|
||||
severity: if(current_attrs.if_oper_status == "up", do: "info", else: "warning"),
|
||||
message: message,
|
||||
|
|
@ -411,18 +411,18 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
}
|
||||
end
|
||||
|
||||
defp maybe_add_admin_status_event(events, interface, current_attrs, equipment_id, timestamp) do
|
||||
defp maybe_add_admin_status_event(events, interface, current_attrs, device_id, timestamp) do
|
||||
if interface.if_admin_status == current_attrs.if_admin_status do
|
||||
events
|
||||
else
|
||||
event = build_admin_status_event(interface, current_attrs, equipment_id, timestamp)
|
||||
event = build_admin_status_event(interface, current_attrs, device_id, timestamp)
|
||||
[{:event, event} | events]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_admin_status_event(interface, current_attrs, equipment_id, timestamp) do
|
||||
defp build_admin_status_event(interface, current_attrs, device_id, timestamp) do
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
event_type: "interface_admin_status_change",
|
||||
severity: "info",
|
||||
message:
|
||||
|
|
@ -437,22 +437,22 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
}
|
||||
end
|
||||
|
||||
defp maybe_add_speed_change_event(events, interface, current_attrs, equipment_id, timestamp) do
|
||||
defp maybe_add_speed_change_event(events, interface, current_attrs, device_id, timestamp) do
|
||||
if interface.if_speed != current_attrs.if_speed && current_attrs.if_speed != nil do
|
||||
event = build_speed_change_event(interface, current_attrs, equipment_id, timestamp)
|
||||
event = build_speed_change_event(interface, current_attrs, device_id, timestamp)
|
||||
[{:event, event} | events]
|
||||
else
|
||||
events
|
||||
end
|
||||
end
|
||||
|
||||
defp build_speed_change_event(interface, current_attrs, equipment_id, timestamp) do
|
||||
defp build_speed_change_event(interface, current_attrs, device_id, timestamp) do
|
||||
is_initial = interface.if_speed == nil
|
||||
severity = determine_speed_change_severity(is_initial, interface.if_speed, current_attrs.if_speed)
|
||||
message = format_speed_change_message(interface.if_name, is_initial, interface.if_speed, current_attrs.if_speed)
|
||||
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
event_type: "interface_speed_change",
|
||||
severity: severity,
|
||||
message: message,
|
||||
|
|
@ -478,23 +478,23 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
"Interface #{if_name} speed changed from #{format_speed(old_speed)} to #{format_speed(new_speed)}"
|
||||
end
|
||||
|
||||
defp maybe_add_mac_change_event(events, interface, current_attrs, equipment_id, timestamp) do
|
||||
defp maybe_add_mac_change_event(events, interface, current_attrs, device_id, timestamp) do
|
||||
if interface.if_phys_address != current_attrs.if_phys_address && current_attrs.if_phys_address != nil do
|
||||
event = build_mac_change_event(interface, current_attrs, equipment_id, timestamp)
|
||||
event = build_mac_change_event(interface, current_attrs, device_id, timestamp)
|
||||
[{:event, event} | events]
|
||||
else
|
||||
events
|
||||
end
|
||||
end
|
||||
|
||||
defp build_mac_change_event(interface, current_attrs, equipment_id, timestamp) do
|
||||
defp build_mac_change_event(interface, current_attrs, device_id, timestamp) do
|
||||
is_initial = interface.if_phys_address == nil
|
||||
|
||||
message =
|
||||
format_mac_change_message(interface.if_name, is_initial, interface.if_phys_address, current_attrs.if_phys_address)
|
||||
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
event_type: "interface_mac_change",
|
||||
severity: if(is_initial, do: "info", else: "warning"),
|
||||
message: message,
|
||||
|
|
@ -521,8 +521,8 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:events",
|
||||
{:equipment_event, event_attrs}
|
||||
"device:events",
|
||||
{:device_event, event_attrs}
|
||||
)
|
||||
|
||||
Logger.debug("Broadcast event: #{event_attrs.message}")
|
||||
|
|
@ -635,30 +635,30 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
nil
|
||||
end
|
||||
|
||||
defp build_client_opts(equipment) do
|
||||
# Get SNMP config with hierarchical fallback (equipment -> site -> organization)
|
||||
snmp_config = Equipment.get_snmp_config(equipment)
|
||||
defp build_client_opts(device) do
|
||||
# Get SNMP config with hierarchical fallback (device -> site -> organization)
|
||||
snmp_config = Devices.get_snmp_config(device)
|
||||
|
||||
[
|
||||
ip: equipment.ip_address,
|
||||
ip: device.ip_address,
|
||||
community: snmp_config.community,
|
||||
version: snmp_config.version,
|
||||
port: equipment.snmp_port || 161,
|
||||
port: device.snmp_port || 161,
|
||||
timeout: 5000
|
||||
]
|
||||
end
|
||||
|
||||
defp get_poll_interval(equipment) do
|
||||
defp get_poll_interval(device) do
|
||||
# Use check_interval_seconds, but minimum of 30 seconds for SNMP polling
|
||||
max(equipment.check_interval_seconds || @default_poll_interval, 30)
|
||||
max(device.check_interval_seconds || @default_poll_interval, 30)
|
||||
end
|
||||
|
||||
defp schedule_next_poll(interval_seconds) do
|
||||
Process.send_after(self(), :poll_data, interval_seconds * 1000)
|
||||
end
|
||||
|
||||
defp should_skip_poll?(equipment, poll_interval_seconds, grace_period_seconds) do
|
||||
case equipment.last_snmp_poll_at do
|
||||
defp should_skip_poll?(device, poll_interval_seconds, grace_period_seconds) do
|
||||
case device.last_snmp_poll_at do
|
||||
nil ->
|
||||
false
|
||||
|
||||
|
|
@ -678,13 +678,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
if sensor.last_value == nil do
|
||||
:ok
|
||||
else
|
||||
equipment_id = get_equipment_id_from_sensor(sensor)
|
||||
device_id = get_device_id_from_sensor(sensor)
|
||||
thresholds = extract_thresholds(sensor.metadata)
|
||||
|
||||
events =
|
||||
[]
|
||||
|> maybe_add_threshold_event(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
|> maybe_add_change_event(sensor, current_value, timestamp, equipment_id)
|
||||
|> maybe_add_threshold_event(sensor, current_value, timestamp, device_id, thresholds)
|
||||
|> maybe_add_change_event(sensor, current_value, timestamp, device_id)
|
||||
|
||||
broadcast_sensor_events(events)
|
||||
end
|
||||
|
|
@ -705,8 +705,8 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
}
|
||||
end
|
||||
|
||||
defp maybe_add_threshold_event(events, sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
threshold_event = check_threshold_violation(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
defp maybe_add_threshold_event(events, sensor, current_value, timestamp, device_id, thresholds) do
|
||||
threshold_event = check_threshold_violation(sensor, current_value, timestamp, device_id, thresholds)
|
||||
|
||||
if threshold_event, do: [threshold_event | events], else: events
|
||||
end
|
||||
|
|
@ -718,7 +718,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_threshold_violation(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_threshold_violation(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
threshold_checks = [
|
||||
&check_critical_high/5,
|
||||
&check_critical_low/5,
|
||||
|
|
@ -727,8 +727,8 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
]
|
||||
|
||||
Enum.find_value(threshold_checks, fn check_fn ->
|
||||
check_fn.(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
end) || check_returned_to_normal(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
check_fn.(sensor, current_value, timestamp, device_id, thresholds)
|
||||
end) || check_returned_to_normal(sensor, current_value, timestamp, device_id, thresholds)
|
||||
end
|
||||
|
||||
@spec check_critical_high(
|
||||
|
|
@ -738,13 +738,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_critical_high(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_critical_high(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
if thresholds.critical_high && current_value >= thresholds.critical_high do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
device_id,
|
||||
"critical_high",
|
||||
thresholds.critical_high,
|
||||
"critical",
|
||||
|
|
@ -760,13 +760,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_critical_low(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_critical_low(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
if thresholds.critical_low && current_value <= thresholds.critical_low do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
device_id,
|
||||
"critical_low",
|
||||
thresholds.critical_low,
|
||||
"critical",
|
||||
|
|
@ -782,13 +782,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_warning_high(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_warning_high(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
if thresholds.warning_high && current_value >= thresholds.warning_high do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
device_id,
|
||||
"warning_high",
|
||||
thresholds.warning_high,
|
||||
"warning",
|
||||
|
|
@ -804,13 +804,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_warning_low(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_warning_low(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
if thresholds.warning_low && current_value <= thresholds.warning_low do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
device_id,
|
||||
"warning_low",
|
||||
thresholds.warning_low,
|
||||
"warning",
|
||||
|
|
@ -823,7 +823,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
device_id,
|
||||
threshold_type,
|
||||
threshold_value,
|
||||
severity,
|
||||
|
|
@ -832,7 +832,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
event_type = if severity == "critical", do: "sensor_threshold_critical", else: "sensor_threshold_warning"
|
||||
|
||||
build_sensor_event(
|
||||
equipment_id,
|
||||
device_id,
|
||||
sensor,
|
||||
event_type,
|
||||
severity,
|
||||
|
|
@ -847,13 +847,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
)
|
||||
end
|
||||
|
||||
defp check_returned_to_normal(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
defp check_returned_to_normal(sensor, current_value, timestamp, device_id, thresholds) do
|
||||
was_over_threshold = value_was_over_threshold?(sensor.last_value, thresholds)
|
||||
is_now_normal = value_is_normal?(current_value, thresholds)
|
||||
|
||||
if was_over_threshold && is_now_normal do
|
||||
build_sensor_event(
|
||||
equipment_id,
|
||||
device_id,
|
||||
sensor,
|
||||
"sensor_threshold_normal",
|
||||
"info",
|
||||
|
|
@ -878,24 +878,24 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
(thresholds.critical_low == nil || current_value > thresholds.critical_low)
|
||||
end
|
||||
|
||||
defp maybe_add_change_event(events, sensor, current_value, timestamp, equipment_id) do
|
||||
defp maybe_add_change_event(events, sensor, current_value, timestamp, device_id) do
|
||||
# Only check for spikes/drops on percentage sensors
|
||||
if sensor.sensor_unit == "%" do
|
||||
change_event = check_significant_change(sensor, current_value, timestamp, equipment_id)
|
||||
change_event = check_significant_change(sensor, current_value, timestamp, device_id)
|
||||
if change_event, do: [change_event | events], else: events
|
||||
else
|
||||
events
|
||||
end
|
||||
end
|
||||
|
||||
defp check_significant_change(sensor, current_value, timestamp, equipment_id) do
|
||||
defp check_significant_change(sensor, current_value, timestamp, device_id) do
|
||||
change_percent = abs(current_value - sensor.last_value)
|
||||
|
||||
cond do
|
||||
# Spike: increase of 30% or more
|
||||
change_percent >= 30 && current_value > sensor.last_value ->
|
||||
build_sensor_event(
|
||||
equipment_id,
|
||||
device_id,
|
||||
sensor,
|
||||
"sensor_value_spike",
|
||||
"warning",
|
||||
|
|
@ -907,7 +907,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
# Drop: decrease of 30% or more
|
||||
change_percent >= 30 && current_value < sensor.last_value ->
|
||||
build_sensor_event(
|
||||
equipment_id,
|
||||
device_id,
|
||||
sensor,
|
||||
"sensor_value_drop",
|
||||
"info",
|
||||
|
|
@ -923,12 +923,12 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
|
||||
defp broadcast_sensor_events(events) do
|
||||
Enum.each(events, fn event ->
|
||||
_ = Phoenix.PubSub.broadcast(Towerops.PubSub, "equipment:events", {:equipment_event, event})
|
||||
_ = Phoenix.PubSub.broadcast(Towerops.PubSub, "device:events", {:device_event, event})
|
||||
Logger.debug("Sensor event: #{event.message}")
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_sensor_event(equipment_id, sensor, event_type, severity, message, metadata, timestamp) do
|
||||
defp build_sensor_event(device_id, sensor, event_type, severity, message, metadata, timestamp) do
|
||||
base_metadata = %{
|
||||
sensor_id: sensor.id,
|
||||
sensor_name: sensor.sensor_descr,
|
||||
|
|
@ -937,7 +937,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
}
|
||||
|
||||
%{
|
||||
equipment_id: equipment_id,
|
||||
device_id: device_id,
|
||||
event_type: event_type,
|
||||
severity: severity,
|
||||
message: message,
|
||||
|
|
@ -946,11 +946,11 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
}
|
||||
end
|
||||
|
||||
defp get_equipment_id_from_sensor(sensor) do
|
||||
# Get the device to find equipment_id
|
||||
defp get_device_id_from_sensor(sensor) do
|
||||
# Get the device to find device_id
|
||||
case Towerops.Repo.get(Towerops.Snmp.Device, sensor.snmp_device_id) do
|
||||
nil -> nil
|
||||
device -> device.equipment_id
|
||||
device -> device.device_id
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -960,7 +960,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
|
||||
defp format_sensor_value(_, _), do: "N/A"
|
||||
|
||||
defp via_tuple(equipment_id) do
|
||||
{:via, Registry, {PollerRegistry, equipment_id}}
|
||||
defp via_tuple(device_id) do
|
||||
{:via, Registry, {PollerRegistry, device_id}}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
alias Towerops.Agent.SnmpQuery
|
||||
alias Towerops.Agent.SnmpResult
|
||||
alias Towerops.Agents
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.Discovery
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
|
||||
Logger.error("Agent job error",
|
||||
agent_token_id: socket.assigns.agent_token_id,
|
||||
equipment_id: error.equipment_id,
|
||||
device_id: error.device_id,
|
||||
error: error.message
|
||||
)
|
||||
|
||||
|
|
@ -121,51 +121,51 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
defp build_jobs_for_agent(agent_token_id) do
|
||||
agent_token_id
|
||||
|> Agents.list_agent_polling_targets()
|
||||
|> Enum.map(&build_job_for_equipment/1)
|
||||
|> Enum.map(&build_job_for_device/1)
|
||||
end
|
||||
|
||||
defp build_job_for_equipment(equipment) do
|
||||
if needs_discovery?(equipment) do
|
||||
build_discovery_job(equipment)
|
||||
defp build_job_for_device(device) do
|
||||
if needs_discovery?(device) do
|
||||
build_discovery_job(device)
|
||||
else
|
||||
build_polling_job(equipment)
|
||||
build_polling_job(device)
|
||||
end
|
||||
end
|
||||
|
||||
defp needs_discovery?(equipment) do
|
||||
defp needs_discovery?(device) do
|
||||
# Need discovery if no SNMP device or last discovery was >24 hours ago
|
||||
is_nil(equipment.snmp_device) or
|
||||
is_nil(equipment.last_discovery_at) or
|
||||
DateTime.diff(DateTime.utc_now(), equipment.last_discovery_at, :hour) > 24
|
||||
is_nil(device.snmp_device) or
|
||||
is_nil(device.last_discovery_at) or
|
||||
DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour) > 24
|
||||
end
|
||||
|
||||
defp build_discovery_job(equipment) do
|
||||
defp build_discovery_job(device) do
|
||||
%AgentJob{
|
||||
job_id: "discover:#{equipment.id}",
|
||||
job_id: "discover:#{device.id}",
|
||||
job_type: :DISCOVER,
|
||||
equipment_id: equipment.id,
|
||||
device: %SnmpDevice{
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port || 161
|
||||
device_id: device.id,
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: device.ip_address,
|
||||
community: device.snmp_community,
|
||||
version: device.snmp_version,
|
||||
port: device.snmp_port || 161
|
||||
},
|
||||
queries: build_discovery_queries()
|
||||
}
|
||||
end
|
||||
|
||||
defp build_polling_job(equipment) do
|
||||
device = equipment.snmp_device
|
||||
defp build_polling_job(device) do
|
||||
_snmp_device = device.snmp_device
|
||||
|
||||
%AgentJob{
|
||||
job_id: "poll:#{equipment.id}",
|
||||
job_id: "poll:#{device.id}",
|
||||
job_type: :POLL,
|
||||
equipment_id: equipment.id,
|
||||
device: %SnmpDevice{
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port || 161
|
||||
device_id: device.id,
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: device.ip_address,
|
||||
community: device.snmp_community,
|
||||
version: device.snmp_version,
|
||||
port: device.snmp_port || 161
|
||||
},
|
||||
queries: build_polling_queries(device)
|
||||
}
|
||||
|
|
@ -272,48 +272,48 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
end
|
||||
|
||||
defp process_snmp_result(organization_id, result) do
|
||||
case Equipment.get_equipment_with_details(result.equipment_id) do
|
||||
case Devices.get_device_with_details(result.device_id) do
|
||||
nil ->
|
||||
Logger.error("Equipment not found: #{result.equipment_id}")
|
||||
Logger.error("Device not found: #{result.device_id}")
|
||||
|
||||
equipment ->
|
||||
# Verify equipment belongs to agent's organization
|
||||
if equipment.site.organization_id == organization_id do
|
||||
device ->
|
||||
# Verify device belongs to agent's organization
|
||||
if device.site.organization_id == organization_id do
|
||||
case result.job_type do
|
||||
:DISCOVER -> process_discovery_result(equipment, result)
|
||||
:POLL -> process_polling_result(equipment, result)
|
||||
:DISCOVER -> process_discovery_result(device, result)
|
||||
:POLL -> process_polling_result(device, result)
|
||||
end
|
||||
else
|
||||
Logger.error("Equipment #{result.equipment_id} not in agent's organization")
|
||||
Logger.error("Device #{result.device_id} not in agent's organization")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp process_discovery_result(equipment, _result) do
|
||||
defp process_discovery_result(device, _result) do
|
||||
# Agent confirmed device is reachable, trigger full discovery from server
|
||||
# This hybrid approach simplifies the initial implementation:
|
||||
# - Agent acts as remote executor
|
||||
# - Server does SNMP queries and parsing using existing logic
|
||||
Logger.info("Discovery results received for #{equipment.name}, triggering full discovery")
|
||||
Logger.info("Discovery results received for #{device.name}, triggering full discovery")
|
||||
|
||||
Task.start(fn ->
|
||||
case Discovery.discover_equipment(equipment) do
|
||||
case Discovery.discover_equipment(device) do
|
||||
{:ok, _device} ->
|
||||
Logger.info("Full discovery completed for #{equipment.name}")
|
||||
Logger.info("Full discovery completed for #{device.name}")
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Discovery failed for #{equipment.name}: #{inspect(reason)}")
|
||||
Logger.error("Discovery failed for #{device.name}: #{inspect(reason)}")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_polling_result(equipment, result) do
|
||||
device = equipment.snmp_device
|
||||
defp process_polling_result(device, result) do
|
||||
snmp_device = device.snmp_device
|
||||
oid_values = Map.new(result.oid_values)
|
||||
timestamp = DateTime.from_unix!(result.timestamp, :second)
|
||||
|
||||
# Process sensor readings
|
||||
Enum.each(device.sensors, fn sensor ->
|
||||
Enum.each(snmp_device.sensors, fn sensor ->
|
||||
if value = Map.get(oid_values, sensor.sensor_oid) do
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ defmodule ToweropsWeb.Layouts do
|
|||
|
||||
attr :active_page, :string,
|
||||
default: nil,
|
||||
doc: "the currently active page (dashboard, sites, equipment, or alerts)"
|
||||
doc: "the currently active page (dashboard, sites, device, or alerts)"
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
|
|
@ -152,10 +152,10 @@ defmodule ToweropsWeb.Layouts do
|
|||
Sites
|
||||
</.nav_link>
|
||||
<.nav_link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
active={@active_page == "equipment"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices"}
|
||||
active={@active_page == "devices"}
|
||||
>
|
||||
Equipment
|
||||
Devices
|
||||
</.nav_link>
|
||||
<.nav_link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Sites
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
"id": "uuid",
|
||||
"name": "Acme Corp",
|
||||
"sites_count": 3,
|
||||
"equipment_count": 15,
|
||||
"device_count": 15,
|
||||
"active_alerts_count": 2
|
||||
}
|
||||
]
|
||||
|
|
@ -40,7 +40,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
id: org.id,
|
||||
name: org.name,
|
||||
sites_count: Sites.count_organization_sites(org.id),
|
||||
equipment_count: Equipment.count_organization_equipment(org.id),
|
||||
device_count: Devices.count_organization_devices(org.id),
|
||||
active_alerts_count: Alerts.count_active_alerts(org.id)
|
||||
}
|
||||
end)
|
||||
|
|
@ -60,7 +60,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
"id": "uuid",
|
||||
"name": "Main Office",
|
||||
"location": "New York, NY",
|
||||
"equipment_count": 5,
|
||||
"device_count": 5,
|
||||
"equipment_down_count": 1
|
||||
}
|
||||
]
|
||||
|
|
@ -79,8 +79,8 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
id: site.id,
|
||||
name: site.name,
|
||||
location: site.location,
|
||||
equipment_count: Equipment.count_site_equipment(site.id),
|
||||
equipment_down_count: Equipment.count_site_equipment_down(site.id)
|
||||
device_count: Devices.count_site_devices(site.id),
|
||||
equipment_down_count: Devices.count_site_devices_down(site.id)
|
||||
}
|
||||
end)
|
||||
|
||||
|
|
@ -94,9 +94,9 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
end
|
||||
|
||||
@doc """
|
||||
GET /api/v1/mobile/organizations/:id/equipment
|
||||
GET /api/v1/mobile/organizations/:id/devices
|
||||
|
||||
Returns list of equipment for an organization with current status.
|
||||
Returns list of devices for an organization with current status.
|
||||
|
||||
Query params:
|
||||
- site_id: Filter by site (optional)
|
||||
|
|
@ -104,7 +104,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
|
||||
Response:
|
||||
{
|
||||
"equipment": [
|
||||
"device": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Core Router",
|
||||
|
|
@ -117,17 +117,17 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
]
|
||||
}
|
||||
"""
|
||||
def list_equipment(conn, %{"organization_id" => org_id} = params) do
|
||||
def list_devices(conn, %{"organization_id" => org_id} = params) do
|
||||
user = conn.assigns.current_user
|
||||
|
||||
case verify_organization_access(user, org_id) do
|
||||
{:ok, _org} ->
|
||||
equipment_list =
|
||||
org_id
|
||||
|> Equipment.list_organization_equipment(params)
|
||||
|> Devices.list_organization_devices(params)
|
||||
|> Enum.map(&format_equipment/1)
|
||||
|
||||
json(conn, %{equipment: equipment_list})
|
||||
json(conn, %{device: equipment_list})
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
conn
|
||||
|
|
@ -137,9 +137,9 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
end
|
||||
|
||||
@doc """
|
||||
GET /api/v1/mobile/equipment/:id
|
||||
GET /api/v1/mobile/devices/:id
|
||||
|
||||
Returns detailed equipment information including interfaces and sensors.
|
||||
Returns detailed device information including interfaces and sensors.
|
||||
|
||||
Response:
|
||||
{
|
||||
|
|
@ -153,24 +153,24 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
"sensors": [...]
|
||||
}
|
||||
"""
|
||||
def get_equipment(conn, %{"id" => equipment_id}) do
|
||||
def get_device(conn, %{"id" => device_id}) do
|
||||
user = conn.assigns.current_user
|
||||
|
||||
case Equipment.get_equipment_with_details(equipment_id) do
|
||||
case Devices.get_device_with_details(device_id) do
|
||||
nil ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Equipment not found"})
|
||||
|> json(%{error: "Device not found"})
|
||||
|
||||
equipment ->
|
||||
case verify_organization_access(user, equipment.organization_id) do
|
||||
device ->
|
||||
case verify_organization_access(user, device.organization_id) do
|
||||
{:ok, _org} ->
|
||||
json(conn, format_equipment_details(equipment))
|
||||
json(conn, format_equipment_details(device))
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Access denied to this equipment"})
|
||||
|> json(%{error: "Access denied to this device"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -192,9 +192,9 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
"id": "uuid",
|
||||
"severity": "critical",
|
||||
"status": "active",
|
||||
"message": "Equipment Down: Core Router",
|
||||
"message": "Device Down: Core Router",
|
||||
"equipment_name": "Core Router",
|
||||
"equipment_id": "uuid",
|
||||
"device_id": "uuid",
|
||||
"occurred_at": "2026-01-15T19:44:25Z"
|
||||
}
|
||||
]
|
||||
|
|
@ -231,31 +231,31 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
end
|
||||
end
|
||||
|
||||
defp format_equipment(equipment) do
|
||||
defp format_equipment(device) do
|
||||
%{
|
||||
id: equipment.id,
|
||||
name: equipment.name,
|
||||
ip_address: equipment.ip_address,
|
||||
site_name: equipment.site && equipment.site.name,
|
||||
status: equipment.status || "unknown",
|
||||
uptime: format_uptime(equipment),
|
||||
last_seen_at: equipment.last_seen_at
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
ip_address: device.ip_address,
|
||||
site_name: device.site && device.site.name,
|
||||
status: device.status || "unknown",
|
||||
uptime: format_uptime(device),
|
||||
last_seen_at: device.last_seen_at
|
||||
}
|
||||
end
|
||||
|
||||
defp format_equipment_details(equipment) do
|
||||
defp format_equipment_details(device) do
|
||||
%{
|
||||
id: equipment.id,
|
||||
name: equipment.name,
|
||||
ip_address: equipment.ip_address,
|
||||
status: equipment.status || "unknown",
|
||||
uptime: format_uptime(equipment),
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
ip_address: device.ip_address,
|
||||
status: device.status || "unknown",
|
||||
uptime: format_uptime(device),
|
||||
site: %{
|
||||
id: equipment.site.id,
|
||||
name: equipment.site.name
|
||||
id: device.site.id,
|
||||
name: device.site.name
|
||||
},
|
||||
interfaces:
|
||||
Enum.map(equipment.snmp_device.interfaces || [], fn interface ->
|
||||
Enum.map(device.snmp_device.interfaces || [], fn interface ->
|
||||
%{
|
||||
id: interface.id,
|
||||
name: interface.if_name,
|
||||
|
|
@ -267,7 +267,7 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
}
|
||||
end),
|
||||
sensors:
|
||||
Enum.map(equipment.snmp_device.sensors || [], fn sensor ->
|
||||
Enum.map(device.snmp_device.sensors || [], fn sensor ->
|
||||
%{
|
||||
id: sensor.id,
|
||||
name: sensor.name,
|
||||
|
|
@ -285,8 +285,8 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
id: alert.id,
|
||||
status: format_alert_status(alert),
|
||||
message: alert.message,
|
||||
equipment_name: alert.equipment && alert.equipment.name,
|
||||
equipment_id: alert.equipment_id,
|
||||
equipment_name: alert.device && alert.device.name,
|
||||
device_id: alert.device_id,
|
||||
occurred_at: alert.triggered_at
|
||||
}
|
||||
end
|
||||
|
|
@ -299,9 +299,9 @@ defmodule ToweropsWeb.Api.MobileController do
|
|||
end
|
||||
end
|
||||
|
||||
defp format_uptime(equipment) do
|
||||
if equipment.snmp_device && equipment.snmp_device.sys_uptime do
|
||||
timeticks_to_string(equipment.snmp_device.sys_uptime)
|
||||
defp format_uptime(device) do
|
||||
if device.snmp_device && device.snmp_device.sys_uptime do
|
||||
timeticks_to_string(device.snmp_device.sys_uptime)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
for network operators.
|
||||
</h1>
|
||||
<p class="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-slate-700">
|
||||
Monitor network equipment via SNMP, track performance metrics in real-time, and respond to alerts instantly.
|
||||
Monitor network devices via SNMP, track performance metrics in real-time, and respond to alerts instantly.
|
||||
Built specifically for telecom tower infrastructure.
|
||||
</p>
|
||||
<div class="mt-10 flex justify-center gap-x-6">
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<p class="mt-4 text-sm text-blue-100">
|
||||
Automatic device discovery via SNMP. Track interface statistics, sensor readings,
|
||||
and equipment health with support for MikroTik, Cisco, and generic SNMP devices.
|
||||
and device health with support for MikroTik, Cisco, and generic SNMP devices.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Feature 2 -->
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
<h3 class="text-lg font-semibold text-white">Real-time Alerts</h3>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-blue-100">
|
||||
Get instant notifications when equipment goes down or thresholds are exceeded.
|
||||
Get instant notifications when devices go down or thresholds are exceeded.
|
||||
Configurable alert rules with email notifications to keep you informed.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
Built for multi-site operations.
|
||||
</h2>
|
||||
<p class="mt-4 text-lg tracking-tight text-slate-700">
|
||||
Organize equipment by site, collaborate with your team, and maintain clear visibility
|
||||
Organize devices by site, collaborate with your team, and maintain clear visibility
|
||||
across your entire network infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -126,11 +126,11 @@
|
|||
</div>
|
||||
<h3 class="mt-6 text-sm font-medium text-blue-600">Site Management</h3>
|
||||
<p class="mt-2 font-display text-xl text-slate-900">
|
||||
Organize equipment by physical location
|
||||
Organize devices by physical location
|
||||
</p>
|
||||
<p class="mt-4 text-sm text-slate-600">
|
||||
Group devices by tower site or facility. View site-wide status and health metrics
|
||||
at a glance, then drill down into individual equipment details.
|
||||
at a glance, then drill down into individual device details.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Feature 2 -->
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<button
|
||||
phx-click="delete_org"
|
||||
phx-value-id={org.id}
|
||||
data-confirm="Are you sure? This will delete all sites, equipment, and data for this organization."
|
||||
data-confirm="Are you sure? This will delete all sites, device, and data for this organization."
|
||||
class="text-sm text-red-600 hover:text-red-800"
|
||||
>
|
||||
Delete
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
organization = socket.assigns.current_organization
|
||||
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
||||
|
||||
# Get equipment counts for each agent (both direct and total with inheritance)
|
||||
# device counts for each agent (both direct and total with inheritance)
|
||||
equipment_counts =
|
||||
Map.new(agent_tokens, fn token ->
|
||||
direct = Agents.count_assigned_equipment(token.id)
|
||||
direct = Agents.count_assigned_devices(token.id)
|
||||
total = length(Agents.list_agent_polling_targets(token.id))
|
||||
{token.id, %{direct: direct, total: total}}
|
||||
end)
|
||||
|
|
@ -32,7 +32,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
socket
|
||||
|> assign(:page_title, "Remote Agents")
|
||||
|> assign(:agent_tokens, agent_tokens)
|
||||
|> assign(:equipment_counts, equipment_counts)
|
||||
|> assign(:device_counts, equipment_counts)
|
||||
|> assign(:agent_health_stats, agent_health_stats)
|
||||
|> assign(:assignment_breakdown, assignment_breakdown)
|
||||
|> assign(:offline_agents, offline_agents)
|
||||
|
|
@ -61,10 +61,10 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
{:ok, agent_token, token} ->
|
||||
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
||||
|
||||
# Refresh equipment counts
|
||||
# device counts
|
||||
equipment_counts =
|
||||
Map.new(agent_tokens, fn t ->
|
||||
direct = Agents.count_assigned_equipment(t.id)
|
||||
direct = Agents.count_assigned_devices(t.id)
|
||||
total = length(Agents.list_agent_polling_targets(t.id))
|
||||
{t.id, %{direct: direct, total: total}}
|
||||
end)
|
||||
|
|
@ -77,7 +77,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
{:noreply,
|
||||
socket
|
||||
|> assign(:agent_tokens, agent_tokens)
|
||||
|> assign(:equipment_counts, equipment_counts)
|
||||
|> assign(:device_counts, equipment_counts)
|
||||
|> assign(:agent_health_stats, agent_health_stats)
|
||||
|> assign(:assignment_breakdown, assignment_breakdown)
|
||||
|> assign(:offline_agents, offline_agents)
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@
|
|||
</span>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Equipment">
|
||||
<% counts = Map.get(@equipment_counts, agent.id, %{direct: 0, total: 0}) %>
|
||||
<:col :let={agent} label="Device">
|
||||
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
|
||||
<div class="text-sm text-zinc-900 dark:text-zinc-100">
|
||||
{counts.total} total
|
||||
</div>
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
type="button"
|
||||
phx-hook="CopyToClipboard"
|
||||
data-target="#agent-token-input"
|
||||
id={"copy-token-#{@new_token.id}"}
|
||||
id={"copy-token-#{@new_token.agent_token.id}"}
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||
>
|
||||
<.icon name="hero-clipboard" class="h-4 w-4" />
|
||||
|
|
@ -264,7 +264,7 @@
|
|||
type="button"
|
||||
phx-hook="CopyToClipboard"
|
||||
data-target="#docker-compose-example"
|
||||
id={"copy-docker-compose-#{@new_token.id}"}
|
||||
id={"copy-docker-compose-#{@new_token.agent_token.id}"}
|
||||
class="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<.icon name="hero-clipboard" class="h-3 w-3" /> Copy to clipboard
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ defmodule ToweropsWeb.AgentLive.Show do
|
|||
raise Ecto.NoResultsError
|
||||
end
|
||||
|
||||
# Get polling targets (all equipment this agent is responsible for)
|
||||
# device this agent is responsible for)
|
||||
polling_targets = Agents.list_agent_polling_targets(agent_token.id)
|
||||
|
||||
# Get direct assignments
|
||||
direct_assignments = Agents.count_assigned_equipment(agent_token.id)
|
||||
direct_assignments = Agents.count_assigned_devices(agent_token.id)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|
|
@ -35,20 +35,20 @@ defmodule ToweropsWeb.AgentLive.Show do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp assignment_source(equipment, agent_token_id) do
|
||||
defp assignment_source(device, agent_token_id) do
|
||||
cond do
|
||||
# Direct assignment
|
||||
Enum.any?(equipment.agent_assignments || [], fn a ->
|
||||
Enum.any?(device.agent_assignments || [], fn a ->
|
||||
a.agent_token_id == agent_token_id and a.enabled
|
||||
end) ->
|
||||
{:direct, "Directly assigned"}
|
||||
|
||||
# Site assignment
|
||||
equipment.site && equipment.site.agent_token_id == agent_token_id ->
|
||||
{:site, "From site: #{equipment.site.name}"}
|
||||
device.site && device.site.agent_token_id == agent_token_id ->
|
||||
{:site, "From site: #{device.site.name}"}
|
||||
|
||||
# Organization assignment
|
||||
equipment.organization && equipment.organization.default_agent_token_id == agent_token_id ->
|
||||
device.organization && device.organization.default_agent_token_id == agent_token_id ->
|
||||
{:organization, "From organization"}
|
||||
|
||||
# Shouldn't happen but handle gracefully
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Count Card -->
|
||||
<!-- Device Count Card -->
|
||||
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Equipment</p>
|
||||
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Device</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{length(@polling_targets)}
|
||||
</p>
|
||||
|
|
@ -186,14 +186,14 @@
|
|||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Equipment List -->
|
||||
<!-- Device List -->
|
||||
<div class="mt-6 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg">
|
||||
<div class="p-6 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Polling Targets
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Equipment that this agent is responsible for polling
|
||||
Device that this agent is responsible for polling
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -201,17 +201,17 @@
|
|||
<div class="p-12 text-center">
|
||||
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
No equipment assigned
|
||||
No devices assigned
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
This agent is not currently polling any equipment.
|
||||
This agent is not currently polling any device.
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<%= for equipment <- @polling_targets do %>
|
||||
<%= for device <- @polling_targets do %>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{equipment.id}"}
|
||||
navigate={~p"/orgs/#{@organization.slug}/devices/#{device.id}"}
|
||||
class="block hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<div class="p-6">
|
||||
|
|
@ -219,9 +219,9 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate">
|
||||
{equipment.name}
|
||||
{device.name}
|
||||
</p>
|
||||
<% {source, source_label} = assignment_source(equipment, @agent_token.id) %>
|
||||
<% {source, source_label} = assignment_source(device, @agent_token.id) %>
|
||||
<span class={"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium #{source_badge_class(source)}"}>
|
||||
{source_label}
|
||||
</span>
|
||||
|
|
@ -229,12 +229,12 @@
|
|||
<div class="mt-1 flex items-center gap-4 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-globe-alt" class="h-4 w-4" />
|
||||
{equipment.ip_address}
|
||||
{device.ip_address}
|
||||
</span>
|
||||
<%= if equipment.site do %>
|
||||
<%= if device.site do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-building-office" class="h-4 w-4" />
|
||||
{equipment.site.name}
|
||||
{device.site.name}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ defmodule ToweropsWeb.AlertLive.Index do
|
|||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Alerts")
|
||||
|> assign(:filter, "active")
|
||||
|> assign(:filter, "all")
|
||||
|> load_alerts(organization.id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
filter = Map.get(params, "filter", "active")
|
||||
filter = Map.get(params, "filter", "all")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -50,12 +50,12 @@ defmodule ToweropsWeb.AlertLive.Index do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:new_alert, _equipment_id, _alert_type}, socket) do
|
||||
def handle_info({:new_alert, _device_id, _alert_type}, socket) do
|
||||
{:noreply, load_alerts(socket, socket.assigns.current_organization.id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:alert_resolved, _equipment_id, _alert_type}, socket) do
|
||||
def handle_info({:alert_resolved, _device_id, _alert_type}, socket) do
|
||||
{:noreply, load_alerts(socket, socket.assigns.current_organization.id)}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@
|
|||
<div class={[
|
||||
"rounded-lg border p-6 shadow-sm",
|
||||
alert.resolved_at && "opacity-60",
|
||||
alert.alert_type == :equipment_down && is_nil(alert.resolved_at) &&
|
||||
alert.alert_type == :device_down && is_nil(alert.resolved_at) &&
|
||||
"border-l-4 border-red-500 bg-red-50 dark:bg-red-950 dark:border-red-700",
|
||||
(alert.alert_type != :equipment_down || alert.resolved_at) &&
|
||||
(alert.alert_type != :device_down || alert.resolved_at) &&
|
||||
"border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900"
|
||||
]}>
|
||||
<div class="flex items-start justify-between">
|
||||
|
|
@ -66,16 +66,16 @@
|
|||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class={[
|
||||
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium",
|
||||
alert.alert_type == :equipment_down &&
|
||||
alert.alert_type == :device_down &&
|
||||
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
alert.alert_type == :equipment_up &&
|
||||
alert.alert_type == :device_up &&
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
]}>
|
||||
<%= case alert.alert_type do %>
|
||||
<% :equipment_down -> %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" /> Equipment Down
|
||||
<% :equipment_up -> %>
|
||||
<.icon name="hero-check-circle" class="h-4 w-4" /> Equipment Recovered
|
||||
<% :device_down -> %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" /> Device Down
|
||||
<% :device_up -> %>
|
||||
<.icon name="hero-check-circle" class="h-4 w-4" /> Device Recovered
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
|
|
@ -94,12 +94,10 @@
|
|||
|
||||
<h3 class="mt-3 font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"
|
||||
}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/#{alert.device.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
{alert.device.name}
|
||||
</.link>
|
||||
</h3>
|
||||
|
||||
|
|
@ -132,18 +130,18 @@
|
|||
<strong class="font-medium">Site:</strong>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/sites/#{alert.equipment.site.id}"
|
||||
~p"/orgs/#{@current_organization.slug}/sites/#{alert.device.site.id}"
|
||||
}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.site.name}
|
||||
{alert.device.site.name}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<%= if alert.alert_type == :equipment_down && is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<%= if alert.alert_type == :device_down && is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<.button phx-click="acknowledge" phx-value-id={alert.id} variant="primary">
|
||||
<.icon name="hero-check" class="h-4 w-4" /> Acknowledge
|
||||
</.button>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ defmodule ToweropsWeb.DashboardLive do
|
|||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Sites
|
||||
|
||||
@impl true
|
||||
|
|
@ -24,31 +24,31 @@ defmodule ToweropsWeb.DashboardLive do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:new_alert, _equipment_id, _alert_type}, socket) do
|
||||
def handle_info({:new_alert, _device_id, _alert_type}, socket) do
|
||||
{:noreply, load_dashboard_data(socket, socket.assigns.current_organization.id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:alert_resolved, _equipment_id, _alert_type}, socket) do
|
||||
def handle_info({:alert_resolved, _device_id, _alert_type}, socket) do
|
||||
{:noreply, load_dashboard_data(socket, socket.assigns.current_organization.id)}
|
||||
end
|
||||
|
||||
defp load_dashboard_data(socket, organization_id) do
|
||||
active_alerts = Alerts.list_organization_active_alerts(organization_id)
|
||||
sites_count = length(Sites.list_organization_sites(organization_id))
|
||||
equipment = Equipment.list_organization_equipment(organization_id)
|
||||
devices = Devices.list_organization_devices(organization_id)
|
||||
|
||||
equipment_by_status =
|
||||
equipment
|
||||
devices_by_status =
|
||||
devices
|
||||
|> Enum.group_by(& &1.status)
|
||||
|> Map.new(fn {status, equip} -> {status, length(equip)} end)
|
||||
|
||||
socket
|
||||
|> assign(:active_alerts, active_alerts)
|
||||
|> assign(:sites_count, sites_count)
|
||||
|> assign(:equipment_count, length(equipment))
|
||||
|> assign(:equipment_up, Map.get(equipment_by_status, :up, 0))
|
||||
|> assign(:equipment_down, Map.get(equipment_by_status, :down, 0))
|
||||
|> assign(:equipment_unknown, Map.get(equipment_by_status, :unknown, 0))
|
||||
|> assign(:device_count, length(devices))
|
||||
|> assign(:device_up, Map.get(devices_by_status, :up, 0))
|
||||
|> assign(:device_down, Map.get(devices_by_status, :down, 0))
|
||||
|> assign(:device_unknown, Map.get(devices_by_status, :unknown, 0))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
2
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-zinc-900 dark:text-zinc-100">Add Equipment</h4>
|
||||
<h4 class="font-medium text-zinc-900 dark:text-zinc-100">Add Device</h4>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Add network devices (routers, switches, servers) to your sites
|
||||
</p>
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
Monitor & Receive Alerts
|
||||
</h4>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Towerops will automatically monitor your equipment and alert you of issues
|
||||
Towerops will automatically monitor your devices and alert you of issues
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,23 +78,23 @@
|
|||
</.link>
|
||||
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Equipment</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment_count}</p>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Device</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@device_count}</p>
|
||||
<div class="mt-3 space-y-1.5 text-sm">
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{@equipment_up} Up
|
||||
{@device_up} Up
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{@equipment_down} Down
|
||||
{@device_down} Down
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-zinc-400"></span>
|
||||
{@equipment_unknown} Unknown
|
||||
{@device_unknown} Unknown
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
|
|
@ -118,15 +118,15 @@
|
|||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">System Status</h3>
|
||||
<div class={[
|
||||
"mt-3 flex items-center gap-2 text-sm font-semibold",
|
||||
(@equipment_down == 0 && "text-green-600 dark:text-green-500") ||
|
||||
(@device_down == 0 && "text-green-600 dark:text-green-500") ||
|
||||
"text-amber-600 dark:text-amber-500"
|
||||
]}>
|
||||
<%= if @equipment_down == 0 do %>
|
||||
<%= if @device_down == 0 do %>
|
||||
<.icon name="hero-check-circle" class="h-5 w-5" />
|
||||
<span>All Systems Operational</span>
|
||||
<% else %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
||||
<span>{@equipment_down} Equipment Down</span>
|
||||
<span>{@device_down} Device Down</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,12 +154,10 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"
|
||||
}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/#{alert.device.id}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
{alert.device.name}
|
||||
</.link>
|
||||
</h3>
|
||||
<p class="mt-0.5 text-sm text-zinc-700 dark:text-zinc-300">{alert.message}</p>
|
||||
|
|
@ -190,11 +188,11 @@
|
|||
<span>Manage Sites</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices"}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
<.icon name="hero-server" class="h-5 w-5" />
|
||||
<span>Manage Equipment</span>
|
||||
<span>Manage Device</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule ToweropsWeb.EquipmentLive.Form do
|
||||
defmodule ToweropsWeb.DeviceLive.Form do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Agents
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Equipment.Equipment, as: EquipmentSchema
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Snmp
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
if Enum.empty?(sites) do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:info, "Please create a site before adding equipment.")
|
||||
|> put_flash(:info, "Please create a site before adding a device.")
|
||||
|> push_navigate(to: ~p"/orgs/#{organization.slug}/sites/new")}
|
||||
else
|
||||
{:ok,
|
||||
|
|
@ -61,27 +61,27 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
equipment_attrs
|
||||
end
|
||||
|
||||
changeset = Equipment.change_equipment(%EquipmentSchema{}, equipment_attrs)
|
||||
changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "New Equipment")
|
||||
|> assign(:equipment, %EquipmentSchema{})
|
||||
|> assign(:page_title, "New Device")
|
||||
|> assign(:device, %DeviceSchema{})
|
||||
|> assign(:form, to_form(changeset))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
equipment = Equipment.get_equipment!(id)
|
||||
device = Devices.get_device!(id)
|
||||
|
||||
# Preload necessary associations for agent resolution
|
||||
equipment = Towerops.Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Towerops.Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
# Get current agent assignment if any
|
||||
current_assignment = Agents.get_equipment_assignment(equipment.id)
|
||||
current_assignment = Agents.get_device_assignment(device.id)
|
||||
|
||||
# Get the effective agent and where it comes from
|
||||
{effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(equipment)
|
||||
{effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(device)
|
||||
|
||||
# For the form, we only want the equipment-specific assignment
|
||||
# For the form, we only want the device-specific assignment
|
||||
agent_token_id =
|
||||
if current_assignment do
|
||||
current_assignment.agent_token_id
|
||||
|
|
@ -95,15 +95,15 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
end
|
||||
|
||||
# Add agent_token_id to the changeset data
|
||||
equipment_with_agent = Map.put(equipment, :agent_token_id, agent_token_id)
|
||||
changeset = Equipment.change_equipment(equipment, %{})
|
||||
device_with_agent = Map.put(device, :agent_token_id, agent_token_id)
|
||||
changeset = Devices.change_device(device, %{})
|
||||
|
||||
# Get effective SNMP configuration and source
|
||||
snmp_config = Equipment.get_snmp_config(equipment)
|
||||
snmp_config = Devices.get_snmp_config(device)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "Edit Equipment")
|
||||
|> assign(:equipment, equipment_with_agent)
|
||||
|> assign(:page_title, "Edit Device")
|
||||
|> assign(:device, device_with_agent)
|
||||
|> assign(:form, to_form(changeset))
|
||||
|> assign(:agent_source, agent_source)
|
||||
|> assign(:effective_agent_name, effective_agent_name)
|
||||
|
|
@ -113,31 +113,31 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"equipment" => equipment_params}, socket) do
|
||||
def handle_event("validate", %{"device" => device_params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.equipment
|
||||
|> Equipment.change_equipment(equipment_params)
|
||||
socket.assigns.device
|
||||
|> Devices.change_device(device_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"equipment" => equipment_params}, socket) do
|
||||
save_equipment(socket, socket.assigns.live_action, equipment_params)
|
||||
def handle_event("save", %{"device" => device_params}, socket) do
|
||||
save_device(socket, socket.assigns.live_action, device_params)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", _params, socket) do
|
||||
case Equipment.delete_equipment(socket.assigns.equipment) do
|
||||
case Devices.delete_device(socket.assigns.device) do
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Equipment deleted successfully")
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/equipment")}
|
||||
|> put_flash(:info, "Device deleted successfully")
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/devices")}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Unable to delete equipment")}
|
||||
{:noreply, put_flash(socket, :error, "Unable to delete device")}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -151,96 +151,96 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
|
||||
@impl true
|
||||
def handle_event("trigger_discovery", _params, socket) do
|
||||
equipment = socket.assigns.equipment
|
||||
device = socket.assigns.device
|
||||
|
||||
if equipment.snmp_enabled do
|
||||
if device.snmp_enabled do
|
||||
# Run discovery in background
|
||||
_ =
|
||||
Task.start(fn ->
|
||||
Snmp.discover_equipment(equipment)
|
||||
Snmp.discover_equipment(device)
|
||||
end)
|
||||
|
||||
{:noreply, put_flash(socket, :info, "Discovery started...")}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "SNMP is not enabled for this equipment")}
|
||||
{:noreply, put_flash(socket, :error, "SNMP is not enabled for this device")}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_equipment(socket, :new, equipment_params) do
|
||||
# Extract agent_token_id before creating equipment
|
||||
agent_token_id = Map.get(equipment_params, "agent_token_id")
|
||||
equipment_params = Map.delete(equipment_params, "agent_token_id")
|
||||
defp save_device(socket, :new, device_params) do
|
||||
# Extract agent_token_id before creating device
|
||||
agent_token_id = Map.get(device_params, "agent_token_id")
|
||||
device_params = Map.delete(device_params, "agent_token_id")
|
||||
|
||||
case Equipment.create_equipment(equipment_params) do
|
||||
{:ok, equipment} ->
|
||||
# Handle agent assignment after equipment creation
|
||||
handle_agent_assignment(equipment.id, agent_token_id)
|
||||
case Devices.create_device(device_params) do
|
||||
{:ok, device} ->
|
||||
# Handle agent assignment after device creation
|
||||
handle_agent_assignment(device.id, agent_token_id)
|
||||
|
||||
flash_message = handle_equipment_creation(equipment)
|
||||
flash_message = handle_device_creation(device)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, flash_message)
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/equipment/#{equipment.id}")}
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/devices/#{device.id}")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_equipment(socket, :edit, equipment_params) do
|
||||
old_equipment = socket.assigns.equipment
|
||||
defp save_device(socket, :edit, device_params) do
|
||||
old_device = socket.assigns.device
|
||||
|
||||
# Extract agent_token_id before updating equipment
|
||||
agent_token_id = Map.get(equipment_params, "agent_token_id")
|
||||
equipment_params = Map.delete(equipment_params, "agent_token_id")
|
||||
# Extract agent_token_id before updating device
|
||||
agent_token_id = Map.get(device_params, "agent_token_id")
|
||||
device_params = Map.delete(device_params, "agent_token_id")
|
||||
|
||||
case Equipment.update_equipment(old_equipment, equipment_params) do
|
||||
{:ok, equipment} ->
|
||||
# Handle agent assignment after equipment update
|
||||
handle_agent_assignment(equipment.id, agent_token_id)
|
||||
case Devices.update_device(old_device, device_params) do
|
||||
{:ok, device} ->
|
||||
# device update
|
||||
handle_agent_assignment(device.id, agent_token_id)
|
||||
|
||||
flash_message = handle_equipment_update(old_equipment, equipment)
|
||||
flash_message = handle_device_update(old_device, device)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, flash_message)
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/equipment/#{equipment.id}")}
|
||||
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/devices/#{device.id}")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_creation(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
_ = Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment created successfully. SNMP discovery started in background."
|
||||
defp handle_device_creation(device) do
|
||||
if device.snmp_enabled do
|
||||
_ = Task.start(fn -> Snmp.discover_equipment(device) end)
|
||||
"Device created successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment created successfully"
|
||||
"Device created successfully"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_update(old_equipment, equipment) do
|
||||
if should_trigger_snmp_discovery?(old_equipment, equipment) do
|
||||
_ = Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment updated successfully. SNMP discovery started in background."
|
||||
defp handle_device_update(old_device, device) do
|
||||
if should_trigger_snmp_discovery?(old_device, device) do
|
||||
_ = Task.start(fn -> Snmp.discover_equipment(device) end)
|
||||
"Device updated successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment updated successfully"
|
||||
"Device updated successfully"
|
||||
end
|
||||
end
|
||||
|
||||
defp should_trigger_snmp_discovery?(old_equipment, equipment) do
|
||||
equipment.snmp_enabled and
|
||||
(!old_equipment.snmp_enabled or
|
||||
snmp_config_changed?(old_equipment, equipment))
|
||||
defp should_trigger_snmp_discovery?(old_device, device) do
|
||||
device.snmp_enabled and
|
||||
(!old_device.snmp_enabled or
|
||||
snmp_config_changed?(old_device, device))
|
||||
end
|
||||
|
||||
defp snmp_config_changed?(old_equipment, equipment) do
|
||||
equipment.snmp_community != old_equipment.snmp_community or
|
||||
equipment.snmp_version != old_equipment.snmp_version or
|
||||
equipment.snmp_port != old_equipment.snmp_port or
|
||||
equipment.ip_address != old_equipment.ip_address
|
||||
defp snmp_config_changed?(old_device, device) do
|
||||
device.snmp_community != old_device.snmp_community or
|
||||
device.snmp_version != old_device.snmp_version or
|
||||
device.snmp_port != old_device.snmp_port or
|
||||
device.ip_address != old_device.ip_address
|
||||
end
|
||||
|
||||
@spec extract_snmp_config(Phoenix.HTML.Form.t()) :: %{
|
||||
|
|
@ -328,19 +328,19 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_agent_assignment(equipment_id, agent_token_id) when is_binary(agent_token_id) do
|
||||
defp handle_agent_assignment(device_id, agent_token_id) when is_binary(agent_token_id) do
|
||||
# Only assign if agent_token_id is not empty
|
||||
if agent_token_id == "" do
|
||||
Agents.update_equipment_assignment(equipment_id, nil)
|
||||
Agents.update_device_assignment(device_id, nil)
|
||||
else
|
||||
Agents.update_equipment_assignment(equipment_id, agent_token_id)
|
||||
Agents.update_device_assignment(device_id, agent_token_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_agent_assignment(equipment_id, nil) do
|
||||
defp handle_agent_assignment(device_id, nil) do
|
||||
# No agent selected, remove any assignment
|
||||
Agents.update_equipment_assignment(equipment_id, nil)
|
||||
Agents.update_device_assignment(device_id, nil)
|
||||
end
|
||||
|
||||
defp handle_agent_assignment(_equipment_id, _), do: :ok
|
||||
defp handle_agent_assignment(_device_id, _), do: :ok
|
||||
end
|
||||
|
|
@ -7,13 +7,13 @@
|
|||
<.link
|
||||
navigate={
|
||||
if @live_action == :edit,
|
||||
do: ~p"/orgs/#{@organization.slug}/equipment/#{@equipment.id}",
|
||||
else: ~p"/orgs/#{@organization.slug}/equipment"
|
||||
do: ~p"/orgs/#{@organization.slug}/devices/#{@device.id}",
|
||||
else: ~p"/orgs/#{@organization.slug}/devices"
|
||||
}
|
||||
class="inline-flex items-center gap-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="h-4 w-4" />
|
||||
{if @live_action == :edit, do: "Back to Equipment", else: "Back to Equipment List"}
|
||||
{if @live_action == :edit, do: "Back to Device", else: "Back to Device List"}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
|
|
@ -21,14 +21,14 @@
|
|||
{@page_title}
|
||||
<:subtitle>
|
||||
{if @live_action == :new,
|
||||
do: "Add new equipment to monitor",
|
||||
else: "Update equipment details"}
|
||||
do: "Add new device to monitor",
|
||||
else: "Update device details"}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
<.form for={@form} id="equipment-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:name]} type="text" label="Equipment Name" required />
|
||||
<.form for={@form} id="device-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:name]} type="text" label="Device Name" required />
|
||||
|
||||
<.input field={@form[:ip_address]} type="text" label="IP Address" required />
|
||||
|
||||
|
|
@ -63,11 +63,11 @@
|
|||
/>
|
||||
<%= if @live_action == :edit and Map.has_key?(assigns, :agent_source) do %>
|
||||
<%= case @agent_source do %>
|
||||
<% :equipment -> %>
|
||||
<% :device -> %>
|
||||
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
|
||||
<strong>Overriding</strong> site/organization defaults -
|
||||
this equipment is explicitly assigned
|
||||
this device is explicitly assigned
|
||||
</p>
|
||||
<% :site -> %>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
<% end %>
|
||||
<% else %>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Assign this equipment to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults.
|
||||
Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults.
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
|
||||
<%= if @live_action == :edit and Map.has_key?(assigns, :snmp_config_source) do %>
|
||||
<%= case @snmp_config_source do %>
|
||||
<% :equipment -> %>
|
||||
<% :device -> %>
|
||||
<p class="text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
|
||||
<strong>Overriding</strong> site/organization SNMP defaults
|
||||
|
|
@ -187,8 +187,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<.button phx-disable-with="Saving..." variant="primary">Save Equipment</.button>
|
||||
<.button navigate={~p"/orgs/#{@organization.slug}/equipment"}>Cancel</.button>
|
||||
<.button phx-disable-with="Saving..." variant="primary">Save Device</.button>
|
||||
<.button navigate={~p"/orgs/#{@organization.slug}/devices"}>Cancel</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
|
|
@ -196,14 +196,14 @@
|
|||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 class="text-lg font-semibold text-red-600 dark:text-red-500 mb-4">Danger Zone</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Once you delete this equipment, there is no going back. All monitoring history and alerts will be permanently deleted.
|
||||
Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted.
|
||||
</p>
|
||||
<.button
|
||||
phx-click="delete"
|
||||
data-confirm="Are you sure you want to delete this equipment? All monitoring history and alerts will be permanently deleted."
|
||||
data-confirm="Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted."
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-trash" class="h-4 w-4" /> Delete Equipment
|
||||
<.icon name="hero-trash" class="h-4 w-4" /> Delete Device
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
defmodule ToweropsWeb.EquipmentLive.Index do
|
||||
defmodule ToweropsWeb.DeviceLive.Index do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Sites
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
organization = socket.assigns.current_organization
|
||||
equipment = Equipment.list_organization_equipment(organization.id)
|
||||
device = Devices.list_organization_devices(organization.id)
|
||||
sites = Sites.list_organization_sites(organization.id)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Equipment")
|
||||
|> assign(:equipment, equipment)
|
||||
|> assign(:page_title, "Devices")
|
||||
|> assign(:device, device)
|
||||
|> assign(:has_sites, sites != [])}
|
||||
end
|
||||
|
||||
|
|
@ -2,18 +2,18 @@
|
|||
flash={@flash}
|
||||
current_scope={@current_scope}
|
||||
current_organization={@current_organization}
|
||||
active_page="equipment"
|
||||
active_page="devices"
|
||||
>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Monitor and manage all your network equipment</:subtitle>
|
||||
<:subtitle>Monitor and manage all your network devices</:subtitle>
|
||||
<:actions>
|
||||
<.button
|
||||
:if={@has_sites}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/new"}
|
||||
variant="primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Equipment
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Device
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
|
@ -28,10 +28,10 @@
|
|||
Create a site first
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Before adding equipment, you need to create at least one site location.
|
||||
Before adding device, you need to create at least one site location.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Sites help you organize your equipment by physical location.
|
||||
Sites help you organize your devices by physical location.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/sites/new"} variant="primary">
|
||||
|
|
@ -40,29 +40,29 @@
|
|||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= if @equipment == [] do %>
|
||||
<%= if @device == [] do %>
|
||||
<div class="text-center py-16">
|
||||
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No equipment</h3>
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No devices</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Get started by adding your first equipment.
|
||||
Get started by adding your first device.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/new"}
|
||||
variant="primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Equipment
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Device
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<.table
|
||||
id="equipment"
|
||||
rows={@equipment}
|
||||
id="devices"
|
||||
rows={@device}
|
||||
row_click={
|
||||
fn eq ->
|
||||
JS.navigate(~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}")
|
||||
JS.navigate(~p"/orgs/#{@current_organization.slug}/devices/#{eq.id}")
|
||||
end
|
||||
}
|
||||
>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule ToweropsWeb.EquipmentLive.Show do
|
||||
defmodule ToweropsWeb.DeviceLive.Show do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Snmp
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
def handle_params(%{"id" => id} = params, _, socket) do
|
||||
_ =
|
||||
if connected?(socket) do
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{id}")
|
||||
Process.send_after(self(), :refresh_data, 10_000)
|
||||
end
|
||||
|
||||
|
|
@ -30,34 +30,34 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
@impl true
|
||||
def handle_info(:refresh_data, socket) do
|
||||
Process.send_after(self(), :refresh_data, 10_000)
|
||||
{:noreply, load_equipment_data(socket, socket.assigns.equipment.id)}
|
||||
{:noreply, load_equipment_data(socket, socket.assigns.device.id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:equipment_status_changed, _equipment_id, _new_status, _response_time}, socket) do
|
||||
{:noreply, load_equipment_data(socket, socket.assigns.equipment.id)}
|
||||
def handle_info({:device_status_changed, _device_id, _new_status, _response_time}, socket) do
|
||||
{:noreply, load_equipment_data(socket, socket.assigns.device.id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:discovery_completed, _equipment_id}, socket) do
|
||||
def handle_info({:discovery_completed, _device_id}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> load_equipment_data(socket.assigns.equipment.id)
|
||||
|> load_equipment_data(socket.assigns.device.id)
|
||||
|> put_flash(:info, "Discovery completed")}
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp load_equipment_data(socket, equipment_id) do
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
recent_checks = Monitoring.list_equipment_checks(equipment_id, 50)
|
||||
snmp_data = load_snmp_data(equipment_id)
|
||||
events = Equipment.list_equipment_events(equipment_id, 100)
|
||||
neighbors = Snmp.list_neighbors_with_equipment(equipment_id)
|
||||
cpu_chart_data = load_sensor_chart_data(equipment_id, ["cpu_load"])
|
||||
memory_chart_data = load_sensor_chart_data(equipment_id, ["memory_usage"])
|
||||
storage_chart_data = load_sensor_chart_data(equipment_id, ["disk_usage"])
|
||||
traffic_chart_data = load_overall_traffic_chart_data(equipment_id)
|
||||
defp load_equipment_data(socket, device_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
recent_checks = Monitoring.list_devices_checks(device_id, 50)
|
||||
snmp_data = load_snmp_data(device_id)
|
||||
events = Devices.list_devices_events(device_id, 100)
|
||||
neighbors = Snmp.list_neighbors_with_equipment(device_id)
|
||||
cpu_chart_data = load_sensor_chart_data(device_id, ["cpu_load"])
|
||||
memory_chart_data = load_sensor_chart_data(device_id, ["memory_usage"])
|
||||
storage_chart_data = load_sensor_chart_data(device_id, ["disk_usage"])
|
||||
traffic_chart_data = load_overall_traffic_chart_data(device_id)
|
||||
|
||||
# Filter sensors by type for temperature, voltage, and storage
|
||||
temperature_sensors =
|
||||
|
|
@ -68,14 +68,14 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
storage_sensors = Enum.filter(snmp_data.sensors, &(&1.sensor_type in ["disk_usage"]))
|
||||
|
||||
# Calculate metrics for dashboard
|
||||
metrics = calculate_metrics(recent_checks, equipment)
|
||||
metrics = calculate_metrics(recent_checks, device)
|
||||
|
||||
# Group interfaces by type for the ports tab
|
||||
interfaces_by_type = group_interfaces_by_type(snmp_data.interfaces)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, equipment.name)
|
||||
|> assign(:equipment, equipment)
|
||||
|> assign(:page_title, device.name)
|
||||
|> assign(:device, device)
|
||||
|> assign(:recent_checks, recent_checks)
|
||||
|> assign(:metrics, metrics)
|
||||
|> assign(:snmp_device, snmp_data.device)
|
||||
|
|
@ -216,8 +216,8 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
|> String.capitalize()
|
||||
end
|
||||
|
||||
defp load_snmp_data(equipment_id) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
defp load_snmp_data(device_id) do
|
||||
case Snmp.get_device_with_associations(device_id) do
|
||||
nil ->
|
||||
%{device: nil, interfaces: [], sensors: []}
|
||||
|
||||
|
|
@ -250,8 +250,8 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
end
|
||||
end
|
||||
|
||||
defp load_sensor_chart_data(equipment_id, sensor_types) when is_list(sensor_types) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
defp load_sensor_chart_data(device_id, sensor_types) when is_list(sensor_types) do
|
||||
case Snmp.get_device_with_associations(device_id) do
|
||||
nil -> nil
|
||||
device -> build_sensor_datasets_json(device, sensor_types)
|
||||
end
|
||||
|
|
@ -291,8 +291,8 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
}
|
||||
end
|
||||
|
||||
defp load_overall_traffic_chart_data(equipment_id) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
defp load_overall_traffic_chart_data(device_id) do
|
||||
case Snmp.get_device_with_associations(device_id) do
|
||||
nil -> nil
|
||||
device -> build_overall_traffic_chart_json(device)
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
flash={@flash}
|
||||
current_scope={@current_scope}
|
||||
current_organization={@current_organization}
|
||||
active_page="equipment"
|
||||
active_page="devices"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<!-- Breadcrumbs -->
|
||||
|
|
@ -10,16 +10,16 @@
|
|||
<ol class="inline-flex items-center space-x-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<li class="inline-flex items-center">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{@equipment.site.id}"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{@device.site.id}"}
|
||||
class="hover:text-zinc-900 dark:hover:text-zinc-200"
|
||||
>
|
||||
{@equipment.site.name}
|
||||
{@device.site.name}
|
||||
</.link>
|
||||
</li>
|
||||
<li aria-current="page">
|
||||
<div class="flex items-center">
|
||||
<.icon name="hero-chevron-right" class="h-4 w-4 mx-1" />
|
||||
<span class="text-zinc-900 dark:text-zinc-100 font-medium">{@equipment.name}</span>
|
||||
<span class="text-zinc-900 dark:text-zinc-100 font-medium">{@device.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -27,13 +27,11 @@
|
|||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment.name}</h1>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 font-mono">{@equipment.ip_address}</p>
|
||||
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@device.name}</h1>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 font-mono">{@device.ip_address}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<.button navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"
|
||||
}>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/edit"}>
|
||||
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
|
||||
</.button>
|
||||
</div>
|
||||
|
|
@ -42,7 +40,7 @@
|
|||
<div class="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=overview"}
|
||||
patch={~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}?tab=overview"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if @active_tab == "overview" do
|
||||
|
|
@ -57,7 +55,7 @@
|
|||
|
||||
<%= if @snmp_device do %>
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=ports"}
|
||||
patch={~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}?tab=ports"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if @active_tab == "ports" do
|
||||
|
|
@ -72,7 +70,7 @@
|
|||
<% end %>
|
||||
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=neighbors"}
|
||||
patch={~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}?tab=neighbors"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if @active_tab == "neighbors" do
|
||||
|
|
@ -86,7 +84,7 @@
|
|||
</.link>
|
||||
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=logs"}
|
||||
patch={~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}?tab=logs"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if @active_tab == "logs" do
|
||||
|
|
@ -121,14 +119,14 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">System Name</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{@snmp_device.sys_name || @equipment.name}
|
||||
{@snmp_device.sys_name || @device.name}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between py-1.5 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Resolved IP</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
|
||||
{@equipment.ip_address}
|
||||
{@device.ip_address}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
|
|
@ -181,14 +179,14 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Name</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{@equipment.name}
|
||||
{@device.name}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between py-1.5 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">IP Address</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
|
||||
{@equipment.ip_address}
|
||||
{@device.ip_address}
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -196,15 +194,15 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Device Added</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{format_device_age(@equipment.inserted_at)}
|
||||
{format_device_age(@device.inserted_at)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between py-1.5">
|
||||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Last Discovered</dt>
|
||||
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{if @equipment.last_discovery_at do
|
||||
format_device_age(@equipment.last_discovery_at)
|
||||
{if @device.last_discovery_at do
|
||||
format_device_age(@device.last_discovery_at)
|
||||
else
|
||||
"Never"
|
||||
end}
|
||||
|
|
@ -218,7 +216,7 @@
|
|||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/traffic"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/traffic"
|
||||
}
|
||||
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
||||
>
|
||||
|
|
@ -252,7 +250,7 @@
|
|||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/processors"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/processors"
|
||||
}
|
||||
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
||||
>
|
||||
|
|
@ -280,7 +278,7 @@
|
|||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/memory"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/memory"
|
||||
}
|
||||
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
||||
>
|
||||
|
|
@ -318,7 +316,7 @@
|
|||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/storage"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/storage"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
|
|
@ -357,7 +355,7 @@
|
|||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/temperature"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/temperature"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
|
|
@ -394,7 +392,7 @@
|
|||
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/voltage"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/voltage"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
|
|
@ -453,7 +451,7 @@
|
|||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/traffic?interface_id=#{interface.id}"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/traffic?interface_id=#{interface.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
|
|
@ -538,14 +536,14 @@
|
|||
{neighbor.interface.if_name || neighbor.interface.if_descr}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<%= if neighbor.matched_equipment do %>
|
||||
<%= if neighbor.matched_device do %>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{neighbor.matched_equipment.id}"
|
||||
~p"/orgs/#{@current_organization.slug}/devices/#{neighbor.matched_device.id}"
|
||||
}
|
||||
class="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{neighbor.matched_equipment.name}
|
||||
{neighbor.matched_device.name}
|
||||
</.link>
|
||||
<% else %>
|
||||
<div class="text-zinc-900 dark:text-zinc-100">
|
||||
|
|
@ -561,7 +559,7 @@
|
|||
<div class="text-zinc-900 dark:text-zinc-100">
|
||||
{neighbor.remote_port_id || "-"}
|
||||
</div>
|
||||
<%= if !neighbor.matched_equipment && neighbor.remote_chassis_id do %>
|
||||
<%= if !neighbor.matched_device && neighbor.remote_chassis_id do %>
|
||||
<div class="text-xs font-mono text-zinc-500 dark:text-zinc-400">
|
||||
{neighbor.remote_chassis_id}
|
||||
</div>
|
||||
|
|
@ -2,7 +2,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Snmp
|
||||
|
||||
@impl true
|
||||
|
|
@ -11,10 +11,10 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => equipment_id, "sensor_type" => sensor_type} = params, _, socket) do
|
||||
def handle_params(%{"id" => device_id, "sensor_type" => sensor_type} = params, _, socket) do
|
||||
_ =
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{equipment_id}")
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}")
|
||||
end
|
||||
|
||||
range = Map.get(params, "range", "24h")
|
||||
|
|
@ -22,7 +22,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:equipment_id, equipment_id)
|
||||
|> assign(:device_id, device_id)
|
||||
|> assign(:sensor_type, sensor_type)
|
||||
|> assign(:range, range)
|
||||
|> assign(:interface_id, interface_id)
|
||||
|
|
@ -38,7 +38,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
{:noreply,
|
||||
push_patch(socket,
|
||||
to:
|
||||
~p"/orgs/#{socket.assigns.current_organization.slug}/equipment/#{socket.assigns.equipment_id}/graph/#{socket.assigns.sensor_type}?#{params}"
|
||||
~p"/orgs/#{socket.assigns.current_organization.slug}/devices/#{socket.assigns.device_id}/graph/#{socket.assigns.sensor_type}?#{params}"
|
||||
)}
|
||||
end
|
||||
|
||||
|
|
@ -55,12 +55,12 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
# Private functions
|
||||
|
||||
defp load_graph_data(socket) do
|
||||
equipment_id = socket.assigns.equipment_id
|
||||
device_id = socket.assigns.device_id
|
||||
sensor_type = socket.assigns.sensor_type
|
||||
range = socket.assigns.range
|
||||
interface_id = socket.assigns.interface_id
|
||||
|
||||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
{chart_data, title_suffix} =
|
||||
cond do
|
||||
|
|
@ -68,11 +68,11 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
{load_interface_traffic_chart_data(interface_id, range), get_interface_name(interface_id)}
|
||||
|
||||
sensor_type == "traffic" ->
|
||||
{load_traffic_chart_data(equipment_id, range), nil}
|
||||
{load_traffic_chart_data(device_id, range), nil}
|
||||
|
||||
true ->
|
||||
sensor_types = get_sensor_types_for_chart(sensor_type)
|
||||
{load_sensor_chart_data(equipment_id, sensor_types, range), nil}
|
||||
{load_sensor_chart_data(device_id, sensor_types, range), nil}
|
||||
end
|
||||
|
||||
{title, unit, auto_scale} = get_chart_config(sensor_type)
|
||||
|
|
@ -80,8 +80,8 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
show_zero_line = sensor_type == "traffic"
|
||||
|
||||
socket
|
||||
|> assign(:equipment, equipment)
|
||||
|> assign(:page_title, "#{equipment.name} - #{chart_title}")
|
||||
|> assign(:device, device)
|
||||
|> assign(:page_title, "#{device.name} - #{chart_title}")
|
||||
|> assign(:chart_title, chart_title)
|
||||
|> assign(:chart_data, chart_data)
|
||||
|> assign(:unit, unit)
|
||||
|
|
@ -105,8 +105,8 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
defp get_chart_config("traffic"), do: {"Overall Traffic", "bps", true}
|
||||
defp get_chart_config(_), do: {"Sensor Data", "", false}
|
||||
|
||||
defp load_sensor_chart_data(equipment_id, sensor_types, range) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
defp load_sensor_chart_data(device_id, sensor_types, range) do
|
||||
case Snmp.get_device_with_associations(device_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
|
|
@ -166,8 +166,8 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
defp get_limit_for_range("30d"), do: 43_200
|
||||
defp get_limit_for_range(_), do: 2880
|
||||
|
||||
defp load_traffic_chart_data(equipment_id, range) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
defp load_traffic_chart_data(device_id, range) do
|
||||
case Snmp.get_device_with_associations(device_id) do
|
||||
nil -> nil
|
||||
device -> build_traffic_chart_json(device, range)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,21 +2,21 @@
|
|||
flash={@flash}
|
||||
current_scope={@current_scope}
|
||||
current_organization={@current_organization}
|
||||
active_page="equipment"
|
||||
active_page="devices"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<!-- Header with back button -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment_id}"}
|
||||
navigate={~p"/orgs/#{@organization.slug}/devices/#{@device_id}"}
|
||||
class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
||||
</.link>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@chart_title}</h1>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400">{@equipment.name}</p>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400">{@device.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ defmodule ToweropsWeb.Org.SettingsLive do
|
|||
{:ok, organization} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:organization, organization)
|
||||
|> put_flash(:info, "Organization settings updated successfully")}
|
||||
|> put_flash(:info, "Organization settings updated successfully")
|
||||
|> push_navigate(to: ~p"/orgs/#{organization.slug}")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
|
|
@ -51,13 +51,13 @@ defmodule ToweropsWeb.Org.SettingsLive do
|
|||
def handle_event("apply_snmp_to_all", _params, socket) do
|
||||
{count, _} = Organizations.apply_snmp_config_to_all_equipment(socket.assigns.organization.id)
|
||||
|
||||
{:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} equipment records across all sites")}
|
||||
{:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} device records across all sites")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("apply_agent_to_all", _params, socket) do
|
||||
{count, _} = Organizations.apply_agent_to_all_equipment(socket.assigns.organization.id)
|
||||
|
||||
{:noreply, put_flash(socket, :info, "Applied default agent to #{count} equipment records across all sites")}
|
||||
{:noreply, put_flash(socket, :info, "Applied default agent to #{count} device records across all sites")}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<div class="mt-6 border-t pt-6">
|
||||
<h3 class="text-lg font-medium mb-4">SNMP Configuration</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Set default SNMP settings for all devices in this organization. These can be overridden at the site or equipment level.
|
||||
Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
|
|
@ -46,24 +46,24 @@
|
|||
|
||||
<p class="mt-3 text-sm text-zinc-500 dark:text-zinc-400 italic">
|
||||
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
||||
SNMP configuration hierarchy: Equipment > Site > Organization
|
||||
SNMP configuration hierarchy: Device > Site > Organization
|
||||
</p>
|
||||
|
||||
<%= if @organization.snmp_community do %>
|
||||
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
||||
Force Apply to All Equipment
|
||||
Force Apply to All Device
|
||||
</p>
|
||||
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
||||
This will override SNMP settings for ALL equipment across all sites in this organization.
|
||||
This will override SNMP settings for ALL devices across all sites in this organization.
|
||||
</p>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="apply_snmp_to_all"
|
||||
data-confirm="This will replace SNMP settings for ALL equipment in this organization. Are you sure?"
|
||||
data-confirm="This will replace SNMP settings for ALL devices in this organization. Are you sure?"
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Equipment
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Device
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
<div class="mt-6 border-t pt-6">
|
||||
<h3 class="text-lg font-medium mb-4">Default Agent</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Select a default agent for SNMP polling. This will be used for all equipment in this organization unless overridden at the site or equipment level.
|
||||
Select a default agent for SNMP polling. This will be used for all devices in this organization unless overridden at the site or device level.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
|
|
@ -86,13 +86,13 @@
|
|||
|
||||
<div class="mt-3 p-3 bg-zinc-50 dark:bg-zinc-800 rounded-lg text-sm">
|
||||
<p class="font-medium text-zinc-700 dark:text-zinc-300 mb-2">
|
||||
Current Equipment Assignment:
|
||||
Current Device Assignment:
|
||||
</p>
|
||||
<div class="space-y-1 text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<.icon name="hero-server" class="h-4 w-4" />
|
||||
<span>
|
||||
<strong>{@assignment_breakdown.direct}</strong> equipment-level override
|
||||
<strong>{@assignment_breakdown.direct}</strong> device-level override
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -117,25 +117,24 @@
|
|||
|
||||
<p class="mt-3 text-sm text-zinc-500 dark:text-zinc-400 italic">
|
||||
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
||||
Agent assignment hierarchy: Equipment > Site > Organization
|
||||
Agent assignment hierarchy: Device > Site > Organization
|
||||
</p>
|
||||
|
||||
<%= if @organization.default_agent_token_id do %>
|
||||
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
||||
Force Apply to All Equipment
|
||||
Force Apply to All Device
|
||||
</p>
|
||||
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
||||
This will assign the default agent to ALL equipment across all sites in this organization.
|
||||
This will assign the default agent to ALL devices across all sites in this organization.
|
||||
</p>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="apply_agent_to_all"
|
||||
data-confirm="This will replace agent assignments for ALL equipment in this organization. Are you sure?"
|
||||
data-confirm="This will replace agent assignments for ALL devices in this organization. Are you sure?"
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" />
|
||||
Apply Default Agent to All Equipment
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply Default Agent to All Device
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Layouts.authenticated flash={@flash} current_scope={@current_scope} current_organization={nil}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Create a new organization to manage your sites and equipment</:subtitle>
|
||||
<:subtitle>Create a new organization to manage your sites and devices</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
|
|
|
|||
|
|
@ -97,14 +97,14 @@ defmodule ToweropsWeb.SiteLive.Form do
|
|||
def handle_event("apply_snmp_to_all", _params, socket) do
|
||||
{count, _} = Sites.apply_snmp_config_to_all_equipment(socket.assigns.site.id)
|
||||
|
||||
{:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} equipment records at this site")}
|
||||
{:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} device records at this site")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("apply_agent_to_all", _params, socket) do
|
||||
{count, _} = Sites.apply_agent_to_all_equipment(socket.assigns.site.id)
|
||||
|
||||
{:noreply, put_flash(socket, :info, "Applied agent to #{count} equipment records at this site")}
|
||||
{:noreply, put_flash(socket, :info, "Applied agent to #{count} device records at this site")}
|
||||
end
|
||||
|
||||
defp save_site(socket, :new, site_params) do
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
<.link
|
||||
navigate={
|
||||
if @live_action == :edit,
|
||||
do: ~p"/orgs/#{@organization.slug}/sites/#{@site.id}",
|
||||
else: ~p"/orgs/#{@organization.slug}/sites"
|
||||
do: ~p"/orgs/#{@current_organization.slug}/sites/#{@site.id}",
|
||||
else: ~p"/orgs/#{@current_organization.slug}/sites"
|
||||
}
|
||||
class="inline-flex items-center gap-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||
>
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
<span class="text-zinc-400 dark:text-zinc-500 font-normal">(optional)</span>
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Set a default agent for SNMP polling at this site. This will override the organization default for all equipment at this site. Leave blank to inherit from organization.
|
||||
Set a default agent for SNMP polling at this site. This will override the organization default for all devices at this site. Leave blank to inherit from organization.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
|
|
@ -101,23 +101,23 @@
|
|||
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
|
||||
<strong>Overriding</strong> organization default -
|
||||
all equipment at this site will use this agent
|
||||
all devices at this site will use this agent
|
||||
</p>
|
||||
|
||||
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
||||
Force Apply to Site Equipment
|
||||
Force Apply to Site Device
|
||||
</p>
|
||||
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
||||
This will assign this agent to ALL equipment at this site.
|
||||
This will assign this agent to ALL devices at this site.
|
||||
</p>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="apply_agent_to_all"
|
||||
data-confirm="This will replace agent assignments for ALL equipment at this site. Are you sure?"
|
||||
data-confirm="This will replace agent assignments for ALL devices at this site. Are you sure?"
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply Agent to All Equipment
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply Agent to All Device
|
||||
</.button>
|
||||
</div>
|
||||
<% else %>
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
</p>
|
||||
<% else %>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Set a default agent for all equipment at this site. Equipment can override this setting individually.
|
||||
Set a default agent for all devices at this site. Device can override this setting individually.
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
<span class="text-zinc-400 dark:text-zinc-500 font-normal">(optional)</span>
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Override organization SNMP defaults for all equipment at this site. Leave blank to inherit from organization.
|
||||
Override organization SNMP defaults for all devices at this site. Leave blank to inherit from organization.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
|
|
@ -162,18 +162,18 @@
|
|||
<%= if @live_action == :edit and @site.snmp_community do %>
|
||||
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
||||
Force Apply to Site Equipment
|
||||
Force Apply to Site Device
|
||||
</p>
|
||||
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
||||
This will override SNMP settings for ALL equipment at this site.
|
||||
This will override SNMP settings for ALL devices at this site.
|
||||
</p>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="apply_snmp_to_all"
|
||||
data-confirm="This will replace SNMP settings for ALL equipment at this site. Are you sure?"
|
||||
data-confirm="This will replace SNMP settings for ALL devices at this site. Are you sure?"
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Equipment
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Device
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<.button phx-disable-with="Saving..." variant="primary">Save Site</.button>
|
||||
<.button navigate={~p"/orgs/#{@organization.slug}/sites"}>Cancel</.button>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/sites"}>Cancel</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
|
|
@ -189,11 +189,11 @@
|
|||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 class="text-lg font-semibold text-red-600 dark:text-red-500 mb-4">Danger Zone</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||||
Once you delete a site, there is no going back. This will also delete all equipment at this site.
|
||||
Once you delete a site, there is no going back. This will also delete all devices at this site.
|
||||
</p>
|
||||
<.button
|
||||
phx-click="delete"
|
||||
data-confirm="Are you sure you want to delete this site? This will also delete all equipment at this site."
|
||||
data-confirm="Are you sure you want to delete this site? This will also delete all devices at this site."
|
||||
variant="danger"
|
||||
>
|
||||
<.icon name="hero-trash" class="h-4 w-4" /> Delete Site
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ defmodule ToweropsWeb.SiteLive.Show do
|
|||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Sites
|
||||
|
||||
@impl true
|
||||
|
|
@ -14,12 +14,12 @@ defmodule ToweropsWeb.SiteLive.Show do
|
|||
def handle_params(%{"id" => id}, _, socket) do
|
||||
organization = socket.assigns.current_organization
|
||||
site = Sites.get_organization_site!(organization.id, id)
|
||||
equipment = Equipment.list_site_equipment(site.id)
|
||||
device = Devices.list_site_devices(site.id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, site.name)
|
||||
|> assign(:site, site)
|
||||
|> assign(:equipment, equipment)}
|
||||
|> assign(:device, device)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -64,41 +64,41 @@
|
|||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Equipment</h3>
|
||||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Device</h3>
|
||||
<.button
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new?site_id=#{@site.id}"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/new?site_id=#{@site.id}"}
|
||||
variant="primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> Add Equipment
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> Add Device
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<%= if @equipment == [] do %>
|
||||
<%= if @device == [] do %>
|
||||
<div class="rounded-lg border-2 border-dashed border-zinc-300 bg-zinc-50 p-8 text-center dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h4 class="mt-4 text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Add your first device
|
||||
</h4>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Start monitoring by adding network equipment to this site.
|
||||
Start monitoring by adding network devices to this site.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
You can add routers, switches, servers, or any device with an IP address.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new?site_id=#{@site.id}"}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/new?site_id=#{@site.id}"}
|
||||
variant="primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> Add Equipment
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> Add Device
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-3">
|
||||
<.link
|
||||
:for={eq <- @equipment}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}"}
|
||||
:for={eq <- @device}
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/devices/#{eq.id}"}
|
||||
class="flex items-center justify-between rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ defmodule ToweropsWeb.Router do
|
|||
# Data endpoints
|
||||
get "/organizations", MobileController, :list_organizations
|
||||
get "/organizations/:organization_id/sites", MobileController, :list_sites
|
||||
get "/organizations/:organization_id/equipment", MobileController, :list_equipment
|
||||
get "/organizations/:organization_id/devices", MobileController, :list_devices
|
||||
get "/organizations/:organization_id/alerts", MobileController, :list_alerts
|
||||
get "/equipment/:id", MobileController, :get_equipment
|
||||
get "/devices/:id", MobileController, :get_equipment
|
||||
end
|
||||
|
||||
# WebAuthn API routes
|
||||
|
|
@ -182,12 +182,12 @@ defmodule ToweropsWeb.Router do
|
|||
live "/sites/:id", SiteLive.Show, :show
|
||||
live "/sites/:id/edit", SiteLive.Form, :edit
|
||||
|
||||
# Equipment routes
|
||||
live "/equipment", EquipmentLive.Index, :index
|
||||
live "/equipment/new", EquipmentLive.Form, :new
|
||||
live "/equipment/:id", EquipmentLive.Show, :show
|
||||
live "/equipment/:id/edit", EquipmentLive.Form, :edit
|
||||
live "/equipment/:id/graph/:sensor_type", GraphLive.Show, :show
|
||||
# Device routes
|
||||
live "/devices", DeviceLive.Index, :index
|
||||
live "/devices/new", DeviceLive.Form, :new
|
||||
live "/devices/:id", DeviceLive.Show, :show
|
||||
live "/devices/:id/edit", DeviceLive.Form, :edit
|
||||
live "/devices/:id/graph/:sensor_type", GraphLive.Show, :show
|
||||
|
||||
# Alert routes
|
||||
live "/alerts", AlertLive.Index, :index
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ defmodule ToweropsWeb.UserAuth do
|
|||
# Get user's organizations (ordered by most recently joined first)
|
||||
case Towerops.Organizations.list_user_organizations(user.id) do
|
||||
[first_org | _] ->
|
||||
# Redirect to the first organization's equipment page
|
||||
~p"/orgs/#{first_org.slug}/equipment"
|
||||
# device page
|
||||
~p"/orgs/#{first_org.slug}/devices"
|
||||
|
||||
[] ->
|
||||
# No organizations yet, go to org list
|
||||
|
|
@ -545,10 +545,10 @@ defmodule ToweropsWeb.UserAuth do
|
|||
ip_address: ip
|
||||
})
|
||||
|
||||
# Restore superuser session and redirect to their default org's equipment page
|
||||
# device page
|
||||
redirect_path =
|
||||
case Towerops.Organizations.list_user_organizations(superuser.id) do
|
||||
[first_org | _] -> ~p"/orgs/#{first_org.slug}/equipment"
|
||||
[first_org | _] -> ~p"/orgs/#{first_org.slug}/devices"
|
||||
[] -> ~p"/orgs"
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ package towerops.agent;
|
|||
message AgentConfig {
|
||||
string version = 1;
|
||||
uint32 poll_interval_seconds = 2;
|
||||
repeated Equipment equipment = 3;
|
||||
repeated Device devices = 3;
|
||||
}
|
||||
|
||||
message Equipment {
|
||||
message Device {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string ip_address = 3;
|
||||
|
|
@ -112,8 +112,8 @@ enum QueryType {
|
|||
message AgentJob {
|
||||
string job_id = 1; // Unique job identifier (e.g., "discover:eq123")
|
||||
JobType job_type = 2; // DISCOVER or POLL
|
||||
string equipment_id = 3; // Equipment UUID
|
||||
SnmpDevice device = 4; // SNMP connection details
|
||||
string device_id = 3; // Device UUID
|
||||
SnmpDevice snmp_device = 4; // SNMP connection details
|
||||
repeated SnmpQuery queries = 5; // Queries to execute
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ message SnmpQuery {
|
|||
}
|
||||
|
||||
message SnmpResult {
|
||||
string equipment_id = 1;
|
||||
string device_id = 1;
|
||||
JobType job_type = 2;
|
||||
map<string, string> oid_values = 3; // OID → value mapping
|
||||
int64 timestamp = 4; // Unix timestamp in seconds
|
||||
|
|
@ -148,7 +148,7 @@ message AgentHeartbeat {
|
|||
}
|
||||
|
||||
message AgentError {
|
||||
string equipment_id = 1;
|
||||
string device_id = 1;
|
||||
string job_id = 2;
|
||||
string message = 3;
|
||||
int64 timestamp = 4;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ defmodule Towerops.Repo.Migrations.AddMonitoringEnabledIndex do
|
|||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# Add index for monitoring_enabled to optimize list_monitored_equipment/0 query
|
||||
# Add index for monitoring_enabled to optimize list_monitored_devices/0 query
|
||||
create index(:equipment, [:monitoring_enabled])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Towerops.Repo.Migrations.AddCompositeIndexesForAgentQueries do
|
|||
create index(:agent_tokens, [:organization_id, :last_seen_at])
|
||||
|
||||
# Composite index for equipment assignment queries (equipment_id + enabled)
|
||||
# This improves queries that check if equipment has an active assignment
|
||||
# if device has an active assignment
|
||||
create index(:agent_assignments, [:equipment_id, :enabled])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
defmodule Towerops.Repo.Migrations.RenameEquipmentToDevices do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
# Rename the main table
|
||||
rename table(:equipment), to: table(:devices)
|
||||
|
||||
# Rename foreign key columns in related tables
|
||||
rename table(:alerts), :equipment_id, to: :device_id
|
||||
rename table(:snmp_devices), :equipment_id, to: :device_id
|
||||
rename table(:monitoring_checks), :equipment_id, to: :device_id
|
||||
rename table(:equipment_events), :equipment_id, to: :device_id
|
||||
rename table(:agent_assignments), :equipment_id, to: :device_id
|
||||
rename table(:snmp_neighbors), :equipment_id, to: :device_id
|
||||
|
||||
# Rename equipment_events table
|
||||
rename table(:equipment_events), to: table(:device_events)
|
||||
|
||||
# Rename indexes (they get renamed automatically with the table, but foreign key indexes need explicit handling)
|
||||
execute "ALTER INDEX IF EXISTS equipment_pkey RENAME TO devices_pkey"
|
||||
execute "ALTER INDEX IF EXISTS equipment_site_id_index RENAME TO devices_site_id_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS equipment_monitoring_enabled_index RENAME TO devices_monitoring_enabled_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS equipment_snmp_enabled_monitoring_enabled_site_id_index RENAME TO devices_snmp_enabled_monitoring_enabled_site_id_index"
|
||||
|
||||
# Rename foreign key constraints
|
||||
execute "ALTER TABLE alerts DROP CONSTRAINT IF EXISTS alerts_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE alerts ADD CONSTRAINT alerts_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE snmp_devices DROP CONSTRAINT IF EXISTS snmp_devices_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE snmp_devices ADD CONSTRAINT snmp_devices_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE monitoring_checks DROP CONSTRAINT IF EXISTS monitoring_checks_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE monitoring_checks ADD CONSTRAINT monitoring_checks_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE device_events DROP CONSTRAINT IF EXISTS equipment_events_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE device_events ADD CONSTRAINT device_events_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE agent_assignments DROP CONSTRAINT IF EXISTS agent_assignments_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE agent_assignments ADD CONSTRAINT agent_assignments_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE snmp_neighbors DROP CONSTRAINT IF EXISTS snmp_neighbors_equipment_id_fkey"
|
||||
|
||||
execute "ALTER TABLE snmp_neighbors ADD CONSTRAINT snmp_neighbors_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE"
|
||||
|
||||
# Rename composite indexes
|
||||
execute "ALTER INDEX IF EXISTS agent_assignments_agent_token_id_equipment_id_index RENAME TO agent_assignments_agent_token_id_device_id_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS agent_assignments_equipment_id_index RENAME TO agent_assignments_device_id_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS agent_assignments_equipment_id_enabled_index RENAME TO agent_assignments_device_id_enabled_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS alerts_equipment_id_severity_status_index RENAME TO alerts_device_id_severity_status_index"
|
||||
end
|
||||
|
||||
def down do
|
||||
# Reverse all changes
|
||||
rename table(:devices), to: table(:equipment)
|
||||
rename table(:alerts), :device_id, to: :equipment_id
|
||||
rename table(:snmp_devices), :device_id, to: :equipment_id
|
||||
rename table(:monitoring_checks), :device_id, to: :equipment_id
|
||||
rename table(:device_events), :device_id, to: :equipment_id
|
||||
rename table(:agent_assignments), :device_id, to: :equipment_id
|
||||
rename table(:snmp_neighbors), :device_id, to: :equipment_id
|
||||
rename table(:device_events), to: table(:equipment_events)
|
||||
|
||||
execute "ALTER INDEX IF EXISTS devices_pkey RENAME TO equipment_pkey"
|
||||
execute "ALTER INDEX IF EXISTS devices_site_id_index RENAME TO equipment_site_id_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS devices_monitoring_enabled_index RENAME TO equipment_monitoring_enabled_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS devices_snmp_enabled_monitoring_enabled_site_id_index RENAME TO equipment_snmp_enabled_monitoring_enabled_site_id_index"
|
||||
|
||||
execute "ALTER TABLE alerts DROP CONSTRAINT IF EXISTS alerts_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE alerts ADD CONSTRAINT alerts_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE snmp_devices DROP CONSTRAINT IF EXISTS snmp_devices_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE snmp_devices ADD CONSTRAINT snmp_devices_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE monitoring_checks DROP CONSTRAINT IF EXISTS monitoring_checks_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE monitoring_checks ADD CONSTRAINT monitoring_checks_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE equipment_events DROP CONSTRAINT IF EXISTS device_events_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE equipment_events ADD CONSTRAINT equipment_events_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE agent_assignments DROP CONSTRAINT IF EXISTS agent_assignments_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE agent_assignments ADD CONSTRAINT agent_assignments_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER TABLE snmp_neighbors DROP CONSTRAINT IF EXISTS snmp_neighbors_device_id_fkey"
|
||||
|
||||
execute "ALTER TABLE snmp_neighbors ADD CONSTRAINT snmp_neighbors_equipment_id_fkey FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE CASCADE"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS agent_assignments_agent_token_id_device_id_index RENAME TO agent_assignments_agent_token_id_equipment_id_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS alerts_device_id_severity_status_index RENAME TO alerts_equipment_id_severity_status_index"
|
||||
end
|
||||
end
|
||||
|
|
@ -7,11 +7,11 @@ defmodule Towerops.Integration.SnmpIntegrationTest do
|
|||
|
||||
Requirements:
|
||||
- Network access to test devices
|
||||
- SNMP-enabled equipment (configure IP/community below)
|
||||
- SNMP-enabled devices (configure IP/community below)
|
||||
"""
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Towerops.Monitoring.EquipmentMonitor
|
||||
alias Towerops.Monitoring.DeviceMonitor
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Poller
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ defmodule Towerops.Integration.SnmpIntegrationTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "Equipment Monitor with SNMP" do
|
||||
describe "Device Monitor with SNMP" do
|
||||
test "creates SNMP-specific alert messages when device goes down" do
|
||||
# This test makes real SNMP calls to an unreachable IP to verify timeout handling
|
||||
import Towerops.AccountsFixtures
|
||||
|
|
@ -110,8 +110,8 @@ defmodule Towerops.Integration.SnmpIntegrationTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "SNMP Router",
|
||||
ip_address: "192.0.2.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -123,21 +123,21 @@ defmodule Towerops.Integration.SnmpIntegrationTest do
|
|||
})
|
||||
|
||||
# Set to up first so status change will trigger
|
||||
{:ok, equipment} = Towerops.Equipment.update_equipment_status(equipment, :up)
|
||||
{:ok, device} = Towerops.Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Trigger check - will timeout trying to reach 192.0.2.1
|
||||
{:noreply, _state} =
|
||||
EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
Process.sleep(200)
|
||||
|
||||
alerts = Towerops.Alerts.list_equipment_alerts(equipment.id)
|
||||
alerts = Towerops.Alerts.list_devices_alerts(device.id)
|
||||
refute Enum.empty?(alerts)
|
||||
|
||||
alert = hd(alerts)
|
||||
assert alert.alert_type == :equipment_down
|
||||
assert alert.alert_type == :device_down
|
||||
assert String.contains?(alert.message, "SNMP")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Towerops.AgentsFixtures do
|
|||
Agents.create_agent_token(organization_id, name)
|
||||
end
|
||||
|
||||
def agent_assignment_fixture(agent_token_id, equipment_id) do
|
||||
Agents.assign_equipment_to_agent(agent_token_id, equipment_id)
|
||||
def agent_assignment_fixture(agent_token_id, device_id) do
|
||||
Agents.assign_device_to_agent(agent_token_id, device_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ defmodule Towerops.AgentsFixturesTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -57,10 +57,10 @@ defmodule Towerops.AgentsFixturesTest do
|
|||
|
||||
{:ok, agent_token, _token_string} = agent_token_fixture(organization.id)
|
||||
|
||||
{:ok, assignment} = agent_assignment_fixture(agent_token.id, equipment.id)
|
||||
{:ok, assignment} = agent_assignment_fixture(agent_token.id, device.id)
|
||||
|
||||
assert assignment.agent_token_id == agent_token.id
|
||||
assert assignment.equipment_id == equipment.id
|
||||
assert assignment.device_id == device.id
|
||||
assert assignment.enabled == true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
|
||||
alias Towerops.Agents
|
||||
alias Towerops.Agents.Stats
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Snmp.Device
|
||||
|
|
@ -23,7 +23,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert Stats.get_organization_agent_health(org.id) == []
|
||||
end
|
||||
|
||||
test "returns online agents with equipment counts" do
|
||||
test "returns online agents with device counts" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
|
@ -43,7 +43,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert agent_health.id == agent_token.id
|
||||
assert agent_health.name == "Test Agent"
|
||||
assert agent_health.online == true
|
||||
assert agent_health.equipment_count == 0
|
||||
assert agent_health.device_count == 0
|
||||
assert agent_health.version == "0.1.0"
|
||||
assert agent_health.hostname == "test-host"
|
||||
end
|
||||
|
|
@ -67,15 +67,15 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert agent_health.online == false
|
||||
end
|
||||
|
||||
test "includes equipment count from hierarchical assignments" do
|
||||
test "includes device count from hierarchical assignments" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
|
||||
agent_token
|
||||
|> Ecto.Changeset.change(%{last_seen_at: DateTime.truncate(DateTime.utc_now(), :second)})
|
||||
|
|
@ -94,12 +94,12 @@ defmodule Towerops.Agents.StatsTest do
|
|||
|
||||
assert length(health) == 1
|
||||
agent_health = hd(health)
|
||||
assert agent_health.equipment_count == 1
|
||||
assert agent_health.device_count == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_equipment_assignment_breakdown/1" do
|
||||
test "returns zero counts for organization with no equipment" do
|
||||
test "returns zero counts for organization with no devices" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
|
||||
|
|
@ -108,15 +108,15 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert breakdown == %{direct: 0, site: 0, organization: 0, cloud: 0}
|
||||
end
|
||||
|
||||
test "counts equipment by assignment type" do
|
||||
test "counts device by assignment type" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
# Direct assignment
|
||||
{:ok, equipment1} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -125,11 +125,11 @@ defmodule Towerops.Agents.StatsTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device1.id)
|
||||
|
||||
# Cloud polling (no agent)
|
||||
{:ok, _equipment2} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, _device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -204,16 +204,16 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert high_load == []
|
||||
end
|
||||
|
||||
test "returns agents with equipment count above threshold" do
|
||||
test "returns agents with device count above threshold" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "High Load Agent")
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
# Create 3 equipment and assign all to agent
|
||||
# Create 3 devices and assign all to agent
|
||||
for i <- 1..3 do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router #{i}",
|
||||
ip_address: "192.168.1.#{i}",
|
||||
site_id: site.id,
|
||||
|
|
@ -222,7 +222,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
end
|
||||
|
||||
high_load = Stats.get_high_load_agents(org.id, 2)
|
||||
|
|
@ -231,7 +231,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
agent = hd(high_load)
|
||||
assert agent.id == agent_token.id
|
||||
assert agent.name == "High Load Agent"
|
||||
assert agent.equipment_count == 3
|
||||
assert agent.device_count == 3
|
||||
end
|
||||
|
||||
test "respects custom threshold parameter" do
|
||||
|
|
@ -242,8 +242,8 @@ defmodule Towerops.Agents.StatsTest do
|
|||
|
||||
# Create 5 equipment
|
||||
for i <- 1..5 do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router #{i}",
|
||||
ip_address: "192.168.1.#{i}",
|
||||
site_id: site.id,
|
||||
|
|
@ -252,7 +252,7 @@ defmodule Towerops.Agents.StatsTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
end
|
||||
|
||||
# Should appear with threshold 4
|
||||
|
|
@ -266,14 +266,14 @@ defmodule Towerops.Agents.StatsTest do
|
|||
end
|
||||
|
||||
describe "get_unmonitored_equipment/1" do
|
||||
test "returns empty list when all equipment has agents" do
|
||||
test "returns empty list when all devices has agents" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Agent")
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -283,20 +283,20 @@ defmodule Towerops.Agents.StatsTest do
|
|||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
|
||||
unmonitored = Stats.get_unmonitored_equipment(org.id)
|
||||
|
||||
assert unmonitored == []
|
||||
end
|
||||
|
||||
test "returns equipment with no agent assigned" do
|
||||
test "returns device with no agent assigned" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Unmonitored Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -310,19 +310,19 @@ defmodule Towerops.Agents.StatsTest do
|
|||
|
||||
assert length(unmonitored) == 1
|
||||
eq = hd(unmonitored)
|
||||
assert eq.id == equipment.id
|
||||
assert eq.id == device.id
|
||||
assert eq.name == "Unmonitored Router"
|
||||
assert eq.ip_address == "192.168.1.1"
|
||||
assert eq.site_name == "Test Site"
|
||||
end
|
||||
|
||||
test "excludes equipment with SNMP disabled" do
|
||||
test "excludes device with SNMP disabled" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, _equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, _device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "SNMP Disabled",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -335,13 +335,13 @@ defmodule Towerops.Agents.StatsTest do
|
|||
assert unmonitored == []
|
||||
end
|
||||
|
||||
test "excludes equipment with monitoring disabled" do
|
||||
test "excludes device with monitoring disabled" do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, _equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, _device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Monitoring Disabled",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -378,8 +378,8 @@ defmodule Towerops.Agents.StatsTest do
|
|||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Agent")
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: org.id})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -388,13 +388,13 @@ defmodule Towerops.Agents.StatsTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
|
||||
# Create SNMP device, sensor, and interface
|
||||
device =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
sys_name: "Test Device",
|
||||
sys_descr: "Test"
|
||||
})
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ defmodule Towerops.AgentsTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "assign_equipment_to_agent/2" do
|
||||
describe "assign_device_to_agent/2" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -106,42 +106,42 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{site: site, equipment: equipment}
|
||||
%{site: site, device: device}
|
||||
end
|
||||
|
||||
test "assigns equipment to agent", %{organization: org, equipment: equipment} do
|
||||
test "assigns equipment to agent", %{organization: org, device: device} do
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
||||
assert {:ok, assignment} =
|
||||
Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
|
||||
assert assignment.agent_token_id == agent_token.id
|
||||
assert assignment.equipment_id == equipment.id
|
||||
assert assignment.device_id == device.id
|
||||
assert assignment.enabled == true
|
||||
end
|
||||
|
||||
test "prevents assigning equipment to multiple agents", %{
|
||||
organization: org,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, agent1, _} = Agents.create_agent_token(org.id, "Agent 1")
|
||||
{:ok, agent2, _} = Agents.create_agent_token(org.id, "Agent 2")
|
||||
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device.id)
|
||||
|
||||
assert {:error, changeset} = Agents.assign_equipment_to_agent(agent2.id, equipment.id)
|
||||
assert "has already been taken" in errors_on(changeset).equipment_id
|
||||
assert {:error, changeset} = Agents.assign_device_to_agent(agent2.id, device.id)
|
||||
assert "has already been taken" in errors_on(changeset).device_id
|
||||
end
|
||||
end
|
||||
|
||||
describe "unassign_equipment/1" do
|
||||
describe "unassign_device/1" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -149,8 +149,8 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -158,14 +158,14 @@ defmodule Towerops.AgentsTest do
|
|||
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
||||
%{site: site, equipment: equipment, agent_token: agent_token}
|
||||
%{site: site, device: device, agent_token: agent_token}
|
||||
end
|
||||
|
||||
test "removes equipment assignment", %{agent_token: agent_token, equipment: equipment} do
|
||||
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{1, _} = Agents.unassign_equipment(equipment.id)
|
||||
test "removes equipment assignment", %{agent_token: agent_token, device: device} do
|
||||
{:ok, _assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
{1, _} = Agents.unassign_device(device.id)
|
||||
|
||||
assert Repo.get_by(AgentAssignment, equipment_id: equipment.id) == nil
|
||||
assert Repo.get_by(AgentAssignment, device_id: device.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ defmodule Towerops.AgentsTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "get_equipment_assignment/1" do
|
||||
describe "get_device_assignment/1" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -215,8 +215,8 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -224,25 +224,25 @@ defmodule Towerops.AgentsTest do
|
|||
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
||||
%{site: site, equipment: equipment, agent_token: agent_token}
|
||||
%{site: site, device: device, agent_token: agent_token}
|
||||
end
|
||||
|
||||
test "returns assignment when equipment is assigned", %{
|
||||
agent_token: agent_token,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
||||
{:ok, assignment} = Agents.assign_device_to_agent(agent_token.id, device.id)
|
||||
|
||||
retrieved_assignment = Agents.get_equipment_assignment(equipment.id)
|
||||
retrieved_assignment = Agents.get_device_assignment(device.id)
|
||||
assert retrieved_assignment.id == assignment.id
|
||||
end
|
||||
|
||||
test "returns nil when equipment is not assigned", %{equipment: equipment} do
|
||||
assert Agents.get_equipment_assignment(equipment.id) == nil
|
||||
test "returns nil when equipment is not assigned", %{device: device} do
|
||||
assert Agents.get_device_assignment(device.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_equipment_assignment/2" do
|
||||
describe "update_device_assignment/2" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -250,8 +250,8 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -262,49 +262,49 @@ defmodule Towerops.AgentsTest do
|
|||
|
||||
%{
|
||||
site: site,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
agent_token1: agent_token1,
|
||||
agent_token2: agent_token2
|
||||
}
|
||||
end
|
||||
|
||||
test "creates new assignment when equipment is unassigned", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
agent_token1: agent_token1
|
||||
} do
|
||||
assert {:ok, assignment} =
|
||||
Agents.update_equipment_assignment(equipment.id, agent_token1.id)
|
||||
Agents.update_device_assignment(device.id, agent_token1.id)
|
||||
|
||||
assert assignment.agent_token_id == agent_token1.id
|
||||
assert assignment.equipment_id == equipment.id
|
||||
assert assignment.device_id == device.id
|
||||
end
|
||||
|
||||
test "updates existing assignment to new agent", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
agent_token1: agent_token1,
|
||||
agent_token2: agent_token2
|
||||
} do
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token1.id, equipment.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token1.id, device.id)
|
||||
|
||||
assert {:ok, updated_assignment} =
|
||||
Agents.update_equipment_assignment(equipment.id, agent_token2.id)
|
||||
Agents.update_device_assignment(device.id, agent_token2.id)
|
||||
|
||||
assert updated_assignment.agent_token_id == agent_token2.id
|
||||
assert updated_assignment.equipment_id == equipment.id
|
||||
assert updated_assignment.device_id == device.id
|
||||
end
|
||||
|
||||
test "removes assignment when set to nil", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
agent_token1: agent_token1
|
||||
} do
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token1.id, equipment.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token1.id, device.id)
|
||||
|
||||
assert {:ok, nil} = Agents.update_equipment_assignment(equipment.id, nil)
|
||||
assert Agents.get_equipment_assignment(equipment.id) == nil
|
||||
assert {:ok, nil} = Agents.update_device_assignment(device.id, nil)
|
||||
assert Agents.get_device_assignment(device.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_agent_equipment/1" do
|
||||
describe "list_agent_devices/1" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -312,50 +312,50 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, equipment3} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device3} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 3",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{site: site, equipment1: equipment1, equipment2: equipment2, equipment3: equipment3}
|
||||
%{site: site, device1: device1, device2: device2, device3: device3}
|
||||
end
|
||||
|
||||
test "returns equipment assigned to agent with preloads", %{
|
||||
test "returns devices assigned to agent with preloads", %{
|
||||
organization: org,
|
||||
equipment1: equipment1,
|
||||
equipment2: equipment2,
|
||||
equipment3: equipment3
|
||||
device1: device1,
|
||||
device2: device2,
|
||||
device3: device3
|
||||
} do
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment2.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token.id, device1.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token.id, device2.id)
|
||||
|
||||
equipment_list = Agents.list_agent_equipment(agent_token.id)
|
||||
device_list = Agents.list_agent_devices(agent_token.id)
|
||||
|
||||
assert length(equipment_list) == 2
|
||||
assert Enum.any?(equipment_list, &(&1.id == equipment1.id))
|
||||
assert Enum.any?(equipment_list, &(&1.id == equipment2.id))
|
||||
refute Enum.any?(equipment_list, &(&1.id == equipment3.id))
|
||||
assert length(device_list) == 2
|
||||
assert Enum.any?(device_list, &(&1.id == device1.id))
|
||||
assert Enum.any?(device_list, &(&1.id == device2.id))
|
||||
refute Enum.any?(device_list, &(&1.id == device3.id))
|
||||
|
||||
# Check preloads
|
||||
equipment = hd(equipment_list)
|
||||
assert Ecto.assoc_loaded?(equipment.site)
|
||||
device = hd(device_list)
|
||||
assert Ecto.assoc_loaded?(device.site)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -371,8 +371,8 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -383,28 +383,28 @@ defmodule Towerops.AgentsTest do
|
|||
agent2: agent2,
|
||||
agent3: agent3,
|
||||
site: site,
|
||||
equipment: equipment
|
||||
device: device
|
||||
}
|
||||
end
|
||||
|
||||
test "returns equipment-level assignment (highest priority)", %{
|
||||
test "returns devices-level assignment (highest priority)", %{
|
||||
organization: org,
|
||||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
agent3: agent3,
|
||||
site: site,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Set up hierarchy: org default, site override, equipment override
|
||||
{:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
|
||||
{:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent3.id, device.id)
|
||||
|
||||
# Preload necessary associations
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
# Equipment assignment should take precedence
|
||||
assert Agents.get_effective_agent_token(equipment) == agent3.id
|
||||
assert Agents.get_effective_agent_token(device) == agent3.id
|
||||
end
|
||||
|
||||
test "returns site-level assignment when equipment not assigned", %{
|
||||
|
|
@ -412,39 +412,39 @@ defmodule Towerops.AgentsTest do
|
|||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
site: site,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Set up hierarchy: org default and site override, but no equipment assignment
|
||||
{:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
|
||||
{:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
|
||||
|
||||
# Preload necessary associations
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
# Site assignment should be used
|
||||
assert Agents.get_effective_agent_token(equipment) == agent2.id
|
||||
assert Agents.get_effective_agent_token(device) == agent2.id
|
||||
end
|
||||
|
||||
test "returns organization-level assignment when site and equipment not assigned", %{
|
||||
organization: org,
|
||||
agent1: agent1,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Set up hierarchy: only org default
|
||||
{:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
|
||||
|
||||
# Preload necessary associations
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
# Organization default should be used
|
||||
assert Agents.get_effective_agent_token(equipment) == agent1.id
|
||||
assert Agents.get_effective_agent_token(device) == agent1.id
|
||||
end
|
||||
|
||||
test "returns nil when no agent assigned at any level", %{equipment: equipment} do
|
||||
test "returns nil when no agent assigned at any level", %{device: device} do
|
||||
# No assignments at any level
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
assert Agents.get_effective_agent_token(equipment) == nil
|
||||
assert Agents.get_effective_agent_token(device) == nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -460,8 +460,8 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -472,56 +472,56 @@ defmodule Towerops.AgentsTest do
|
|||
agent2: agent2,
|
||||
agent3: agent3,
|
||||
site: site,
|
||||
equipment: equipment
|
||||
device: device
|
||||
}
|
||||
end
|
||||
|
||||
test "returns equipment source for equipment-level assignment", %{
|
||||
test "returns devices source for device-level assignment", %{
|
||||
agent3: agent3,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent3.id, device.id)
|
||||
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
assert {agent_id, :equipment} = Agents.get_effective_agent_token_with_source(equipment)
|
||||
assert {agent_id, :device} = Agents.get_effective_agent_token_with_source(device)
|
||||
assert agent_id == agent3.id
|
||||
end
|
||||
|
||||
test "returns site source for site-level assignment", %{
|
||||
agent2: agent2,
|
||||
site: site,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
|
||||
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
assert {agent_id, :site} = Agents.get_effective_agent_token_with_source(equipment)
|
||||
assert {agent_id, :site} = Agents.get_effective_agent_token_with_source(device)
|
||||
assert agent_id == agent2.id
|
||||
end
|
||||
|
||||
test "returns organization source for organization-level assignment", %{
|
||||
organization: org,
|
||||
agent1: agent1,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
|
||||
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
assert {agent_id, :organization} = Agents.get_effective_agent_token_with_source(equipment)
|
||||
assert {agent_id, :organization} = Agents.get_effective_agent_token_with_source(device)
|
||||
assert agent_id == agent1.id
|
||||
end
|
||||
|
||||
test "returns none source when no assignment", %{equipment: equipment} do
|
||||
equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
|
||||
test "returns none source when no assignment", %{device: device} do
|
||||
device = Repo.preload(device, site: [organization: :default_agent_token])
|
||||
|
||||
assert {nil, :none} = Agents.get_effective_agent_token_with_source(equipment)
|
||||
assert {nil, :none} = Agents.get_effective_agent_token_with_source(device)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_assigned_equipment/1" do
|
||||
describe "count_assigned_devices/1" do
|
||||
setup %{organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -529,63 +529,63 @@ defmodule Towerops.AgentsTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, equipment3} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device3} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment 3",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{site: site, equipment1: equipment1, equipment2: equipment2, equipment3: equipment3}
|
||||
%{site: site, device1: device1, device2: device2, device3: device3}
|
||||
end
|
||||
|
||||
test "counts directly assigned equipment", %{
|
||||
organization: org,
|
||||
equipment1: equipment1,
|
||||
equipment2: equipment2
|
||||
device1: device1,
|
||||
device2: device2
|
||||
} do
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
|
||||
# Initially no assignments
|
||||
assert Agents.count_assigned_equipment(agent_token.id) == 0
|
||||
assert Agents.count_assigned_devices(agent_token.id) == 0
|
||||
|
||||
# Assign two pieces of equipment
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
|
||||
assert Agents.count_assigned_equipment(agent_token.id) == 1
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token.id, device1.id)
|
||||
assert Agents.count_assigned_devices(agent_token.id) == 1
|
||||
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment2.id)
|
||||
assert Agents.count_assigned_equipment(agent_token.id) == 2
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent_token.id, device2.id)
|
||||
assert Agents.count_assigned_devices(agent_token.id) == 2
|
||||
end
|
||||
|
||||
test "does not count disabled assignments", %{
|
||||
organization: org,
|
||||
equipment1: equipment1
|
||||
device1: device1
|
||||
} do
|
||||
{:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
|
||||
{:ok, assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
|
||||
{:ok, assignment} = Agents.assign_device_to_agent(agent_token.id, device1.id)
|
||||
|
||||
assert Agents.count_assigned_equipment(agent_token.id) == 1
|
||||
assert Agents.count_assigned_devices(agent_token.id) == 1
|
||||
|
||||
# Disable the assignment
|
||||
assignment
|
||||
|> Ecto.Changeset.change(enabled: false)
|
||||
|> Repo.update!()
|
||||
|
||||
assert Agents.count_assigned_equipment(agent_token.id) == 0
|
||||
assert Agents.count_assigned_devices(agent_token.id) == 0
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -616,13 +616,13 @@ defmodule Towerops.AgentsTest do
|
|||
}
|
||||
end
|
||||
|
||||
test "returns equipment with direct agent assignment", %{
|
||||
test "returns device with direct agent assignment", %{
|
||||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
site1: site1
|
||||
} do
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -631,8 +631,8 @@ defmodule Towerops.AgentsTest do
|
|||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site1.id,
|
||||
|
|
@ -642,21 +642,21 @@ defmodule Towerops.AgentsTest do
|
|||
})
|
||||
|
||||
# Assign equipment to agent1
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment2.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device1.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device2.id)
|
||||
|
||||
# Agent1 should see both equipment
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
assert length(targets) == 2
|
||||
assert Enum.any?(targets, &(&1.id == equipment1.id))
|
||||
assert Enum.any?(targets, &(&1.id == equipment2.id))
|
||||
assert Enum.any?(targets, &(&1.id == device1.id))
|
||||
assert Enum.any?(targets, &(&1.id == device2.id))
|
||||
|
||||
# Agent2 should see nothing
|
||||
targets = Agents.list_agent_polling_targets(agent2.id)
|
||||
assert targets == []
|
||||
end
|
||||
|
||||
test "returns equipment inheriting from site agent", %{
|
||||
test "returns device inheriting from site agent", %{
|
||||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
site1: site1
|
||||
|
|
@ -664,8 +664,8 @@ defmodule Towerops.AgentsTest do
|
|||
# Set site1 to use agent1
|
||||
{:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent1.id})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -674,8 +674,8 @@ defmodule Towerops.AgentsTest do
|
|||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site1.id,
|
||||
|
|
@ -687,15 +687,15 @@ defmodule Towerops.AgentsTest do
|
|||
# Agent1 should see both equipment (inherited from site)
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
assert length(targets) == 2
|
||||
assert Enum.any?(targets, &(&1.id == equipment1.id))
|
||||
assert Enum.any?(targets, &(&1.id == equipment2.id))
|
||||
assert Enum.any?(targets, &(&1.id == device1.id))
|
||||
assert Enum.any?(targets, &(&1.id == device2.id))
|
||||
|
||||
# Agent2 should see nothing
|
||||
targets = Agents.list_agent_polling_targets(agent2.id)
|
||||
assert targets == []
|
||||
end
|
||||
|
||||
test "returns equipment inheriting from organization default", %{
|
||||
test "returns device inheriting from organization default", %{
|
||||
organization: org,
|
||||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
|
|
@ -705,8 +705,8 @@ defmodule Towerops.AgentsTest do
|
|||
{:ok, _org} =
|
||||
Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -715,8 +715,8 @@ defmodule Towerops.AgentsTest do
|
|||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site1.id,
|
||||
|
|
@ -728,8 +728,8 @@ defmodule Towerops.AgentsTest do
|
|||
# Agent1 should see both equipment (inherited from organization)
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
assert length(targets) == 2
|
||||
assert Enum.any?(targets, &(&1.id == equipment1.id))
|
||||
assert Enum.any?(targets, &(&1.id == equipment2.id))
|
||||
assert Enum.any?(targets, &(&1.id == device1.id))
|
||||
assert Enum.any?(targets, &(&1.id == device2.id))
|
||||
|
||||
# Agent2 should see nothing
|
||||
targets = Agents.list_agent_polling_targets(agent2.id)
|
||||
|
|
@ -749,8 +749,8 @@ defmodule Towerops.AgentsTest do
|
|||
|
||||
{:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent2.id})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -760,12 +760,12 @@ defmodule Towerops.AgentsTest do
|
|||
})
|
||||
|
||||
# Directly assign to agent3
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment1.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent3.id, device1.id)
|
||||
|
||||
# Only agent3 should see the equipment
|
||||
targets = Agents.list_agent_polling_targets(agent3.id)
|
||||
assert length(targets) == 1
|
||||
assert hd(targets).id == equipment1.id
|
||||
assert hd(targets).id == device1.id
|
||||
|
||||
# Agent1 and agent2 should not see it
|
||||
assert Agents.list_agent_polling_targets(agent1.id) == []
|
||||
|
|
@ -784,8 +784,8 @@ defmodule Towerops.AgentsTest do
|
|||
|
||||
{:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent2.id})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -797,19 +797,19 @@ defmodule Towerops.AgentsTest do
|
|||
# Agent2 should see the equipment (site level)
|
||||
targets = Agents.list_agent_polling_targets(agent2.id)
|
||||
assert length(targets) == 1
|
||||
assert hd(targets).id == equipment1.id
|
||||
assert hd(targets).id == device1.id
|
||||
|
||||
# Agent1 should not see it (org default is overridden by site)
|
||||
assert Agents.list_agent_polling_targets(agent1.id) == []
|
||||
end
|
||||
|
||||
test "only returns SNMP-enabled equipment", %{
|
||||
test "only returns SNMP-enabled devices", %{
|
||||
agent1: agent1,
|
||||
site1: site1
|
||||
} do
|
||||
# Equipment with SNMP enabled
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -819,8 +819,8 @@ defmodule Towerops.AgentsTest do
|
|||
})
|
||||
|
||||
# Equipment with SNMP disabled
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site1.id,
|
||||
|
|
@ -828,17 +828,17 @@ defmodule Towerops.AgentsTest do
|
|||
})
|
||||
|
||||
# Assign both to agent1
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment2.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device1.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device2.id)
|
||||
|
||||
# Agent1 should only see SNMP-enabled equipment
|
||||
# Agent1 should only see SNMP-enabled devices
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
assert length(targets) == 1
|
||||
assert hd(targets).id == equipment1.id
|
||||
assert hd(targets).id == device1.id
|
||||
assert hd(targets).snmp_enabled == true
|
||||
end
|
||||
|
||||
test "returns equipment from multiple sites with different assignment levels", %{
|
||||
test "returns device from multiple sites with different assignment levels", %{
|
||||
organization: org,
|
||||
agent1: agent1,
|
||||
agent2: agent2,
|
||||
|
|
@ -853,8 +853,8 @@ defmodule Towerops.AgentsTest do
|
|||
{:ok, _site} = Towerops.Sites.update_site(site2, %{agent_token_id: agent2.id})
|
||||
|
||||
# Equipment at site1 (inherits org default - agent1)
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -864,8 +864,8 @@ defmodule Towerops.AgentsTest do
|
|||
})
|
||||
|
||||
# Equipment at site2 (inherits site default - agent2)
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site2.id,
|
||||
|
|
@ -874,23 +874,23 @@ defmodule Towerops.AgentsTest do
|
|||
snmp_community: "public"
|
||||
})
|
||||
|
||||
# Agent1 should see equipment1 (from site1)
|
||||
# Agent1 should see device1 (from site1)
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
assert length(targets) == 1
|
||||
assert hd(targets).id == equipment1.id
|
||||
assert hd(targets).id == device1.id
|
||||
|
||||
# Agent2 should see equipment2 (from site2)
|
||||
# Agent2 should see device2 (from site2)
|
||||
targets = Agents.list_agent_polling_targets(agent2.id)
|
||||
assert length(targets) == 1
|
||||
assert hd(targets).id == equipment2.id
|
||||
assert hd(targets).id == device2.id
|
||||
end
|
||||
|
||||
test "preloads necessary associations for API response", %{
|
||||
agent1: agent1,
|
||||
site1: site1
|
||||
} do
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Equipment 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site1.id,
|
||||
|
|
@ -899,14 +899,14 @@ defmodule Towerops.AgentsTest do
|
|||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
|
||||
{:ok, _} = Agents.assign_device_to_agent(agent1.id, device1.id)
|
||||
|
||||
targets = Agents.list_agent_polling_targets(agent1.id)
|
||||
equipment = hd(targets)
|
||||
device = hd(targets)
|
||||
|
||||
# Verify preloads
|
||||
assert Ecto.assoc_loaded?(equipment.site)
|
||||
assert Ecto.assoc_loaded?(equipment.site.organization)
|
||||
assert Ecto.assoc_loaded?(device.site)
|
||||
assert Ecto.assoc_loaded?(device.site.organization)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -47,7 +47,7 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
assert_email_sent(subject: "Confirmation instructions")
|
||||
assert_email_sent(subject: "Confirmation instructions")
|
||||
|
||||
%{owner: owner, admin: admin, member: member, organization: organization, equipment: equipment}
|
||||
%{owner: owner, admin: admin, member: member, organization: organization, device: device}
|
||||
end
|
||||
|
||||
describe "deliver_alert_notification/1" do
|
||||
|
|
@ -55,14 +55,14 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
owner: owner,
|
||||
admin: admin,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
|
@ -71,22 +71,22 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
assert length(results) == 2
|
||||
|
||||
# Verify emails were sent to owner and admin
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}", to: owner.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}", to: admin.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Device Down: #{device.name}", to: owner.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Device Down: #{device.name}", to: admin.email)
|
||||
end
|
||||
|
||||
test "sends equipment_up alert to owners and admins", %{
|
||||
owner: owner,
|
||||
admin: admin,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
device_id: device.id,
|
||||
alert_type: :device_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
message: "Device recovered"
|
||||
})
|
||||
|
||||
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
|
@ -95,21 +95,21 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
assert length(results) == 2
|
||||
|
||||
# Verify emails were sent
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}", to: owner.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}", to: admin.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Device Recovered: #{device.name}", to: owner.email)
|
||||
assert_email_sent(subject: "[#{organization.name}] Device Recovered: #{device.name}", to: admin.email)
|
||||
end
|
||||
|
||||
test "equipment_down alert includes correct information", %{
|
||||
owner: owner,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
|
@ -119,46 +119,46 @@ defmodule Towerops.Alerts.AlertNotifierTest do
|
|||
# Verify email sent to owner contains all expected information
|
||||
assert_email_sent(fn email ->
|
||||
email.to |> List.first() |> elem(1) == owner.email &&
|
||||
email.subject =~ "Equipment Down" &&
|
||||
email.subject =~ "Device Down" &&
|
||||
email.text_body =~ organization.name &&
|
||||
email.text_body =~ equipment.name &&
|
||||
email.text_body =~ equipment.ip_address &&
|
||||
email.text_body =~ device.name &&
|
||||
email.text_body =~ device.ip_address &&
|
||||
email.text_body =~ "Test Site" &&
|
||||
email.text_body =~ "not responding"
|
||||
end)
|
||||
end
|
||||
|
||||
test "equipment_up alert includes correct information", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
device_id: device.id,
|
||||
alert_type: :device_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
message: "Device recovered"
|
||||
})
|
||||
|
||||
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.subject =~ "Equipment Recovered" &&
|
||||
email.subject =~ "Device Recovered" &&
|
||||
email.text_body =~ organization.name &&
|
||||
email.text_body =~ equipment.name &&
|
||||
email.text_body =~ equipment.ip_address &&
|
||||
email.text_body =~ device.name &&
|
||||
email.text_body =~ device.ip_address &&
|
||||
email.text_body =~ "Test Site" &&
|
||||
email.text_body =~ "now responding"
|
||||
end)
|
||||
end
|
||||
|
||||
test "uses correct from address", %{equipment: equipment} do
|
||||
test "uses correct from address", %{device: device} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
|
|
|||
|
|
@ -18,26 +18,26 @@ defmodule Towerops.AlertsTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{equipment: equipment, organization: organization, user: user}
|
||||
%{device: device, organization: organization, user: user}
|
||||
end
|
||||
|
||||
@valid_attrs %{
|
||||
alert_type: :equipment_down,
|
||||
alert_type: :device_down,
|
||||
triggered_at: ~U[2025-12-21 12:00:00Z],
|
||||
message: "Equipment is not responding"
|
||||
}
|
||||
|
||||
test "create_alert/1 with valid data creates an alert", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "create_alert/1 with valid data creates an alert", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
assert {:ok, %Alert{} = alert} = Alerts.create_alert(attrs)
|
||||
assert alert.alert_type == :equipment_down
|
||||
assert alert.alert_type == :device_down
|
||||
assert alert.message == "Equipment is not responding"
|
||||
end
|
||||
|
||||
|
|
@ -45,18 +45,18 @@ defmodule Towerops.AlertsTest do
|
|||
assert {:error, %Ecto.Changeset{}} = Alerts.create_alert(%{})
|
||||
end
|
||||
|
||||
test "list_equipment_alerts/2 returns alerts for equipment", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "list_devices_alerts/2 returns alerts for device", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
[result] = Alerts.list_equipment_alerts(equipment.id)
|
||||
[result] = Alerts.list_devices_alerts(device.id)
|
||||
assert result.id == alert.id
|
||||
end
|
||||
|
||||
test "list_organization_active_alerts/1 returns unresolved alerts", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: organization
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
|
||||
# Create resolved alert
|
||||
|
|
@ -70,25 +70,25 @@ defmodule Towerops.AlertsTest do
|
|||
end
|
||||
|
||||
test "list_organization_alerts/2 returns all alerts", %{
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: organization
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, _alert1} = Alerts.create_alert(attrs)
|
||||
{:ok, _alert2} = Alerts.create_alert(Map.put(attrs, :alert_type, :equipment_up))
|
||||
{:ok, _alert2} = Alerts.create_alert(Map.put(attrs, :alert_type, :device_up))
|
||||
|
||||
alerts = Alerts.list_organization_alerts(organization.id, 100)
|
||||
assert length(alerts) == 2
|
||||
end
|
||||
|
||||
test "get_alert!/1 returns the alert with given id", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "get_alert!/1 returns the alert with given id", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
assert Alerts.get_alert!(alert.id).id == alert.id
|
||||
end
|
||||
|
||||
test "acknowledge_alert/2 marks alert as acknowledged", %{equipment: equipment, user: user} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "acknowledge_alert/2 marks alert as acknowledged", %{device: device, user: user} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
|
||||
assert {:ok, acknowledged} = Alerts.acknowledge_alert(alert, user.id)
|
||||
|
|
@ -96,48 +96,48 @@ defmodule Towerops.AlertsTest do
|
|||
assert acknowledged.acknowledged_by_id == user.id
|
||||
end
|
||||
|
||||
test "resolve_alert/1 marks alert as resolved", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "resolve_alert/1 marks alert as resolved", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
|
||||
assert {:ok, resolved} = Alerts.resolve_alert(alert)
|
||||
assert resolved.resolved_at
|
||||
end
|
||||
|
||||
test "has_active_alert?/2 returns true when active alert exists", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "has_active_alert?/2 returns true when active alert exists", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, _alert} = Alerts.create_alert(attrs)
|
||||
|
||||
assert Alerts.has_active_alert?(equipment.id, :equipment_down) == true
|
||||
assert Alerts.has_active_alert?(equipment.id, :equipment_up) == false
|
||||
assert Alerts.has_active_alert?(device.id, :device_down) == true
|
||||
assert Alerts.has_active_alert?(device.id, :device_up) == false
|
||||
end
|
||||
|
||||
test "has_active_alert?/2 returns false for resolved alerts", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "has_active_alert?/2 returns false for resolved alerts", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
{:ok, _resolved} = Alerts.resolve_alert(alert)
|
||||
|
||||
assert Alerts.has_active_alert?(equipment.id, :equipment_down) == false
|
||||
assert Alerts.has_active_alert?(device.id, :device_down) == false
|
||||
end
|
||||
|
||||
test "get_active_alert/2 returns active alert of specific type", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "get_active_alert/2 returns active alert of specific type", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
|
||||
found = Alerts.get_active_alert(equipment.id, :equipment_down)
|
||||
found = Alerts.get_active_alert(device.id, :device_down)
|
||||
assert found.id == alert.id
|
||||
end
|
||||
|
||||
test "get_active_alert/2 returns nil when no active alert exists", %{equipment: equipment} do
|
||||
assert Alerts.get_active_alert(equipment.id, :equipment_down) == nil
|
||||
test "get_active_alert/2 returns nil when no active alert exists", %{device: device} do
|
||||
assert Alerts.get_active_alert(device.id, :device_down) == nil
|
||||
end
|
||||
|
||||
test "get_active_alert/2 returns nil for resolved alerts", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "get_active_alert/2 returns nil for resolved alerts", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, alert} = Alerts.create_alert(attrs)
|
||||
{:ok, _resolved} = Alerts.resolve_alert(alert)
|
||||
|
||||
assert Alerts.get_active_alert(equipment.id, :equipment_down) == nil
|
||||
assert Alerts.get_active_alert(device.id, :device_down) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
296
test/towerops/devices_test.exs
Normal file
296
test/towerops/devices_test.exs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
defmodule Towerops.EquipmentTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
describe "device" do
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Device, as: DeviceSchema
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, user: user}
|
||||
end
|
||||
|
||||
@valid_attrs %{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 300
|
||||
}
|
||||
@update_attrs %{
|
||||
name: "Updated Router",
|
||||
ip_address: "192.168.1.2",
|
||||
description: "Updated",
|
||||
monitoring_enabled: false
|
||||
}
|
||||
@invalid_attrs %{name: nil, ip_address: nil}
|
||||
|
||||
test "list_site_devices/1 returns all devices for a site", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert Devices.list_site_devices(site.id) == [device]
|
||||
end
|
||||
|
||||
test "list_organization_devices/1 returns all devices for an organization", %{
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
result = Devices.list_organization_devices(organization.id)
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == device.id
|
||||
end
|
||||
|
||||
test "list_monitored_devices/0 returns only device with monitoring enabled", %{
|
||||
site: site
|
||||
} do
|
||||
{:ok, monitored} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
{:ok, _not_monitored} =
|
||||
Devices.create_device(%{
|
||||
name: "Not Monitored",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: false
|
||||
})
|
||||
|
||||
result = Devices.list_monitored_devices()
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == monitored.id
|
||||
end
|
||||
|
||||
test "get_device!/1 returns the device with given id", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert Devices.get_device!(device.id).id == device.id
|
||||
end
|
||||
|
||||
test "get_site_equipment!/2 returns device for specific site", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert Devices.get_site_device!(site.id, device.id).id == device.id
|
||||
end
|
||||
|
||||
test "create_device/1 with valid data creates device", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :site_id, site.id)
|
||||
assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
|
||||
assert device.name == "Router 1"
|
||||
assert device.ip_address == "192.168.1.1"
|
||||
assert device.status == :unknown
|
||||
assert device.monitoring_enabled == true
|
||||
assert device.check_interval_seconds == 300
|
||||
end
|
||||
|
||||
test "create_device/1 with valid IPv6 address", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :ip_address, "2001:0db8:85a3::8a2e:0370:7334")
|
||||
attrs = Map.put(attrs, :site_id, site.id)
|
||||
assert {:ok, %DeviceSchema{}} = Devices.create_device(attrs)
|
||||
end
|
||||
|
||||
test "create_device/1 with invalid IP address returns error", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :ip_address, "invalid-ip")
|
||||
attrs = Map.put(attrs, :site_id, site.id)
|
||||
assert {:error, changeset} = Devices.create_device(attrs)
|
||||
assert "must be a valid IPv4 or IPv6 address" in errors_on(changeset).ip_address
|
||||
end
|
||||
|
||||
test "create_device/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Devices.create_device(@invalid_attrs)
|
||||
end
|
||||
|
||||
test "update_device/2 with valid data updates the device", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:ok, %DeviceSchema{} = device} =
|
||||
Devices.update_device(device, @update_attrs)
|
||||
|
||||
assert device.name == "Updated Router"
|
||||
assert device.ip_address == "192.168.1.2"
|
||||
assert device.monitoring_enabled == false
|
||||
end
|
||||
|
||||
test "update_device/2 with invalid data returns error changeset", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Devices.update_device(device, @invalid_attrs)
|
||||
|
||||
assert Devices.get_device!(device.id).name == device.name
|
||||
end
|
||||
|
||||
test "delete_device/1 deletes the device", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert {:ok, %DeviceSchema{}} = Devices.delete_device(device)
|
||||
assert_raise Ecto.NoResultsError, fn -> Devices.get_device!(device.id) end
|
||||
end
|
||||
|
||||
test "change_device/1 returns an device changeset", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert %Ecto.Changeset{} = Devices.change_device(device)
|
||||
end
|
||||
|
||||
test "update_device_status/2 updates status and timestamps", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:ok, updated} = Devices.update_device_status(device, :up)
|
||||
assert updated.status == :up
|
||||
assert updated.last_checked_at
|
||||
assert updated.last_status_change_at
|
||||
end
|
||||
|
||||
test "update_device_status/2 only updates last_checked_at if status unchanged", %{
|
||||
site: site
|
||||
} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
{:ok, device} = Devices.update_device_status(device, :up)
|
||||
first_change_at = device.last_status_change_at
|
||||
first_checked_at = device.last_checked_at
|
||||
|
||||
# Sleep for 1+ second to ensure timestamps differ (truncated to seconds in update_device_status)
|
||||
Process.sleep(1010)
|
||||
{:ok, updated} = Devices.update_device_status(device, :up)
|
||||
# Status change timestamp should not change
|
||||
assert updated.last_status_change_at == first_change_at
|
||||
# But checked_at should be updated
|
||||
assert DateTime.after?(updated.last_checked_at, first_checked_at)
|
||||
end
|
||||
|
||||
test "list_snmp_enabled_devices/0 returns only device with SNMP enabled", %{site: site} do
|
||||
{:ok, snmp_enabled} =
|
||||
Devices.create_device(%{
|
||||
name: "SNMP Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, _snmp_disabled} =
|
||||
Devices.create_device(%{
|
||||
name: "Non-SNMP Router",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
snmp_enabled: false
|
||||
})
|
||||
|
||||
result = Devices.list_snmp_enabled_devices()
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == snmp_enabled.id
|
||||
end
|
||||
|
||||
test "update_snmp_poll_time/1 updates last_snmp_poll_at timestamp", %{site: site} do
|
||||
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert device.last_snmp_poll_at == nil
|
||||
|
||||
assert {:ok, updated} = Devices.update_snmp_poll_time(device)
|
||||
assert updated.last_snmp_poll_at
|
||||
assert DateTime.before?(updated.last_snmp_poll_at, DateTime.utc_now())
|
||||
end
|
||||
end
|
||||
|
||||
describe "events" do
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Devices.Event
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
test "create_event/1 creates an device event", %{device: device} do
|
||||
attrs = %{
|
||||
device_id: device.id,
|
||||
event_type: "interface_up",
|
||||
severity: "info",
|
||||
message: "Interface came online",
|
||||
metadata: %{"interface" => "eth0"},
|
||||
occurred_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
}
|
||||
|
||||
assert {:ok, %Event{} = event} = Devices.create_event(attrs)
|
||||
assert event.device_id == device.id
|
||||
assert event.event_type == "interface_up"
|
||||
assert event.severity == "info"
|
||||
assert event.message == "Interface came online"
|
||||
assert event.metadata == %{"interface" => "eth0"}
|
||||
end
|
||||
|
||||
test "create_event/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Devices.create_event(%{})
|
||||
end
|
||||
|
||||
test "list_devices_events/2 returns events for device ordered by most recent", %{
|
||||
device: device
|
||||
} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, event1} =
|
||||
Devices.create_event(%{
|
||||
device_id: device.id,
|
||||
event_type: "interface_up",
|
||||
severity: "info",
|
||||
message: "Interface came up",
|
||||
occurred_at: DateTime.add(now, -3600, :second)
|
||||
})
|
||||
|
||||
{:ok, event2} =
|
||||
Devices.create_event(%{
|
||||
device_id: device.id,
|
||||
event_type: "interface_down",
|
||||
severity: "warning",
|
||||
message: "Interface went down",
|
||||
occurred_at: DateTime.add(now, -1800, :second)
|
||||
})
|
||||
|
||||
events = Devices.list_devices_events(device.id)
|
||||
assert length(events) == 2
|
||||
# Should be ordered by most recent first
|
||||
assert hd(events).id == event2.id
|
||||
assert List.last(events).id == event1.id
|
||||
end
|
||||
|
||||
test "list_devices_events/2 limits results", %{device: device} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Create 10 events
|
||||
for i <- 1..10 do
|
||||
Devices.create_event(%{
|
||||
device_id: device.id,
|
||||
event_type: "interface_down",
|
||||
severity: "warning",
|
||||
message: "Interface down event #{i}",
|
||||
occurred_at: DateTime.add(now, -i * 60, :second)
|
||||
})
|
||||
end
|
||||
|
||||
events = Devices.list_devices_events(device.id, 5)
|
||||
assert length(events) == 5
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
defmodule Towerops.Equipment.EventLoggerTest do
|
||||
defmodule Towerops.Devices.EventLoggerTest do
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Equipment.EventLogger
|
||||
alias Towerops.Devices.EventLogger
|
||||
|
||||
describe "event logging via PubSub" do
|
||||
setup do
|
||||
|
|
@ -19,19 +18,19 @@ defmodule Towerops.Equipment.EventLoggerTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Equipment",
|
||||
site_id: site.id,
|
||||
ip_address: "192.168.1.1"
|
||||
})
|
||||
|
||||
%{user: user, organization: organization, site: site, equipment: equipment}
|
||||
%{user: user, organization: organization, site: site, device: device}
|
||||
end
|
||||
|
||||
test "EventLogger logs events broadcast via PubSub", %{equipment: equipment} do
|
||||
test "EventLogger logs events broadcast via PubSub", %{device: device} do
|
||||
event_attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
event_type: "interface_speed_change",
|
||||
severity: "info",
|
||||
message: "Interface eth0 speed detected: 1.0 Gbps",
|
||||
|
|
@ -47,35 +46,35 @@ defmodule Towerops.Equipment.EventLoggerTest do
|
|||
# Broadcast event via PubSub
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:events",
|
||||
{:equipment_event, event_attrs}
|
||||
"device:events",
|
||||
{:device_event, event_attrs}
|
||||
)
|
||||
|
||||
# Give the EventLogger a moment to process
|
||||
Process.sleep(100)
|
||||
|
||||
# Verify event was created in database
|
||||
events = Equipment.list_equipment_events(equipment.id, 10)
|
||||
events = Towerops.Devices.list_devices_events(device.id, 10)
|
||||
assert length(events) == 1
|
||||
|
||||
event = hd(events)
|
||||
assert event.equipment_id == equipment.id
|
||||
assert event.device_id == device.id
|
||||
assert event.event_type == "interface_speed_change"
|
||||
assert event.severity == "info"
|
||||
assert event.message == "Interface eth0 speed detected: 1.0 Gbps"
|
||||
end
|
||||
|
||||
test "EventLogger handles multiple events in sequence", %{equipment: equipment} do
|
||||
test "EventLogger handles multiple events in sequence", %{device: device} do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
# Broadcast multiple events
|
||||
for i <- 1..3 do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:events",
|
||||
{:equipment_event,
|
||||
"device:events",
|
||||
{:device_event,
|
||||
%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
event_type: "interface_speed_change",
|
||||
severity: "info",
|
||||
message: "Event #{i}",
|
||||
|
|
@ -89,14 +88,14 @@ defmodule Towerops.Equipment.EventLoggerTest do
|
|||
Process.sleep(300)
|
||||
|
||||
# Verify all events were created
|
||||
events = Equipment.list_equipment_events(equipment.id, 10)
|
||||
events = Towerops.Devices.list_devices_events(device.id, 10)
|
||||
assert length(events) == 3
|
||||
end
|
||||
|
||||
test "EventLogger logs errors for invalid events", %{equipment: equipment} do
|
||||
test "EventLogger logs errors for invalid events", %{device: device} do
|
||||
# Broadcast an invalid event (missing required fields)
|
||||
invalid_event = %{
|
||||
equipment_id: equipment.id
|
||||
device_id: device.id
|
||||
# Missing required fields
|
||||
}
|
||||
|
||||
|
|
@ -105,8 +104,8 @@ defmodule Towerops.Equipment.EventLoggerTest do
|
|||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:events",
|
||||
{:equipment_event, invalid_event}
|
||||
"device:events",
|
||||
{:device_event, invalid_event}
|
||||
)
|
||||
|
||||
Process.sleep(100)
|
||||
|
|
@ -120,7 +119,7 @@ defmodule Towerops.Equipment.EventLoggerTest do
|
|||
# Send a message that's not an event
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"equipment:events",
|
||||
"device:events",
|
||||
{:some_other_message, "data"}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,295 +0,0 @@
|
|||
defmodule Towerops.EquipmentTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
alias Towerops.Equipment
|
||||
|
||||
describe "equipment" do
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Equipment.Equipment, as: EquipmentSchema
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, user: user}
|
||||
end
|
||||
|
||||
@valid_attrs %{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 300
|
||||
}
|
||||
@update_attrs %{
|
||||
name: "Updated Router",
|
||||
ip_address: "192.168.1.2",
|
||||
description: "Updated",
|
||||
monitoring_enabled: false
|
||||
}
|
||||
@invalid_attrs %{name: nil, ip_address: nil}
|
||||
|
||||
test "list_site_equipment/1 returns all equipment for a site", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert Equipment.list_site_equipment(site.id) == [equipment]
|
||||
end
|
||||
|
||||
test "list_organization_equipment/1 returns all equipment for an organization", %{
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
result = Equipment.list_organization_equipment(organization.id)
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == equipment.id
|
||||
end
|
||||
|
||||
test "list_monitored_equipment/0 returns only equipment with monitoring enabled", %{
|
||||
site: site
|
||||
} do
|
||||
{:ok, monitored} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
{:ok, _not_monitored} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Not Monitored",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: false
|
||||
})
|
||||
|
||||
result = Equipment.list_monitored_equipment()
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == monitored.id
|
||||
end
|
||||
|
||||
test "get_equipment!/1 returns the equipment with given id", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert Equipment.get_equipment!(equipment.id).id == equipment.id
|
||||
end
|
||||
|
||||
test "get_site_equipment!/2 returns equipment for specific site", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert Equipment.get_site_equipment!(site.id, equipment.id).id == equipment.id
|
||||
end
|
||||
|
||||
test "create_equipment/1 with valid data creates equipment", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :site_id, site.id)
|
||||
assert {:ok, %EquipmentSchema{} = equipment} = Equipment.create_equipment(attrs)
|
||||
assert equipment.name == "Router 1"
|
||||
assert equipment.ip_address == "192.168.1.1"
|
||||
assert equipment.status == :unknown
|
||||
assert equipment.monitoring_enabled == true
|
||||
assert equipment.check_interval_seconds == 300
|
||||
end
|
||||
|
||||
test "create_equipment/1 with valid IPv6 address", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :ip_address, "2001:0db8:85a3::8a2e:0370:7334")
|
||||
attrs = Map.put(attrs, :site_id, site.id)
|
||||
assert {:ok, %EquipmentSchema{}} = Equipment.create_equipment(attrs)
|
||||
end
|
||||
|
||||
test "create_equipment/1 with invalid IP address returns error", %{site: site} do
|
||||
attrs = Map.put(@valid_attrs, :ip_address, "invalid-ip")
|
||||
attrs = Map.put(attrs, :site_id, site.id)
|
||||
assert {:error, changeset} = Equipment.create_equipment(attrs)
|
||||
assert "must be a valid IPv4 or IPv6 address" in errors_on(changeset).ip_address
|
||||
end
|
||||
|
||||
test "create_equipment/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Equipment.create_equipment(@invalid_attrs)
|
||||
end
|
||||
|
||||
test "update_equipment/2 with valid data updates the equipment", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:ok, %EquipmentSchema{} = equipment} =
|
||||
Equipment.update_equipment(equipment, @update_attrs)
|
||||
|
||||
assert equipment.name == "Updated Router"
|
||||
assert equipment.ip_address == "192.168.1.2"
|
||||
assert equipment.monitoring_enabled == false
|
||||
end
|
||||
|
||||
test "update_equipment/2 with invalid data returns error changeset", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Equipment.update_equipment(equipment, @invalid_attrs)
|
||||
|
||||
assert Equipment.get_equipment!(equipment.id).name == equipment.name
|
||||
end
|
||||
|
||||
test "delete_equipment/1 deletes the equipment", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert {:ok, %EquipmentSchema{}} = Equipment.delete_equipment(equipment)
|
||||
assert_raise Ecto.NoResultsError, fn -> Equipment.get_equipment!(equipment.id) end
|
||||
end
|
||||
|
||||
test "change_equipment/1 returns an equipment changeset", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert %Ecto.Changeset{} = Equipment.change_equipment(equipment)
|
||||
end
|
||||
|
||||
test "update_equipment_status/2 updates status and timestamps", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
|
||||
assert {:ok, updated} = Equipment.update_equipment_status(equipment, :up)
|
||||
assert updated.status == :up
|
||||
assert updated.last_checked_at
|
||||
assert updated.last_status_change_at
|
||||
end
|
||||
|
||||
test "update_equipment_status/2 only updates last_checked_at if status unchanged", %{
|
||||
site: site
|
||||
} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
{:ok, equipment} = Equipment.update_equipment_status(equipment, :up)
|
||||
first_change_at = equipment.last_status_change_at
|
||||
first_checked_at = equipment.last_checked_at
|
||||
|
||||
# Sleep for 1+ second to ensure timestamps differ (truncated to seconds in update_equipment_status)
|
||||
Process.sleep(1010)
|
||||
{:ok, updated} = Equipment.update_equipment_status(equipment, :up)
|
||||
# Status change timestamp should not change
|
||||
assert updated.last_status_change_at == first_change_at
|
||||
# But checked_at should be updated
|
||||
assert DateTime.after?(updated.last_checked_at, first_checked_at)
|
||||
end
|
||||
|
||||
test "list_snmp_enabled_equipment/0 returns only equipment with SNMP enabled", %{site: site} do
|
||||
{:ok, snmp_enabled} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "SNMP Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, _snmp_disabled} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Non-SNMP Router",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
snmp_enabled: false
|
||||
})
|
||||
|
||||
result = Equipment.list_snmp_enabled_equipment()
|
||||
assert length(result) == 1
|
||||
assert hd(result).id == snmp_enabled.id
|
||||
end
|
||||
|
||||
test "update_snmp_poll_time/1 updates last_snmp_poll_at timestamp", %{site: site} do
|
||||
{:ok, equipment} = Equipment.create_equipment(Map.put(@valid_attrs, :site_id, site.id))
|
||||
assert equipment.last_snmp_poll_at == nil
|
||||
|
||||
assert {:ok, updated} = Equipment.update_snmp_poll_time(equipment)
|
||||
assert updated.last_snmp_poll_at
|
||||
assert DateTime.before?(updated.last_snmp_poll_at, DateTime.utc_now())
|
||||
end
|
||||
end
|
||||
|
||||
describe "events" do
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Equipment.Event
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
end
|
||||
|
||||
test "create_event/1 creates an equipment event", %{equipment: equipment} do
|
||||
attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
event_type: "interface_up",
|
||||
severity: "info",
|
||||
message: "Interface came online",
|
||||
metadata: %{"interface" => "eth0"},
|
||||
occurred_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
}
|
||||
|
||||
assert {:ok, %Event{} = event} = Equipment.create_event(attrs)
|
||||
assert event.equipment_id == equipment.id
|
||||
assert event.event_type == "interface_up"
|
||||
assert event.severity == "info"
|
||||
assert event.message == "Interface came online"
|
||||
assert event.metadata == %{"interface" => "eth0"}
|
||||
end
|
||||
|
||||
test "create_event/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Equipment.create_event(%{})
|
||||
end
|
||||
|
||||
test "list_equipment_events/2 returns events for equipment ordered by most recent", %{
|
||||
equipment: equipment
|
||||
} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, event1} =
|
||||
Equipment.create_event(%{
|
||||
equipment_id: equipment.id,
|
||||
event_type: "interface_up",
|
||||
severity: "info",
|
||||
message: "Interface came up",
|
||||
occurred_at: DateTime.add(now, -3600, :second)
|
||||
})
|
||||
|
||||
{:ok, event2} =
|
||||
Equipment.create_event(%{
|
||||
equipment_id: equipment.id,
|
||||
event_type: "interface_down",
|
||||
severity: "warning",
|
||||
message: "Interface went down",
|
||||
occurred_at: DateTime.add(now, -1800, :second)
|
||||
})
|
||||
|
||||
events = Equipment.list_equipment_events(equipment.id)
|
||||
assert length(events) == 2
|
||||
# Should be ordered by most recent first
|
||||
assert hd(events).id == event2.id
|
||||
assert List.last(events).id == event1.id
|
||||
end
|
||||
|
||||
test "list_equipment_events/2 limits results", %{equipment: equipment} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Create 10 events
|
||||
for i <- 1..10 do
|
||||
Equipment.create_event(%{
|
||||
equipment_id: equipment.id,
|
||||
event_type: "interface_down",
|
||||
severity: "warning",
|
||||
message: "Interface down event #{i}",
|
||||
occurred_at: DateTime.add(now, -i * 60, :second)
|
||||
})
|
||||
end
|
||||
|
||||
events = Equipment.list_equipment_events(equipment.id, 5)
|
||||
assert length(events) == 5
|
||||
end
|
||||
end
|
||||
end
|
||||
207
test/towerops/monitoring/device_monitor_test.exs
Normal file
207
test/towerops/monitoring/device_monitor_test.exs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
defmodule Towerops.Monitoring.DeviceMonitorTest do
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Monitoring.DeviceMonitor
|
||||
alias Towerops.Monitoring.PingMock
|
||||
|
||||
# Set up Mox to verify expectations on exit
|
||||
setup :verify_on_exit!
|
||||
setup :set_mox_from_context
|
||||
|
||||
setup do
|
||||
# Stub default ping behavior using the PingStub module
|
||||
Mox.stub_with(PingMock, Towerops.Monitoring.PingStub)
|
||||
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "127.0.0.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 1
|
||||
})
|
||||
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
describe "init/1" do
|
||||
test "initializes with device_id and schedules check if monitoring enabled", %{
|
||||
device: device
|
||||
} do
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
assert state.device_id == device.id
|
||||
end
|
||||
|
||||
test "initializes but doesn't schedule if monitoring disabled", %{device: device} do
|
||||
{:ok, updated} = Towerops.Devices.update_device(device, %{monitoring_enabled: false})
|
||||
{:ok, state} = DeviceMonitor.init(updated.id)
|
||||
assert state.device_id == updated.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "trigger_check/1" do
|
||||
test "can trigger an immediate check", %{device: device} do
|
||||
# Start the monitor
|
||||
start_supervised!({DeviceMonitor, device.id})
|
||||
|
||||
# Trigger a check
|
||||
DeviceMonitor.trigger_check(device.id)
|
||||
|
||||
# Give it time to process (GenServer needs time to handle the message)
|
||||
Process.sleep(200)
|
||||
|
||||
# Should have created a monitoring check
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
refute Enum.empty?(checks)
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info :check_device" do
|
||||
test "performs check and creates monitoring record", %{device: device} do
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Simulate the periodic check message
|
||||
{:noreply, _new_state} = DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
# Should have created a check
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
assert length(checks) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "SNMP monitoring configuration" do
|
||||
test "equipment can be configured with SNMP settings", %{device: device} do
|
||||
{:ok, updated_device} =
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
assert updated_device.snmp_enabled == true
|
||||
assert updated_device.snmp_version == "2c"
|
||||
assert updated_device.snmp_community == "public"
|
||||
assert updated_device.snmp_port == 161
|
||||
end
|
||||
|
||||
test "SNMP version defaults to 2c", %{device: device} do
|
||||
{:ok, updated_device} =
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
assert updated_device.snmp_version == "2c"
|
||||
end
|
||||
end
|
||||
|
||||
describe "alert creation" do
|
||||
setup %{device: device} do
|
||||
# Set equipment to up status first
|
||||
{:ok, device} = Towerops.Devices.update_device_status(device, :up)
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
test "creates device_down alert when equipment goes down", %{device: device} do
|
||||
# Mock failed ping
|
||||
expect(PingMock, :ping, fn _ip ->
|
||||
{:error, :timeout_or_unreachable}
|
||||
end)
|
||||
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Trigger check
|
||||
{:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
# Should create an alert (no need to sleep since test env doesn't send emails)
|
||||
alerts = Alerts.list_devices_alerts(device.id)
|
||||
refute Enum.empty?(alerts)
|
||||
|
||||
alert = hd(alerts)
|
||||
assert alert.alert_type == :device_down
|
||||
end
|
||||
|
||||
test "creates device_up alert when equipment recovers", %{device: device} do
|
||||
# First set to down
|
||||
{:ok, device} = Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
# Create a down alert
|
||||
{:ok, _} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Trigger check
|
||||
{:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
# Should create recovery alert
|
||||
alerts = Alerts.list_devices_alerts(device.id, 10)
|
||||
up_alerts = Enum.filter(alerts, &(&1.alert_type == :device_up))
|
||||
refute Enum.empty?(up_alerts)
|
||||
end
|
||||
|
||||
test "does not create duplicate down alerts", %{device: device} do
|
||||
# Mock failed ping for both calls
|
||||
expect(PingMock, :ping, 2, fn _ip ->
|
||||
{:error, :timeout_or_unreachable}
|
||||
end)
|
||||
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Trigger check twice
|
||||
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
|
||||
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
# Should only have one alert
|
||||
alerts =
|
||||
device.id
|
||||
|> Alerts.list_devices_alerts()
|
||||
|> Enum.filter(&(&1.alert_type == :device_down && is_nil(&1.resolved_at)))
|
||||
|
||||
assert length(alerts) == 1
|
||||
end
|
||||
|
||||
test "resolves down alert when equipment comes back up", %{device: device} do
|
||||
# Set to down first
|
||||
{:ok, device} = Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
# Create down alert
|
||||
{:ok, down_alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
message: "Down"
|
||||
})
|
||||
|
||||
{:ok, state} = DeviceMonitor.init(device.id)
|
||||
|
||||
# Check
|
||||
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
|
||||
|
||||
# Down alert should be resolved
|
||||
updated_alert = Alerts.get_alert!(down_alert.id)
|
||||
assert updated_alert.resolved_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
defmodule Towerops.Monitoring.EquipmentMonitorTest do
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Monitoring.EquipmentMonitor
|
||||
alias Towerops.Monitoring.PingMock
|
||||
|
||||
# Set up Mox to verify expectations on exit
|
||||
setup :verify_on_exit!
|
||||
setup :set_mox_from_context
|
||||
|
||||
setup do
|
||||
# Stub default ping behavior using the PingStub module
|
||||
Mox.stub_with(PingMock, Towerops.Monitoring.PingStub)
|
||||
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "127.0.0.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 1
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
end
|
||||
|
||||
describe "init/1" do
|
||||
test "initializes with equipment_id and schedules check if monitoring enabled", %{
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
assert state.equipment_id == equipment.id
|
||||
end
|
||||
|
||||
test "initializes but doesn't schedule if monitoring disabled", %{equipment: equipment} do
|
||||
{:ok, updated} = Equipment.update_equipment(equipment, %{monitoring_enabled: false})
|
||||
{:ok, state} = EquipmentMonitor.init(updated.id)
|
||||
assert state.equipment_id == updated.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "trigger_check/1" do
|
||||
test "can trigger an immediate check", %{equipment: equipment} do
|
||||
# Start the monitor
|
||||
start_supervised!({EquipmentMonitor, equipment.id})
|
||||
|
||||
# Trigger a check
|
||||
EquipmentMonitor.trigger_check(equipment.id)
|
||||
|
||||
# Give it time to process (GenServer needs time to handle the message)
|
||||
Process.sleep(200)
|
||||
|
||||
# Should have created a monitoring check
|
||||
checks = Monitoring.list_equipment_checks(equipment.id)
|
||||
refute Enum.empty?(checks)
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info :check_equipment" do
|
||||
test "performs check and creates monitoring record", %{equipment: equipment} do
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
|
||||
# Simulate the periodic check message
|
||||
{:noreply, _new_state} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
|
||||
# Should have created a check
|
||||
checks = Monitoring.list_equipment_checks(equipment.id)
|
||||
assert length(checks) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "SNMP monitoring configuration" do
|
||||
test "equipment can be configured with SNMP settings", %{equipment: equipment} do
|
||||
{:ok, updated_equipment} =
|
||||
Equipment.update_equipment(equipment, %{
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
assert updated_equipment.snmp_enabled == true
|
||||
assert updated_equipment.snmp_version == "2c"
|
||||
assert updated_equipment.snmp_community == "public"
|
||||
assert updated_equipment.snmp_port == 161
|
||||
end
|
||||
|
||||
test "SNMP version defaults to 2c", %{equipment: equipment} do
|
||||
{:ok, updated_equipment} =
|
||||
Equipment.update_equipment(equipment, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
assert updated_equipment.snmp_version == "2c"
|
||||
end
|
||||
end
|
||||
|
||||
describe "alert creation" do
|
||||
setup %{equipment: equipment} do
|
||||
# Set equipment to up status first
|
||||
{:ok, equipment} = Equipment.update_equipment_status(equipment, :up)
|
||||
%{equipment: equipment}
|
||||
end
|
||||
|
||||
test "creates equipment_down alert when equipment goes down", %{equipment: equipment} do
|
||||
# Mock failed ping
|
||||
expect(PingMock, :ping, fn _ip ->
|
||||
{:error, :timeout_or_unreachable}
|
||||
end)
|
||||
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
|
||||
# Trigger check
|
||||
{:noreply, _state} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
|
||||
# Should create an alert (no need to sleep since test env doesn't send emails)
|
||||
alerts = Alerts.list_equipment_alerts(equipment.id)
|
||||
refute Enum.empty?(alerts)
|
||||
|
||||
alert = hd(alerts)
|
||||
assert alert.alert_type == :equipment_down
|
||||
end
|
||||
|
||||
test "creates equipment_up alert when equipment recovers", %{equipment: equipment} do
|
||||
# First set to down
|
||||
{:ok, equipment} = Equipment.update_equipment_status(equipment, :down)
|
||||
|
||||
# Create a down alert
|
||||
{:ok, _} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
|
||||
# Trigger check
|
||||
{:noreply, _state} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
|
||||
# Should create recovery alert
|
||||
alerts = Alerts.list_equipment_alerts(equipment.id, 10)
|
||||
up_alerts = Enum.filter(alerts, &(&1.alert_type == :equipment_up))
|
||||
refute Enum.empty?(up_alerts)
|
||||
end
|
||||
|
||||
test "does not create duplicate down alerts", %{equipment: equipment} do
|
||||
# Mock failed ping for both calls
|
||||
expect(PingMock, :ping, 2, fn _ip ->
|
||||
{:error, :timeout_or_unreachable}
|
||||
end)
|
||||
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
|
||||
# Trigger check twice
|
||||
{:noreply, _} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
{:noreply, _} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
|
||||
# Should only have one alert
|
||||
alerts =
|
||||
equipment.id
|
||||
|> Alerts.list_equipment_alerts()
|
||||
|> Enum.filter(&(&1.alert_type == :equipment_down && is_nil(&1.resolved_at)))
|
||||
|
||||
assert length(alerts) == 1
|
||||
end
|
||||
|
||||
test "resolves down alert when equipment comes back up", %{equipment: equipment} do
|
||||
# Set to down first
|
||||
{:ok, equipment} = Equipment.update_equipment_status(equipment, :down)
|
||||
|
||||
# Create down alert
|
||||
{:ok, down_alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
message: "Down"
|
||||
})
|
||||
|
||||
{:ok, state} = EquipmentMonitor.init(equipment.id)
|
||||
|
||||
# Check
|
||||
{:noreply, _} = EquipmentMonitor.handle_info(:check_equipment, state)
|
||||
|
||||
# Down alert should be resolved
|
||||
updated_alert = Alerts.get_alert!(down_alert.id)
|
||||
assert updated_alert.resolved_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -16,117 +16,117 @@ defmodule Towerops.Monitoring.SupervisorTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
%{equipment: equipment, site: site}
|
||||
%{device: device, site: site}
|
||||
end
|
||||
|
||||
describe "start_monitor/1" do
|
||||
test "starts a monitor for equipment", %{equipment: equipment} do
|
||||
assert {:ok, pid} = MonitoringSupervisor.start_monitor(equipment.id)
|
||||
test "starts a monitor for device", %{device: device} do
|
||||
assert {:ok, pid} = MonitoringSupervisor.start_monitor(device.id)
|
||||
assert is_pid(pid)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_monitor(equipment.id)
|
||||
MonitoringSupervisor.stop_monitor(device.id)
|
||||
end
|
||||
|
||||
test "returns error when monitor already running", %{equipment: equipment} do
|
||||
{:ok, _pid} = MonitoringSupervisor.start_monitor(equipment.id)
|
||||
test "returns error when monitor already running", %{device: device} do
|
||||
{:ok, _pid} = MonitoringSupervisor.start_monitor(device.id)
|
||||
|
||||
assert {:error, {:already_started, _pid}} =
|
||||
MonitoringSupervisor.start_monitor(equipment.id)
|
||||
MonitoringSupervisor.start_monitor(device.id)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_monitor(equipment.id)
|
||||
MonitoringSupervisor.stop_monitor(device.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "stop_monitor/1" do
|
||||
test "stops a running monitor", %{equipment: equipment} do
|
||||
{:ok, pid} = MonitoringSupervisor.start_monitor(equipment.id)
|
||||
test "stops a running monitor", %{device: device} do
|
||||
{:ok, pid} = MonitoringSupervisor.start_monitor(device.id)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
assert :ok = MonitoringSupervisor.stop_monitor(equipment.id)
|
||||
assert :ok = MonitoringSupervisor.stop_monitor(device.id)
|
||||
|
||||
# Give it a moment to terminate
|
||||
Process.sleep(10)
|
||||
refute Process.alive?(pid)
|
||||
end
|
||||
|
||||
test "returns :ok when monitor not running", %{equipment: equipment} do
|
||||
assert :ok = MonitoringSupervisor.stop_monitor(equipment.id)
|
||||
test "returns :ok when monitor not running", %{device: device} do
|
||||
assert :ok = MonitoringSupervisor.stop_monitor(device.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "start_snmp_poller/1" do
|
||||
test "starts an SNMP poller for equipment", %{equipment: equipment} do
|
||||
test "starts an SNMP poller for device", %{device: device} do
|
||||
# Enable SNMP for the equipment
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.update_equipment(equipment, %{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
assert {:ok, pid} = MonitoringSupervisor.start_snmp_poller(equipment.id)
|
||||
assert {:ok, pid} = MonitoringSupervisor.start_snmp_poller(device.id)
|
||||
assert is_pid(pid)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_snmp_poller(equipment.id)
|
||||
MonitoringSupervisor.stop_snmp_poller(device.id)
|
||||
end
|
||||
|
||||
test "returns error when poller already running", %{equipment: equipment} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.update_equipment(equipment, %{
|
||||
test "returns error when poller already running", %{device: device} do
|
||||
{:ok, device} =
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, _pid} = MonitoringSupervisor.start_snmp_poller(equipment.id)
|
||||
{:ok, _pid} = MonitoringSupervisor.start_snmp_poller(device.id)
|
||||
|
||||
assert {:error, {:already_started, _pid}} =
|
||||
MonitoringSupervisor.start_snmp_poller(equipment.id)
|
||||
MonitoringSupervisor.start_snmp_poller(device.id)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_snmp_poller(equipment.id)
|
||||
MonitoringSupervisor.stop_snmp_poller(device.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "stop_snmp_poller/1" do
|
||||
test "stops a running SNMP poller", %{equipment: equipment} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.update_equipment(equipment, %{
|
||||
test "stops a running SNMP poller", %{device: device} do
|
||||
{:ok, device} =
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
{:ok, pid} = MonitoringSupervisor.start_snmp_poller(equipment.id)
|
||||
{:ok, pid} = MonitoringSupervisor.start_snmp_poller(device.id)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
assert :ok = MonitoringSupervisor.stop_snmp_poller(equipment.id)
|
||||
assert :ok = MonitoringSupervisor.stop_snmp_poller(device.id)
|
||||
|
||||
# Give it a moment to terminate
|
||||
Process.sleep(10)
|
||||
refute Process.alive?(pid)
|
||||
end
|
||||
|
||||
test "returns :ok when poller not running", %{equipment: equipment} do
|
||||
assert :ok = MonitoringSupervisor.stop_snmp_poller(equipment.id)
|
||||
test "returns :ok when poller not running", %{device: device} do
|
||||
assert :ok = MonitoringSupervisor.stop_snmp_poller(device.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "start_all_monitors/0" do
|
||||
test "starts monitors for all monitored equipment", %{equipment: equipment, site: site} do
|
||||
# Create another monitored equipment
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
test "starts monitors for all monitored devices", %{device: device, site: site} do
|
||||
# Create another monitored device
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -137,29 +137,29 @@ defmodule Towerops.Monitoring.SupervisorTest do
|
|||
MonitoringSupervisor.start_all_monitors()
|
||||
|
||||
# Check that both are running
|
||||
assert [{pid1, _}] = Registry.lookup(Towerops.Monitoring.Registry, equipment.id)
|
||||
assert [{pid2, _}] = Registry.lookup(Towerops.Monitoring.Registry, equipment2.id)
|
||||
assert [{pid1, _}] = Registry.lookup(Towerops.Monitoring.Registry, device.id)
|
||||
assert [{pid2, _}] = Registry.lookup(Towerops.Monitoring.Registry, device2.id)
|
||||
assert Process.alive?(pid1)
|
||||
assert Process.alive?(pid2)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_monitor(equipment.id)
|
||||
MonitoringSupervisor.stop_monitor(equipment2.id)
|
||||
MonitoringSupervisor.stop_monitor(device.id)
|
||||
MonitoringSupervisor.stop_monitor(device2.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "start_all_snmp_pollers/0" do
|
||||
test "starts pollers for all SNMP-enabled equipment", %{equipment: equipment, site: site} do
|
||||
# Enable SNMP on equipment
|
||||
test "starts pollers for all SNMP-enabled devices", %{device: device, site: site} do
|
||||
# Enable SNMP on device
|
||||
{:ok, _} =
|
||||
Towerops.Equipment.update_equipment(equipment, %{
|
||||
Towerops.Devices.update_device(device, %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public"
|
||||
})
|
||||
|
||||
# Create another SNMP-enabled equipment
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
# Create another SNMP-enabled device
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -171,14 +171,14 @@ defmodule Towerops.Monitoring.SupervisorTest do
|
|||
MonitoringSupervisor.start_all_snmp_pollers()
|
||||
|
||||
# Check that both are running
|
||||
assert [{pid1, _}] = Registry.lookup(PollerRegistry, equipment.id)
|
||||
assert [{pid2, _}] = Registry.lookup(PollerRegistry, equipment2.id)
|
||||
assert [{pid1, _}] = Registry.lookup(PollerRegistry, device.id)
|
||||
assert [{pid2, _}] = Registry.lookup(PollerRegistry, device2.id)
|
||||
assert Process.alive?(pid1)
|
||||
assert Process.alive?(pid2)
|
||||
|
||||
# Cleanup
|
||||
MonitoringSupervisor.stop_snmp_poller(equipment.id)
|
||||
MonitoringSupervisor.stop_snmp_poller(equipment2.id)
|
||||
MonitoringSupervisor.stop_snmp_poller(device.id)
|
||||
MonitoringSupervisor.stop_snmp_poller(device2.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ defmodule Towerops.MonitoringTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
@valid_attrs %{
|
||||
|
|
@ -34,8 +34,8 @@ defmodule Towerops.MonitoringTest do
|
|||
checked_at: ~U[2025-12-21 12:00:00Z]
|
||||
}
|
||||
|
||||
test "create_check/1 with valid data creates a check", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "create_check/1 with valid data creates a check", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
assert {:ok, %Check{} = check} = Monitoring.create_check(attrs)
|
||||
assert check.status == :success
|
||||
assert check.response_time_ms == 50
|
||||
|
|
@ -45,57 +45,57 @@ defmodule Towerops.MonitoringTest do
|
|||
assert {:error, %Ecto.Changeset{}} = Monitoring.create_check(%{})
|
||||
end
|
||||
|
||||
test "list_equipment_checks/2 returns checks for equipment", %{equipment: equipment} do
|
||||
attrs = Map.put(@valid_attrs, :equipment_id, equipment.id)
|
||||
test "list_devices_checks/2 returns checks for device", %{device: device} do
|
||||
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
||||
{:ok, check} = Monitoring.create_check(attrs)
|
||||
assert Monitoring.list_equipment_checks(equipment.id) == [check]
|
||||
assert Monitoring.list_devices_checks(device.id) == [check]
|
||||
end
|
||||
|
||||
test "list_equipment_checks/2 limits results", %{equipment: equipment} do
|
||||
test "list_devices_checks/2 limits results", %{device: device} do
|
||||
# Create 10 checks
|
||||
for i <- 1..10 do
|
||||
attrs =
|
||||
@valid_attrs
|
||||
|> Map.put(:equipment_id, equipment.id)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:checked_at, DateTime.add(~U[2025-12-21 12:00:00Z], i, :second))
|
||||
|
||||
{:ok, _check} = Monitoring.create_check(attrs)
|
||||
end
|
||||
|
||||
assert length(Monitoring.list_equipment_checks(equipment.id, 5)) == 5
|
||||
assert length(Monitoring.list_devices_checks(device.id, 5)) == 5
|
||||
end
|
||||
|
||||
test "get_latest_check/1 returns most recent check", %{equipment: equipment} do
|
||||
test "get_latest_check/1 returns most recent check", %{device: device} do
|
||||
attrs1 =
|
||||
@valid_attrs
|
||||
|> Map.put(:equipment_id, equipment.id)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:checked_at, ~U[2025-12-21 12:00:00Z])
|
||||
|
||||
attrs2 =
|
||||
@valid_attrs
|
||||
|> Map.put(:equipment_id, equipment.id)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:checked_at, ~U[2025-12-21 13:00:00Z])
|
||||
|
||||
{:ok, _check1} = Monitoring.create_check(attrs1)
|
||||
{:ok, check2} = Monitoring.create_check(attrs2)
|
||||
|
||||
latest = Monitoring.get_latest_check(equipment.id)
|
||||
latest = Monitoring.get_latest_check(device.id)
|
||||
assert latest.id == check2.id
|
||||
end
|
||||
|
||||
test "get_latest_check/1 returns nil when no checks exist", %{equipment: equipment} do
|
||||
assert Monitoring.get_latest_check(equipment.id) == nil
|
||||
test "get_latest_check/1 returns nil when no checks exist", %{device: device} do
|
||||
assert Monitoring.get_latest_check(device.id) == nil
|
||||
end
|
||||
|
||||
test "delete_old_checks/1 deletes checks older than date", %{equipment: equipment} do
|
||||
test "delete_old_checks/1 deletes checks older than date", %{device: device} do
|
||||
old_attrs =
|
||||
@valid_attrs
|
||||
|> Map.put(:equipment_id, equipment.id)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:checked_at, ~U[2025-01-01 12:00:00Z])
|
||||
|
||||
new_attrs =
|
||||
@valid_attrs
|
||||
|> Map.put(:equipment_id, equipment.id)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:checked_at, ~U[2025-12-21 12:00:00Z])
|
||||
|
||||
{:ok, _old_check} = Monitoring.create_check(old_attrs)
|
||||
|
|
@ -103,16 +103,16 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{deleted, _} = Monitoring.delete_old_checks(~U[2025-06-01 00:00:00Z])
|
||||
assert deleted == 1
|
||||
assert length(Monitoring.list_equipment_checks(equipment.id)) == 1
|
||||
assert length(Monitoring.list_devices_checks(device.id)) == 1
|
||||
end
|
||||
|
||||
test "get_hourly_stats/3 returns stats for equipment", %{equipment: equipment} do
|
||||
test "get_hourly_stats/3 returns stats for device", %{device: device} do
|
||||
base_time = ~U[2025-12-21 12:00:00Z]
|
||||
|
||||
# Create checks in two different hours
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 50,
|
||||
checked_at: base_time
|
||||
|
|
@ -120,7 +120,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 100,
|
||||
checked_at: DateTime.add(base_time, 30 * 60, :second)
|
||||
|
|
@ -128,7 +128,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: DateTime.add(base_time, 3600, :second)
|
||||
|
|
@ -137,7 +137,7 @@ defmodule Towerops.MonitoringTest do
|
|||
start_time = DateTime.add(base_time, -3600, :second)
|
||||
end_time = DateTime.add(base_time, 7200, :second)
|
||||
|
||||
result = Monitoring.get_hourly_stats(equipment.id, start_time, end_time)
|
||||
result = Monitoring.get_hourly_stats(device.id, start_time, end_time)
|
||||
assert %Postgrex.Result{} = result
|
||||
assert length(result.rows) == 2
|
||||
|
||||
|
|
@ -153,14 +153,14 @@ defmodule Towerops.MonitoringTest do
|
|||
assert uptime == Decimal.new("100.00")
|
||||
end
|
||||
|
||||
test "get_daily_stats/3 returns stats for equipment", %{equipment: equipment} do
|
||||
test "get_daily_stats/3 returns stats for device", %{device: device} do
|
||||
day1 = ~U[2025-12-20 12:00:00Z]
|
||||
day2 = ~U[2025-12-21 12:00:00Z]
|
||||
|
||||
# Create checks on two different days
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 50,
|
||||
checked_at: day1
|
||||
|
|
@ -168,7 +168,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 60,
|
||||
checked_at: DateTime.add(day1, 3600, :second)
|
||||
|
|
@ -176,7 +176,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: day2
|
||||
|
|
@ -184,7 +184,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 80,
|
||||
checked_at: DateTime.add(day2, 3600, :second)
|
||||
|
|
@ -193,7 +193,7 @@ defmodule Towerops.MonitoringTest do
|
|||
start_time = DateTime.add(day1, -24 * 60 * 60, :second)
|
||||
end_time = DateTime.add(day2, 24 * 60 * 60, :second)
|
||||
|
||||
result = Monitoring.get_daily_stats(equipment.id, start_time, end_time)
|
||||
result = Monitoring.get_daily_stats(device.id, start_time, end_time)
|
||||
assert %Postgrex.Result{} = result
|
||||
assert length(result.rows) == 2
|
||||
|
||||
|
|
@ -207,13 +207,13 @@ defmodule Towerops.MonitoringTest do
|
|||
assert uptime == Decimal.new("100.00")
|
||||
end
|
||||
|
||||
test "get_uptime_percentage/1 returns uptime percentage", %{equipment: equipment} do
|
||||
test "get_uptime_percentage/1 returns uptime percentage", %{device: device} do
|
||||
base_time = DateTime.utc_now()
|
||||
|
||||
# Create 3 successful and 1 failed check
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 50,
|
||||
checked_at: DateTime.add(base_time, -60, :second)
|
||||
|
|
@ -221,7 +221,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 60,
|
||||
checked_at: DateTime.add(base_time, -120, :second)
|
||||
|
|
@ -229,7 +229,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: DateTime.add(base_time, -180, :second)
|
||||
|
|
@ -237,23 +237,23 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 70,
|
||||
checked_at: DateTime.add(base_time, -240, :second)
|
||||
})
|
||||
|
||||
result = Monitoring.get_uptime_percentage(equipment.id)
|
||||
result = Monitoring.get_uptime_percentage(device.id)
|
||||
assert result == 75.0
|
||||
end
|
||||
|
||||
test "get_uptime_percentage/2 accepts custom days parameter", %{equipment: equipment} do
|
||||
test "get_uptime_percentage/2 accepts custom days parameter", %{device: device} do
|
||||
base_time = DateTime.utc_now()
|
||||
|
||||
# Create checks within the last 7 days
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 50,
|
||||
checked_at: DateTime.add(base_time, -3 * 24 * 60 * 60, :second)
|
||||
|
|
@ -261,7 +261,7 @@ defmodule Towerops.MonitoringTest do
|
|||
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 60,
|
||||
checked_at: DateTime.add(base_time, -5 * 24 * 60 * 60, :second)
|
||||
|
|
@ -270,18 +270,18 @@ defmodule Towerops.MonitoringTest do
|
|||
# Create a check older than 7 days (should not be included)
|
||||
{:ok, _} =
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: DateTime.add(base_time, -10 * 24 * 60 * 60, :second)
|
||||
})
|
||||
|
||||
result = Monitoring.get_uptime_percentage(equipment.id, 7)
|
||||
result = Monitoring.get_uptime_percentage(device.id, 7)
|
||||
assert result == 100.0
|
||||
end
|
||||
|
||||
test "get_uptime_percentage/1 returns nil when no checks exist", %{equipment: equipment} do
|
||||
result = Monitoring.get_uptime_percentage(equipment.id)
|
||||
test "get_uptime_percentage/1 returns nil when no checks exist", %{device: device} do
|
||||
result = Monitoring.get_uptime_percentage(device.id)
|
||||
assert is_nil(result)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
assert Policy.can?(membership, :edit, :organization) == true
|
||||
assert Policy.can?(membership, :manage, :members) == true
|
||||
assert Policy.can?(membership, :create, :site) == true
|
||||
assert Policy.can?(membership, :edit, :equipment) == true
|
||||
assert Policy.can?(membership, :edit, :device) == true
|
||||
assert Policy.can?(membership, :view, :anything) == true
|
||||
end
|
||||
|
||||
|
|
@ -23,11 +23,11 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
assert Policy.can?(membership, :edit, :organization) == true
|
||||
assert Policy.can?(membership, :manage, :members) == true
|
||||
assert Policy.can?(membership, :create, :site) == true
|
||||
assert Policy.can?(membership, :edit, :equipment) == true
|
||||
assert Policy.can?(membership, :edit, :device) == true
|
||||
assert Policy.can?(membership, :view, :anything) == true
|
||||
end
|
||||
|
||||
test "member can create and edit sites/equipment but not manage org" do
|
||||
test "member can create and edit sites/devices but not manage org" do
|
||||
membership = %Membership{role: :member}
|
||||
|
||||
assert Policy.can?(membership, :view, :organization) == true
|
||||
|
|
@ -35,8 +35,8 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
assert Policy.can?(membership, :delete, :organization) == false
|
||||
assert Policy.can?(membership, :create, :site) == true
|
||||
assert Policy.can?(membership, :edit, :site) == true
|
||||
assert Policy.can?(membership, :edit, :equipment) == true
|
||||
assert Policy.can?(membership, :view, :equipment) == true
|
||||
assert Policy.can?(membership, :edit, :device) == true
|
||||
assert Policy.can?(membership, :view, :device) == true
|
||||
assert Policy.can?(membership, :delete, :site) == false
|
||||
end
|
||||
|
||||
|
|
@ -47,9 +47,9 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
assert Policy.can?(membership, :edit, :organization) == false
|
||||
assert Policy.can?(membership, :manage, :members) == false
|
||||
assert Policy.can?(membership, :create, :site) == false
|
||||
assert Policy.can?(membership, :edit, :equipment) == false
|
||||
assert Policy.can?(membership, :edit, :device) == false
|
||||
assert Policy.can?(membership, :view, :site) == true
|
||||
assert Policy.can?(membership, :view, :equipment) == true
|
||||
assert Policy.can?(membership, :view, :device) == true
|
||||
end
|
||||
|
||||
test "viewer can acknowledge alerts" do
|
||||
|
|
@ -69,7 +69,7 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
test "nil membership cannot do anything" do
|
||||
assert Policy.can?(nil, :view, :organization) == false
|
||||
assert Policy.can?(nil, :edit, :site) == false
|
||||
assert Policy.can?(nil, :delete, :equipment) == false
|
||||
assert Policy.can?(nil, :delete, :device) == false
|
||||
end
|
||||
|
||||
test "member cannot manage memberships" do
|
||||
|
|
@ -90,7 +90,7 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
membership = %Membership{role: :member}
|
||||
|
||||
assert Policy.can?(membership, :list, :site) == true
|
||||
assert Policy.can?(membership, :list, :equipment) == true
|
||||
assert Policy.can?(membership, :list, :device) == true
|
||||
assert Policy.can?(membership, :list, :alert) == true
|
||||
end
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ defmodule Towerops.Organizations.PolicyTest do
|
|||
membership = %Membership{role: :viewer}
|
||||
|
||||
assert Policy.can?(membership, :list, :site) == true
|
||||
assert Policy.can?(membership, :list, :equipment) == true
|
||||
assert Policy.can?(membership, :list, :device) == true
|
||||
assert Policy.can?(membership, :list, :alert) == true
|
||||
assert Policy.can?(membership, :list, :organization) == true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ defmodule Towerops.Proto.AgentPbTest do
|
|||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.Agent.AgentConfig
|
||||
alias Towerops.Agent.Equipment
|
||||
alias Towerops.Agent.Device
|
||||
alias Towerops.Agent.HeartbeatMetadata
|
||||
alias Towerops.Agent.HeartbeatResponse
|
||||
alias Towerops.Agent.Interface
|
||||
|
|
@ -18,22 +18,22 @@ defmodule Towerops.Proto.AgentPbTest do
|
|||
config = %AgentConfig{}
|
||||
assert config.version == ""
|
||||
assert config.poll_interval_seconds == 0
|
||||
assert config.equipment == []
|
||||
assert config.devices == []
|
||||
end
|
||||
|
||||
test "creates struct with values" do
|
||||
equipment = %Equipment{id: "eq1", name: "Router"}
|
||||
device = %Device{id: "eq1", name: "Router"}
|
||||
|
||||
config = %AgentConfig{
|
||||
version: "1.0.0",
|
||||
poll_interval_seconds: 60,
|
||||
equipment: [equipment]
|
||||
devices: [device]
|
||||
}
|
||||
|
||||
assert config.version == "1.0.0"
|
||||
assert config.poll_interval_seconds == 60
|
||||
assert length(config.equipment) == 1
|
||||
assert hd(config.equipment).id == "eq1"
|
||||
assert length(config.devices) == 1
|
||||
assert hd(config.devices).id == "eq1"
|
||||
end
|
||||
|
||||
test "encodes and decodes" do
|
||||
|
|
@ -51,16 +51,16 @@ defmodule Towerops.Proto.AgentPbTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "Equipment" do
|
||||
describe "Device" do
|
||||
test "creates struct with default values" do
|
||||
equipment = %Equipment{}
|
||||
assert equipment.id == ""
|
||||
assert equipment.name == ""
|
||||
assert equipment.ip_address == ""
|
||||
assert equipment.snmp == nil
|
||||
assert equipment.poll_interval_seconds == 0
|
||||
assert equipment.sensors == []
|
||||
assert equipment.interfaces == []
|
||||
device = %Device{}
|
||||
assert device.id == ""
|
||||
assert device.name == ""
|
||||
assert device.ip_address == ""
|
||||
assert device.snmp == nil
|
||||
assert device.poll_interval_seconds == 0
|
||||
assert device.sensors == []
|
||||
assert device.interfaces == []
|
||||
end
|
||||
|
||||
test "creates struct with SNMP config" do
|
||||
|
|
@ -71,7 +71,7 @@ defmodule Towerops.Proto.AgentPbTest do
|
|||
port: 161
|
||||
}
|
||||
|
||||
equipment = %Equipment{
|
||||
device = %Device{
|
||||
id: "eq1",
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
|
|
@ -79,38 +79,38 @@ defmodule Towerops.Proto.AgentPbTest do
|
|||
poll_interval_seconds: 60
|
||||
}
|
||||
|
||||
assert equipment.id == "eq1"
|
||||
assert equipment.name == "Router 1"
|
||||
assert equipment.ip_address == "192.168.1.1"
|
||||
assert equipment.snmp.enabled == true
|
||||
assert equipment.poll_interval_seconds == 60
|
||||
assert device.id == "eq1"
|
||||
assert device.name == "Router 1"
|
||||
assert device.ip_address == "192.168.1.1"
|
||||
assert device.snmp.enabled == true
|
||||
assert device.poll_interval_seconds == 60
|
||||
end
|
||||
|
||||
test "creates struct with sensors and interfaces" do
|
||||
sensor = %Sensor{id: "s1", type: "temperature", oid: "1.3.6.1.2.1"}
|
||||
interface = %Interface{id: "i1", if_index: 1, if_name: "eth0"}
|
||||
|
||||
equipment = %Equipment{
|
||||
device = %Device{
|
||||
id: "eq1",
|
||||
sensors: [sensor],
|
||||
interfaces: [interface]
|
||||
}
|
||||
|
||||
assert length(equipment.sensors) == 1
|
||||
assert length(equipment.interfaces) == 1
|
||||
assert hd(equipment.sensors).id == "s1"
|
||||
assert hd(equipment.interfaces).id == "i1"
|
||||
assert length(device.sensors) == 1
|
||||
assert length(device.interfaces) == 1
|
||||
assert hd(device.sensors).id == "s1"
|
||||
assert hd(device.interfaces).id == "i1"
|
||||
end
|
||||
|
||||
test "encodes and decodes" do
|
||||
equipment = %Equipment{
|
||||
device = %Device{
|
||||
id: "eq1",
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1"
|
||||
}
|
||||
|
||||
encoded = Equipment.encode(equipment)
|
||||
decoded = Equipment.decode(encoded)
|
||||
encoded = Device.encode(device)
|
||||
decoded = Device.decode(encoded)
|
||||
|
||||
assert decoded.id == "eq1"
|
||||
assert decoded.name == "Router 1"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Snmp.Device
|
||||
alias Towerops.Snmp.Discovery
|
||||
alias Towerops.Snmp.Interface
|
||||
|
|
@ -28,20 +28,20 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
|
||||
describe "discover_equipment/1" do
|
||||
test "returns error when SNMP is not enabled", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
snmp_enabled: false
|
||||
})
|
||||
|
||||
assert {:error, :snmp_not_enabled} = Discovery.discover_equipment(equipment)
|
||||
assert {:error, :snmp_not_enabled} = Discovery.discover_equipment(device)
|
||||
end
|
||||
|
||||
test "successfully discovers MikroTik device", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "MikroTik Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -147,7 +147,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
end)
|
||||
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
assert {:ok, device} = Discovery.discover_equipment(device)
|
||||
|
||||
assert device.manufacturer == "MikroTik"
|
||||
assert device.model == "RB750"
|
||||
|
|
@ -164,8 +164,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
|
||||
test "successfully discovers Cisco device", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Cisco Switch",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -212,7 +212,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
assert {:ok, device} = Discovery.discover_equipment(device)
|
||||
|
||||
assert device.manufacturer == "Cisco"
|
||||
assert device.model == "C2960"
|
||||
|
|
@ -220,8 +220,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
|
||||
test "handles connection failure", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Unreachable Device",
|
||||
ip_address: "192.168.1.99",
|
||||
site_id: site.id,
|
||||
|
|
@ -236,12 +236,12 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :timeout} = Discovery.discover_equipment(equipment)
|
||||
assert {:error, :timeout} = Discovery.discover_equipment(device)
|
||||
end
|
||||
|
||||
test "handles partial discovery failure gracefully", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Partial Device",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id,
|
||||
|
|
@ -292,7 +292,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end)
|
||||
|
||||
# Should succeed even with failed interface/sensor discovery
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
assert {:ok, device} = Discovery.discover_equipment(device)
|
||||
|
||||
assert device.manufacturer == "Unknown"
|
||||
|
||||
|
|
@ -305,8 +305,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
|
||||
test "updates existing device on re-discovery", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Existing Device",
|
||||
ip_address: "192.168.1.4",
|
||||
site_id: site.id,
|
||||
|
|
@ -320,7 +320,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
{:ok, initial_device} =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
manufacturer: "Old Manufacturer",
|
||||
model: "Old Model",
|
||||
sys_name: "old-name"
|
||||
|
|
@ -356,7 +356,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, updated_device} = Discovery.discover_equipment(equipment)
|
||||
assert {:ok, updated_device} = Discovery.discover_equipment(device)
|
||||
|
||||
# Should be same device, updated
|
||||
assert updated_device.id == initial_device.id
|
||||
|
|
@ -368,9 +368,9 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
|
||||
describe "discover_all/1" do
|
||||
test "discovers multiple devices concurrently", %{organization: organization, site: site} do
|
||||
# Create multiple SNMP-enabled equipment
|
||||
{:ok, equipment1} =
|
||||
Equipment.create_equipment(%{
|
||||
# Create multiple SNMP-enabled devices
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Device 1",
|
||||
ip_address: "192.168.1.10",
|
||||
site_id: site.id,
|
||||
|
|
@ -380,8 +380,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Device 2",
|
||||
ip_address: "192.168.1.11",
|
||||
site_id: site.id,
|
||||
|
|
@ -391,8 +391,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, _equipment3} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, _device3} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Device 3 - No SNMP",
|
||||
ip_address: "192.168.1.12",
|
||||
site_id: site.id,
|
||||
|
|
@ -426,13 +426,13 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
assert summary.failed == 0
|
||||
|
||||
# Verify devices were created
|
||||
assert Repo.get_by(Device, equipment_id: equipment1.id)
|
||||
assert Repo.get_by(Device, equipment_id: equipment2.id)
|
||||
assert Repo.get_by(Device, device_id: device1.id)
|
||||
assert Repo.get_by(Device, device_id: device2.id)
|
||||
end
|
||||
|
||||
test "handles mixed success and failure", %{organization: organization, site: site} do
|
||||
{:ok, equipment1} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Good Device",
|
||||
ip_address: "192.168.1.20",
|
||||
site_id: site.id,
|
||||
|
|
@ -442,8 +442,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, _equipment2} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, _device2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Bad Device",
|
||||
ip_address: "192.168.1.21",
|
||||
site_id: site.id,
|
||||
|
|
@ -483,7 +483,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
assert :auth_failure in summary.errors
|
||||
|
||||
# Verify only good device was created
|
||||
assert Repo.get_by(Device, equipment_id: equipment1.id)
|
||||
assert Repo.get_by(Device, device_id: device1.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment1} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device_schema1} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Switch 1",
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_enabled: true,
|
||||
|
|
@ -30,8 +30,8 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device_schema2} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Switch 2",
|
||||
ip_address: "192.168.1.2",
|
||||
snmp_enabled: true,
|
||||
|
|
@ -44,7 +44,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
device1 =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment1.id,
|
||||
device_id: device_schema1.id,
|
||||
sys_name: "test-switch-1",
|
||||
sys_descr: "Test Switch 1"
|
||||
})
|
||||
|
|
@ -53,7 +53,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
device2 =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment2.id,
|
||||
device_id: device_schema2.id,
|
||||
sys_name: "test-switch-2",
|
||||
sys_descr: "Test Switch 2"
|
||||
})
|
||||
|
|
@ -84,8 +84,10 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
|> Repo.insert!()
|
||||
|
||||
%{
|
||||
equipment1: equipment1,
|
||||
equipment2: equipment2,
|
||||
device_schema1: device_schema1,
|
||||
device_schema2: device_schema2,
|
||||
device1: device1,
|
||||
device2: device2,
|
||||
interface1: interface1,
|
||||
interface2: interface2
|
||||
}
|
||||
|
|
@ -98,20 +100,22 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "cleanup removes stale neighbors across all equipment", %{
|
||||
equipment1: equipment1,
|
||||
equipment2: equipment2,
|
||||
test "cleanup removes stale neighbors across all devices", %{
|
||||
device_schema1: device_schema1,
|
||||
device_schema2: device_schema2,
|
||||
device1: device1,
|
||||
device2: device2,
|
||||
interface1: interface1,
|
||||
interface2: interface2
|
||||
} do
|
||||
# Create old neighbors on both equipment
|
||||
# Create old neighbors on both devices
|
||||
two_days_ago = DateTime.add(DateTime.utc_now(), -48, :hour)
|
||||
one_hour_ago = DateTime.add(DateTime.utc_now(), -1, :hour)
|
||||
|
||||
# Old neighbor on equipment1
|
||||
# Old neighbor on device1
|
||||
{:ok, old_neighbor1} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment1.id,
|
||||
device_id: device_schema1.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "old-neighbor-1",
|
||||
|
|
@ -119,10 +123,10 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
last_discovered_at: two_days_ago
|
||||
})
|
||||
|
||||
# Old neighbor on equipment2
|
||||
# Old neighbor on device2
|
||||
{:ok, old_neighbor2} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment2.id,
|
||||
device_id: device_schema2.id,
|
||||
interface_id: interface2.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "old-neighbor-2",
|
||||
|
|
@ -130,10 +134,10 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
last_discovered_at: two_days_ago
|
||||
})
|
||||
|
||||
# Recent neighbor on equipment1
|
||||
# Recent neighbor on device1
|
||||
{:ok, recent_neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment1.id,
|
||||
device_id: device_schema1.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "recent-neighbor",
|
||||
|
|
@ -158,9 +162,9 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "cleanup handles equipment with no neighbors gracefully", %{equipment1: equipment1} do
|
||||
test "cleanup handles device with no neighbors gracefully", %{device1: device1} do
|
||||
# Ensure no neighbors exist
|
||||
assert Snmp.list_neighbors(equipment1.id) == []
|
||||
assert Snmp.list_neighbors(device1.id) == []
|
||||
|
||||
{:ok, pid} = NeighborCleanupWorker.start_link()
|
||||
send(pid, :cleanup)
|
||||
|
|
@ -173,7 +177,8 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
end
|
||||
|
||||
test "cleanup preserves neighbors within 24 hour threshold", %{
|
||||
equipment1: equipment1,
|
||||
device_schema1: device_schema1,
|
||||
device1: device1,
|
||||
interface1: interface1
|
||||
} do
|
||||
# Create neighbors at various ages
|
||||
|
|
@ -184,7 +189,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
# Neighbor just within threshold
|
||||
{:ok, recent_neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment1.id,
|
||||
device_id: device_schema1.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "recent",
|
||||
|
|
@ -195,7 +200,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
# Neighbor just outside threshold
|
||||
{:ok, stale_neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment1.id,
|
||||
device_id: device_schema1.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "stale",
|
||||
|
|
@ -217,7 +222,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "cleanup does not affect equipment without SNMP enabled" do
|
||||
test "cleanup does not affect device without SNMP enabled" do
|
||||
user = user_fixture()
|
||||
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Test Org 2"}, user.id)
|
||||
|
||||
|
|
@ -227,10 +232,10 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
organization_id: org.id
|
||||
})
|
||||
|
||||
# Create equipment without SNMP enabled
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "No SNMP Equipment",
|
||||
# Create device without SNMP enabled
|
||||
{:ok, device_schema} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "No SNMP Device",
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_enabled: false,
|
||||
site_id: site.id
|
||||
|
|
@ -239,7 +244,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
device =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
sys_name: "no-snmp-device",
|
||||
sys_descr: "No SNMP Device"
|
||||
})
|
||||
|
|
@ -260,7 +265,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
|
||||
{:ok, neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "neighbor",
|
||||
|
|
@ -273,8 +278,8 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
|
|||
|
||||
Process.sleep(100)
|
||||
|
||||
# Neighbor should still exist (equipment not included in cleanup)
|
||||
# because cleanup only processes SNMP-enabled equipment
|
||||
# Neighbor should still exist (device not included in cleanup)
|
||||
# because cleanup only processes SNMP-enabled devices
|
||||
assert Repo.get(Neighbor, neighbor.id)
|
||||
|
||||
GenServer.stop(pid)
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device_schema} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Switch",
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_enabled: true,
|
||||
|
|
@ -37,7 +37,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
device =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
sys_name: "test-switch",
|
||||
sys_descr: "Test Switch"
|
||||
})
|
||||
|
|
@ -68,7 +68,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|> Repo.insert!()
|
||||
|
||||
%{
|
||||
equipment: equipment,
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1,
|
||||
interface2: interface2,
|
||||
|
|
@ -77,7 +77,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
describe "discover_neighbors/2" do
|
||||
test "discovers LLDP neighbors", %{equipment: equipment, interface1: interface1} do
|
||||
test "discovers LLDP neighbors", %{device_schema: device_schema, device: device, interface1: interface1} do
|
||||
# Mock LLDP walk response
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
|
|
@ -101,13 +101,13 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end)
|
||||
|
||||
client_opts = [
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port
|
||||
ip: device_schema.ip_address,
|
||||
community: device_schema.snmp_community,
|
||||
version: device_schema.snmp_version,
|
||||
port: device_schema.snmp_port
|
||||
]
|
||||
|
||||
interfaces = [Map.put(interface1, :equipment_id, equipment.id)]
|
||||
interfaces = [Map.put(interface1, :device_id, device_schema.id)]
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
|
|
@ -116,14 +116,14 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
assert neighbor.protocol == "lldp"
|
||||
assert neighbor.interface_id == interface1.id
|
||||
assert neighbor.equipment_id == equipment.id
|
||||
assert neighbor.device_id == device_schema.id
|
||||
assert neighbor.remote_system_name == "neighbor-switch"
|
||||
assert neighbor.remote_port_id == "Gi0/1"
|
||||
assert neighbor.remote_port_description == "GigabitEthernet0/1"
|
||||
assert neighbor.remote_system_description == "Cisco IOS Software"
|
||||
end
|
||||
|
||||
test "discovers CDP neighbors", %{equipment: equipment, interface2: interface2} do
|
||||
test "discovers CDP neighbors", %{device_schema: device_schema, device: device, interface2: interface2} do
|
||||
# Mock LLDP walk response (empty)
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
|
|
@ -145,13 +145,13 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end)
|
||||
|
||||
client_opts = [
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port
|
||||
ip: device_schema.ip_address,
|
||||
community: device_schema.snmp_community,
|
||||
version: device_schema.snmp_version,
|
||||
port: device_schema.snmp_port
|
||||
]
|
||||
|
||||
interfaces = [Map.put(interface2, :equipment_id, equipment.id)]
|
||||
interfaces = [Map.put(interface2, :device_id, device_schema.id)]
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
assert neighbor.protocol == "cdp"
|
||||
assert neighbor.interface_id == interface2.id
|
||||
assert neighbor.equipment_id == equipment.id
|
||||
assert neighbor.device_id == device_schema.id
|
||||
assert neighbor.remote_system_name == "neighbor-router.example.com"
|
||||
assert neighbor.remote_port_id == "FastEthernet0/1"
|
||||
assert neighbor.remote_platform == "cisco ISR4331/K9"
|
||||
|
|
@ -168,7 +168,8 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
test "discovers both LLDP and CDP neighbors", %{
|
||||
equipment: equipment,
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1,
|
||||
interface2: interface2
|
||||
} do
|
||||
|
|
@ -191,15 +192,15 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end)
|
||||
|
||||
client_opts = [
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port
|
||||
ip: device_schema.ip_address,
|
||||
community: device_schema.snmp_community,
|
||||
version: device_schema.snmp_version,
|
||||
port: device_schema.snmp_port
|
||||
]
|
||||
|
||||
interfaces = [
|
||||
Map.put(interface1, :equipment_id, equipment.id),
|
||||
Map.put(interface2, :equipment_id, equipment.id)
|
||||
Map.put(interface1, :device_id, device_schema.id),
|
||||
Map.put(interface2, :device_id, device_schema.id)
|
||||
]
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
|
@ -210,7 +211,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
assert "cdp" in protocols
|
||||
end
|
||||
|
||||
test "returns empty list when no neighbors found", %{equipment: equipment} do
|
||||
test "returns empty list when no neighbors found", %{device_schema: device_schema, device: device} do
|
||||
# Mock LLDP walk response (empty)
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
|
|
@ -222,10 +223,10 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end)
|
||||
|
||||
client_opts = [
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port
|
||||
ip: device_schema.ip_address,
|
||||
community: device_schema.snmp_community,
|
||||
version: device_schema.snmp_version,
|
||||
port: device_schema.snmp_port
|
||||
]
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, [])
|
||||
|
|
@ -233,7 +234,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
assert neighbors == []
|
||||
end
|
||||
|
||||
test "handles SNMP errors gracefully", %{equipment: equipment, interface1: interface1} do
|
||||
test "handles SNMP errors gracefully", %{device_schema: device_schema, device: device, interface1: interface1} do
|
||||
# Mock LLDP walk error
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:error, :timeout}
|
||||
|
|
@ -245,13 +246,13 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end)
|
||||
|
||||
client_opts = [
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
port: equipment.snmp_port
|
||||
ip: device_schema.ip_address,
|
||||
community: device_schema.snmp_community,
|
||||
version: device_schema.snmp_version,
|
||||
port: device_schema.snmp_port
|
||||
]
|
||||
|
||||
interfaces = [Map.put(interface1, :equipment_id, equipment.id)]
|
||||
interfaces = [Map.put(interface1, :device_id, device_schema.id)]
|
||||
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
|
|
@ -261,9 +262,9 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
describe "upsert_neighbor/1" do
|
||||
test "creates a new neighbor record", %{equipment: equipment, interface1: interface1} do
|
||||
test "creates a new neighbor record", %{device_schema: device_schema, device: device, interface1: interface1} do
|
||||
attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
||||
|
|
@ -284,9 +285,9 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
assert neighbor.remote_capabilities == ["router", "bridge"]
|
||||
end
|
||||
|
||||
test "updates existing neighbor record", %{equipment: equipment, interface1: interface1} do
|
||||
test "updates existing neighbor record", %{device_schema: device_schema, device: device, interface1: interface1} do
|
||||
attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "neighbor-switch",
|
||||
|
|
@ -299,7 +300,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
# Update with new data
|
||||
updated_attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "neighbor-switch",
|
||||
|
|
@ -317,12 +318,13 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
test "creates separate records for different protocols", %{
|
||||
equipment: equipment,
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1
|
||||
} do
|
||||
# Create LLDP neighbor
|
||||
lldp_attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "same-chassis-id",
|
||||
|
|
@ -332,7 +334,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
# Create CDP neighbor with same chassis ID
|
||||
cdp_attrs = %{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "same-chassis-id",
|
||||
|
|
@ -352,14 +354,15 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
describe "list_neighbors/1" do
|
||||
test "returns all neighbors for equipment", %{
|
||||
equipment: equipment,
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1,
|
||||
interface2: interface2
|
||||
} do
|
||||
# Create neighbors on different interfaces
|
||||
{:ok, _neighbor1} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
||||
|
|
@ -369,7 +372,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
{:ok, _neighbor2} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface2.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "neighbor2",
|
||||
|
|
@ -377,27 +380,28 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
last_discovered_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
neighbors = Snmp.list_neighbors(equipment.id)
|
||||
neighbors = Snmp.list_neighbors(device_schema.id)
|
||||
|
||||
assert length(neighbors) == 2
|
||||
assert Enum.all?(neighbors, &(&1.equipment_id == equipment.id))
|
||||
assert Enum.all?(neighbors, &(&1.device_id == device_schema.id))
|
||||
# Check that interface is preloaded
|
||||
assert Enum.all?(neighbors, &Ecto.assoc_loaded?(&1.interface))
|
||||
end
|
||||
|
||||
test "returns empty list for equipment with no neighbors", %{equipment: equipment} do
|
||||
neighbors = Snmp.list_neighbors(equipment.id)
|
||||
test "returns empty list for equipment with no neighbors", %{device_schema: device_schema, device: device} do
|
||||
neighbors = Snmp.list_neighbors(device_schema.id)
|
||||
assert neighbors == []
|
||||
end
|
||||
|
||||
test "orders neighbors by protocol and system name", %{
|
||||
equipment: equipment,
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1
|
||||
} do
|
||||
# Create neighbors in reverse alphabetical order
|
||||
{:ok, _} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "chassis-z",
|
||||
|
|
@ -407,7 +411,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
{:ok, _} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "chassis-a",
|
||||
|
|
@ -415,7 +419,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
last_discovered_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
neighbors = Snmp.list_neighbors(equipment.id)
|
||||
neighbors = Snmp.list_neighbors(device_schema.id)
|
||||
|
||||
# Should be ordered by protocol, then system name
|
||||
assert length(neighbors) == 2
|
||||
|
|
@ -426,13 +430,17 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
describe "delete_stale_neighbors/2" do
|
||||
test "deletes neighbors not seen since cutoff", %{equipment: equipment, interface1: interface1} do
|
||||
test "deletes neighbors not seen since cutoff", %{
|
||||
device_schema: device_schema,
|
||||
device: device,
|
||||
interface1: interface1
|
||||
} do
|
||||
# Create old neighbor
|
||||
two_days_ago = DateTime.add(DateTime.utc_now(), -48, :hour)
|
||||
|
||||
{:ok, old_neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "old-neighbor",
|
||||
|
|
@ -443,7 +451,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
# Create recent neighbor
|
||||
{:ok, recent_neighbor} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "cdp",
|
||||
remote_chassis_id: "recent-neighbor",
|
||||
|
|
@ -453,7 +461,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
# Delete neighbors older than 24 hours
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
{count, _} = Snmp.delete_stale_neighbors(equipment.id, cutoff)
|
||||
{count, _} = Snmp.delete_stale_neighbors(device_schema.id, cutoff)
|
||||
|
||||
assert count == 1
|
||||
|
||||
|
|
@ -465,15 +473,16 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
end
|
||||
|
||||
test "only deletes neighbors for specified equipment", %{
|
||||
device_schema: device_schema,
|
||||
interface1: interface1,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
user = user_fixture()
|
||||
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Org 2"}, user.id)
|
||||
{:ok, site} = Towerops.Sites.create_site(%{name: "Site 2", organization_id: org.id})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Other Equipment",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id
|
||||
|
|
@ -482,7 +491,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
device2 =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment2.id,
|
||||
device_id: equipment2.id,
|
||||
sys_name: "other-device",
|
||||
sys_descr: "Other Device"
|
||||
})
|
||||
|
|
@ -503,7 +512,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
# Create old neighbors on both equipment
|
||||
{:ok, neighbor1} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device_schema.id,
|
||||
interface_id: interface1.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "neighbor1",
|
||||
|
|
@ -513,7 +522,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
{:ok, neighbor2} =
|
||||
Snmp.upsert_neighbor(%{
|
||||
equipment_id: equipment2.id,
|
||||
device_id: equipment2.id,
|
||||
interface_id: interface2.id,
|
||||
protocol: "lldp",
|
||||
remote_chassis_id: "neighbor2",
|
||||
|
|
@ -523,7 +532,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
|
||||
# Delete only from first equipment
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
{count, _} = Snmp.delete_stale_neighbors(equipment.id, cutoff)
|
||||
{count, _} = Snmp.delete_stale_neighbors(device_schema.id, cutoff)
|
||||
|
||||
assert count == 1
|
||||
|
||||
|
|
@ -534,9 +543,9 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
|
|||
assert Repo.get(Neighbor, neighbor2.id)
|
||||
end
|
||||
|
||||
test "returns count of 0 when no stale neighbors", %{equipment: equipment} do
|
||||
test "returns count of 0 when no stale neighbors", %{device_schema: device_schema, device: device} do
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
{count, _} = Snmp.delete_stale_neighbors(equipment.id, cutoff)
|
||||
{count, _} = Snmp.delete_stale_neighbors(device_schema.id, cutoff)
|
||||
|
||||
assert count == 0
|
||||
end
|
||||
|
|
|
|||
|
|
@ -97,15 +97,15 @@ defmodule Towerops.Snmp.PollerTest do
|
|||
end
|
||||
|
||||
describe "build_client_opts/1" do
|
||||
test "builds client options from equipment map" do
|
||||
equipment = %{
|
||||
test "builds client options from device map" do
|
||||
device = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "private",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
client_opts = Poller.build_client_opts(device)
|
||||
|
||||
assert client_opts[:ip] == "192.168.1.100"
|
||||
assert client_opts[:community] == "private"
|
||||
|
|
@ -115,53 +115,53 @@ defmodule Towerops.Snmp.PollerTest do
|
|||
end
|
||||
|
||||
test "uses default port 161 when snmp_port is nil" do
|
||||
equipment = %{
|
||||
device = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: nil
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
client_opts = Poller.build_client_opts(device)
|
||||
|
||||
assert client_opts[:port] == 161
|
||||
end
|
||||
|
||||
test "handles custom SNMP port" do
|
||||
equipment = %{
|
||||
device = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 1161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
client_opts = Poller.build_client_opts(device)
|
||||
|
||||
assert client_opts[:port] == 1161
|
||||
end
|
||||
|
||||
test "builds options for SNMPv1" do
|
||||
equipment = %{
|
||||
device = %{
|
||||
ip_address: "10.0.0.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "1",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
client_opts = Poller.build_client_opts(device)
|
||||
|
||||
assert client_opts[:version] == "1"
|
||||
end
|
||||
|
||||
test "includes standard timeout of 5 seconds" do
|
||||
equipment = %{
|
||||
device = %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
client_opts = Poller.build_client_opts(device)
|
||||
|
||||
assert client_opts[:timeout] == 5000
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ defmodule Towerops.SnmpTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_enabled: true,
|
||||
|
|
@ -32,16 +32,16 @@ defmodule Towerops.SnmpTest do
|
|||
site_id: site.id
|
||||
})
|
||||
|
||||
device =
|
||||
snmp_device =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
sys_name: "test-router",
|
||||
sys_descr: "Test Device"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
%{equipment: equipment, device: device, organization: organization}
|
||||
%{device: device, snmp_device: snmp_device, organization: organization}
|
||||
end
|
||||
|
||||
describe "test_connection/4" do
|
||||
|
|
@ -77,36 +77,36 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "get_device/1" do
|
||||
test "returns device for equipment_id", %{device: device, equipment: equipment} do
|
||||
found_device = Snmp.get_device(equipment.id)
|
||||
assert found_device.id == device.id
|
||||
test "returns device for device_id", %{device: device, snmp_device: snmp_device} do
|
||||
found_device = Snmp.get_device(device.id)
|
||||
assert found_device.id == snmp_device.id
|
||||
assert found_device.sys_name == "test-router"
|
||||
end
|
||||
|
||||
test "returns nil for non-existent equipment_id" do
|
||||
test "returns nil for non-existent device_id" do
|
||||
assert Snmp.get_device(Ecto.UUID.generate()) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_device_with_associations/1" do
|
||||
test "returns device with preloaded associations", %{device: device, equipment: equipment} do
|
||||
found_device = Snmp.get_device_with_associations(equipment.id)
|
||||
assert found_device.id == device.id
|
||||
test "returns device with preloaded associations", %{device: device, snmp_device: snmp_device} do
|
||||
found_device = Snmp.get_device_with_associations(device.id)
|
||||
assert found_device.id == snmp_device.id
|
||||
assert Ecto.assoc_loaded?(found_device.interfaces)
|
||||
assert Ecto.assoc_loaded?(found_device.sensors)
|
||||
end
|
||||
|
||||
test "returns nil for non-existent equipment_id" do
|
||||
test "returns nil for non-existent device_id" do
|
||||
assert Snmp.get_device_with_associations(Ecto.UUID.generate()) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_interfaces/1" do
|
||||
test "returns all interfaces for a device ordered by if_index", %{device: device} do
|
||||
test "returns all interfaces for a device ordered by if_index", %{device: device, snmp_device: snmp_device} do
|
||||
interface1 =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 2,
|
||||
if_name: "eth1"
|
||||
})
|
||||
|
|
@ -115,29 +115,29 @@ defmodule Towerops.SnmpTest do
|
|||
interface2 =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interfaces = Snmp.list_interfaces(device.id)
|
||||
interfaces = Snmp.list_interfaces(snmp_device.id)
|
||||
assert length(interfaces) == 2
|
||||
assert hd(interfaces).id == interface2.id
|
||||
assert List.last(interfaces).id == interface1.id
|
||||
end
|
||||
|
||||
test "returns empty list for device with no interfaces", %{device: device} do
|
||||
assert Snmp.list_interfaces(device.id) == []
|
||||
test "returns empty list for device with no interfaces", %{device: device, snmp_device: snmp_device} do
|
||||
assert Snmp.list_interfaces(snmp_device.id) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_monitored_interfaces/1" do
|
||||
test "returns only monitored interfaces", %{device: device} do
|
||||
test "returns only monitored interfaces", %{device: device, snmp_device: snmp_device} do
|
||||
monitored =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0",
|
||||
monitored: true
|
||||
|
|
@ -147,25 +147,25 @@ defmodule Towerops.SnmpTest do
|
|||
_unmonitored =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 2,
|
||||
if_name: "eth1",
|
||||
monitored: false
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interfaces = Snmp.list_monitored_interfaces(device.id)
|
||||
interfaces = Snmp.list_monitored_interfaces(snmp_device.id)
|
||||
assert length(interfaces) == 1
|
||||
assert hd(interfaces).id == monitored.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_interface/1" do
|
||||
test "returns interface by id", %{device: device} do
|
||||
test "returns interface by id", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -182,11 +182,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "update_interface/2" do
|
||||
test "updates interface attributes", %{device: device} do
|
||||
test "updates interface attributes", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0",
|
||||
monitored: false
|
||||
|
|
@ -197,11 +197,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert updated.monitored == true
|
||||
end
|
||||
|
||||
test "returns error for invalid attributes", %{device: device} do
|
||||
test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -213,11 +213,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "list_sensors/1" do
|
||||
test "returns all sensors for a device ordered by type and index", %{device: device} do
|
||||
test "returns all sensors for a device ordered by type and index", %{device: device, snmp_device: snmp_device} do
|
||||
sensor1 =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -227,30 +227,30 @@ defmodule Towerops.SnmpTest do
|
|||
sensor2 =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "power",
|
||||
sensor_index: "2",
|
||||
sensor_oid: "1.2.3.5"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
sensors = Snmp.list_sensors(device.id)
|
||||
sensors = Snmp.list_sensors(snmp_device.id)
|
||||
assert length(sensors) == 2
|
||||
assert sensor1.id in Enum.map(sensors, & &1.id)
|
||||
assert sensor2.id in Enum.map(sensors, & &1.id)
|
||||
end
|
||||
|
||||
test "returns empty list for device with no sensors", %{device: device} do
|
||||
assert Snmp.list_sensors(device.id) == []
|
||||
test "returns empty list for device with no sensors", %{device: device, snmp_device: snmp_device} do
|
||||
assert Snmp.list_sensors(snmp_device.id) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_monitored_sensors/1" do
|
||||
test "returns only monitored sensors", %{device: device} do
|
||||
test "returns only monitored sensors", %{device: device, snmp_device: snmp_device} do
|
||||
monitored =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4",
|
||||
|
|
@ -261,7 +261,7 @@ defmodule Towerops.SnmpTest do
|
|||
_unmonitored =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "power",
|
||||
sensor_index: "2",
|
||||
sensor_oid: "1.2.3.5",
|
||||
|
|
@ -269,18 +269,18 @@ defmodule Towerops.SnmpTest do
|
|||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
sensors = Snmp.list_monitored_sensors(device.id)
|
||||
sensors = Snmp.list_monitored_sensors(snmp_device.id)
|
||||
assert length(sensors) == 1
|
||||
assert hd(sensors).id == monitored.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_sensors_by_type/1" do
|
||||
test "groups sensors by type", %{device: device} do
|
||||
test "groups sensors by type", %{device: device, snmp_device: snmp_device} do
|
||||
_temp1 =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -290,7 +290,7 @@ defmodule Towerops.SnmpTest do
|
|||
_temp2 =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "2",
|
||||
sensor_oid: "1.2.3.5"
|
||||
|
|
@ -300,31 +300,31 @@ defmodule Towerops.SnmpTest do
|
|||
_power =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "power",
|
||||
sensor_index: "3",
|
||||
sensor_oid: "1.2.3.6"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
grouped = Snmp.list_sensors_by_type(device.id)
|
||||
grouped = Snmp.list_sensors_by_type(snmp_device.id)
|
||||
assert Map.has_key?(grouped, "temperature")
|
||||
assert Map.has_key?(grouped, "power")
|
||||
assert length(grouped["temperature"]) == 2
|
||||
assert length(grouped["power"]) == 1
|
||||
end
|
||||
|
||||
test "returns empty map for device with no sensors", %{device: device} do
|
||||
assert Snmp.list_sensors_by_type(device.id) == %{}
|
||||
test "returns empty map for device with no sensors", %{device: device, snmp_device: snmp_device} do
|
||||
assert Snmp.list_sensors_by_type(snmp_device.id) == %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_sensor/1" do
|
||||
test "returns sensor by id", %{device: device} do
|
||||
test "returns sensor by id", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -342,11 +342,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "update_sensor/2" do
|
||||
test "updates sensor attributes", %{device: device} do
|
||||
test "updates sensor attributes", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4",
|
||||
|
|
@ -358,11 +358,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert updated.monitored == true
|
||||
end
|
||||
|
||||
test "returns error for invalid attributes", %{device: device} do
|
||||
test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -375,11 +375,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "get_sensor_readings/2" do
|
||||
test "returns recent readings for a sensor", %{device: device} do
|
||||
test "returns recent readings for a sensor", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -401,11 +401,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert length(readings) == 5
|
||||
end
|
||||
|
||||
test "respects limit option", %{device: device} do
|
||||
test "respects limit option", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -427,11 +427,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert length(readings) == 3
|
||||
end
|
||||
|
||||
test "respects since option", %{device: device} do
|
||||
test "respects since option", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -465,11 +465,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert hd(readings).value == 25.0
|
||||
end
|
||||
|
||||
test "returns empty list for sensor with no readings", %{device: device} do
|
||||
test "returns empty list for sensor with no readings", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -481,11 +481,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "get_latest_sensor_reading/1" do
|
||||
test "returns most recent reading", %{device: device} do
|
||||
test "returns most recent reading", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -517,11 +517,11 @@ defmodule Towerops.SnmpTest do
|
|||
refute latest.id == old_reading.id
|
||||
end
|
||||
|
||||
test "returns nil for sensor with no readings", %{device: device} do
|
||||
test "returns nil for sensor with no readings", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -533,11 +533,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "get_interface_stats/2" do
|
||||
test "returns recent stats for an interface", %{device: device} do
|
||||
test "returns recent stats for an interface", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -558,11 +558,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert length(stats) == 5
|
||||
end
|
||||
|
||||
test "respects limit option", %{device: device} do
|
||||
test "respects limit option", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -583,11 +583,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert length(stats) == 3
|
||||
end
|
||||
|
||||
test "respects since option", %{device: device} do
|
||||
test "respects since option", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -620,11 +620,11 @@ defmodule Towerops.SnmpTest do
|
|||
assert hd(stats).if_in_octets == 5000
|
||||
end
|
||||
|
||||
test "returns empty list for interface with no stats", %{device: device} do
|
||||
test "returns empty list for interface with no stats", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -635,11 +635,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "get_latest_interface_stat/1" do
|
||||
test "returns most recent stat", %{device: device} do
|
||||
test "returns most recent stat", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -670,11 +670,11 @@ defmodule Towerops.SnmpTest do
|
|||
refute latest.id == old_stat.id
|
||||
end
|
||||
|
||||
test "returns nil for interface with no stats", %{device: device} do
|
||||
test "returns nil for interface with no stats", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
@ -685,11 +685,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "create_sensor_reading/1" do
|
||||
test "creates a sensor reading with valid attributes", %{device: device} do
|
||||
test "creates a sensor reading with valid attributes", %{device: device, snmp_device: snmp_device} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.2.3.4"
|
||||
|
|
@ -716,11 +716,11 @@ defmodule Towerops.SnmpTest do
|
|||
end
|
||||
|
||||
describe "create_interface_stat/1" do
|
||||
test "creates an interface stat with valid attributes", %{device: device} do
|
||||
test "creates an interface stat with valid attributes", %{device: device, snmp_device: snmp_device} do
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: device.id,
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0"
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, equipment: equipment}
|
||||
%{organization: organization, site: site, device: device}
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
|
|
@ -38,57 +38,57 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
assert html =~ "No alerts"
|
||||
end
|
||||
|
||||
test "lists all alerts", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "lists all alerts", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert html =~ "Test Router"
|
||||
assert html =~ "Equipment is down"
|
||||
assert html =~ "Equipment Down"
|
||||
assert html =~ "Device is down"
|
||||
assert html =~ "Device Down"
|
||||
end
|
||||
|
||||
test "filters active alerts", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "filters active alerts", %{conn: conn, organization: organization, device: device} do
|
||||
# Create an active equipment_down alert
|
||||
{:ok, _active_alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
# Create a recovery alert (equipment_up)
|
||||
# Create a recovery alert (device_up)
|
||||
{:ok, _recovery_alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
device_id: device.id,
|
||||
alert_type: :device_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
message: "Device recovered"
|
||||
})
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/alerts?filter=active")
|
||||
|
||||
# Should show equipment_down alert
|
||||
assert html =~ "Equipment is down"
|
||||
assert html =~ "Device is down"
|
||||
# Should NOT show equipment_up alert in active filter
|
||||
refute html =~ "Equipment recovered"
|
||||
refute html =~ "Device recovered"
|
||||
end
|
||||
|
||||
test "acknowledges an alert", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "acknowledges an alert", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
|
@ -101,36 +101,36 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
assert html =~ "Alert acknowledged"
|
||||
end
|
||||
|
||||
test "does not show acknowledge button for equipment_up alerts", %{
|
||||
test "does not show acknowledge button for device_up alerts", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
device_id: device.id,
|
||||
alert_type: :device_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
message: "Device recovered"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
# Should not show acknowledge button for equipment_up alerts
|
||||
# Should not show acknowledge button for device_up alerts
|
||||
refute html =~ "Acknowledge"
|
||||
end
|
||||
|
||||
test "does not show acknowledge button for resolved alerts", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
# Resolve the alert
|
||||
|
|
@ -145,7 +145,7 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
test "updates in real-time when new alert is created", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
{:new_alert, device.id, :device_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
|
|
@ -163,14 +163,14 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
test "updates in real-time when alert is resolved", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
|
@ -181,7 +181,7 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
|
|||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, equipment.id, :equipment_down}
|
||||
{:alert_resolved, device.id, :device_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
|
||||
assert html =~ organization.name
|
||||
assert html =~ "Sites"
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ "Device"
|
||||
assert html =~ "Active Alerts"
|
||||
end
|
||||
|
||||
|
|
@ -39,37 +39,37 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
assert html =~ "Sites"
|
||||
end
|
||||
|
||||
test "displays correct equipment counts", %{conn: conn, organization: organization, site: site} do
|
||||
# Create equipment with different statuses
|
||||
{:ok, equipment_up} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
test "displays correct device counts", %{conn: conn, organization: organization, site: site} do
|
||||
# Create devices with different statuses
|
||||
{:ok, device_up} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
Towerops.Equipment.update_equipment_status(equipment_up, :up)
|
||||
Towerops.Devices.update_device_status(device_up, :up)
|
||||
|
||||
{:ok, equipment_down} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device_down} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
Towerops.Equipment.update_equipment_status(equipment_down, :down)
|
||||
Towerops.Devices.update_device_status(device_down, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ "Device"
|
||||
assert html =~ "Up"
|
||||
assert html =~ "Down"
|
||||
end
|
||||
|
||||
test "displays active alerts count", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -78,10 +78,10 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
# Create an active alert
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
|
@ -93,8 +93,8 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -106,7 +106,7 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
{:new_alert, device.id, :device_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
|
|
@ -118,7 +118,7 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
|
||||
# Check for navigation links
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/sites"
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/equipment"
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/devices"
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/alerts"
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
defmodule ToweropsWeb.EquipmentLive.FormTest do
|
||||
defmodule ToweropsWeb.DeviceLive.FormTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Mox
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
|
@ -26,7 +25,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "New Equipment Form" do
|
||||
describe "New Device Form" do
|
||||
setup %{organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -37,11 +36,11 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
%{site: site}
|
||||
end
|
||||
|
||||
test "renders new equipment form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
test "renders new device form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
assert html =~ "Add new equipment to monitor"
|
||||
assert html =~ "New Device"
|
||||
assert html =~ "Add new device to monitor"
|
||||
end
|
||||
|
||||
test "redirects to sites page when no sites exist", %{conn: conn, user: user} do
|
||||
|
|
@ -50,25 +49,25 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
|
||||
{:ok, _view, html} =
|
||||
conn
|
||||
|> live(~p"/orgs/#{empty_org.slug}/equipment/new")
|
||||
|> live(~p"/orgs/#{empty_org.slug}/devices/new")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{empty_org.slug}/sites/new")
|
||||
|
||||
assert html =~ "Please create a site before adding equipment"
|
||||
assert html =~ "Please create a site before adding a device"
|
||||
end
|
||||
|
||||
test "validates form inputs", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{name: "", ip_address: ""})
|
||||
|> form("#device-form", device: %{name: "", ip_address: ""})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "equipment-form"
|
||||
assert html =~ "device-form"
|
||||
end
|
||||
|
||||
test "auto-selects single site", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# The form should have the site pre-selected
|
||||
assert html =~ site.name
|
||||
|
|
@ -82,7 +81,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
})
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/new?site_id=#{site2.id}")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/new?site_id=#{site2.id}")
|
||||
|
||||
# Should use the query param site
|
||||
assert html =~ site2.name
|
||||
|
|
@ -99,7 +98,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Should show both sites in the form
|
||||
assert html =~ "Test Site"
|
||||
|
|
@ -107,7 +106,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "Edit Equipment Form" do
|
||||
describe "Edit Device Form" do
|
||||
setup %{organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -115,52 +114,52 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{site: site, equipment: equipment}
|
||||
%{site: site, device: device}
|
||||
end
|
||||
|
||||
test "renders edit form", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "renders edit form", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
assert html =~ "Edit Equipment"
|
||||
assert html =~ equipment.name
|
||||
assert html =~ "Edit Device"
|
||||
assert html =~ device.name
|
||||
end
|
||||
|
||||
test "validates edited form inputs", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{name: ""})
|
||||
|> form("#device-form", device: %{name: ""})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "equipment-form"
|
||||
assert html =~ "device-form"
|
||||
end
|
||||
|
||||
test "handles save error on edit", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Submit with invalid data
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{ip_address: "invalid-ip"})
|
||||
|> form("#device-form", device: %{ip_address: "invalid-ip"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "must be a valid IPv4 or IPv6 address"
|
||||
|
|
@ -169,24 +168,24 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "loads current agent assignment if exists", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Create an agent token
|
||||
{:ok, agent_token, _token} =
|
||||
Towerops.Agents.create_agent_token(organization.id, "Test Agent")
|
||||
|
||||
# Assign the agent to the equipment
|
||||
Towerops.Agents.update_equipment_assignment(equipment.id, agent_token.id)
|
||||
# Assign the agent to the device
|
||||
Towerops.Agents.update_device_assignment(device.id, agent_token.id)
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Should show the form successfully
|
||||
assert html =~ "Edit Equipment"
|
||||
assert html =~ "Edit Device"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Delete Equipment" do
|
||||
describe "Delete Device" do
|
||||
setup %{organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
|
|
@ -194,31 +193,31 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{site: site, equipment: equipment}
|
||||
%{site: site, device: device}
|
||||
end
|
||||
|
||||
test "deletes equipment successfully", %{
|
||||
test "deletes device successfully", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> element("button", "Delete Equipment")
|
||||
|> element("button", "Delete Device")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert html =~ "Equipment deleted successfully"
|
||||
assert html =~ "Device deleted successfully"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -234,7 +233,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
end
|
||||
|
||||
test "tests SNMP connection successfully", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Mock successful SNMP test
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts ->
|
||||
|
|
@ -243,8 +242,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
|
||||
# Fill in form with valid data
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
|
|
@ -257,11 +256,11 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
html = render_click(view, "test_snmp")
|
||||
|
||||
# Should show success message in test result
|
||||
assert html =~ "test-result" or html =~ "Equipment"
|
||||
assert html =~ "test-result" or html =~ "Device"
|
||||
end
|
||||
|
||||
test "tests SNMP connection with failure", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Mock failed SNMP test
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts ->
|
||||
|
|
@ -270,8 +269,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
|
||||
# Fill in form
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
|
|
@ -288,7 +287,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
end
|
||||
|
||||
test "validates SNMP test requires IP address", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Test without IP
|
||||
html = render_click(view, "test_snmp")
|
||||
|
|
@ -301,11 +300,11 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Set IP but not community
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{ip_address: "192.168.1.1"})
|
||||
|> form("#device-form", device: %{ip_address: "192.168.1.1"})
|
||||
|> render_change()
|
||||
|
||||
html = render_click(view, "test_snmp")
|
||||
|
|
@ -318,12 +317,12 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Set IP with empty community
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{ip_address: "192.168.1.1", snmp_community: ""}
|
||||
|> form("#device-form",
|
||||
device: %{ip_address: "192.168.1.1", snmp_community: ""}
|
||||
)
|
||||
|> render_change()
|
||||
|
||||
|
|
@ -337,12 +336,12 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Set invalid IP
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{ip_address: "invalid", snmp_community: "public"}
|
||||
|> form("#device-form",
|
||||
device: %{ip_address: "invalid", snmp_community: "public"}
|
||||
)
|
||||
|> render_change()
|
||||
|
||||
|
|
@ -353,7 +352,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
end
|
||||
|
||||
test "normalizes port numbers in SNMP test", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Mock SNMP test
|
||||
expect(SnmpMock, :get, fn _target, _oid, opts ->
|
||||
|
|
@ -364,8 +363,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
|
||||
# Set string port
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
|
|
@ -378,7 +377,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
end
|
||||
|
||||
test "handles invalid port in SNMP test", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
# Mock SNMP test
|
||||
expect(SnmpMock, :get, fn _target, _oid, opts ->
|
||||
|
|
@ -389,8 +388,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
|
||||
# Set invalid port
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
|
|
@ -411,8 +410,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -421,29 +420,29 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
snmp_version: "2c"
|
||||
})
|
||||
|
||||
%{site: site, equipment: equipment}
|
||||
%{site: site, device: device}
|
||||
end
|
||||
|
||||
test "triggers discovery for SNMP-enabled equipment", %{
|
||||
test "triggers discovery for SNMP-enabled devices", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
html = render_click(view, "trigger_discovery")
|
||||
|
||||
assert html =~ "Discovery started"
|
||||
end
|
||||
|
||||
test "rejects discovery for non-SNMP equipment", %{
|
||||
test "rejects discovery for non-SNMP device", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, non_snmp_equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, non_snmp_device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -451,7 +450,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
})
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{non_snmp_equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{non_snmp_device.id}/edit")
|
||||
|
||||
html = render_click(view, "trigger_discovery")
|
||||
|
||||
|
|
@ -467,8 +466,8 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
|
|
@ -478,7 +477,7 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
%{site: site, equipment: equipment}
|
||||
%{site: site, device: device}
|
||||
end
|
||||
|
||||
test "triggers discovery when enabling SNMP", %{
|
||||
|
|
@ -486,9 +485,9 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
# Create equipment without SNMP
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
# Create device without SNMP
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
|
|
@ -496,25 +495,25 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
})
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# First, toggle SNMP on via change event to reveal fields
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{snmp_enabled: true})
|
||||
|> form("#device-form", device: %{snmp_enabled: true})
|
||||
|> render_change()
|
||||
|
||||
# Now submit with SNMP settings
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
snmp_enabled: true,
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "SNMP discovery started in background"
|
||||
end
|
||||
|
|
@ -522,17 +521,17 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "triggers discovery when changing SNMP community", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Change community
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{snmp_community: "private"})
|
||||
|> form("#device-form", device: %{snmp_community: "private"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "SNMP discovery started in background"
|
||||
end
|
||||
|
|
@ -540,17 +539,17 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "triggers discovery when changing SNMP version", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Change version
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{snmp_version: "1"})
|
||||
|> form("#device-form", device: %{snmp_version: "1"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "SNMP discovery started in background"
|
||||
end
|
||||
|
|
@ -558,17 +557,17 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "triggers discovery when changing SNMP port", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Change port
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{snmp_port: 162})
|
||||
|> form("#device-form", device: %{snmp_port: 162})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "SNMP discovery started in background"
|
||||
end
|
||||
|
|
@ -576,17 +575,17 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "triggers discovery when changing IP address", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Change IP
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{ip_address: "192.168.1.100"})
|
||||
|> form("#device-form", device: %{ip_address: "192.168.1.100"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "SNMP discovery started in background"
|
||||
end
|
||||
|
|
@ -594,21 +593,21 @@ defmodule ToweropsWeb.EquipmentLive.FormTest do
|
|||
test "does not trigger discovery for unrelated changes", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
# Change only name
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{name: "Updated Router"})
|
||||
|> form("#device-form", device: %{name: "Updated Router"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
# Should not mention discovery
|
||||
refute html =~ "SNMP discovery started in background"
|
||||
assert html =~ "Equipment updated successfully"
|
||||
assert html =~ "Device updated successfully"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
||||
defmodule ToweropsWeb.DeviceLive.ShowTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Sites
|
||||
|
|
@ -19,78 +18,78 @@ defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, equipment: equipment}
|
||||
%{organization: organization, site: site, device: device}
|
||||
end
|
||||
|
||||
describe "Show" do
|
||||
test "displays equipment information", %{conn: conn, equipment: equipment, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
test "displays device information", %{conn: conn, device: device, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ equipment.ip_address
|
||||
assert html =~ device.name
|
||||
assert html =~ device.ip_address
|
||||
end
|
||||
|
||||
test "displays overview tab by default", %{conn: conn, equipment: equipment, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
test "displays overview tab by default", %{conn: conn, device: device, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "Overview"
|
||||
end
|
||||
|
||||
test "displays metrics when equipment has checks", %{
|
||||
test "displays metrics when device has checks", %{
|
||||
conn: conn,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: org
|
||||
} do
|
||||
# Create some checks
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 10,
|
||||
checked_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 15,
|
||||
checked_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "Device Information"
|
||||
end
|
||||
|
||||
test "displays empty state when no checks exist", %{
|
||||
conn: conn,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: org
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
end
|
||||
|
||||
test "refreshes data periodically", %{conn: conn, equipment: equipment, organization: org} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
test "refreshes data periodically", %{conn: conn, device: device, organization: org} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
# Trigger a status change
|
||||
Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :failure,
|
||||
response_time_ms: nil,
|
||||
checked_at: DateTime.utc_now()
|
||||
|
|
@ -106,9 +105,9 @@ defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
|||
assert render(view)
|
||||
end
|
||||
|
||||
test "requires authentication", %{equipment: equipment, organization: org} do
|
||||
test "requires authentication", %{device: device, organization: org} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
|
|
@ -116,13 +115,13 @@ defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
|||
|
||||
test "handles equipment_status_changed event", %{
|
||||
conn: conn,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: org
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
# Simulate status change event
|
||||
send(view.pid, {:equipment_status_changed, equipment.id, :up, 25})
|
||||
send(view.pid, {:device_status_changed, device.id, :up, 25})
|
||||
|
||||
# Give it time to process
|
||||
:timer.sleep(100)
|
||||
|
|
@ -133,13 +132,13 @@ defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
|||
|
||||
test "handles discovery_completed event", %{
|
||||
conn: conn,
|
||||
equipment: equipment,
|
||||
device: device,
|
||||
organization: org
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
|
||||
|
||||
# Simulate discovery completed event
|
||||
send(view.pid, {:discovery_completed, equipment.id})
|
||||
send(view.pid, {:discovery_completed, device.id})
|
||||
|
||||
# Give it time to process
|
||||
:timer.sleep(100)
|
||||
|
|
@ -148,10 +147,10 @@ defmodule ToweropsWeb.EquipmentLive.ShowTest do
|
|||
assert html =~ "Discovery completed"
|
||||
end
|
||||
|
||||
test "switches to different tabs", %{conn: conn, equipment: equipment, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/equipment/#{equipment.id}?tab=events")
|
||||
test "switches to different tabs", %{conn: conn, device: device, organization: org} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}?tab=events")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule ToweropsWeb.EquipmentLiveTest do
|
||||
defmodule ToweropsWeb.DeviceLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Mox
|
||||
|
|
@ -35,37 +35,37 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
end
|
||||
|
||||
describe "Index" do
|
||||
test "lists all equipment", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
test "lists all devices", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ equipment.name
|
||||
assert html =~ "Device"
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "displays empty state when no equipment", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
test "displays empty state when no device", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert html =~ "No equipment"
|
||||
assert html =~ "No devices"
|
||||
end
|
||||
|
||||
test "has link to add new equipment", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
test "has link to add new device", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
assert html =~ "New Device"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
|
|
@ -74,8 +74,8 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
|
||||
describe "Show" do
|
||||
setup %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
|
|
@ -84,64 +84,64 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
check_interval_seconds: 300
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
test "displays equipment details", %{
|
||||
test "displays device details", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
assert html =~ "Device Information"
|
||||
end
|
||||
|
||||
test "displays equipment with tab parameter", %{
|
||||
test "displays device with tab parameter", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}?tab=interfaces")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}?tab=interfaces")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "shows equipment status when monitoring enabled", %{
|
||||
test "shows device status when monitoring enabled", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Create a monitoring check
|
||||
{:ok, _check} =
|
||||
Towerops.Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
status: :success,
|
||||
response_time_ms: 25,
|
||||
checked_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
# Should display monitoring information
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "shows SNMP device information when available", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Create SNMP device record using Repo
|
||||
_device =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
device_id: device.id,
|
||||
sys_descr: "Test Device",
|
||||
sys_name: "test-device",
|
||||
manufacturer: "Test Manufacturer",
|
||||
|
|
@ -149,22 +149,22 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
})
|
||||
|> Towerops.Repo.insert!()
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
# Verify the page loads successfully with equipment data
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "displays recent events", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
device: device
|
||||
} do
|
||||
# Create an event with all required fields (use valid event_type)
|
||||
{:ok, _event} =
|
||||
Towerops.Equipment.create_event(%{
|
||||
equipment_id: equipment.id,
|
||||
Towerops.Devices.create_event(%{
|
||||
device_id: device.id,
|
||||
event_type: "device_discovered",
|
||||
severity: "info",
|
||||
message: "Device was discovered",
|
||||
|
|
@ -172,36 +172,36 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
occurred_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
# Verify page loads with events section
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
end
|
||||
|
||||
test "handles missing equipment gracefully", %{conn: conn, organization: organization} do
|
||||
test "handles missing device gracefully", %{conn: conn, organization: organization} do
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{fake_id}")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{fake_id}")
|
||||
end
|
||||
end
|
||||
|
||||
test "deletes equipment", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "deletes device", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> element("button", "Delete Equipment")
|
||||
|> element("button", "Delete Device")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices")
|
||||
|
||||
assert html =~ "Equipment deleted successfully"
|
||||
assert html =~ "Device deleted successfully"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, equipment: equipment} do
|
||||
test "requires authentication", %{organization: organization, device: device} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
|
|
@ -209,19 +209,19 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
end
|
||||
|
||||
describe "New" do
|
||||
test "renders new equipment form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
test "renders new device form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
assert html =~ "Add new equipment to monitor"
|
||||
assert html =~ "New Device"
|
||||
assert html =~ "Add new device to monitor"
|
||||
end
|
||||
|
||||
test "creates new equipment", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
test "creates new device", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
name: "New Router",
|
||||
ip_address: "192.168.1.100",
|
||||
site_id: site.id,
|
||||
|
|
@ -237,32 +237,32 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
|> render_submit()
|
||||
|
||||
# Verify equipment was created
|
||||
equipment_list = Towerops.Equipment.list_organization_equipment(organization.id)
|
||||
refute Enum.empty?(equipment_list)
|
||||
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
||||
refute Enum.empty?(device_list)
|
||||
|
||||
new_equipment = Enum.find(equipment_list, &(&1.name == "New Router"))
|
||||
assert new_equipment
|
||||
assert new_equipment.ip_address == "192.168.1.100"
|
||||
new_device = Enum.find(device_list, &(&1.name == "New Router"))
|
||||
assert new_device
|
||||
assert new_device.ip_address == "192.168.1.100"
|
||||
end
|
||||
|
||||
test "validates required fields", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{name: "", ip_address: ""})
|
||||
|> form("#device-form", device: %{name: "", ip_address: ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
end
|
||||
|
||||
test "validates IP address format", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
name: "Router",
|
||||
ip_address: "invalid-ip",
|
||||
site_id: site.id
|
||||
|
|
@ -279,14 +279,14 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
site: site
|
||||
} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/new?site_id=#{site.id}")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/new?site_id=#{site.id}")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
assert html =~ "New Device"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
|
|
@ -295,48 +295,48 @@ defmodule ToweropsWeb.EquipmentLiveTest do
|
|||
|
||||
describe "Edit" do
|
||||
setup %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
%{device: device}
|
||||
end
|
||||
|
||||
test "renders edit form", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "renders edit form", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
assert html =~ "Edit Equipment"
|
||||
assert html =~ "Update equipment details"
|
||||
assert html =~ "Edit Device"
|
||||
assert html =~ "Update device details"
|
||||
end
|
||||
|
||||
test "updates equipment", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
test "updates device", %{conn: conn, organization: organization, device: device} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
|> form("#device-form",
|
||||
device: %{
|
||||
name: "Updated Router",
|
||||
ip_address: "192.168.1.2"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
||||
|
||||
assert html =~ "Equipment updated successfully"
|
||||
assert html =~ "Device updated successfully"
|
||||
assert html =~ "Updated Router"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, equipment: equipment} do
|
||||
test "requires authentication", %{organization: organization, device: device} do
|
||||
conn = build_conn()
|
||||
|
||||
{:error, redirect} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
|
|
@ -69,8 +69,8 @@ defmodule ToweropsWeb.SiteLiveTest do
|
|||
end
|
||||
|
||||
test "displays equipment at site", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
|
|
@ -78,7 +78,7 @@ defmodule ToweropsWeb.SiteLiveTest do
|
|||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ device.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@ defmodule ToweropsWeb.UserAuthTest do
|
|||
refute get_session(conn, :superuser_id)
|
||||
refute get_session(conn, :target_user_id)
|
||||
assert conn.assigns.current_scope.user.id == superuser.id
|
||||
assert redirected_to(conn) == ~p"/orgs/#{organization.slug}/equipment"
|
||||
assert redirected_to(conn) == ~p"/orgs/#{organization.slug}/devices"
|
||||
end
|
||||
|
||||
test "redirects to /orgs when superuser has no organizations", %{conn: conn} do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue