towerops/lib/towerops/snmp.ex
2026-06-14 08:27:57 -05:00

442 lines
16 KiB
Elixir

defmodule Towerops.Snmp do
@moduledoc """
The SNMP context.
Provides the public API for SNMP discovery and monitoring functionality.
"""
import Ecto.Query
alias Towerops.Devices.Device, as: DeviceSchema
alias Towerops.Repo
alias Towerops.Snmp.ArpEntries
alias Towerops.Snmp.Client
alias Towerops.Snmp.Device
alias Towerops.Snmp.DiscoveredDevices
alias Towerops.Snmp.Discovery
alias Towerops.Snmp.EntityPhysicals
alias Towerops.Snmp.Interfaces
alias Towerops.Snmp.IpAddresses
alias Towerops.Snmp.MacAddresses
alias Towerops.Snmp.Mempools
alias Towerops.Snmp.Monitoring
alias Towerops.Snmp.Neighbors
alias Towerops.Snmp.PrinterSupplies
alias Towerops.Snmp.Processors
alias Towerops.Snmp.Queries
alias Towerops.Snmp.Sensors
alias Towerops.Snmp.StorageQueries
alias Towerops.Snmp.Topology
alias Towerops.Snmp.Transceivers
alias Towerops.Snmp.Vlans
alias Towerops.Snmp.WirelessClients
require Logger
@doc """
Tests SNMP connectivity to a device.
## Examples
iex> test_connection("192.168.1.1", "public", "2c")
{:ok, "Connection successful"}
iex> test_connection("192.168.1.99", "wrong", "2c")
{:error, :timeout}
"""
@spec test_connection(String.t(), String.t(), String.t(), integer()) :: {:ok, String.t()} | {:error, term()}
def test_connection(ip, community, version, port \\ 161) do
Client.test_connection(
ip: ip,
community: community,
version: version,
port: port
)
end
@doc """
Runs SNMP discovery for a piece of device.
Discovers:
- Device information (manufacturer, model, firmware)
- Network interfaces
- Sensors (temperature, power, fans, etc.)
Updates the database with discovered data.
## Examples
iex> discover_device(device)
{:ok, %Device{}}
"""
@spec discover_device(DeviceSchema.t()) :: {:ok, Device.t()} | {:error, term()}
def discover_device(%DeviceSchema{} = device) do
Discovery.discover_device(device)
end
@doc """
Runs SNMP discovery for all SNMP-enabled devices in an organization.
Returns a summary of successful and failed discoveries.
## Examples
iex> discover_all_for_org(org_id)
{:ok, %{success: 10, failed: 2, errors: [:timeout, :no_response]}}
"""
@spec discover_all_for_org(String.t()) ::
{:ok, %{enqueued: non_neg_integer(), failed: non_neg_integer(), errors: [term()]}}
def discover_all_for_org(org_id) do
Discovery.discover_all(org_id)
end
@doc """
Creates monitoring checks from discovered SNMP entities.
After discovery completes, this function creates Check records for:
- Each sensor (temperature, voltage, power, etc.)
- Each interface (operational status monitoring)
- Each processor (CPU load monitoring)
- Each storage volume (disk usage monitoring)
Checks are automatically enabled and scheduled for execution.
## Examples
iex> create_checks_from_discovery(device, snmp_device)
{:ok, %{sensors: 45, interfaces: 12, processors: 2, storage: 5}}
"""
def create_checks_from_discovery(%DeviceSchema{} = device, %Device{} = snmp_device) do
snmp_device = Repo.preload(snmp_device, [:sensors, :interfaces, :processors, :storage, :mempools])
initial = %{sensors: 0, interfaces: 0, processors: 0, storage: 0, errors: []}
results =
initial
|> create_discovered_checks(device, snmp_device.sensors, :sensor, :sensors, &create_sensor_check/2)
|> create_discovered_checks(device, snmp_device.interfaces, :interface, :interfaces, &create_interface_check/2)
|> create_discovered_checks(device, snmp_device.processors, :processor, :processors, &create_processor_check/2)
|> create_discovered_checks(device, snmp_device.storage, :storage, :storage, &create_storage_check/2)
Logger.info(
"Created checks for device #{device.id}: #{results.sensors} sensors, #{results.interfaces} interfaces, #{results.processors} processors, #{results.storage} storage"
)
{:ok, results}
end
# Generic check creator: iterates entities, calls check_fun for each, and
# tallies successes under success_key while collecting errors tagged with
# error_tag. Eliminates per-entity copy-pasted reduce blocks.
defp create_discovered_checks(acc, device, entities, error_tag, success_key, check_fun) do
Enum.reduce(entities, acc, fn entity, acc ->
case check_fun.(device, entity) do
{:ok, _check} -> Map.update!(acc, success_key, &(&1 + 1))
{:error, reason} -> Map.update!(acc, :errors, &[{error_tag, entity.id, reason} | &1])
end
end)
end
defp create_sensor_check(device, sensor) do
attrs = %{
organization_id: device.organization_id,
device_id: device.id,
name: sensor.sensor_descr,
check_type: "snmp_sensor",
source_type: "auto_discovery",
source_id: sensor.id,
interval_seconds: 60,
enabled: sensor.monitored,
config: %{
"sensor_type" => sensor.sensor_type,
"sensor_oid" => sensor.sensor_oid,
"sensor_unit" => sensor.sensor_unit
}
}
case Towerops.Monitoring.create_check(attrs) do
{:ok, check} ->
# Schedule first execution
_ = Towerops.Monitoring.schedule_check(check)
{:ok, check}
error ->
error
end
end
defp create_interface_check(device, interface) do
name = interface.if_alias || interface.if_name || interface.if_descr || "Interface #{interface.if_index}"
attrs = %{
organization_id: device.organization_id,
device_id: device.id,
name: "#{name} Status",
check_type: "snmp_interface",
source_type: "auto_discovery",
source_id: interface.id,
interval_seconds: 60,
enabled: interface.monitored,
config: %{
"if_index" => interface.if_index,
"if_descr" => interface.if_descr
}
}
case Towerops.Monitoring.create_check(attrs) do
{:ok, check} ->
_ = Towerops.Monitoring.schedule_check(check)
{:ok, check}
error ->
error
end
end
defp create_processor_check(device, processor) do
attrs = %{
organization_id: device.organization_id,
device_id: device.id,
name: processor.description || "Processor #{processor.processor_index}",
check_type: "snmp_processor",
source_type: "auto_discovery",
source_id: processor.id,
interval_seconds: 60,
enabled: true,
config: %{
"processor_type" => processor.processor_type,
"processor_index" => processor.processor_index
}
}
case Towerops.Monitoring.create_check(attrs) do
{:ok, check} ->
_ = Towerops.Monitoring.schedule_check(check)
{:ok, check}
error ->
error
end
end
defp create_storage_check(device, storage) do
name = storage.description || storage.device_name || "Storage #{storage.storage_index}"
attrs = %{
organization_id: device.organization_id,
device_id: device.id,
name: "#{name} Usage",
check_type: "snmp_storage",
source_type: "auto_discovery",
source_id: storage.id,
interval_seconds: 300,
enabled: true,
config: %{
"storage_type" => storage.storage_type,
"storage_index" => storage.storage_index
}
}
case Towerops.Monitoring.create_check(attrs) do
{:ok, check} ->
_ = Towerops.Monitoring.schedule_check(check)
{:ok, check}
error ->
error
end
end
# Device queries
@doc """
Gets the SNMP device for a piece of device.
## Examples
iex> get_device(device_id)
%Device{}
iex> get_device(nonexistent_id)
nil
"""
@spec get_device(String.t()) :: Device.t() | nil
def get_device(device_id) do
Repo.get_by(Device, device_id: device_id)
end
@doc """
Gets the SNMP device for a device with preloaded associations.
"""
def get_device_with_associations(device_id) do
Device
|> where([d], d.device_id == ^device_id)
|> preload([:device, :interfaces, :sensors, :state_sensors, :processors, :storage, :mempools])
|> Repo.one()
end
## Interface metadata delegates (implementation in `Towerops.Snmp.Interfaces`).
defdelegate list_interfaces(device_id), to: Interfaces
defdelegate list_monitored_interfaces(device_id), to: Interfaces
defdelegate get_interface(interface_id), to: Interfaces
defdelegate update_interface(interface, attrs), to: Interfaces
defdelegate set_manual_capacity(interface_id, capacity_bps), to: Interfaces
defdelegate clear_manual_capacity(interface_id), to: Interfaces
## Sensor metadata delegates (implementation in `Towerops.Snmp.Sensors`).
defdelegate list_sensors(device_id), to: Sensors
defdelegate list_monitored_sensors(device_id), to: Sensors
defdelegate list_sensors_by_type(device_id), to: Sensors
defdelegate get_sensor(sensor_id), to: Sensors
defdelegate update_sensor(sensor, attrs), to: Sensors
defdelegate update_state_sensor(state_sensor, attrs), to: Sensors
defdelegate list_state_sensors(device_id), to: Sensors
## Sensor / interface time-series query delegates
## (implementation in `Towerops.Snmp.Queries`).
defdelegate get_sensor_readings(sensor_id, opts \\ []), to: Queries
defdelegate get_latest_sensor_reading(sensor_id), to: Queries
defdelegate get_latest_sensor_readings_batch(sensor_ids), to: Queries
defdelegate get_interface_stats(interface_id, opts \\ []), to: Queries
defdelegate get_latest_interface_stat(interface_id), to: Queries
defdelegate get_latest_interface_stats_batch(interface_ids), to: Queries
## Time-series write delegates (implementation in `Towerops.Snmp.Monitoring`).
defdelegate create_sensor_reading(attrs), to: Monitoring
defdelegate create_sensor_readings_batch(entries), to: Monitoring
defdelegate create_interface_stat(attrs), to: Monitoring
defdelegate create_interface_stats_batch(entries), to: Monitoring
## Neighbor delegates (implementation in `Towerops.Snmp.Neighbors`).
defdelegate list_neighbors(device_id), to: Neighbors
defdelegate list_neighbors_with_equipment(device_id), to: Neighbors
defdelegate get_neighbor(neighbor_id), to: Neighbors
defdelegate upsert_neighbor(attrs), to: Neighbors
defdelegate delete_stale_neighbors(device_id, cutoff_datetime), to: Neighbors
defdelegate delete_stale_and_upsert_neighbors(device_id, neighbors, cutoff), to: Neighbors
## Discovered device aggregation (implementation in `Towerops.Snmp.DiscoveredDevices`).
defdelegate list_discovered_devices_for_organization(organization_id), to: DiscoveredDevices
## VLAN delegates (implementation in `Towerops.Snmp.Vlans`).
defdelegate list_vlans(snmp_device_id), to: Vlans
defdelegate get_vlan(vlan_id), to: Vlans
## IP address delegates (implementation in `Towerops.Snmp.IpAddresses`).
defdelegate list_ip_addresses(snmp_device_id), to: IpAddresses
defdelegate list_interface_ip_addresses(interface_id), to: IpAddresses
## Processor metadata + reading delegates
## (queries in `Towerops.Snmp.Processors`, reading inserts in
## `Towerops.Snmp.Monitoring`).
defdelegate list_processors(snmp_device_id), to: Processors
defdelegate get_processor(processor_id), to: Processors
defdelegate update_processor(processor, attrs), to: Processors
defdelegate get_processor_readings(processor_id, opts \\ []), to: Processors
defdelegate create_processor_reading(attrs), to: Monitoring
defdelegate create_processor_readings_batch(entries), to: Monitoring
## Storage metadata + reading delegates
## (implementation in `Towerops.Snmp.StorageQueries`).
defdelegate list_storage(snmp_device_id), to: StorageQueries
defdelegate get_storage(storage_id), to: StorageQueries
defdelegate update_storage(storage, attrs), to: StorageQueries
defdelegate get_storage_readings(storage_id, opts \\ []), to: StorageQueries
defdelegate get_latest_storage_reading(storage_id), to: StorageQueries
defdelegate get_latest_storage_readings_batch(storage_ids), to: StorageQueries
defdelegate create_storage_reading(attrs), to: StorageQueries
defdelegate create_storage_readings_batch(entries), to: StorageQueries
## Mempool metadata + reading delegates
## (implementation in `Towerops.Snmp.Mempools`).
defdelegate list_mempools(snmp_device_id), to: Mempools
defdelegate update_mempool(mempool, attrs), to: Mempools
defdelegate get_mempool_readings(mempool_id, opts \\ []), to: Mempools
defdelegate get_latest_mempool_reading(mempool_id), to: Mempools
defdelegate get_latest_mempool_readings_batch(mempool_ids), to: Mempools
defdelegate create_mempool_reading(attrs), to: Mempools
defdelegate create_mempool_readings_batch(entries), to: Mempools
## Entity physical delegates (implementation in `Towerops.Snmp.EntityPhysicals`).
defdelegate list_entity_physical(snmp_device_id), to: EntityPhysicals
defdelegate get_entity_physical(id), to: EntityPhysicals
defdelegate get_entity_physical_by_class(snmp_device_id, entity_class), to: EntityPhysicals
defdelegate get_entity_physical_tree(snmp_device_id), to: EntityPhysicals
defdelegate update_entity_physical(entity, attrs), to: EntityPhysicals
defdelegate create_entity_physical_readings_batch(entries), to: EntityPhysicals
## Transceiver delegates (implementation in `Towerops.Snmp.Transceivers`).
defdelegate list_transceivers(snmp_device_id), to: Transceivers
defdelegate get_transceiver(id), to: Transceivers
defdelegate get_transceivers_by_type(snmp_device_id, transceiver_type), to: Transceivers
defdelegate list_transceivers_with_dom(snmp_device_id), to: Transceivers
defdelegate get_transceiver_with_latest_reading(id), to: Transceivers
defdelegate update_transceiver(transceiver, attrs), to: Transceivers
defdelegate create_transceiver_readings_batch(entries), to: Transceivers
## Printer supply delegates (implementation in `Towerops.Snmp.PrinterSupplies`).
defdelegate list_printer_supplies(snmp_device_id), to: PrinterSupplies
defdelegate get_printer_supply(id), to: PrinterSupplies
defdelegate get_printer_supplies_by_type(snmp_device_id, supply_type), to: PrinterSupplies
defdelegate get_low_printer_supplies(snmp_device_id, threshold_percent), to: PrinterSupplies
defdelegate update_printer_supply(printer_supply, attrs), to: PrinterSupplies
## ARP entry delegates (implementation in `Towerops.Snmp.ArpEntries`).
defdelegate list_arp_entries(device_id), to: ArpEntries
defdelegate get_arp_entry(arp_entry_id), to: ArpEntries
defdelegate upsert_arp_entry(attrs), to: ArpEntries
defdelegate delete_stale_arp_entries(device_id, cutoff_datetime), to: ArpEntries
defdelegate delete_stale_and_upsert_arp_entries(device_id, arp_entries, interfaces, cutoff),
to: ArpEntries
defdelegate upsert_arp_entries(device_id, arp_entries, interfaces), to: ArpEntries
## MAC address delegates (implementation in `Towerops.Snmp.MacAddresses`).
defdelegate list_mac_addresses(device_id), to: MacAddresses
defdelegate upsert_mac_address(attrs), to: MacAddresses
defdelegate delete_stale_mac_addresses(device_id, cutoff_datetime), to: MacAddresses
defdelegate delete_stale_and_upsert_mac_addresses(device_id, mac_entries, interfaces, cutoff),
to: MacAddresses
defdelegate upsert_mac_addresses(device_id, mac_entries, interfaces), to: MacAddresses
## Network topology delegate (implementation in `Towerops.Snmp.Topology`).
defdelegate get_network_topology(organization_id), to: Topology
## Wireless client delegates (implementation in `Towerops.Snmp.WirelessClients`).
defdelegate list_wireless_clients(device_id), to: WirelessClients
defdelegate get_wireless_clients_by_mac(organization_id, mac_addresses), to: WirelessClients
defdelegate upsert_wireless_client(attrs), to: WirelessClients
defdelegate upsert_wireless_clients(device_id, organization_id, clients), to: WirelessClients
defdelegate delete_stale_wireless_clients(device_id, cutoff_datetime), to: WirelessClients
defdelegate delete_stale_and_upsert_wireless_clients(device_id, organization_id, clients, cutoff),
to: WirelessClients
defdelegate create_wireless_client_readings_batch(entries), to: WirelessClients
defdelegate list_weak_signal_clients(organization_id, threshold_dbm \\ -75), to: WirelessClients
defdelegate list_low_snr_clients(organization_id, threshold_db \\ 15), to: WirelessClients
defdelegate get_wireless_client_count_by_device(organization_id), to: WirelessClients
defdelegate list_overloaded_aps(organization_id, threshold \\ 50), to: WirelessClients
defdelegate get_wireless_client_last_seen(organization_id, mac_address), to: WirelessClients
end