towerops/test/towerops/snmp/profiles/base_test.exs
Graham McIntire 1cf6f327ea
feat: add IP address discovery (Phase 1.3)
- 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
2026-01-21 10:36:19 -06:00

774 lines
26 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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