feat: add FortiAP, D-Link AP, and Moxa AWK vendor modules

This commit is contained in:
Graham McIntire 2026-01-22 10:12:49 -06:00
parent 508ad99919
commit 928055cbe3
No known key found for this signature in database
8 changed files with 577 additions and 0 deletions

View file

@ -0,0 +1,73 @@
defmodule Towerops.Snmp.Profiles.Vendors.Dlinkap do
@moduledoc """
D-Link Access Point device-specific SNMP handling.
Supports D-Link wireless access points.
Supported devices:
- DAP series (DAP-2660, DAP-2695, etc.)
- DWL series
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
alias Towerops.Snmp.Client
alias Towerops.Snmp.Profiles.Vendors.Vendor
# D-Link Enterprise OID: 1.3.6.1.4.1.171
@model_oid "1.3.6.1.4.1.171.10.37.1.1.1.0"
@impl true
def profile_names, do: ["dlinkap"]
@impl true
def detect_hardware(client_opts) do
case Client.get(client_opts, @model_oid) do
{:ok, model} when is_binary(model) -> model
_ -> 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
[
# Total Associated Clients
%{
oid: "1.3.6.1.4.1.171.10.37.1.2.1.0",
sensor_type: "clients",
sensor_descr: "Associated Clients",
sensor_unit: "",
sensor_divisor: 1
},
# 2.4GHz Radio Clients
%{
oid: "1.3.6.1.4.1.171.10.37.1.2.2.0",
sensor_type: "clients",
sensor_descr: "2.4GHz Clients",
sensor_unit: "",
sensor_divisor: 1
},
# 5GHz Radio Clients
%{
oid: "1.3.6.1.4.1.171.10.37.1.2.3.0",
sensor_type: "clients",
sensor_descr: "5GHz Clients",
sensor_unit: "",
sensor_divisor: 1
},
# CPU Utilization
%{
oid: "1.3.6.1.4.1.171.10.37.1.3.1.0",
sensor_type: "load",
sensor_descr: "CPU Utilization",
sensor_unit: "%",
sensor_divisor: 1
}
]
end
end

View file

@ -0,0 +1,82 @@
defmodule Towerops.Snmp.Profiles.Vendors.Fortiap do
@moduledoc """
Fortinet FortiAP device-specific SNMP handling.
Supports FortiAP wireless access points.
Supported devices:
- FortiAP series (221E, 231F, 431F, etc.)
- FortiAP-S series
- FortiAP-U series
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
alias Towerops.Snmp.Client
alias Towerops.Snmp.Profiles.Vendors.Vendor
# Fortinet Enterprise OID: 1.3.6.1.4.1.12356
@model_oid "1.3.6.1.4.1.12356.120.10.1.1.0"
@impl true
def profile_names, do: ["fortiap"]
@impl true
def detect_hardware(client_opts) do
case Client.get(client_opts, @model_oid) do
{:ok, model} when is_binary(model) -> model
_ -> 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
[
# Total Associated Clients
%{
oid: "1.3.6.1.4.1.12356.120.10.2.1.0",
sensor_type: "clients",
sensor_descr: "Associated Clients",
sensor_unit: "",
sensor_divisor: 1
},
# CPU Utilization
%{
oid: "1.3.6.1.4.1.12356.120.10.3.1.0",
sensor_type: "load",
sensor_descr: "CPU Utilization",
sensor_unit: "%",
sensor_divisor: 1
},
# Memory Utilization
%{
oid: "1.3.6.1.4.1.12356.120.10.3.2.0",
sensor_type: "load",
sensor_descr: "Memory Utilization",
sensor_unit: "%",
sensor_divisor: 1
},
# 2.4GHz Radio Clients
%{
oid: "1.3.6.1.4.1.12356.120.10.2.2.0",
sensor_type: "clients",
sensor_descr: "2.4GHz Clients",
sensor_unit: "",
sensor_divisor: 1
},
# 5GHz Radio Clients
%{
oid: "1.3.6.1.4.1.12356.120.10.2.3.0",
sensor_type: "clients",
sensor_descr: "5GHz Clients",
sensor_unit: "",
sensor_divisor: 1
}
]
end
end

View file

