- 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)
379 lines
12 KiB
Elixir
379 lines
12 KiB
Elixir
defmodule Towerops.Snmp.Profiles.Cisco do
|
|
@moduledoc """
|
|
Cisco-specific SNMP device profile.
|
|
Extends Base profile with Cisco-specific sensor discovery from CISCO-ENTITY-SENSOR-MIB.
|
|
|
|
Discovers additional sensors:
|
|
- Temperature sensors (chassis, modules, power supplies)
|
|
- Power supply sensors
|
|
- Fan speed sensors
|
|
- Voltage sensors
|
|
"""
|
|
|
|
alias Towerops.Snmp.Client
|
|
alias Towerops.Snmp.Discovery
|
|
alias Towerops.Snmp.Profiles.Base
|
|
|
|
require Logger
|
|
|
|
@cisco_sensor_oids %{
|
|
# CISCO-ENTITY-SENSOR-MIB
|
|
ent_sensor_type: "1.3.6.1.4.1.9.9.91.1.1.1.1.1",
|
|
ent_sensor_scale: "1.3.6.1.4.1.9.9.91.1.1.1.1.2",
|
|
ent_sensor_precision: "1.3.6.1.4.1.9.9.91.1.1.1.1.3",
|
|
ent_sensor_value: "1.3.6.1.4.1.9.9.91.1.1.1.1.4",
|
|
ent_sensor_status: "1.3.6.1.4.1.9.9.91.1.1.1.1.5",
|
|
|
|
# ENTITY-MIB for physical descriptions
|
|
ent_phys_descr: "1.3.6.1.2.1.47.1.1.1.1.2",
|
|
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.
|
|
"""
|
|
@spec discover_system_info(Client.connection_opts()) ::
|
|
{:ok, Discovery.system_info()} | {:error, term()}
|
|
def discover_system_info(_client_opts) do
|
|
# No additional system info beyond Base profile
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@doc """
|
|
Discovers network interfaces using Base profile.
|
|
"""
|
|
@spec discover_interfaces(Client.connection_opts()) ::
|
|
{:ok, [Discovery.interface_data()]} | {:error, term()}
|
|
def discover_interfaces(client_opts) do
|
|
Base.discover_interfaces(client_opts)
|
|
end
|
|
|
|
@doc """
|
|
Discovers sensors from both standard ENTITY-SENSOR-MIB and CISCO-ENTITY-SENSOR-MIB.
|
|
Cisco-specific sensors provide more detailed temperature, power, and fan monitoring.
|
|
"""
|
|
@spec discover_sensors(Client.connection_opts()) ::
|
|
{:ok, [Discovery.sensor_data()]} | {:error, term()}
|
|
def discover_sensors(client_opts) do
|
|
# Try Cisco-specific sensors first
|
|
case discover_cisco_sensors(client_opts) do
|
|
{:ok, [_ | _] = sensors} ->
|
|
{:ok, sensors}
|
|
|
|
_ ->
|
|
# Fall back to standard ENTITY-SENSOR-MIB
|
|
Logger.debug("Cisco-specific sensors not available, using standard discovery")
|
|
Base.discover_sensors(client_opts)
|
|
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.
|
|
"""
|
|
@spec identify_device(Discovery.system_info()) :: Discovery.device_info()
|
|
def identify_device(system_info) do
|
|
sys_descr = Map.get(system_info, :sys_descr, "")
|
|
|
|
model = extract_cisco_model_detailed(sys_descr)
|
|
|
|
Map.merge(system_info, %{
|
|
manufacturer: "Cisco",
|
|
model: model,
|
|
firmware_version: extract_firmware_version(sys_descr)
|
|
})
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp discover_cisco_sensors(client_opts) do
|
|
case Client.walk(client_opts, @cisco_sensor_oids.ent_sensor_type) do
|
|
{:ok, sensor_types} when map_size(sensor_types) > 0 ->
|
|
sensor_indices = extract_indices_from_oids(sensor_types)
|
|
|
|
sensors =
|
|
sensor_indices
|
|
|> Enum.map(fn index ->
|
|
build_cisco_sensor(client_opts, index)
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
{:ok, sensors}
|
|
|
|
_ ->
|
|
{:error, :cisco_sensors_not_available}
|
|
end
|
|
end
|
|
|
|
defp build_cisco_sensor(client_opts, index) do
|
|
oids = [
|
|
@cisco_sensor_oids.ent_sensor_type <> ".#{index}",
|
|
@cisco_sensor_oids.ent_sensor_scale <> ".#{index}",
|
|
@cisco_sensor_oids.ent_sensor_precision <> ".#{index}",
|
|
@cisco_sensor_oids.ent_sensor_value <> ".#{index}",
|
|
@cisco_sensor_oids.ent_sensor_status <> ".#{index}",
|
|
@cisco_sensor_oids.ent_phys_descr <> ".#{index}",
|
|
@cisco_sensor_oids.ent_phys_name <> ".#{index}"
|
|
]
|
|
|
|
case Client.get_multiple(client_opts, oids) do
|
|
{:ok, [type, scale, precision, value, status, descr, name]} ->
|
|
sensor_type = parse_cisco_sensor_type(type)
|
|
divisor = calculate_divisor(scale, precision)
|
|
|
|
%{
|
|
sensor_type: sensor_type,
|
|
sensor_index: "#{index}",
|
|
sensor_oid: @cisco_sensor_oids.ent_sensor_value <> ".#{index}",
|
|
sensor_descr: parse_sensor_description(descr, name),
|
|
sensor_unit: cisco_sensor_type_to_unit(sensor_type),
|
|
sensor_divisor: divisor,
|
|
last_value: parse_sensor_value(value, divisor),
|
|
status: parse_cisco_sensor_status(status)
|
|
}
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("Failed to fetch Cisco sensor #{index}: #{inspect(reason)}")
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp extract_indices_from_oids(oid_map) do
|
|
oid_map
|
|
|> Map.keys()
|
|
|> Enum.map(fn oid_string ->
|
|
oid_string
|
|
|> String.split(".")
|
|
|> List.last()
|
|
|> String.to_integer()
|
|
end)
|
|
end
|
|
|
|
defp extract_cisco_model_detailed(sys_descr) do
|
|
cond do
|
|
# Catalyst switches: "Cisco IOS Software, C2960 Software..."
|
|
match = Regex.run(~r/\b(C\d+\w*|WS-C\d+\w*)\b/, sys_descr) ->
|
|
[_, model] = match
|
|
model
|
|
|
|
# ISR routers: "Cisco IOS Software, 1900 Software..."
|
|
match = Regex.run(~r/\b(ISR\d+\w*|[0-9]{4})\b/, sys_descr) ->
|
|
[_, model] = match
|
|
model
|
|
|
|
# ASR routers
|
|
match = Regex.run(~r/\b(ASR\d+\w*)\b/, sys_descr) ->
|
|
[_, model] = match
|
|
model
|
|
|
|
true ->
|
|
"Unknown Cisco Model"
|
|
end
|
|
end
|
|
|
|
defp extract_firmware_version(sys_descr) do
|
|
# Try to extract version like "Version 15.2(4)E7"
|
|
case Regex.run(~r/Version\s+([\d.()A-Za-z]+)/, sys_descr) do
|
|
[_, version] -> version
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
# CISCO-ENTITY-SENSOR-MIB sensor types
|
|
defp parse_cisco_sensor_type(1), do: "other"
|
|
defp parse_cisco_sensor_type(2), do: "unknown"
|
|
defp parse_cisco_sensor_type(3), do: "volts_ac"
|
|
defp parse_cisco_sensor_type(4), do: "volts_dc"
|
|
defp parse_cisco_sensor_type(5), do: "amperes"
|
|
defp parse_cisco_sensor_type(6), do: "watts"
|
|
defp parse_cisco_sensor_type(7), do: "hertz"
|
|
defp parse_cisco_sensor_type(8), do: "celsius"
|
|
defp parse_cisco_sensor_type(9), do: "percent_rh"
|
|
defp parse_cisco_sensor_type(10), do: "rpm"
|
|
defp parse_cisco_sensor_type(11), do: "cmm"
|
|
defp parse_cisco_sensor_type(12), do: "truthvalue"
|
|
defp parse_cisco_sensor_type(13), do: "specialenum"
|
|
defp parse_cisco_sensor_type(14), do: "dBm"
|
|
defp parse_cisco_sensor_type(_), do: "unknown"
|
|
|
|
defp cisco_sensor_type_to_unit("celsius"), do: "°C"
|
|
defp cisco_sensor_type_to_unit("volts_ac"), do: "VAC"
|
|
defp cisco_sensor_type_to_unit("volts_dc"), do: "VDC"
|
|
defp cisco_sensor_type_to_unit("amperes"), do: "A"
|
|
defp cisco_sensor_type_to_unit("watts"), do: "W"
|
|
defp cisco_sensor_type_to_unit("hertz"), do: "Hz"
|
|
defp cisco_sensor_type_to_unit("percent_rh"), do: "%RH"
|
|
defp cisco_sensor_type_to_unit("rpm"), do: "RPM"
|
|
defp cisco_sensor_type_to_unit("dBm"), do: "dBm"
|
|
defp cisco_sensor_type_to_unit(_), do: ""
|
|
|
|
# Calculate divisor from scale and precision
|
|
# Scale: 10^scale (e.g., -3 = milli, 0 = units)
|
|
# Precision: number of decimal places
|
|
defp calculate_divisor(scale, _precision) when is_integer(scale) do
|
|
case scale do
|
|
-24 -> 1_000_000_000_000_000_000_000_000
|
|
-12 -> 1_000_000_000_000
|
|
-9 -> 1_000_000_000
|
|
-6 -> 1_000_000
|
|
-3 -> 1_000
|
|
0 -> 1
|
|
3 -> 0.001
|
|
_ -> 1
|
|
end
|
|
end
|
|
|
|
defp calculate_divisor(_, _), do: 1
|
|
|
|
defp parse_sensor_value(value, divisor) when is_integer(value) and is_number(divisor) do
|
|
value / divisor
|
|
end
|
|
|
|
defp parse_sensor_value(_, _), do: nil
|
|
|
|
defp parse_sensor_description(descr, _name) when is_binary(descr) and byte_size(descr) > 0 do
|
|
descr
|
|
end
|
|
|
|
defp parse_sensor_description(_descr, name) when is_binary(name) and byte_size(name) > 0 do
|
|
name
|
|
end
|
|
|
|
defp parse_sensor_description(_, _), do: "Unknown Sensor"
|
|
|
|
# CISCO-ENTITY-SENSOR-MIB sensor status
|
|
defp parse_cisco_sensor_status(1), do: "ok"
|
|
defp parse_cisco_sensor_status(2), do: "unavailable"
|
|
defp parse_cisco_sensor_status(3), do: "nonoperational"
|
|
defp parse_cisco_sensor_status(_), do: "unknown"
|
|
|
|
@doc """
|
|
Collects raw debug data for troubleshooting.
|
|
Includes Cisco-specific sensor OIDs and entity tables.
|
|
"""
|
|
def collect_raw_debug_data(client_opts) do
|
|
# Walk Cisco entity sensor table
|
|
cisco_sensor_table =
|
|
case Client.walk(client_opts, "1.3.6.1.4.1.9.9.91.1.1.1") do
|
|
{:ok, results} -> results
|
|
{:error, _} -> %{}
|
|
end
|
|
|
|
# Walk entity physical table for descriptions
|
|
entity_phys_table =
|
|
case Client.walk(client_opts, "1.3.6.1.2.1.47.1.1.1") do
|
|
{:ok, results} -> results
|
|
{:error, _} -> %{}
|
|
end
|
|
|
|
%{
|
|
timestamp: DateTime.utc_now(),
|
|
cisco_sensor_oids: @cisco_sensor_oids,
|
|
cisco_sensor_table: cisco_sensor_table,
|
|
entity_phys_table: entity_phys_table
|
|
}
|
|
end
|
|
end
|