From a810e75fc4eef49a0aa154528646c6bc00ce2e74 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 17 Jan 2026 14:48:46 -0600 Subject: [PATCH] rename equipment to device --- lib/towerops/admin.ex | 2 +- lib/towerops/agents.ex | 128 +++---- lib/towerops/agents/agent_assignment.ex | 22 +- lib/towerops/agents/stats.ex | 78 ++-- lib/towerops/alerts.ex | 40 +- lib/towerops/alerts/alert.ex | 18 +- lib/towerops/alerts/alert_notifier.ex | 46 +-- lib/towerops/application.ex | 2 +- lib/towerops/devices.ex | 300 +++++++++++++++ .../equipment.ex => devices/device.ex} | 14 +- lib/towerops/{equipment => devices}/event.ex | 14 +- .../{equipment => devices}/event_logger.ex | 16 +- lib/towerops/equipment.ex | 300 --------------- lib/towerops/monitoring.ex | 36 +- lib/towerops/monitoring/check.ex | 8 +- lib/towerops/monitoring/device_monitor.ex | 221 +++++++++++ lib/towerops/monitoring/equipment_monitor.ex | 221 ----------- lib/towerops/monitoring/ping.ex | 2 +- lib/towerops/monitoring/supervisor.ex | 42 +-- lib/towerops/organizations.ex | 40 +- lib/towerops/organizations/organization.ex | 2 +- lib/towerops/organizations/policy.ex | 4 +- lib/towerops/proto/agent.pb.ex | 14 +- lib/towerops/sites.ex | 36 +- lib/towerops/sites/site.ex | 6 +- lib/towerops/snmp.ex | 74 ++-- lib/towerops/snmp/device.ex | 16 +- lib/towerops/snmp/discovery.ex | 92 ++--- lib/towerops/snmp/neighbor.ex | 10 +- lib/towerops/snmp/neighbor_cleanup_worker.ex | 12 +- lib/towerops/snmp/neighbor_discovery.ex | 4 +- lib/towerops/snmp/poller.ex | 12 +- lib/towerops/snmp/poller_worker.ex | 254 ++++++------- lib/towerops_web/channels/agent_channel.ex | 88 ++--- lib/towerops_web/components/layouts.ex | 8 +- .../controllers/api/mobile_controller.ex | 92 ++--- .../controllers/page_html/home.html.heex | 12 +- .../live/admin/org_live/index.html.heex | 2 +- lib/towerops_web/live/agent_live/index.ex | 12 +- .../live/agent_live/index.html.heex | 8 +- lib/towerops_web/live/agent_live/show.ex | 14 +- .../live/agent_live/show.html.heex | 26 +- lib/towerops_web/live/alert_live/index.ex | 8 +- .../live/alert_live/index.html.heex | 28 +- lib/towerops_web/live/dashboard_live.ex | 20 +- .../live/dashboard_live.html.heex | 32 +- .../{equipment_live => device_live}/form.ex | 152 ++++---- .../form.html.heex | 32 +- .../{equipment_live => device_live}/index.ex | 10 +- .../index.html.heex | 28 +- .../{equipment_live => device_live}/show.ex | 54 +-- .../show.html.heex | 60 ++- lib/towerops_web/live/graph_live/show.ex | 30 +- .../live/graph_live/show.html.heex | 6 +- lib/towerops_web/live/org/settings_live.ex | 8 +- .../live/org/settings_live.html.heex | 29 +- lib/towerops_web/live/org_live/new.html.heex | 2 +- lib/towerops_web/live/site_live/form.ex | 4 +- .../live/site_live/form.html.heex | 34 +- lib/towerops_web/live/site_live/show.ex | 6 +- .../live/site_live/show.html.heex | 18 +- lib/towerops_web/router.ex | 16 +- lib/towerops_web/user_auth.ex | 8 +- priv/proto/agent.proto | 12 +- ...224202206_add_monitoring_enabled_index.exs | 2 +- ...dd_composite_indexes_for_agent_queries.exs | 2 +- ...0117190134_rename_equipment_to_devices.exs | 108 ++++++ test/integration/snmp_integration_test.exs | 20 +- test/support/fixtures/agents_fixtures.ex | 4 +- .../support/fixtures/agents_fixtures_test.exs | 8 +- test/towerops/agents/stats_test.exs | 84 ++--- test/towerops/agents_test.exs | 342 +++++++++--------- test/towerops/alerts/alert_notifier_test.exs | 66 ++-- test/towerops/alerts_test.exs | 72 ++-- test/towerops/devices_test.exs | 296 +++++++++++++++ test/towerops/equipment/event_logger_test.exs | 43 ++- test/towerops/equipment_test.exs | 295 --------------- .../monitoring/device_monitor_test.exs | 207 +++++++++++ .../monitoring/equipment_monitor_test.exs | 208 ----------- test/towerops/monitoring/supervisor_test.exs | 102 +++--- test/towerops/monitoring_test.exs | 90 ++--- test/towerops/organizations/policy_test.exs | 20 +- test/towerops/proto/agent_pb_test.exs | 58 +-- test/towerops/snmp/discovery_test.exs | 68 ++-- .../snmp/neighbor_cleanup_worker_test.exs | 69 ++-- .../towerops/snmp/neighbor_discovery_test.exs | 143 ++++---- test/towerops/snmp/poller_test.exs | 22 +- test/towerops/snmp_test.exs | 160 ++++---- test/towerops_web/live/alert_live_test.exs | 80 ++-- .../towerops_web/live/dashboard_live_test.exs | 38 +- .../form_test.exs | 217 ++++++----- .../show_test.exs | 67 ++-- ...ent_live_test.exs => device_live_test.exs} | 168 ++++----- test/towerops_web/live/site_live_test.exs | 6 +- test/towerops_web/user_auth_test.exs | 2 +- 95 files changed, 3062 insertions(+), 2950 deletions(-) create mode 100644 lib/towerops/devices.ex rename lib/towerops/{equipment/equipment.ex => devices/device.ex} (93%) rename lib/towerops/{equipment => devices}/event.ex (77%) rename lib/towerops/{equipment => devices}/event_logger.ex (65%) delete mode 100644 lib/towerops/equipment.ex create mode 100644 lib/towerops/monitoring/device_monitor.ex delete mode 100644 lib/towerops/monitoring/equipment_monitor.ex rename lib/towerops_web/live/{equipment_live => device_live}/form.ex (63%) rename lib/towerops_web/live/{equipment_live => device_live}/form.html.heex (86%) rename lib/towerops_web/live/{equipment_live => device_live}/index.ex (70%) rename lib/towerops_web/live/{equipment_live => device_live}/index.html.heex (79%) rename lib/towerops_web/live/{equipment_live => device_live}/show.ex (87%) rename lib/towerops_web/live/{equipment_live => device_live}/show.html.heex (94%) create mode 100644 priv/repo/migrations/20260117190134_rename_equipment_to_devices.exs create mode 100644 test/towerops/devices_test.exs delete mode 100644 test/towerops/equipment_test.exs create mode 100644 test/towerops/monitoring/device_monitor_test.exs delete mode 100644 test/towerops/monitoring/equipment_monitor_test.exs rename test/towerops_web/live/{equipment_live => device_live_nested}/form_test.exs (69%) rename test/towerops_web/live/{equipment_live => device_live_nested}/show_test.exs (52%) rename test/towerops_web/live/{equipment_live_test.exs => device_live_test.exs} (63%) diff --git a/lib/towerops/admin.ex b/lib/towerops/admin.ex index 6334da97..5f982e23 100644 --- a/lib/towerops/admin.ex +++ b/lib/towerops/admin.ex @@ -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 diff --git a/lib/towerops/agents.ex b/lib/towerops/agents.ex index 0d6bd9d5..7ad6579d 100644 --- a/lib/towerops/agents.ex +++ b/lib/towerops/agents.ex @@ -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) -> diff --git a/lib/towerops/agents/agent_assignment.ex b/lib/towerops/agents/agent_assignment.ex index 6dbac746..8e23e89d 100644 --- a/lib/towerops/agents/agent_assignment.ex +++ b/lib/towerops/agents/agent_assignment.ex @@ -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 diff --git a/lib/towerops/agents/stats.ex b/lib/towerops/agents/stats.ex index 39874613..951a4d0f 100644 --- a/lib/towerops/agents/stats.ex +++ b/lib/towerops/agents/stats.ex @@ -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)} diff --git a/lib/towerops/alerts.ex b/lib/towerops/alerts.ex index 23e573a1..5fb23146 100644 --- a/lib/towerops/alerts.ex +++ b/lib/towerops/alerts.ex @@ -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], diff --git a/lib/towerops/alerts/alert.ex b/lib/towerops/alerts/alert.ex index bc9d4365..fcf0f4d1 100644 --- a/lib/towerops/alerts/alert.ex +++ b/lib/towerops/alerts/alert.ex @@ -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 diff --git a/lib/towerops/alerts/alert_notifier.ex b/lib/towerops/alerts/alert_notifier.ex index 3441fca9..f3a904be 100644 --- a/lib/towerops/alerts/alert_notifier.ex +++ b/lib/towerops/alerts/alert_notifier.ex @@ -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. ============================== """ diff --git a/lib/towerops/application.ex b/lib/towerops/application.ex index e57934c9..8b59bb4f 100644 --- a/lib/towerops/application.ex +++ b/lib/towerops/application.ex @@ -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) diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex new file mode 100644 index 00000000..0b7b72ba --- /dev/null +++ b/lib/towerops/devices.ex @@ -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 diff --git a/lib/towerops/equipment/equipment.ex b/lib/towerops/devices/device.ex similarity index 93% rename from lib/towerops/equipment/equipment.ex rename to lib/towerops/devices/device.ex index e32896fd..2c1ff57a 100644 --- a/lib/towerops/equipment/equipment.ex +++ b/lib/towerops/devices/device.ex @@ -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, diff --git a/lib/towerops/equipment/event.ex b/lib/towerops/devices/event.ex similarity index 77% rename from lib/towerops/equipment/event.ex rename to lib/towerops/devices/event.ex index 9ec09846..42049db9 100644 --- a/lib/towerops/equipment/event.ex +++ b/lib/towerops/devices/event.ex @@ -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 diff --git a/lib/towerops/equipment/event_logger.ex b/lib/towerops/devices/event_logger.ex similarity index 65% rename from lib/towerops/equipment/event_logger.ex rename to lib/towerops/devices/event_logger.ex index de460daf..18441d0a 100644 --- a/lib/towerops/equipment/event_logger.ex +++ b/lib/towerops/devices/event_logger.ex @@ -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}") diff --git a/lib/towerops/equipment.ex b/lib/towerops/equipment.ex deleted file mode 100644 index 7f6a600b..00000000 --- a/lib/towerops/equipment.ex +++ /dev/null @@ -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 diff --git a/lib/towerops/monitoring.ex b/lib/towerops/monitoring.ex index 32cee7b8..eec7fbac 100644 --- a/lib/towerops/monitoring.ex +++ b/lib/towerops/monitoring.ex @@ -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) diff --git a/lib/towerops/monitoring/check.ex b/lib/towerops/monitoring/check.ex index 8bdc9e20..755d6893 100644 --- a/lib/towerops/monitoring/check.ex +++ b/lib/towerops/monitoring/check.ex @@ -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 diff --git a/lib/towerops/monitoring/device_monitor.ex b/lib/towerops/monitoring/device_monitor.ex new file mode 100644 index 00000000..8dbfa78c --- /dev/null +++ b/lib/towerops/monitoring/device_monitor.ex @@ -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 diff --git a/lib/towerops/monitoring/equipment_monitor.ex b/lib/towerops/monitoring/equipment_monitor.ex deleted file mode 100644 index 7999c035..00000000 --- a/lib/towerops/monitoring/equipment_monitor.ex +++ /dev/null @@ -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 diff --git a/lib/towerops/monitoring/ping.ex b/lib/towerops/monitoring/ping.ex index e59c7fda..b4d928a9 100644 --- a/lib/towerops/monitoring/ping.ex +++ b/lib/towerops/monitoring/ping.ex @@ -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 diff --git a/lib/towerops/monitoring/supervisor.ex b/lib/towerops/monitoring/supervisor.ex index 2a17961d..037cfdc6 100644 --- a/lib/towerops/monitoring/supervisor.ex +++ b/lib/towerops/monitoring/supervisor.ex @@ -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 diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index 02fb7e60..42855f9c 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -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) diff --git a/lib/towerops/organizations/organization.ex b/lib/towerops/organizations/organization.ex index 43286ce8..0053f1db 100644 --- a/lib/towerops/organizations/organization.ex +++ b/lib/towerops/organizations/organization.ex @@ -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 diff --git a/lib/towerops/organizations/policy.ex b/lib/towerops/organizations/policy.ex index 1eb1993c..c31b1040 100644 --- a/lib/towerops/organizations/policy.ex +++ b/lib/towerops/organizations/policy.ex @@ -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 diff --git a/lib/towerops/proto/agent.pb.ex b/lib/towerops/proto/agent.pb.ex index 3bc1fb17..5fa95be0 100644 --- a/lib/towerops/proto/agent.pb.ex +++ b/lib/towerops/proto/agent.pb.ex @@ -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 diff --git a/lib/towerops/sites.ex b/lib/towerops/sites.ex index 0cc66739..6c5de14f 100644 --- a/lib/towerops/sites.ex +++ b/lib/towerops/sites.ex @@ -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 diff --git a/lib/towerops/sites/site.ex b/lib/towerops/sites/site.ex index 063b7d5f..48d0ca16 100644 --- a/lib/towerops/sites/site.ex +++ b/lib/towerops/sites/site.ex @@ -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() } diff --git a/lib/towerops/snmp.ex b/lib/towerops/snmp.ex index d955cec7..d7e283ed 100644 --- a/lib/towerops/snmp.ex +++ b/lib/towerops/snmp.ex @@ -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 diff --git a/lib/towerops/snmp/device.ex b/lib/towerops/snmp/device.ex index 0ac78f63..bd61f9e5 100644 --- a/lib/towerops/snmp/device.ex +++ b/lib/towerops/snmp/device.ex @@ -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 diff --git a/lib/towerops/snmp/discovery.ex b/lib/towerops/snmp/discovery.ex index 6ba82830..f1073881 100644 --- a/lib/towerops/snmp/discovery.ex +++ b/lib/towerops/snmp/discovery.ex @@ -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 diff --git a/lib/towerops/snmp/neighbor.ex b/lib/towerops/snmp/neighbor.ex index e8ba58dc..3e5866fb 100644 --- a/lib/towerops/snmp/neighbor.ex +++ b/lib/towerops/snmp/neighbor.ex @@ -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 diff --git a/lib/towerops/snmp/neighbor_cleanup_worker.ex b/lib/towerops/snmp/neighbor_cleanup_worker.ex index 1c54516f..776aed17 100644 --- a/lib/towerops/snmp/neighbor_cleanup_worker.ex +++ b/lib/towerops/snmp/neighbor_cleanup_worker.ex @@ -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 diff --git a/lib/towerops/snmp/neighbor_discovery.ex b/lib/towerops/snmp/neighbor_discovery.ex index c2f96ea5..b68b699a 100644 --- a/lib/towerops/snmp/neighbor_discovery.ex +++ b/lib/towerops/snmp/neighbor_discovery.ex @@ -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]), diff --git a/lib/towerops/snmp/poller.ex b/lib/towerops/snmp/poller.ex index c2af5987..db1a721b 100644 --- a/lib/towerops/snmp/poller.ex +++ b/lib/towerops/snmp/poller.ex @@ -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 diff --git a/lib/towerops/snmp/poller_worker.ex b/lib/towerops/snmp/poller_worker.ex index 207e0f9c..c4fe0329 100644 --- a/lib/towerops/snmp/poller_worker.ex +++ b/lib/towerops/snmp/poller_worker.ex @@ -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 diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index 14be8812..ddb936fc 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -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, diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index 938802a4..d6b85096 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -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 - 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 navigate={~p"/orgs/#{@current_organization.slug}/alerts"} diff --git a/lib/towerops_web/controllers/api/mobile_controller.ex b/lib/towerops_web/controllers/api/mobile_controller.ex index 0b24d1a4..2132857e 100644 --- a/lib/towerops_web/controllers/api/mobile_controller.ex +++ b/lib/towerops_web/controllers/api/mobile_controller.ex @@ -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 diff --git a/lib/towerops_web/controllers/page_html/home.html.heex b/lib/towerops_web/controllers/page_html/home.html.heex index 475bf01b..2d4663f6 100644 --- a/lib/towerops_web/controllers/page_html/home.html.heex +++ b/lib/towerops_web/controllers/page_html/home.html.heex @@ -17,7 +17,7 @@ for network operators.

- 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.

@@ -55,7 +55,7 @@

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.

@@ -67,7 +67,7 @@

Real-time Alerts

- 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.

@@ -99,7 +99,7 @@ Built for multi-site operations.

- 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.

@@ -126,11 +126,11 @@

Site Management

- Organize equipment by physical location + Organize devices by physical location

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.

diff --git a/lib/towerops_web/live/admin/org_live/index.html.heex b/lib/towerops_web/live/admin/org_live/index.html.heex index efec085b..dc498dfa 100644 --- a/lib/towerops_web/live/admin/org_live/index.html.heex +++ b/lib/towerops_web/live/admin/org_live/index.html.heex @@ -15,7 +15,7 @@