@ -0,0 +1,73 @@
defmodule Towerops.Snmp.Profiles.Vendors.MoxaAwk do
@moduledoc """
Moxa AWK industrial wireless device-specific SNMP handling.
Supports Moxa industrial wireless access points and clients.
Supported devices:
- AWK series (AWK-3131A, AWK-4131A, etc.)
- AWK-1137C industrial wireless
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
alias Towerops.Snmp.Client
alias Towerops.Snmp.Profiles.Vendors.Vendor
# Moxa Enterprise OID: 1.3.6.1.4.1.8691
@model_oid "1.3.6.1.4.1.8691.15.34.1.1.0"
@impl true
def profile_names, do: ["moxa-awk"]
@impl true
def detect_hardware(client_opts) do
case Client.get(client_opts, @model_oid) do
{:ok, model} when is_binary(model) -> model
_ -> 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
[
# Associated Clients
%{
oid: "1.3.6.1.4.1.8691.15.34.2.1.0",
sensor_type: "clients",
sensor_descr: "Associated Clients",
sensor_unit: "",
sensor_divisor: 1
},
# CPU Utilization
%{
oid: "1.3.6.1.4.1.8691.15.34.3.1.0",
sensor_type: "load",
sensor_descr: "CPU Utilization",
sensor_unit: "%",
sensor_divisor: 1
},
# Memory Utilization
%{
oid: "1.3.6.1.4.1.8691.15.34.3.2.0",
sensor_type: "load",
sensor_descr: "Memory Utilization",
sensor_unit: "%",
sensor_divisor: 1
},
# Wireless Signal Strength
%{
oid: "1.3.6.1.4.1.8691.15.34.2.2.0",
sensor_type: "rssi",
sensor_descr: "Signal Strength",
sensor_unit: "dBm",
sensor_divisor: 1
}
]
end
end

View file

@ -22,13 +22,16 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do
alias Towerops.Snmp.Profiles.Vendors.Cnpilot
alias Towerops.Snmp.Profiles.Vendors.Cnwave60
alias Towerops.Snmp.Profiles.Vendors.Deliberant
alias Towerops.Snmp.Profiles.Vendors.Dlinkap
alias Towerops.Snmp.Profiles.Vendors.Dragonwave
alias Towerops.Snmp.Profiles.Vendors.Engenius
alias Towerops.Snmp.Profiles.Vendors.Epmp
alias Towerops.Snmp.Profiles.Vendors.Exalt
alias Towerops.Snmp.Profiles.Vendors.Fortiap
alias Towerops.Snmp.Profiles.Vendors.Grandstream
alias Towerops.Snmp.Profiles.Vendors.Ligoos
alias Towerops.Snmp.Profiles.Vendors.Mimosa
alias Towerops.Snmp.Profiles.Vendors.MoxaAwk
alias Towerops.Snmp.Profiles.Vendors.Netonix
alias Towerops.Snmp.Profiles.Vendors.Pmp
alias Towerops.Snmp.Profiles.Vendors.Proxim
@ -63,13 +66,16 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do
Cnpilot,
Cnwave60,
Deliberant,
Dlinkap,
Dragonwave,
Engenius,
Epmp,
Exalt,
Fortiap,
Grandstream,
Ligoos,
Mimosa,
MoxaAwk,
Netonix,
Pmp,
Proxim,

View file

@ -0,0 +1,105 @@
defmodule Towerops.Snmp.Profiles.Vendors.DlinkapTest do
use Towerops.DataCase, async: true
import Mox
alias Towerops.Snmp.Profiles.Vendors.Dlinkap
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 dlinkap profile name" do
assert Dlinkap.profile_names() == ["dlinkap"]
end
end
describe "detect_hardware/1" do
test "detects hardware from model OID" do
expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.171.10.37.1.1.1.0", _ ->
{:ok, "DAP-2660"}
end)
assert Dlinkap.detect_hardware(@client_opts) == "DAP-2660"
end
test "returns nil when OID unavailable" do
expect(SnmpMock, :get, fn _, _, _ ->
{:error, :no_such_object}
end)
assert Dlinkap.detect_hardware(@client_opts) == nil
end
end
describe "wireless_oid_defs/0" do
test "returns list of wireless sensor definitions" do
defs = Dlinkap.wireless_oid_defs()
assert is_list(defs)
assert [_ | _] = defs
end
test "includes associated clients sensor" do
defs = Dlinkap.wireless_oid_defs()
clients = Enum.find(defs, &(&1.sensor_descr == "Associated Clients"))
assert clients
assert clients.sensor_type == "clients"
end
test "includes CPU sensor" do
defs = Dlinkap.wireless_oid_defs()
cpu = Enum.find(defs, &(&1.sensor_descr == "CPU Utilization"))
assert cpu
assert cpu.sensor_type == "load"
end
test "includes band-specific client sensors" do
defs = Dlinkap.wireless_oid_defs()
clients_2g = Enum.find(defs, &(&1.sensor_descr == "2.4GHz Clients"))
clients_5g = Enum.find(defs, &(&1.sensor_descr == "5GHz Clients"))
assert clients_2g
assert clients_5g
end
end
describe "discover_wireless_sensors/1" do
test "discovers sensors when SNMP responds" do
expect(SnmpMock, :get, 4, fn _, oid, _ ->
cond do
String.contains?(oid, "171.10.37.1.2.1.0") -> {:ok, 85}
String.contains?(oid, "171.10.37.1.2.2.0") -> {:ok, 40}
String.contains?(oid, "171.10.37.1.2.3.0") -> {:ok, 45}
String.contains?(oid, "171.10.37.1.3.1.0") -> {:ok, 30}
true -> {:error, :no_such_object}
end
end)
sensors = Dlinkap.discover_wireless_sensors(@client_opts)
assert is_list(sensors)
assert length(sensors) == 4
end
test "returns empty list when no sensors respond" do
expect(SnmpMock, :get, 4, fn _, _, _ ->
{:error, :no_such_object}
end)
sensors = Dlinkap.discover_wireless_sensors(@client_opts)
assert sensors == []
end
end
end

View file

@ -0,0 +1,109 @@
defmodule Towerops.Snmp.Profiles.Vendors.FortiapTest do
use Towerops.DataCase, async: true
import Mox
alias Towerops.Snmp.Profiles.Vendors.Fortiap
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 fortiap profile name" do
assert Fortiap.profile_names() == ["fortiap"]
end
end
describe "detect_hardware/1" do
test "detects hardware from model OID" do
expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.12356.120.10.1.1.0", _ ->
{:ok, "FAP-431F"}
end)
assert Fortiap.detect_hardware(@client_opts) == "FAP-431F"
end
test "returns nil when OID unavailable" do
expect(SnmpMock, :get, fn _, _, _ ->
{:error, :no_such_object}
end)
assert Fortiap.detect_hardware(@client_opts) == nil
end
end
describe "wireless_oid_defs/0" do
test "returns list of wireless sensor definitions" do
defs = Fortiap.wireless_oid_defs()
assert is_list(defs)
assert [_ | _] = defs
end
test "includes associated clients sensor" do
defs = Fortiap.wireless_oid_defs()
clients = Enum.find(defs, &(&1.sensor_descr == "Associated Clients"))
assert clients
assert clients.sensor_type == "clients"
end
test "includes CPU and memory sensors" do
defs = Fortiap.wireless_oid_defs()
cpu = Enum.find(defs, &(&1.sensor_descr == "CPU Utilization"))
mem = Enum.find(defs, &(&1.sensor_descr == "Memory Utilization"))
assert cpu
assert cpu.sensor_type == "load"
assert mem
assert mem.sensor_type == "load"
end
test "includes band-specific client sensors" do
defs = Fortiap.wireless_oid_defs()
clients_2g = Enum.find(defs, &(&1.sensor_descr == "2.4GHz Clients"))
clients_5g = Enum.find(defs, &(&1.sensor_descr == "5GHz Clients"))
assert clients_2g
assert clients_5g
end
end
describe "discover_wireless_sensors/1" do
test "discovers sensors when SNMP responds" do
expect(SnmpMock, :get, 5, fn _, oid, _ ->
cond do
String.contains?(oid, "12356.120.10.2.1.0") -> {:ok, 120}
String.contains?(oid, "12356.120.10.3.1.0") -> {:ok, 25}
String.contains?(oid, "12356.120.10.3.2.0") -> {:ok, 55}
String.contains?(oid, "12356.120.10.2.2.0") -> {:ok, 50}
String.contains?(oid, "12356.120.10.2.3.0") -> {:ok, 70}
true -> {:error, :no_such_object}
end
end)
sensors = Fortiap.discover_wireless_sensors(@client_opts)
assert is_list(sensors)
assert length(sensors) == 5
end
test "returns empty list when no sensors respond" do
expect(SnmpMock, :get, 5, fn _, _, _ ->
{:error, :no_such_object}
end)
sensors = Fortiap.discover_wireless_sensors(@client_opts)
assert sensors == []
end
end
end

View file

@ -0,0 +1,108 @@
defmodule Towerops.Snmp.Profiles.Vendors.MoxaAwkTest do
use Towerops.DataCase, async: true
import Mox
alias Towerops.Snmp.Profiles.Vendors.MoxaAwk
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 moxa-awk profile name" do
assert MoxaAwk.profile_names() == ["moxa-awk"]
end
end
describe "detect_hardware/1" do
test "detects hardware from model OID" do
expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.8691.15.34.1.1.0", _ ->
{:ok, "AWK-3131A"}
end)
assert MoxaAwk.detect_hardware(@client_opts) == "AWK-3131A"
end
test "returns nil when OID unavailable" do
expect(SnmpMock, :get, fn _, _, _ ->
{:error, :no_such_object}
end)
assert MoxaAwk.detect_hardware(@client_opts) == nil
end
end
describe "wireless_oid_defs/0" do
test "returns list of wireless sensor definitions" do
defs = MoxaAwk.wireless_oid_defs()
assert is_list(defs)
assert [_ | _] = defs
end
test "includes associated clients sensor" do
defs = MoxaAwk.wireless_oid_defs()
clients = Enum.find(defs, &(&1.sensor_descr == "Associated Clients"))
assert clients
assert clients.sensor_type == "clients"
end
test "includes CPU and memory sensors" do
defs = MoxaAwk.wireless_oid_defs()
cpu = Enum.find(defs, &(&1.sensor_descr == "CPU Utilization"))
mem = Enum.find(defs, &(&1.sensor_descr == "Memory Utilization"))
assert cpu
assert cpu.sensor_type == "load"
assert mem
assert mem.sensor_type == "load"
end
test "includes signal strength sensor" do
defs = MoxaAwk.wireless_oid_defs()
signal = Enum.find(defs, &(&1.sensor_descr == "Signal Strength"))
assert signal
assert signal.sensor_type == "rssi"
assert signal.sensor_unit == "dBm"
end
end
describe "discover_wireless_sensors/1" do
test "discovers sensors when SNMP responds" do
expect(SnmpMock, :get, 4, fn _, oid, _ ->
cond do
String.contains?(oid, "8691.15.34.2.1.0") -> {:ok, 12}
String.contains?(oid, "8691.15.34.3.1.0") -> {:ok, 25}
String.contains?(oid, "8691.15.34.3.2.0") -> {:ok, 60}
String.contains?(oid, "8691.15.34.2.2.0") -> {:ok, -65}
true -> {:error, :no_such_object}
end
end)
sensors = MoxaAwk.discover_wireless_sensors(@client_opts)
assert is_list(sensors)
assert length(sensors) == 4
end
test "returns empty list when no sensors respond" do
expect(SnmpMock, :get, 4, fn _, _, _ ->
{:error, :no_such_object}
end)
sensors = MoxaAwk.discover_wireless_sensors(@client_opts)
assert sensors == []
end
end
end

View file

@ -9,11 +9,14 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
alias Towerops.Snmp.Profiles.Vendors.Alvarion
alias Towerops.Snmp.Profiles.Vendors.Cnpilot
alias Towerops.Snmp.Profiles.Vendors.Deliberant
alias Towerops.Snmp.Profiles.Vendors.Dlinkap
alias Towerops.Snmp.Profiles.Vendors.Dragonwave
alias Towerops.Snmp.Profiles.Vendors.Epmp
alias Towerops.Snmp.Profiles.Vendors.Exalt
alias Towerops.Snmp.Profiles.Vendors.Fortiap
alias Towerops.Snmp.Profiles.Vendors.Ligoos
alias Towerops.Snmp.Profiles.Vendors.Mimosa
alias Towerops.Snmp.Profiles.Vendors.MoxaAwk
alias Towerops.Snmp.Profiles.Vendors.Pmp
alias Towerops.Snmp.Profiles.Vendors.Proxim
alias Towerops.Snmp.Profiles.Vendors.Racom
@ -68,6 +71,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert Stellar in vendors
assert Xirrus in vendors
assert ZyxelWlc in vendors
assert Dlinkap in vendors
assert Fortiap in vendors
assert MoxaAwk in vendors
end
end
@ -101,6 +107,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert "stellar" in names
assert "xirrus_aos" in names
assert "zyxelwlc" in names
assert "dlinkap" in names
assert "fortiap" in names
assert "moxa-awk" in names
end
end
@ -201,6 +210,18 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert Registry.get_vendor("zyxelwlc") == ZyxelWlc
end
test "returns Dlinkap for dlinkap profile" do
assert Registry.get_vendor("dlinkap") == Dlinkap
end
test "returns Fortiap for fortiap profile" do
assert Registry.get_vendor("fortiap") == Fortiap
end
test "returns MoxaAwk for moxa-awk profile" do
assert Registry.get_vendor("moxa-awk") == MoxaAwk
end
test "returns nil for unknown profile" do
assert Registry.get_vendor("unknown_vendor") == nil
end