feat: complete WISP vendor coverage (ePMP, Raisecom, Netonix)
**1. Consolidated ePMP YAML profiles:** - Merged enhancements from cambium-epmp.yaml into epmp.yaml - Added GPS satellite count sensors (tracked/visible) - Added proper state_name fields for state sensors - Improved field naming (descr vs type, divisor vs precision) - Deleted duplicate cambium-epmp.yaml file **2. Implemented Raisecom vendor module:** - Created lib/towerops/snmp/profiles/vendors/raisecom.ex - Fills major 0% coverage gap for Asia-Pacific WISP market - Hardware detection via RAISECOM-SYSTEM-MIB with sysDescr fallback - Sensor discovery handled by existing YAML profiles (15+ sensors): - CPU utilization, memory pools - Fan speed and state monitoring - Voltage sensors with thresholds - Power supply state monitoring - Config load state tracking - Comprehensive test coverage (8 tests, all passing) **3. Verified Netonix coverage:** - YAML profile matches LibreNMS 100% (PoE status, voltage, temp, fan, power, current) - Vendor module provides additional scalar sensors beyond LibreNMS - Coverage exceeds LibreNMS baseline **4. Investigated Tarana:** - No SNMP support in LibreNMS (newer vendor, likely cloud-managed) - Would require vendor MIBs and documentation to implement Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1602322518
commit
dd3a6234cb
4 changed files with 197 additions and 76 deletions
63
lib/towerops/snmp/profiles/vendors/raisecom.ex
vendored
Normal file
63
lib/towerops/snmp/profiles/vendors/raisecom.ex
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.Raisecom do
|
||||
@moduledoc """
|
||||
Raisecom device-specific SNMP handling.
|
||||
|
||||
Raisecom manufactures networking equipment widely used in Asia-Pacific WISP deployments,
|
||||
including switches, routers, and wireless access equipment.
|
||||
|
||||
Sensor discovery is primarily handled via YAML profiles (raisecom.yaml, raisecom-ros.yaml)
|
||||
which define processors, memory pools, fan speed, voltage, and state sensors.
|
||||
"""
|
||||
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
# Raisecom enterprise OID: 1.3.6.1.4.1.8886
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["raisecom", "raisecom-ros"]
|
||||
|
||||
@impl true
|
||||
def detect_hardware(client_opts) do
|
||||
# Try to get hardware model from RAISECOM-SYSTEM-MIB
|
||||
case Client.get(client_opts, "1.3.6.1.4.1.8886.6.1.1.1.2.0") do
|
||||
{:ok, hardware} when is_binary(hardware) and byte_size(hardware) > 0 ->
|
||||
String.slice(hardware, 0, 64)
|
||||
|
||||
_ ->
|
||||
# Fallback to sysDescr
|
||||
case Client.get(client_opts, "1.3.6.1.2.1.1.1.0") do
|
||||
{:ok, descr} when is_binary(descr) ->
|
||||
parse_hardware_from_descr(descr)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_hardware_from_descr(descr) do
|
||||
descr
|
||||
|> String.split(~r/[\r\n]/)
|
||||
|> List.first()
|
||||
|> case do
|
||||
nil -> nil
|
||||
line -> String.slice(line, 0, 64)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def wireless_oid_defs do
|
||||
# All Raisecom sensors (CPU, memory, fans, voltage, state) are defined in YAML profiles
|
||||
# No additional wireless-specific sensors needed here
|
||||
[]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def discover_wireless_sensors(client_opts) do
|
||||
# Fetch any static sensors defined in wireless_oid_defs (currently none)
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
mib: CAMBIUM-PMP80211-MIB
|
||||
modules:
|
||||
os:
|
||||
version: CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0
|
||||
serial: CAMBIUM-PMP80211-MIB::cambiumEPMPMSN.0
|
||||
lat: CAMBIUM-PMP80211-MIB::cambiumDeviceLatitude.0
|
||||
long: CAMBIUM-PMP80211-MIB::cambiumDeviceLongitude.0
|
||||
processors:
|
||||
data:
|
||||
-
|
||||
oid: sysCPUUsage
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.64.{{ $index }}'
|
||||
descr: CPU
|
||||
divisor: 10
|
||||
sensors:
|
||||
count:
|
||||
data:
|
||||
-
|
||||
oid: sysDFSDetectedCount
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}'
|
||||
index: sysDFSDetectedCount
|
||||
descr: DFS Detected Count
|
||||
-
|
||||
oid: cambiumGPSNumTrackedSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.84.{{ $index }}'
|
||||
index: cambiumGPSNumTrackedSat
|
||||
descr: GPS Satellites Tracked
|
||||
-
|
||||
oid: cambiumGPSNumVisibleSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.85.{{ $index }}'
|
||||
index: cambiumGPSNumVisibleSat
|
||||
descr: GPS Satellites Visible
|
||||
state:
|
||||
data:
|
||||
-
|
||||
oid: cambiumEffectiveSyncSource
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.7.{{ $index }}'
|
||||
index: cambiumEffectiveSyncSource
|
||||
descr: GPS Sync Status
|
||||
state_name: cambiumEffectiveSyncSource
|
||||
states:
|
||||
- { value: 1, generic: 0, graph: 1, descr: GPS Sync Up }
|
||||
- { value: 2, generic: 2, graph: 1, descr: GPS Sync Down }
|
||||
- { value: 3, generic: 0, graph: 1, descr: CMM4 Sync }
|
||||
- { value: 4, generic: 0, graph: 1, descr: CMM3 Sync }
|
||||
-
|
||||
oid: cambiumDFSStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.6.{{ $index }}'
|
||||
index: cambiumDFSStatus
|
||||
descr: DFS Status
|
||||
state_name: cambiumDFSStatus
|
||||
states:
|
||||
- { value: 1, generic: 1, graph: 1, descr: N/A }
|
||||
- { value: 2, generic: 1, graph: 1, descr: Channel Availability Check }
|
||||
- { value: 3, generic: 0, graph: 1, descr: In-Service }
|
||||
- { value: 4, generic: 2, graph: 1, descr: Radar Signal Detected }
|
||||
- { value: 5, generic: 0, graph: 1, descr: In-Service Monitoring at Alternative Channel }
|
||||
- { value: 6, generic: 2, graph: 1, descr: System Not In Service due to DFS }
|
||||
-
|
||||
oid: ePMP2000SmartAntennaStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.41.{{ $index }}'
|
||||
index: ePMP2000SmartAntennaStatus
|
||||
descr: Smart Antenna Status
|
||||
state_name: ePMP2000SmartAntennaStatus
|
||||
states:
|
||||
- { value: 0, generic: 1, graph: 0, descr: Not Powered }
|
||||
- { value: 1, generic: 3, graph: 0, descr: Powered / Not Detected }
|
||||
- { value: 3, generic: 0, graph: 0, descr: Detected }
|
||||
|
|
@ -10,22 +10,34 @@ modules:
|
|||
-
|
||||
oid: sysCPUUsage
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.64.{{ $index }}'
|
||||
type: cambium-cpu
|
||||
precision: 10
|
||||
descr: CPU
|
||||
divisor: 10
|
||||
sensors:
|
||||
count:
|
||||
data:
|
||||
-
|
||||
oid: sysDFSDetectedCount
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}'
|
||||
descr: DFS Detected Count
|
||||
index: sysDFSDetectedCount
|
||||
descr: DFS Detected Count
|
||||
-
|
||||
oid: cambiumGPSNumTrackedSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.84.{{ $index }}'
|
||||
index: cambiumGPSNumTrackedSat
|
||||
descr: GPS Satellites Tracked
|
||||
-
|
||||
oid: cambiumGPSNumVisibleSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.85.{{ $index }}'
|
||||
index: cambiumGPSNumVisibleSat
|
||||
descr: GPS Satellites Visible
|
||||
state:
|
||||
data:
|
||||
-
|
||||
oid: cambiumEffectiveSyncSource
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.7.{{ $index }}'
|
||||
index: '{{ $index }}'
|
||||
descr: GPS Status
|
||||
index: cambiumEffectiveSyncSource
|
||||
descr: GPS Sync Status
|
||||
state_name: cambiumEffectiveSyncSource
|
||||
states:
|
||||
- { value: 1, generic: 0, graph: 1, descr: GPS Sync Up }
|
||||
- { value: 2, generic: 2, graph: 1, descr: GPS Sync Down }
|
||||
|
|
@ -34,8 +46,9 @@ modules:
|
|||
-
|
||||
oid: cambiumDFSStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.6.{{ $index }}'
|
||||
index: '{{ $index }}'
|
||||
index: cambiumDFSStatus
|
||||
descr: DFS Status
|
||||
state_name: cambiumDFSStatus
|
||||
states:
|
||||
- { value: 1, generic: 1, graph: 1, descr: N/A }
|
||||
- { value: 2, generic: 1, graph: 1, descr: Channel Availability Check }
|
||||
|
|
@ -46,8 +59,9 @@ modules:
|
|||
-
|
||||
oid: ePMP2000SmartAntennaStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.41.{{ $index }}'
|
||||
index: '{{ $index }}'
|
||||
descr: Smart Antenna
|
||||
index: ePMP2000SmartAntennaStatus
|
||||
descr: Smart Antenna Status
|
||||
state_name: ePMP2000SmartAntennaStatus
|
||||
states:
|
||||
- { value: 0, generic: 1, graph: 0, descr: Not Powered }
|
||||
- { value: 1, generic: 3, graph: 0, descr: Powered / Not Detected }
|
||||
|
|
|
|||
112
test/towerops/snmp/profiles/vendors/raisecom_test.exs
vendored
Normal file
112
test/towerops/snmp/profiles/vendors/raisecom_test.exs
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
defmodule Towerops.Snmp.Profiles.Vendors.RaisecomTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.Profiles.Vendors.Raisecom
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@client_opts [
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
describe "profile_names/0" do
|
||||
test "returns expected profile names" do
|
||||
assert Raisecom.profile_names() == ["raisecom", "raisecom-ros"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "detect_hardware/1" do
|
||||
test "returns hardware model from RAISECOM-SYSTEM-MIB when available" do
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.8886.6.1.1.1.2.0" ->
|
||||
{:ok, "RAISECOM ISCOM2128EA-MA"}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert Raisecom.detect_hardware(@client_opts) == "RAISECOM ISCOM2128EA-MA"
|
||||
end
|
||||
|
||||
test "falls back to sysDescr when RAISECOM-SYSTEM-MIB is unavailable" do
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.8886.6.1.1.1.2.0" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, "Raisecom Switch Model XYZ"}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert Raisecom.detect_hardware(@client_opts) == "Raisecom Switch Model XYZ"
|
||||
end
|
||||
|
||||
test "returns nil when all hardware detection methods fail" do
|
||||
stub(SnmpMock, :get, fn _, _, _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert Raisecom.detect_hardware(@client_opts) == nil
|
||||
end
|
||||
|
||||
test "truncates long hardware descriptions to 64 characters" do
|
||||
long_hardware = String.duplicate("A", 100)
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.8886.6.1.1.1.2.0" ->
|
||||
{:ok, long_hardware}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
result = Raisecom.detect_hardware(@client_opts)
|
||||
assert String.length(result) == 64
|
||||
end
|
||||
|
||||
test "handles empty hardware string by falling back to sysDescr" do
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.8886.6.1.1.1.2.0" ->
|
||||
{:ok, ""}
|
||||
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, "Fallback Description"}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert Raisecom.detect_hardware(@client_opts) == "Fallback Description"
|
||||
end
|
||||
end
|
||||
|
||||
describe "wireless_oid_defs/0" do
|
||||
test "returns empty list (all sensors defined in YAML)" do
|
||||
assert Raisecom.wireless_oid_defs() == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_wireless_sensors/1" do
|
||||
test "returns empty list when no static sensors defined" do
|
||||
sensors = Raisecom.discover_wireless_sensors(@client_opts)
|
||||
assert sensors == []
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue