diff --git a/lib/towerops/snmp/profiles/vendors/raisecom.ex b/lib/towerops/snmp/profiles/vendors/raisecom.ex new file mode 100644 index 00000000..329ea2f4 --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/raisecom.ex @@ -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 diff --git a/priv/profiles/os_discovery/cambium-epmp.yaml b/priv/profiles/os_discovery/cambium-epmp.yaml deleted file mode 100644 index ee7907c3..00000000 --- a/priv/profiles/os_discovery/cambium-epmp.yaml +++ /dev/null @@ -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 } diff --git a/priv/profiles/os_discovery/epmp.yaml b/priv/profiles/os_discovery/epmp.yaml index 98bb9980..ee7907c3 100644 --- a/priv/profiles/os_discovery/epmp.yaml +++ b/priv/profiles/os_discovery/epmp.yaml @@ -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 } diff --git a/test/towerops/snmp/profiles/vendors/raisecom_test.exs b/test/towerops/snmp/profiles/vendors/raisecom_test.exs new file mode 100644 index 00000000..f9334737 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/raisecom_test.exs @@ -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