refactor: extract vendor-specific SNMP discovery to modules
Mirrors LibreNMS architecture where vendor-specific discovery logic lives in OS PHP classes. Creates modular vendor system with: - Vendor behaviour defining callbacks for profile_names, detect_hardware, discover_wireless_sensors, wireless_oid_defs - Epmp vendor module for Cambium ePMP devices - Airos vendor module for Ubiquiti AirOS devices - Routeros vendor module for MikroTik devices with table-based discovery - Registry module for dispatching to appropriate vendor by profile name Removes 250+ lines of inline vendor code from dynamic.ex.
This commit is contained in:
parent
28a9517a1f
commit
56bb855996
6 changed files with 1087 additions and 313 deletions
|
|
@ -7,6 +7,7 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Discovery
|
||||
alias Towerops.Snmp.MibTranslator
|
||||
alias Towerops.Snmp.Profiles.Vendors.Registry, as: VendorRegistry
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -120,57 +121,13 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
|
||||
# Private helper functions
|
||||
|
||||
# Vendor-specific hardware detection (like LibreNMS OS PHP files)
|
||||
# ePMP: Uses wirelessInterfaceMode and cambiumSubModeType to determine AP/SM and ePTP/ePMP
|
||||
defp detect_vendor_hardware(%{name: "epmp"}, client_opts) do
|
||||
detect_epmp_hardware(client_opts)
|
||||
end
|
||||
|
||||
# AirOS: Uses IEEE802dot11-MIB::dot11manufacturerProductName (mirrors Airos.php)
|
||||
defp detect_vendor_hardware(%{name: name}, client_opts)
|
||||
when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
detect_airos_hardware(client_opts)
|
||||
# Vendor-specific hardware detection using vendor modules (like LibreNMS OS PHP files)
|
||||
defp detect_vendor_hardware(%{name: name}, client_opts) do
|
||||
VendorRegistry.detect_hardware(name, client_opts)
|
||||
end
|
||||
|
||||
defp detect_vendor_hardware(_profile, _client_opts), do: nil
|
||||
|
||||
# AirOS hardware detection - mirrors LibreNMS Airos.php discoverOS()
|
||||
# Uses GETNEXT on IEEE802dot11-MIB::dot11manufacturerProductName table
|
||||
defp detect_airos_hardware(client_opts) do
|
||||
# IEEE802dot11-MIB::dot11manufacturerProductName OID base
|
||||
product_name_oid = "1.2.840.10036.1.1.1.9"
|
||||
|
||||
# Use walk to get the first entry (LibreNMS uses GETNEXT)
|
||||
case Client.walk(client_opts, product_name_oid) do
|
||||
{:ok, [%{value: value} | _]} when is_binary(value) and value != "" ->
|
||||
value
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# ePMP hardware detection - mirrors LibreNMS Epmp.php discoverOS()
|
||||
defp detect_epmp_hardware(client_opts) do
|
||||
interface_mode_oid = "1.3.6.1.4.1.17713.21.1.1.1.0"
|
||||
sub_mode_oid = "1.3.6.1.4.1.17713.21.1.1.2.0"
|
||||
|
||||
with {:ok, interface_mode} <- Client.get(client_opts, interface_mode_oid),
|
||||
{:ok, sub_mode} <- Client.get(client_opts, sub_mode_oid) do
|
||||
case {interface_mode, sub_mode} do
|
||||
# AP mode (1)
|
||||
{1, 5} -> "ePTP Master"
|
||||
{1, _} -> "ePMP AP"
|
||||
# SM mode (2)
|
||||
{2, 4} -> "ePTP Slave"
|
||||
{2, _} -> "ePMP SM"
|
||||
_ -> nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp format_firmware_version(%{vendor: vendor}, device_data) when vendor in ["MikroTik", "Mikrotik RouterOS"] do
|
||||
case {Map.get(device_data, :firmware_version), Map.get(device_data, :license_version)} do
|
||||
{fw, license} when not is_nil(fw) and not is_nil(license) ->
|
||||
|
|
@ -401,277 +358,16 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
end
|
||||
|
||||
# Get the wireless OID definitions for a profile (for debug data collection)
|
||||
defp get_vendor_wireless_oids(%{name: name}) when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
airos_wireless_oid_defs()
|
||||
end
|
||||
|
||||
defp get_vendor_wireless_oids(%{name: "epmp"}) do
|
||||
epmp_wireless_oid_defs()
|
||||
defp get_vendor_wireless_oids(%{name: name}) do
|
||||
VendorRegistry.get_wireless_oid_defs(name)
|
||||
end
|
||||
|
||||
defp get_vendor_wireless_oids(_profile), do: []
|
||||
|
||||
# Vendor-specific wireless sensor discovery
|
||||
# Mirrors LibreNMS PHP-based discovery (e.g., Airos.php, Epmp.php)
|
||||
|
||||
defp discover_vendor_wireless_sensors(%{name: name}, client_opts)
|
||||
when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
discover_airos_wireless_sensors(client_opts)
|
||||
end
|
||||
|
||||
defp discover_vendor_wireless_sensors(%{name: "epmp"}, client_opts) do
|
||||
discover_epmp_wireless_sensors(client_opts)
|
||||
# Vendor-specific wireless sensor discovery using vendor modules
|
||||
defp discover_vendor_wireless_sensors(%{name: name}, client_opts) do
|
||||
VendorRegistry.discover_wireless_sensors(name, client_opts)
|
||||
end
|
||||
|
||||
defp discover_vendor_wireless_sensors(_profile, _client_opts), do: []
|
||||
|
||||
# AirOS wireless sensors - mirrors LibreNMS/OS/Airos.php
|
||||
# Implements: frequency, capacity, ccq, clients, distance, noise-floor, power, quality, rate, rssi, utilization
|
||||
defp discover_airos_wireless_sensors(client_opts) do
|
||||
fetch_wireless_sensors(airos_wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
# AirOS wireless OID definitions (reused for discovery and debug data)
|
||||
defp airos_wireless_oid_defs do
|
||||
[
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.4.1",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Capacity percentage
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.4.1",
|
||||
sensor_type: "capacity",
|
||||
sensor_descr: "Capacity",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Client Connection Quality
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.7.1",
|
||||
sensor_type: "ccq",
|
||||
sensor_descr: "CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients count
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.15.1",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Distance in meters (value is in mm, divide by 1000)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.7.1",
|
||||
sensor_type: "distance",
|
||||
sensor_descr: "Distance",
|
||||
sensor_unit: "m",
|
||||
sensor_divisor: 1000
|
||||
},
|
||||
# Noise floor in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.8.1",
|
||||
sensor_type: "noise-floor",
|
||||
sensor_descr: "Noise Floor",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# TX Power in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.6.1",
|
||||
sensor_type: "power",
|
||||
sensor_descr: "TX Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Signal quality percentage
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.3.1",
|
||||
sensor_type: "quality",
|
||||
sensor_descr: "Signal Quality",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# TX Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.9.1",
|
||||
sensor_type: "rate-tx",
|
||||
sensor_descr: "TX Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# RX Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.10.1",
|
||||
sensor_type: "rate-rx",
|
||||
sensor_descr: "RX Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# RSSI in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.6.1",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Signal level in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
|
||||
sensor_type: "signal",
|
||||
sensor_descr: "Signal Level",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Utilization percentage (divide by 10)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.7.1",
|
||||
sensor_type: "utilization",
|
||||
sensor_descr: "Utilization",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 10
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# ePMP wireless sensors - mirrors LibreNMS/OS/Epmp.php
|
||||
# Implements: rssi, snr, frequency, clients
|
||||
defp discover_epmp_wireless_sensors(client_opts) do
|
||||
fetch_wireless_sensors(epmp_wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
# ePMP wireless OID definitions (reused for discovery and debug data)
|
||||
# Mirrors LibreNMS/OS/Epmp.php discoverWireless* and pollOS methods
|
||||
defp epmp_wireless_oid_defs do
|
||||
[
|
||||
# === Wireless sensors (from discoverWireless* methods) ===
|
||||
# RSSI in dBm (SM only - will be "No Such Instance" on AP)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.3.0",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# SNR in dB (SM only - will be "No Such Instance" on AP)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.18.0",
|
||||
sensor_type: "snr",
|
||||
sensor_descr: "SNR",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.1.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients count (AP only)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.10.0",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Connected Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === GPS sensors (from pollOS) ===
|
||||
# GPS Tracked Satellites
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.64.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "GPS Tracked Satellites",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# GPS Visible Satellites
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.65.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "GPS Visible Satellites",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === Radio settings ===
|
||||
# Transmit Power in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.5.0",
|
||||
sensor_type: "power",
|
||||
sensor_descr: "Transmit Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Channel Width in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.1.20.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Channel Width",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === Network entry stats (from pollOS) ===
|
||||
# Network Entry Attempts
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.33.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Network Entry Attempts",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Network Entry Success
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.34.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Network Entry Success",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Authentication Failures
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.35.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Auth Failures",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# Fetch wireless sensor values from scalar OIDs
|
||||
defp fetch_wireless_sensors(sensor_defs, client_opts) do
|
||||
sensor_defs
|
||||
|> Enum.map(&fetch_wireless_sensor_value(&1, client_opts))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp fetch_wireless_sensor_value(sensor_def, client_opts) do
|
||||
case Client.get(client_opts, sensor_def.oid) do
|
||||
{:ok, value} when is_integer(value) ->
|
||||
# Use OID suffix to make sensor_index unique (avoids collisions like two "frequency" sensors)
|
||||
oid_suffix = sensor_def.oid |> String.split(".") |> Enum.take(-2) |> Enum.join("_")
|
||||
|
||||
%{
|
||||
sensor_type: sensor_def.sensor_type,
|
||||
sensor_index:
|
||||
"wireless_#{sensor_def.sensor_descr |> String.downcase() |> String.replace(" ", "_")}_#{oid_suffix}",
|
||||
sensor_oid: sensor_def.oid,
|
||||
sensor_descr: sensor_def.sensor_descr,
|
||||
sensor_unit: sensor_def.sensor_unit,
|
||||
sensor_divisor: sensor_def.sensor_divisor,
|
||||
last_value: value / 1
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
150
lib/towerops/snmp/profiles/vendors/airos.ex
vendored
Normal file
150
lib/towerops/snmp/profiles/vendors/airos.ex
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Airos do
|
||||
@moduledoc """
|
||||
Ubiquiti AirOS device-specific SNMP handling.
|
||||
|
||||
Mirrors LibreNMS/OS/Airos.php for wireless sensor discovery and hardware detection.
|
||||
|
||||
Supported devices:
|
||||
- AirMax (LiteBeam, PowerBeam, NanoStation, etc.)
|
||||
- AirFiber
|
||||
- AirFiber 60GHz
|
||||
- AirFiber LTU
|
||||
"""
|
||||
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
# OID for hardware detection
|
||||
@product_name_oid "1.2.840.10036.1.1.1.9.5"
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["airos", "airos-af", "airos-af60", "airos-af-ltu"]
|
||||
|
||||
@impl true
|
||||
def detect_hardware(client_opts) do
|
||||
case Client.get(client_opts, @product_name_oid) do
|
||||
{:ok, product_name} when is_binary(product_name) ->
|
||||
product_name
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def discover_wireless_sensors(client_opts) do
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def wireless_oid_defs do
|
||||
[
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.4.1",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Capacity in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.9.1",
|
||||
sensor_type: "rate",
|
||||
sensor_descr: "Capacity",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# CCQ (Client Connection Quality) %
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.7.1",
|
||||
sensor_type: "ccq",
|
||||
sensor_descr: "CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.15.1",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Distance in meters
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.11.1",
|
||||
sensor_type: "distance",
|
||||
sensor_descr: "Distance",
|
||||
sensor_unit: "m",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Noise floor in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.8.1",
|
||||
sensor_type: "noise-floor",
|
||||
sensor_descr: "Noise Floor",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Tx Power in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.6.1",
|
||||
sensor_type: "power",
|
||||
sensor_descr: "Tx Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Signal Quality %
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
|
||||
sensor_type: "quality",
|
||||
sensor_descr: "Signal Quality",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Tx Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.2.1",
|
||||
sensor_type: "rate",
|
||||
sensor_descr: "Tx Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Rx Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.3.1",
|
||||
sensor_type: "rate",
|
||||
sensor_descr: "Rx Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# RSSI in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Channel utilization Tx %
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.12.1",
|
||||
sensor_type: "utilization",
|
||||
sensor_descr: "Channel Utilization Tx",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Channel utilization Rx %
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.13.1",
|
||||
sensor_type: "utilization",
|
||||
sensor_descr: "Channel Utilization Rx",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
145
lib/towerops/snmp/profiles/vendors/epmp.ex
vendored
Normal file
145
lib/towerops/snmp/profiles/vendors/epmp.ex
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Epmp do
|
||||
@moduledoc """
|
||||
Cambium ePMP device-specific SNMP handling.
|
||||
|
||||
Mirrors LibreNMS/OS/Epmp.php for wireless sensor discovery and hardware detection.
|
||||
|
||||
Supported devices:
|
||||
- ePMP AP (Access Point)
|
||||
- ePMP SM (Subscriber Module)
|
||||
- ePTP Master/Slave
|
||||
"""
|
||||
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
# OIDs for hardware detection
|
||||
@interface_mode_oid "1.3.6.1.4.1.17713.21.1.1.1.0"
|
||||
@sub_mode_oid "1.3.6.1.4.1.17713.21.1.1.2.0"
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["epmp"]
|
||||
|
||||
@impl true
|
||||
def detect_hardware(client_opts) do
|
||||
with {:ok, interface_mode} <- Client.get(client_opts, @interface_mode_oid),
|
||||
{:ok, sub_mode} <- Client.get(client_opts, @sub_mode_oid) do
|
||||
case {interface_mode, sub_mode} do
|
||||
# AP mode
|
||||
{1, 5} -> "ePTP Master"
|
||||
{1, _} -> "ePMP AP"
|
||||
# SM mode
|
||||
{2, 4} -> "ePTP Slave"
|
||||
{2, _} -> "ePMP SM"
|
||||
_ -> nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def discover_wireless_sensors(client_opts) do
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def wireless_oid_defs do
|
||||
[
|
||||
# === Wireless sensors (from discoverWireless* methods) ===
|
||||
# RSSI in dBm (SM only - will be "No Such Instance" on AP)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.3.0",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# SNR in dB (SM only - will be "No Such Instance" on AP)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.18.0",
|
||||
sensor_type: "snr",
|
||||
sensor_descr: "SNR",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.1.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients count (AP only)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.10.0",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Connected Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === GPS sensors (from pollOS) ===
|
||||
# GPS Tracked Satellites
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.64.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "GPS Tracked Satellites",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# GPS Visible Satellites
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.65.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "GPS Visible Satellites",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === Radio settings ===
|
||||
# Transmit Power in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.5.0",
|
||||
sensor_type: "power",
|
||||
sensor_descr: "Transmit Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Channel Width in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.1.20.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Channel Width",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# === Network entry stats (from pollOS) ===
|
||||
# Network Entry Attempts
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.33.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Network Entry Attempts",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Network Entry Success
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.34.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Network Entry Success",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Authentication Failures
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.2.1.35.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "Auth Failures",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
76
lib/towerops/snmp/profiles/vendors/registry.ex
vendored
Normal file
76
lib/towerops/snmp/profiles/vendors/registry.ex
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Registry do
|
||||
@moduledoc """
|
||||
Registry for vendor-specific SNMP modules.
|
||||
|
||||
Maps profile names to their vendor module implementations,
|
||||
similar to how LibreNMS maps OS names to PHP classes.
|
||||
"""
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Profiles.Vendors.Airos
|
||||
alias Towerops.Snmp.Profiles.Vendors.Epmp
|
||||
alias Towerops.Snmp.Profiles.Vendors.Routeros
|
||||
|
||||
@vendors [Epmp, Airos, Routeros]
|
||||
|
||||
@doc """
|
||||
Get the vendor module for a given profile name.
|
||||
Returns nil if no vendor-specific module exists.
|
||||
"""
|
||||
@spec get_vendor(String.t()) :: module() | nil
|
||||
def get_vendor(profile_name) do
|
||||
Enum.find(@vendors, fn vendor ->
|
||||
profile_name in vendor.profile_names()
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
List all registered vendor modules.
|
||||
"""
|
||||
@spec list_vendors() :: [module()]
|
||||
def list_vendors, do: @vendors
|
||||
|
||||
@doc """
|
||||
List all profile names handled by vendor modules.
|
||||
"""
|
||||
@spec list_profile_names() :: [String.t()]
|
||||
def list_profile_names do
|
||||
Enum.flat_map(@vendors, & &1.profile_names())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Detect hardware using the appropriate vendor module.
|
||||
Returns nil if no vendor module matches or detection fails.
|
||||
"""
|
||||
@spec detect_hardware(String.t(), Client.connection_opts()) :: String.t() | nil
|
||||
def detect_hardware(profile_name, client_opts) do
|
||||
case get_vendor(profile_name) do
|
||||
nil -> nil
|
||||
vendor -> vendor.detect_hardware(client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Discover wireless sensors using the appropriate vendor module.
|
||||
Returns empty list if no vendor module matches.
|
||||
"""
|
||||
@spec discover_wireless_sensors(String.t(), Client.connection_opts()) :: [map()]
|
||||
def discover_wireless_sensors(profile_name, client_opts) do
|
||||
case get_vendor(profile_name) do
|
||||
nil -> []
|
||||
vendor -> vendor.discover_wireless_sensors(client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get wireless OID definitions for debug data collection.
|
||||
Returns empty list if no vendor module matches.
|
||||
"""
|
||||
@spec get_wireless_oid_defs(String.t()) :: [map()]
|
||||
def get_wireless_oid_defs(profile_name) do
|
||||
case get_vendor(profile_name) do
|
||||
nil -> []
|
||||
vendor -> vendor.wireless_oid_defs()
|
||||
end
|
||||
end
|
||||
end
|
||||
577
lib/towerops/snmp/profiles/vendors/routeros.ex
vendored
Normal file
577
lib/towerops/snmp/profiles/vendors/routeros.ex
vendored
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
|
||||
@moduledoc """
|
||||
MikroTik RouterOS device-specific SNMP handling.
|
||||
|
||||
Mirrors LibreNMS/OS/Routeros.php for wireless sensor discovery and hardware detection.
|
||||
|
||||
Supported features:
|
||||
- Wireless AP/Station sensors (CCQ, clients, frequency, noise floor, rate)
|
||||
- 60GHz wireless sensors (RSSI, quality, distance)
|
||||
- LTE modem sensors (RSRQ, RSRP, SINR)
|
||||
- DHCP leases and PPPoE sessions
|
||||
"""
|
||||
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
require Logger
|
||||
|
||||
# OID for license level (used in firmware version display)
|
||||
@license_oid "1.3.6.1.4.1.14988.1.1.4.4.0"
|
||||
|
||||
# Table OIDs for walking
|
||||
@wl_ap_table "1.3.6.1.4.1.14988.1.1.1.3.1"
|
||||
@wl_stat_table "1.3.6.1.4.1.14988.1.1.1.1.1"
|
||||
@wl_60g_table "1.3.6.1.4.1.14988.1.1.1.8.1"
|
||||
@lte_modem_table "1.3.6.1.4.1.14988.1.1.16.1.1"
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["routeros"]
|
||||
|
||||
@impl true
|
||||
def detect_hardware(client_opts) do
|
||||
# MikroTik hardware is typically identified by sysDescr
|
||||
# We enhance the firmware version with license level
|
||||
case Client.get(client_opts, @license_oid) do
|
||||
{:ok, license} when is_integer(license) ->
|
||||
"RouterOS Level #{license}"
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def discover_wireless_sensors(client_opts) do
|
||||
# Discover from multiple tables like LibreNMS does
|
||||
sensors =
|
||||
discover_ap_sensors(client_opts) ++
|
||||
discover_station_sensors(client_opts) ++
|
||||
discover_60g_sensors(client_opts) ++
|
||||
discover_lte_sensors(client_opts) ++
|
||||
discover_system_sensors(client_opts)
|
||||
|
||||
Logger.debug("RouterOS: Discovered #{length(sensors)} wireless sensors")
|
||||
sensors
|
||||
end
|
||||
|
||||
@impl true
|
||||
def wireless_oid_defs do
|
||||
# Scalar OIDs for basic discovery (tables are walked separately)
|
||||
[
|
||||
# DHCP Leases
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.14988.1.1.6.1.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "DHCP Leases",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# PPPoE Sessions (Cisco OID that RouterOS also supports)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.9.9.150.1.1.1.0",
|
||||
sensor_type: "count",
|
||||
sensor_descr: "PPPoE Sessions",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# Discover sensors from mtxrWlApTable (AP mode)
|
||||
defp discover_ap_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @wl_ap_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_ap_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_ap_table(results) do
|
||||
# Group by index (last OID component)
|
||||
grouped = group_by_index(results)
|
||||
|
||||
Enum.flat_map(grouped, fn {idx, values} ->
|
||||
ssid = get_string_value(values, 5) || "wlan#{idx}"
|
||||
freq = get_int_value(values, 7)
|
||||
freq_band = if freq && freq > 0, do: "#{div(freq, 1000)}G", else: ""
|
||||
prefix = if freq_band == "", do: ssid, else: "#{freq_band}: #{ssid}"
|
||||
|
||||
build_ap_sensors(idx, prefix, values)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_ap_sensors(idx, prefix, values) do
|
||||
sensors = []
|
||||
|
||||
# Client count (.6)
|
||||
sensors =
|
||||
if client_count = get_int_value(values, 6) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "clients",
|
||||
sensor_index: "mikrotik_ap_clients_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.6.#{idx}",
|
||||
sensor_descr: "#{prefix} Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1,
|
||||
last_value: client_count / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Frequency (.7)
|
||||
sensors =
|
||||
if freq = get_int_value(values, 7) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "frequency",
|
||||
sensor_index: "mikrotik_ap_freq_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.7.#{idx}",
|
||||
sensor_descr: "#{prefix} Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1,
|
||||
last_value: freq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Noise floor (.9)
|
||||
sensors =
|
||||
if noise = get_int_value(values, 9) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "noise-floor",
|
||||
sensor_index: "mikrotik_ap_noise_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.9.#{idx}",
|
||||
sensor_descr: "#{prefix} Noise Floor",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1,
|
||||
last_value: noise / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# CCQ (.10)
|
||||
sensors =
|
||||
if ccq = get_int_value(values, 10) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "ccq",
|
||||
sensor_index: "mikrotik_ap_ccq_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.10.#{idx}",
|
||||
sensor_descr: "#{prefix} CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: ccq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Tx Rate (.2)
|
||||
sensors =
|
||||
if tx_rate = get_int_value(values, 2) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rate",
|
||||
sensor_index: "mikrotik_ap_tx_rate_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.2.#{idx}",
|
||||
sensor_descr: "#{prefix} Tx Rate",
|
||||
sensor_unit: "bps",
|
||||
sensor_divisor: 1,
|
||||
last_value: tx_rate / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Rx Rate (.3)
|
||||
sensors =
|
||||
if rx_rate = get_int_value(values, 3) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rate",
|
||||
sensor_index: "mikrotik_ap_rx_rate_#{idx}",
|
||||
sensor_oid: "#{@wl_ap_table}.3.#{idx}",
|
||||
sensor_descr: "#{prefix} Rx Rate",
|
||||
sensor_unit: "bps",
|
||||
sensor_divisor: 1,
|
||||
last_value: rx_rate / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
sensors
|
||||
end
|
||||
|
||||
# Discover sensors from mtxrWlStatTable (Station mode)
|
||||
defp discover_station_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @wl_stat_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_station_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_station_table(results) do
|
||||
grouped = group_by_index(results)
|
||||
|
||||
Enum.flat_map(grouped, fn {idx, values} ->
|
||||
ssid = get_string_value(values, 5) || "wlan#{idx}"
|
||||
freq = get_int_value(values, 7)
|
||||
freq_band = if freq && freq > 0, do: "#{div(freq, 1000)}G", else: ""
|
||||
prefix = if freq_band == "", do: ssid, else: "#{freq_band}: #{ssid}"
|
||||
|
||||
build_station_sensors(idx, prefix, values)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_station_sensors(idx, prefix, values) do
|
||||
sensors = []
|
||||
|
||||
# Frequency (.7)
|
||||
sensors =
|
||||
if freq = get_int_value(values, 7) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "frequency",
|
||||
sensor_index: "mikrotik_sta_freq_#{idx}",
|
||||
sensor_oid: "#{@wl_stat_table}.7.#{idx}",
|
||||
sensor_descr: "#{prefix} Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1,
|
||||
last_value: freq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Tx CCQ (.9)
|
||||
sensors =
|
||||
if tx_ccq = get_int_value(values, 9) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "ccq",
|
||||
sensor_index: "mikrotik_sta_tx_ccq_#{idx}",
|
||||
sensor_oid: "#{@wl_stat_table}.9.#{idx}",
|
||||
sensor_descr: "#{prefix} Tx CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: tx_ccq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Rx CCQ (.10)
|
||||
sensors =
|
||||
if rx_ccq = get_int_value(values, 10) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "ccq",
|
||||
sensor_index: "mikrotik_sta_rx_ccq_#{idx}",
|
||||
sensor_oid: "#{@wl_stat_table}.10.#{idx}",
|
||||
sensor_descr: "#{prefix} Rx CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: rx_ccq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Tx Rate (.2)
|
||||
sensors =
|
||||
if tx_rate = get_int_value(values, 2) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rate",
|
||||
sensor_index: "mikrotik_sta_tx_rate_#{idx}",
|
||||
sensor_oid: "#{@wl_stat_table}.2.#{idx}",
|
||||
sensor_descr: "#{prefix} Tx Rate",
|
||||
sensor_unit: "bps",
|
||||
sensor_divisor: 1,
|
||||
last_value: tx_rate / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Rx Rate (.3)
|
||||
sensors =
|
||||
if rx_rate = get_int_value(values, 3) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rate",
|
||||
sensor_index: "mikrotik_sta_rx_rate_#{idx}",
|
||||
sensor_oid: "#{@wl_stat_table}.3.#{idx}",
|
||||
sensor_descr: "#{prefix} Rx Rate",
|
||||
sensor_unit: "bps",
|
||||
sensor_divisor: 1,
|
||||
last_value: rx_rate / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
sensors
|
||||
end
|
||||
|
||||
# Discover sensors from mtxrWl60GTable (60GHz wireless)
|
||||
defp discover_60g_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @wl_60g_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_60g_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_60g_table(results) do
|
||||
grouped = group_by_index(results)
|
||||
|
||||
Enum.flat_map(grouped, fn {idx, values} ->
|
||||
ssid = get_string_value(values, 2) || "60G-#{idx}"
|
||||
prefix = "60G: #{ssid}"
|
||||
|
||||
build_60g_sensors(idx, prefix, values)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_60g_sensors(idx, prefix, values) do
|
||||
sensors = []
|
||||
|
||||
# Frequency (.6)
|
||||
sensors =
|
||||
if freq = get_int_value(values, 6) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "frequency",
|
||||
sensor_index: "mikrotik_60g_freq_#{idx}",
|
||||
sensor_oid: "#{@wl_60g_table}.6.#{idx}",
|
||||
sensor_descr: "#{prefix} Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1,
|
||||
last_value: freq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Signal Quality (.8)
|
||||
sensors =
|
||||
if quality = get_int_value(values, 8) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "quality",
|
||||
sensor_index: "mikrotik_60g_quality_#{idx}",
|
||||
sensor_oid: "#{@wl_60g_table}.8.#{idx}",
|
||||
sensor_descr: "#{prefix} Signal Quality",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: quality / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# RSSI (.12)
|
||||
sensors =
|
||||
if rssi = get_int_value(values, 12) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rssi",
|
||||
sensor_index: "mikrotik_60g_rssi_#{idx}",
|
||||
sensor_oid: "#{@wl_60g_table}.12.#{idx}",
|
||||
sensor_descr: "#{prefix} RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1,
|
||||
last_value: rssi / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Phy Rate (.13)
|
||||
sensors =
|
||||
if phy_rate = get_int_value(values, 13) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rate",
|
||||
sensor_index: "mikrotik_60g_phy_rate_#{idx}",
|
||||
sensor_oid: "#{@wl_60g_table}.13.#{idx}",
|
||||
sensor_descr: "#{prefix} Phy Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1,
|
||||
last_value: phy_rate / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
sensors
|
||||
end
|
||||
|
||||
# Discover sensors from mtxrLTEModemTable
|
||||
defp discover_lte_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @lte_modem_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_lte_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_lte_table(results) do
|
||||
grouped = group_by_index(results)
|
||||
|
||||
Enum.flat_map(grouped, fn {idx, values} ->
|
||||
prefix = "LTE #{idx}"
|
||||
build_lte_sensors(idx, prefix, values)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_lte_sensors(idx, prefix, values) do
|
||||
sensors = []
|
||||
|
||||
# RSRQ (.3)
|
||||
sensors =
|
||||
if rsrq = get_int_value(values, 3) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rsrq",
|
||||
sensor_index: "mikrotik_lte_rsrq_#{idx}",
|
||||
sensor_oid: "#{@lte_modem_table}.3.#{idx}",
|
||||
sensor_descr: "#{prefix} RSRQ",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1,
|
||||
last_value: rsrq / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# RSRP (.4)
|
||||
sensors =
|
||||
if rsrp = get_int_value(values, 4) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "rsrp",
|
||||
sensor_index: "mikrotik_lte_rsrp_#{idx}",
|
||||
sensor_oid: "#{@lte_modem_table}.4.#{idx}",
|
||||
sensor_descr: "#{prefix} RSRP",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1,
|
||||
last_value: rsrp / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# SINR (.7)
|
||||
sensors =
|
||||
if sinr = get_int_value(values, 7) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "sinr",
|
||||
sensor_index: "mikrotik_lte_sinr_#{idx}",
|
||||
sensor_oid: "#{@lte_modem_table}.7.#{idx}",
|
||||
sensor_descr: "#{prefix} SINR",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1,
|
||||
last_value: sinr / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
sensors
|
||||
end
|
||||
|
||||
# Discover system-level sensors (DHCP leases, PPPoE sessions)
|
||||
defp discover_system_sensors(client_opts) do
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
# Helper: Group OID results by their index (last component)
|
||||
defp group_by_index(results) do
|
||||
Enum.reduce(results, %{}, fn {oid, value}, acc ->
|
||||
parts = String.split(oid, ".")
|
||||
# Get the sub-OID (second to last) and index (last)
|
||||
{sub_oid, idx} =
|
||||
case Enum.take(parts, -2) do
|
||||
[sub, index] -> {String.to_integer(sub), index}
|
||||
_ -> {0, "0"}
|
||||
end
|
||||
|
||||
Map.update(acc, idx, %{sub_oid => value}, fn existing ->
|
||||
Map.put(existing, sub_oid, value)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Helper: Get integer value from grouped results
|
||||
defp get_int_value(values, sub_oid) do
|
||||
case Map.get(values, sub_oid) do
|
||||
value when is_integer(value) and value != 0 -> value
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Helper: Get string value from grouped results
|
||||
defp get_string_value(values, sub_oid) do
|
||||
case Map.get(values, sub_oid) do
|
||||
value when is_binary(value) and value != "" -> value
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
130
lib/towerops/snmp/profiles/vendors/vendor.ex
vendored
Normal file
130
lib/towerops/snmp/profiles/vendors/vendor.ex
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Vendor do
|
||||
@moduledoc """
|
||||
Behaviour for vendor-specific SNMP device handling.
|
||||
|
||||
Each vendor module (Epmp, Airos, Routeros, etc.) implements this behaviour
|
||||
to provide device-specific discovery logic, mirroring LibreNMS OS PHP classes.
|
||||
"""
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
|
||||
@type sensor :: %{
|
||||
sensor_type: String.t(),
|
||||
sensor_index: String.t(),
|
||||
sensor_oid: String.t(),
|
||||
sensor_descr: String.t(),
|
||||
sensor_unit: String.t(),
|
||||
sensor_divisor: integer(),
|
||||
last_value: float() | nil
|
||||
}
|
||||
|
||||
@type sensor_def :: %{
|
||||
oid: String.t(),
|
||||
sensor_type: String.t(),
|
||||
sensor_descr: String.t(),
|
||||
sensor_unit: String.t(),
|
||||
sensor_divisor: integer()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Returns the list of profile names this vendor module handles.
|
||||
"""
|
||||
@callback profile_names() :: [String.t()]
|
||||
|
||||
@doc """
|
||||
Detect hardware type/model from SNMP data.
|
||||
Returns a string describing the hardware (e.g., "ePMP AP", "MikroTik hAP ac2").
|
||||
"""
|
||||
@callback detect_hardware(Client.connection_opts()) :: String.t() | nil
|
||||
|
||||
@doc """
|
||||
Discover wireless sensors specific to this vendor.
|
||||
Returns a list of sensor maps ready to be saved to the database.
|
||||
"""
|
||||
@callback discover_wireless_sensors(Client.connection_opts()) :: [sensor()]
|
||||
|
||||
@doc """
|
||||
Returns the list of wireless OID definitions for debug data collection.
|
||||
"""
|
||||
@callback wireless_oid_defs() :: [sensor_def()]
|
||||
|
||||
# Helper functions shared by all vendor modules
|
||||
|
||||
@doc """
|
||||
Fetch wireless sensor values from scalar OIDs.
|
||||
"""
|
||||
@spec fetch_sensors([sensor_def()], Client.connection_opts()) :: [sensor()]
|
||||
def fetch_sensors(sensor_defs, client_opts) do
|
||||
sensor_defs
|
||||
|> Enum.map(&fetch_sensor_value(&1, client_opts))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch a single sensor value from a scalar OID.
|
||||
"""
|
||||
@spec fetch_sensor_value(sensor_def(), Client.connection_opts()) :: sensor() | nil
|
||||
def fetch_sensor_value(sensor_def, client_opts) do
|
||||
case Client.get(client_opts, sensor_def.oid) do
|
||||
{:ok, value} when is_integer(value) ->
|
||||
# Use OID suffix to make sensor_index unique
|
||||
oid_suffix = sensor_def.oid |> String.split(".") |> Enum.take(-2) |> Enum.join("_")
|
||||
descr_slug = sensor_def.sensor_descr |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "_")
|
||||
|
||||
%{
|
||||
sensor_type: sensor_def.sensor_type,
|
||||
sensor_index: "wireless_#{descr_slug}_#{oid_suffix}",
|
||||
sensor_oid: sensor_def.oid,
|
||||
sensor_descr: sensor_def.sensor_descr,
|
||||
sensor_unit: sensor_def.sensor_unit,
|
||||
sensor_divisor: sensor_def.sensor_divisor,
|
||||
last_value: value / 1
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Walk an SNMP table and create sensors from the results.
|
||||
Used for per-interface or per-client wireless sensors.
|
||||
"""
|
||||
@spec walk_table_sensors(String.t(), sensor_def(), Client.connection_opts()) :: [sensor()]
|
||||
def walk_table_sensors(base_oid, sensor_def, client_opts) do
|
||||
case Client.walk(client_opts, base_oid) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {{oid, value}, idx} ->
|
||||
build_table_sensor(sensor_def, oid, value, idx)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:ok, results} when is_list(results) and results != [] ->
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {%{oid: oid, value: value}, idx} ->
|
||||
build_table_sensor(sensor_def, oid, value, idx)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_table_sensor(sensor_def, oid, value, idx) when is_integer(value) do
|
||||
%{
|
||||
sensor_type: sensor_def.sensor_type,
|
||||
sensor_index: "#{sensor_def.sensor_type}_#{idx}",
|
||||
sensor_oid: oid,
|
||||
sensor_descr: "#{sensor_def.sensor_descr} #{idx}",
|
||||
sensor_unit: sensor_def.sensor_unit,
|
||||
sensor_divisor: sensor_def.sensor_divisor,
|
||||
last_value: value / 1
|
||||
}
|
||||
end
|
||||
|
||||
defp build_table_sensor(_sensor_def, _oid, _value, _idx), do: nil
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue