- Create snmp_ip_addresses table with migration - Add IpAddress schema with ip_type validation (ipv4/ipv6) - Add prefix_length validation based on ip_type - Implement discover_ip_addresses/1 in Base profile - Extract IP addresses from IP-MIB ipAdEntTable - Support multiple IPs per interface with subnet masks - Add 12 schema tests and 4 discovery tests
774 lines
26 KiB
Elixir
774 lines
26 KiB
Elixir
defmodule Towerops.Snmp.Profiles.BaseTest do
|
||
use Towerops.DataCase, async: true
|
||
|
||
import Mox
|
||
|
||
alias Towerops.Snmp.Profiles.Base
|
||
alias Towerops.Snmp.SnmpMock
|
||
|
||
setup :verify_on_exit!
|
||
|
||
@client_opts [
|
||
ip: "192.168.1.1",
|
||
community: "public",
|
||
version: "2c",
|
||
port: 161,
|
||
timeout: 5000
|
||
]
|
||
|
||
describe "discover_system_info/1" do
|
||
test "successfully discovers system information" do
|
||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.1.1.0" ->
|
||
{:ok, {:octet_string, "Linux server 5.4.0-42-generic"}}
|
||
|
||
"1.3.6.1.2.1.1.2.0" ->
|
||
{:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 8072, 3, 2, 10]}}
|
||
|
||
"1.3.6.1.2.1.1.3.0" ->
|
||
{:ok, {:timeticks, 123_456_789}}
|
||
|
||
"1.3.6.1.2.1.1.4.0" ->
|
||
{:ok, {:octet_string, "admin@example.com"}}
|
||
|
||
"1.3.6.1.2.1.1.5.0" ->
|
||
{:ok, {:octet_string, "webserver01"}}
|
||
|
||
"1.3.6.1.2.1.1.6.0" ->
|
||
{:ok, {:octet_string, "DC1 Rack 42"}}
|
||
|
||
_ ->
|
||
{:error, :no_such_object}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, system_info} = Base.discover_system_info(@client_opts)
|
||
|
||
assert system_info.sys_descr == "Linux server 5.4.0-42-generic"
|
||
assert system_info.sys_object_id == "1.3.6.1.4.1.8072.3.2.10"
|
||
assert system_info.sys_name == "webserver01"
|
||
assert system_info.sys_uptime == 123_456_789
|
||
assert system_info.sys_contact == "admin@example.com"
|
||
assert system_info.sys_location == "DC1 Rack 42"
|
||
end
|
||
|
||
test "handles partial failure gracefully" do
|
||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.1.1.0" ->
|
||
{:ok, {:octet_string, "Test Device"}}
|
||
|
||
_ ->
|
||
{:error, :no_such_object}
|
||
end
|
||
end)
|
||
|
||
# Should fail because get_multiple returns error on any failure
|
||
assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
|
||
end
|
||
|
||
test "handles timeout errors" do
|
||
expect(SnmpMock, :get, 6, fn _, _, _ ->
|
||
{:error, :timeout}
|
||
end)
|
||
|
||
assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
|
||
end
|
||
end
|
||
|
||
describe "discover_interfaces/1" do
|
||
test "successfully discovers multiple interfaces" do
|
||
# Mock ifIndex walk
|
||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.2.2.1.1.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.2.2.1.1.2", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.2.2.1.1.3", value: {:integer, 3}}
|
||
]}
|
||
end)
|
||
|
||
# Mock interface data for each interface (8 OIDs per interface × 3 interfaces = 24 calls)
|
||
expect(SnmpMock, :get, 24, fn _, oid, _ ->
|
||
cond do
|
||
String.ends_with?(oid, ".1") ->
|
||
parts = String.split(oid, ".")
|
||
second_to_last = Enum.at(parts, -2)
|
||
|
||
cond do
|
||
second_to_last == "2" -> {:ok, {:octet_string, "eth0"}}
|
||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||
second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01>>}}
|
||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "eth0"}}
|
||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "WAN"}}
|
||
true -> {:error, :no_such_object}
|
||
end
|
||
|
||
String.ends_with?(oid, ".2") ->
|
||
parts = String.split(oid, ".")
|
||
second_to_last = Enum.at(parts, -2)
|
||
|
||
cond do
|
||
second_to_last == "2" -> {:ok, {:octet_string, "eth1"}}
|
||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||
second_to_last == "5" -> {:ok, {:gauge32, 100_000_000}}
|
||
second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02>>}}
|
||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||
second_to_last == "8" -> {:ok, {:integer, 2}}
|
||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "eth1"}}
|
||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
|
||
true -> {:error, :no_such_object}
|
||
end
|
||
|
||
String.ends_with?(oid, ".3") ->
|
||
parts = String.split(oid, ".")
|
||
second_to_last = Enum.at(parts, -2)
|
||
|
||
cond do
|
||
second_to_last == "2" -> {:ok, {:octet_string, "lo"}}
|
||
second_to_last == "3" -> {:ok, {:integer, 24}}
|
||
second_to_last == "5" -> {:ok, {:gauge32, 10_000_000}}
|
||
second_to_last == "6" -> {:ok, {:octet_string, <<>>}}
|
||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "lo"}}
|
||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "Loopback"}}
|
||
true -> {:error, :no_such_object}
|
||
end
|
||
|
||
true ->
|
||
{:error, :no_such_object}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)
|
||
|
||
assert length(interfaces) == 3
|
||
|
||
eth0 = Enum.find(interfaces, &(&1.if_index == 1))
|
||
assert eth0.if_descr == "eth0"
|
||
assert eth0.if_name == "eth0"
|
||
assert eth0.if_alias == "WAN"
|
||
assert eth0.if_type == 6
|
||
assert eth0.if_speed == 1_000_000_000
|
||
assert eth0.if_phys_address == "aa:bb:cc:dd:ee:01"
|
||
assert eth0.if_admin_status == "up"
|
||
assert eth0.if_oper_status == "up"
|
||
|
||
eth1 = Enum.find(interfaces, &(&1.if_index == 2))
|
||
assert eth1.if_descr == "eth1"
|
||
assert eth1.if_oper_status == "down"
|
||
|
||
lo = Enum.find(interfaces, &(&1.if_index == 3))
|
||
assert lo.if_descr == "lo"
|
||
assert lo.if_phys_address == nil
|
||
end
|
||
|
||
test "handles empty interface list" do
|
||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||
{:ok, []}
|
||
end)
|
||
|
||
assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)
|
||
assert interfaces == []
|
||
end
|
||
|
||
test "handles walk failure" do
|
||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||
{:error, :timeout}
|
||
end)
|
||
|
||
assert {:error, :timeout} = Base.discover_interfaces(@client_opts)
|
||
end
|
||
end
|
||
|
||
describe "discover_sensors/1" do
|
||
test "discovers sensors from ENTITY-SENSOR-MIB" do
|
||
# Mock sensor type walk and ENTITY-MIB description walks
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.99.1.1.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
|
||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 3}}
|
||
]}
|
||
|
||
# ENTITY-MIB walks for descriptions (return empty for simplicity)
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
# Mock sensor data for each sensor (5 OIDs per sensor × 2 sensors = 10 calls)
|
||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||
cond do
|
||
String.ends_with?(oid, ".1") ->
|
||
case oid |> String.split(".") |> Enum.at(-2) do
|
||
"1" -> {:ok, {:integer, 8}}
|
||
"2" -> {:ok, {:integer, 9}}
|
||
"3" -> {:ok, {:integer, 0}}
|
||
"4" -> {:ok, {:integer, 45}}
|
||
"5" -> {:ok, {:integer, 1}}
|
||
_ -> {:error, :no_such_object}
|
||
end
|
||
|
||
String.ends_with?(oid, ".2") ->
|
||
case oid |> String.split(".") |> Enum.at(-2) do
|
||
"1" -> {:ok, {:integer, 3}}
|
||
"2" -> {:ok, {:integer, 6}}
|
||
"3" -> {:ok, {:integer, 3}}
|
||
"4" -> {:ok, {:integer, 12_000}}
|
||
"5" -> {:ok, {:integer, 1}}
|
||
_ -> {:error, :no_such_object}
|
||
end
|
||
|
||
true ->
|
||
{:error, :no_such_object}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||
|
||
assert length(sensors) == 2
|
||
|
||
temp_sensor = Enum.find(sensors, &(&1.sensor_index == "1"))
|
||
assert temp_sensor.sensor_type == "celsius"
|
||
assert temp_sensor.sensor_unit == "°C"
|
||
assert temp_sensor.last_value == 45.0
|
||
assert temp_sensor.status == "ok"
|
||
|
||
voltage_sensor = Enum.find(sensors, &(&1.sensor_index == "2"))
|
||
assert voltage_sensor.sensor_type == "volts"
|
||
assert voltage_sensor.sensor_unit == "V"
|
||
assert voltage_sensor.last_value == 12_000.0
|
||
end
|
||
|
||
test "returns empty list when ENTITY-SENSOR-MIB not supported" do
|
||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||
{:ok, []}
|
||
end)
|
||
|
||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||
assert sensors == []
|
||
end
|
||
|
||
test "returns empty list on walk error" do
|
||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||
{:error, :no_such_object}
|
||
end)
|
||
|
||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||
assert sensors == []
|
||
end
|
||
|
||
test "filters out sensors with missing data" do
|
||
# Mock sensor type walk and ENTITY-MIB description walks
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.99.1.1.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
|
||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 8}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
# First sensor succeeds, second fails (5 OIDs per sensor)
|
||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||
if String.ends_with?(oid, ".1") do
|
||
case oid |> String.split(".") |> Enum.at(-2) do
|
||
"1" -> {:ok, {:integer, 8}}
|
||
"2" -> {:ok, {:integer, 9}}
|
||
"3" -> {:ok, {:integer, 0}}
|
||
"4" -> {:ok, {:integer, 42}}
|
||
"5" -> {:ok, {:integer, 1}}
|
||
_ -> {:error, :no_such_object}
|
||
end
|
||
else
|
||
{:error, :timeout}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||
|
||
# Should only have the successful sensor
|
||
assert length(sensors) == 1
|
||
assert hd(sensors).sensor_index == "1"
|
||
end
|
||
end
|
||
|
||
describe "identify_device/1" do
|
||
test "identifies Cisco device from sysDescr" do
|
||
system_info = %{
|
||
sys_descr: "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(2)SE"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.manufacturer == "Cisco"
|
||
assert device_info.model == "C2960"
|
||
end
|
||
|
||
test "identifies Linux server from sysDescr" do
|
||
system_info = %{
|
||
sys_descr: "Linux server 5.4.0-42-generic #46-Ubuntu SMP"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.manufacturer == "Linux"
|
||
assert device_info.model == "Server"
|
||
end
|
||
|
||
test "identifies Windows server from sysDescr" do
|
||
system_info = %{
|
||
sys_descr: "Windows Server 2019 Standard"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.manufacturer == "Microsoft"
|
||
assert device_info.model == "Windows Server"
|
||
end
|
||
|
||
test "falls back to unknown for unrecognized devices" do
|
||
system_info = %{
|
||
sys_descr: "Some Unknown Device"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.manufacturer == "Unknown"
|
||
assert device_info.model == "Generic Device"
|
||
end
|
||
|
||
test "extracts Cisco model from complex sysDescr" do
|
||
system_info = %{
|
||
sys_descr: "Cisco IOS Software, WS-C3750X Software (cat3k_caa-universalk9-M), Version 15.2(4)E1"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.manufacturer == "Cisco"
|
||
assert device_info.model == "WS-C3750X"
|
||
end
|
||
|
||
test "preserves other system_info fields" do
|
||
system_info = %{
|
||
sys_descr: "Linux server",
|
||
sys_name: "webserver01",
|
||
sys_contact: "admin@example.com",
|
||
sys_location: "DC1"
|
||
}
|
||
|
||
device_info = Base.identify_device(system_info)
|
||
|
||
assert device_info.sys_name == "webserver01"
|
||
assert device_info.sys_contact == "admin@example.com"
|
||
assert device_info.sys_location == "DC1"
|
||
end
|
||
end
|
||
|
||
describe "discover_state_sensors/1" do
|
||
test "discovers state sensors from ENTITY-STATE-MIB" do
|
||
# Mock the walk for entity physical class to find modules/power supplies
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
# entPhysicalClass walk - find entities by class
|
||
"1.3.6.1.2.1.47.1.1.1.1.5" ->
|
||
{:ok,
|
||
[
|
||
# powerSupply = 6, fan = 7, sensor = 8, module = 9
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1001", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1002", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2001", value: {:integer, 7}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2002", value: {:integer, 7}}
|
||
]}
|
||
|
||
# entPhysicalDescr walk - descriptions
|
||
"1.3.6.1.2.1.47.1.1.1.1.2" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1001", value: {:octet_string, "Power Supply 1"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1002", value: {:octet_string, "Power Supply 2"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2001", value: {:octet_string, "Fan Tray 1"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2002", value: {:octet_string, "Fan Tray 2"}}
|
||
]}
|
||
|
||
# ENTITY-STATE-MIB operational status walk
|
||
"1.3.6.1.2.1.131.1.1.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.1001", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.1002", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.2001", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.2002", value: {:integer, 3}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)
|
||
|
||
assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
|
||
|
||
assert length(state_sensors) == 4
|
||
|
||
psu1 = Enum.find(state_sensors, &(&1.sensor_index == "1001"))
|
||
assert psu1.sensor_descr == "Power Supply 1"
|
||
assert psu1.entity_type == "power_supply"
|
||
assert psu1.state_value == 2
|
||
assert psu1.status == "ok"
|
||
|
||
fan2 = Enum.find(state_sensors, &(&1.sensor_index == "2002"))
|
||
assert fan2.sensor_descr == "Fan Tray 2"
|
||
assert fan2.entity_type == "fan"
|
||
assert fan2.state_value == 3
|
||
assert fan2.status == "warning"
|
||
end
|
||
|
||
test "returns empty list when ENTITY-STATE-MIB not supported" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||
|
||
assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
|
||
assert state_sensors == []
|
||
end
|
||
|
||
test "handles walk errors gracefully" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)
|
||
|
||
assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
|
||
assert state_sensors == []
|
||
end
|
||
|
||
test "filters only power supplies and fans by default" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.47.1.1.1.1.5" ->
|
||
{:ok,
|
||
[
|
||
# chassis = 3, container = 5, powerSupply = 6, fan = 7
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1", value: {:integer, 3}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.100", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.200", value: {:integer, 7}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.300", value: {:integer, 5}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.47.1.1.1.1.2" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "Chassis"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.100", value: {:octet_string, "PSU"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.200", value: {:octet_string, "Fan"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.300", value: {:octet_string, "Container"}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.131.1.1.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.1", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.100", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.200", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.300", value: {:integer, 2}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)
|
||
|
||
assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
|
||
|
||
# Should only include power supplies and fans (not chassis or container)
|
||
assert length(state_sensors) == 2
|
||
entity_types = Enum.map(state_sensors, & &1.entity_type)
|
||
assert "power_supply" in entity_types
|
||
assert "fan" in entity_types
|
||
refute "chassis" in entity_types
|
||
refute "container" in entity_types
|
||
end
|
||
|
||
test "maps ENTITY-STATE-MIB operational status to status correctly" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.47.1.1.1.1.5" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.3", value: {:integer, 6}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.5.4", value: {:integer, 6}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.47.1.1.1.1.2" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "PSU1"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2", value: {:octet_string, "PSU2"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.3", value: {:octet_string, "PSU3"}},
|
||
%{oid: "1.3.6.1.2.1.47.1.1.1.1.2.4", value: {:octet_string, "PSU4"}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.131.1.1.1.1" ->
|
||
{:ok,
|
||
[
|
||
# 1=unknown, 2=enabled(ok), 3=disabled(warning), 4=testing(unknown)
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.2", value: {:integer, 2}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.3", value: {:integer, 3}},
|
||
%{oid: "1.3.6.1.2.1.131.1.1.1.1.4", value: {:integer, 4}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)
|
||
|
||
assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
|
||
|
||
statuses = Map.new(state_sensors, &{&1.sensor_index, &1.status})
|
||
assert statuses["1"] == "unknown"
|
||
assert statuses["2"] == "ok"
|
||
assert statuses["3"] == "warning"
|
||
assert statuses["4"] == "unknown"
|
||
end
|
||
end
|
||
|
||
describe "discover_vlans/1" do
|
||
test "discovers VLANs from Q-BRIDGE-MIB" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
# dot1qVlanStaticName - VLAN names
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.10", value: {:octet_string, "Management"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.100", value: {:octet_string, "Production"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.200", value: {:octet_string, "Guest"}}
|
||
]}
|
||
|
||
# dot1qVlanStaticRowStatus - VLAN status
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.5" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.10", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.100", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.200", value: {:integer, 2}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, vlans} = Base.discover_vlans(@client_opts)
|
||
|
||
assert length(vlans) == 4
|
||
|
||
vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
|
||
assert vlan1.vlan_name == "default"
|
||
assert vlan1.status == "active"
|
||
|
||
vlan100 = Enum.find(vlans, &(&1.vlan_id == 100))
|
||
assert vlan100.vlan_name == "Production"
|
||
assert vlan100.status == "active"
|
||
|
||
vlan200 = Enum.find(vlans, &(&1.vlan_id == 200))
|
||
assert vlan200.vlan_name == "Guest"
|
||
assert vlan200.status == "suspended"
|
||
end
|
||
|
||
test "returns empty list when Q-BRIDGE-MIB not supported" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||
|
||
assert {:ok, vlans} = Base.discover_vlans(@client_opts)
|
||
assert vlans == []
|
||
end
|
||
|
||
test "handles walk errors gracefully" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)
|
||
|
||
assert {:ok, vlans} = Base.discover_vlans(@client_opts)
|
||
assert vlans == []
|
||
end
|
||
|
||
test "handles VLANs with missing names" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
# dot1qVlanStaticName - Only some VLANs have names
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.10", value: {:octet_string, "Management"}}
|
||
]}
|
||
|
||
# dot1qVlanStaticRowStatus - More VLANs exist than have names
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.5" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.10", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.50", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.100", value: {:integer, 1}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, vlans} = Base.discover_vlans(@client_opts)
|
||
|
||
# Only 2 VLANs from the name walk (status-only VLANs require fallback)
|
||
assert length(vlans) == 2
|
||
|
||
# Named VLANs
|
||
vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
|
||
assert vlan1.vlan_name == "default"
|
||
|
||
vlan10 = Enum.find(vlans, &(&1.vlan_id == 10))
|
||
assert vlan10.vlan_name == "Management"
|
||
end
|
||
|
||
test "validates VLAN ID range (1-4094)" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.1" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.0", value: {:octet_string, "invalid"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.4094", value: {:octet_string, "max"}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.4095", value: {:octet_string, "too_high"}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.17.7.1.4.3.1.5" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.0", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.4094", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.4095", value: {:integer, 1}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, vlans} = Base.discover_vlans(@client_opts)
|
||
|
||
vlan_ids = Enum.map(vlans, & &1.vlan_id)
|
||
|
||
# Should include valid VLANs
|
||
assert 1 in vlan_ids
|
||
assert 4094 in vlan_ids
|
||
|
||
# Should exclude invalid VLANs (0 and 4095+)
|
||
refute 0 in vlan_ids
|
||
refute 4095 in vlan_ids
|
||
end
|
||
end
|
||
|
||
describe "discover_ip_addresses/1" do
|
||
test "discovers IPv4 addresses from IP-MIB" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
# ipAdEntIfIndex - maps IP to interface index
|
||
"1.3.6.1.2.1.4.20.1.2" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.1", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.100", value: {:integer, 1}},
|
||
%{oid: "1.3.6.1.2.1.4.20.1.2.10.0.0.1", value: {:integer, 2}}
|
||
]}
|
||
|
||
# ipAdEntNetMask - subnet masks
|
||
"1.3.6.1.2.1.4.20.1.3" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.4.20.1.3.192.168.1.1", value: {:ip_address, {255, 255, 255, 0}}},
|
||
%{oid: "1.3.6.1.2.1.4.20.1.3.192.168.1.100", value: {:ip_address, {255, 255, 255, 0}}},
|
||
%{oid: "1.3.6.1.2.1.4.20.1.3.10.0.0.1", value: {:ip_address, {255, 0, 0, 0}}}
|
||
]}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
|
||
|
||
assert length(ip_addresses) == 3
|
||
|
||
ip1 = Enum.find(ip_addresses, &(&1.ip_address == "192.168.1.1"))
|
||
assert ip1.if_index == 1
|
||
assert ip1.subnet_mask == "255.255.255.0"
|
||
assert ip1.prefix_length == 24
|
||
assert ip1.ip_type == "ipv4"
|
||
|
||
ip2 = Enum.find(ip_addresses, &(&1.ip_address == "10.0.0.1"))
|
||
assert ip2.if_index == 2
|
||
assert ip2.subnet_mask == "255.0.0.0"
|
||
assert ip2.prefix_length == 8
|
||
end
|
||
|
||
test "returns empty list when IP-MIB not supported" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||
|
||
assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
|
||
assert ip_addresses == []
|
||
end
|
||
|
||
test "handles walk errors gracefully" do
|
||
stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)
|
||
|
||
assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
|
||
assert ip_addresses == []
|
||
end
|
||
|
||
test "handles missing subnet mask gracefully" do
|
||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||
case oid do
|
||
"1.3.6.1.2.1.4.20.1.2" ->
|
||
{:ok,
|
||
[
|
||
%{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.1", value: {:integer, 1}}
|
||
]}
|
||
|
||
"1.3.6.1.2.1.4.20.1.3" ->
|
||
{:ok, []}
|
||
|
||
_ ->
|
||
{:ok, []}
|
||
end
|
||
end)
|
||
|
||
assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
|
||
|
||
assert length(ip_addresses) == 1
|
||
ip = hd(ip_addresses)
|
||
assert ip.ip_address == "192.168.1.1"
|
||
assert ip.subnet_mask == nil
|
||
assert ip.prefix_length == nil
|
||
end
|
||
end
|
||
end
|