feat: add Cisco VTP VLAN discovery
- Add discover_vlans/1 to Cisco profile for VTP MIB discovery - VTP OIDs: vtpVlanName, vtpVlanState, vtpVlanType - Falls back to Q-BRIDGE-MIB when VTP not available - Maps VTP states (operational, suspended, mtuTooBig) - Maps VTP types (ethernet, fddi, tokenRing, etc.) - Validates VLAN ID range (1-4094)
This commit is contained in:
parent
7e1fc35f2c
commit
f8c417a338
2 changed files with 320 additions and 0 deletions
|
|
@ -29,6 +29,13 @@ defmodule Towerops.Snmp.Profiles.Cisco do
|
|||
ent_phys_name: "1.3.6.1.2.1.47.1.1.1.1.7"
|
||||
}
|
||||
|
||||
@cisco_vtp_oids %{
|
||||
# CISCO-VTP-MIB - VTP VLAN discovery
|
||||
vtp_vlan_state: "1.3.6.1.4.1.9.9.46.1.3.1.1.2",
|
||||
vtp_vlan_type: "1.3.6.1.4.1.9.9.46.1.3.1.1.3",
|
||||
vtp_vlan_name: "1.3.6.1.4.1.9.9.46.1.3.1.1.4"
|
||||
}
|
||||
|
||||
@doc """
|
||||
Discovers system information.
|
||||
Cisco profile doesn't query additional system OIDs beyond Base profile.
|
||||
|
|
@ -68,6 +75,102 @@ defmodule Towerops.Snmp.Profiles.Cisco do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Discovers VLANs from Cisco VTP MIB, falling back to Q-BRIDGE-MIB if VTP not available.
|
||||
"""
|
||||
@spec discover_vlans(Client.connection_opts()) :: {:ok, [map()]}
|
||||
def discover_vlans(client_opts) do
|
||||
# Try Cisco VTP MIB first
|
||||
case Client.walk(client_opts, @cisco_vtp_oids.vtp_vlan_name) do
|
||||
{:ok, name_results} when is_map(name_results) and map_size(name_results) > 0 ->
|
||||
do_discover_vtp_vlans(client_opts, name_results)
|
||||
|
||||
_ ->
|
||||
# Fall back to Q-BRIDGE-MIB
|
||||
Logger.debug("Cisco VTP MIB not available, falling back to Q-BRIDGE-MIB")
|
||||
Base.discover_vlans(client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_discover_vtp_vlans(client_opts, name_results) do
|
||||
# Fetch VLAN states
|
||||
state_map =
|
||||
case Client.walk(client_opts, @cisco_vtp_oids.vtp_vlan_state) do
|
||||
{:ok, results} when is_map(results) -> build_vtp_vlan_index_map(results)
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
# Fetch VLAN types
|
||||
type_map =
|
||||
case Client.walk(client_opts, @cisco_vtp_oids.vtp_vlan_type) do
|
||||
{:ok, results} when is_map(results) -> build_vtp_vlan_index_map(results)
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
# Build VLAN list from names (name_results is a map of OID -> name)
|
||||
vlans =
|
||||
name_results
|
||||
|> Enum.map(fn {oid, name} ->
|
||||
vlan_id = extract_vtp_vlan_id_from_oid(oid)
|
||||
state = Map.get(state_map, vlan_id, 1)
|
||||
type = Map.get(type_map, vlan_id, 1)
|
||||
|
||||
if valid_vlan_id?(vlan_id) do
|
||||
%{
|
||||
vlan_id: vlan_id,
|
||||
vlan_name: name,
|
||||
vlan_type: vtp_vlan_type_to_string(type),
|
||||
status: vtp_vlan_state_to_status(state),
|
||||
last_checked_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
Logger.debug("Discovered #{length(vlans)} VLANs from Cisco VTP MIB")
|
||||
{:ok, vlans}
|
||||
end
|
||||
|
||||
defp build_vtp_vlan_index_map(results) when is_map(results) do
|
||||
Map.new(results, fn {oid, value} ->
|
||||
vlan_id = extract_vtp_vlan_id_from_oid(oid)
|
||||
{vlan_id, value}
|
||||
end)
|
||||
end
|
||||
|
||||
# VTP VLAN OIDs are indexed by managementDomainIndex.vlanIndex
|
||||
# e.g., 1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.100 = domain 1, VLAN 100
|
||||
defp extract_vtp_vlan_id_from_oid(oid) when is_binary(oid) do
|
||||
oid
|
||||
|> String.split(".")
|
||||
|> List.last()
|
||||
|> String.to_integer()
|
||||
rescue
|
||||
_ -> 0
|
||||
end
|
||||
|
||||
defp extract_vtp_vlan_id_from_oid(_), do: 0
|
||||
|
||||
defp valid_vlan_id?(vlan_id) when is_integer(vlan_id) do
|
||||
vlan_id >= 1 and vlan_id <= 4094
|
||||
end
|
||||
|
||||
defp valid_vlan_id?(_), do: false
|
||||
|
||||
# CISCO-VTP-MIB vtpVlanState values:
|
||||
# 1=operational, 2=suspended, 3=mtuTooBigForDevice, 4=mtuTooBigForTrunk
|
||||
defp vtp_vlan_state_to_status(1), do: "active"
|
||||
defp vtp_vlan_state_to_status(_), do: "suspended"
|
||||
|
||||
# CISCO-VTP-MIB vtpVlanType values:
|
||||
# 1=ethernet, 2=fddi, 3=tokenRing, 4=fddiNet, 5=trNet, 6=deprecated
|
||||
defp vtp_vlan_type_to_string(1), do: "ethernet"
|
||||
defp vtp_vlan_type_to_string(2), do: "fddi"
|
||||
defp vtp_vlan_type_to_string(3), do: "tokenRing"
|
||||
defp vtp_vlan_type_to_string(4), do: "fddiNet"
|
||||
defp vtp_vlan_type_to_string(5), do: "trNet"
|
||||
defp vtp_vlan_type_to_string(_), do: "ethernet"
|
||||
|
||||
@doc """
|
||||
Identifies Cisco device model more accurately from sysDescr.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -171,6 +171,223 @@ defmodule Towerops.Snmp.Profiles.CiscoTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "discover_vlans/1" do
|
||||
test "discovers VLANs from Cisco VTP MIB" do
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# vtpVlanName - VLAN names
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.1", value: "default"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.10", value: "Management"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.100", value: "Production"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.200", value: "Guest"}
|
||||
]}
|
||||
|
||||
# vtpVlanState - VLAN status (1=operational, 2=suspended)
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.2" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.1", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.10", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.100", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.200", value: 2}
|
||||
]}
|
||||
|
||||
# vtpVlanType - VLAN type (1=ethernet)
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.3" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.1", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.10", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.100", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.200", value: 1}
|
||||
]}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.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"
|
||||
assert vlan1.vlan_type == "ethernet"
|
||||
|
||||
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 "falls back to Q-BRIDGE-MIB when VTP not available" do
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# VTP returns empty
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.4" ->
|
||||
{:ok, []}
|
||||
|
||||
# Q-BRIDGE-MIB has VLANs
|
||||
"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: "default"},
|
||||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.10", value: "Management"}
|
||||
]}
|
||||
|
||||
"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: 1},
|
||||
%{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.10", value: 1}
|
||||
]}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
|
||||
# Should have found VLANs via Q-BRIDGE fallback
|
||||
assert length(vlans) == 2
|
||||
end
|
||||
|
||||
test "returns empty list when no VLANs found" do
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
assert vlans == []
|
||||
end
|
||||
|
||||
test "handles VTP walk errors gracefully" do
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
assert vlans == []
|
||||
end
|
||||
|
||||
test "handles various VTP VLAN states" do
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.1", value: "vlan1"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.2", value: "vlan2"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.3", value: "vlan3"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.4", value: "vlan4"}
|
||||
]}
|
||||
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.2" ->
|
||||
{:ok,
|
||||
[
|
||||
# 1=operational, 2=suspended, 3=mtuTooBigForDevice, 4=mtuTooBigForTrunk
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.1", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.2", value: 2},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.3", value: 3},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.2.1.4", value: 4}
|
||||
]}
|
||||
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.3" ->
|
||||
{:ok, []}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
|
||||
vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
|
||||
assert vlan1.status == "active"
|
||||
|
||||
vlan2 = Enum.find(vlans, &(&1.vlan_id == 2))
|
||||
assert vlan2.status == "suspended"
|
||||
|
||||
vlan3 = Enum.find(vlans, &(&1.vlan_id == 3))
|
||||
assert vlan3.status == "suspended"
|
||||
|
||||
vlan4 = Enum.find(vlans, &(&1.vlan_id == 4))
|
||||
assert vlan4.status == "suspended"
|
||||
end
|
||||
|
||||
test "handles various VTP VLAN types" do
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.1", value: "vlan1"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.2", value: "vlan2"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.3", value: "vlan3"}
|
||||
]}
|
||||
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.2" ->
|
||||
{:ok, []}
|
||||
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.3" ->
|
||||
{:ok,
|
||||
[
|
||||
# 1=ethernet, 2=fddi, 3=tokenRing
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.1", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.2", value: 2},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.3.1.3", value: 3}
|
||||
]}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
|
||||
vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
|
||||
assert vlan1.vlan_type == "ethernet"
|
||||
|
||||
vlan2 = Enum.find(vlans, &(&1.vlan_id == 2))
|
||||
assert vlan2.vlan_type == "fddi"
|
||||
|
||||
vlan3 = Enum.find(vlans, &(&1.vlan_id == 3))
|
||||
assert vlan3.vlan_type == "tokenRing"
|
||||
end
|
||||
|
||||
test "validates VLAN ID range (1-4094)" do
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.9.9.46.1.3.1.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.0", value: "invalid0"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.1", value: "valid1"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.4094", value: "valid4094"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.46.1.3.1.1.4.1.4095", value: "invalid4095"}
|
||||
]}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, vlans} = Cisco.discover_vlans(@client_opts)
|
||||
|
||||
vlan_ids = Enum.map(vlans, & &1.vlan_id)
|
||||
|
||||
assert 1 in vlan_ids
|
||||
assert 4094 in vlan_ids
|
||||
refute 0 in vlan_ids
|
||||
refute 4095 in vlan_ids
|
||||
end
|
||||
end
|
||||
|
||||
describe "sensor type parsing" do
|
||||
test "attempts to discover Cisco-specific sensors" do
|
||||
# Mock walk to indicate Cisco sensors available
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue