1287 lines
42 KiB
Elixir
1287 lines
42 KiB
Elixir
defmodule SnmpKit.SnmpMgr.MIB do
|
|
@moduledoc """
|
|
MIB compilation and symbolic name resolution.
|
|
|
|
This module provides MIB compilation using Erlang's :snmpc when available,
|
|
and includes a built-in registry of standard MIB objects for basic operations.
|
|
"""
|
|
|
|
use GenServer
|
|
|
|
alias SnmpKit.SnmpLib.MIB
|
|
alias SnmpKit.SnmpLib.MIB.Parser
|
|
alias SnmpKit.SnmpLib.OID
|
|
|
|
require Logger
|
|
|
|
@compile {:no_warn_undefined, [:snmpc, :snmp_misc]}
|
|
|
|
@standard_mibs %{
|
|
# System group (1.3.6.1.2.1.1)
|
|
"sysDescr" => [1, 3, 6, 1, 2, 1, 1, 1],
|
|
"sysObjectID" => [1, 3, 6, 1, 2, 1, 1, 2],
|
|
"sysUpTime" => [1, 3, 6, 1, 2, 1, 1, 3],
|
|
"sysContact" => [1, 3, 6, 1, 2, 1, 1, 4],
|
|
"sysName" => [1, 3, 6, 1, 2, 1, 1, 5],
|
|
"sysLocation" => [1, 3, 6, 1, 2, 1, 1, 6],
|
|
"sysServices" => [1, 3, 6, 1, 2, 1, 1, 7],
|
|
|
|
# Interface group (1.3.6.1.2.1.2)
|
|
"ifNumber" => [1, 3, 6, 1, 2, 1, 2, 1],
|
|
"ifTable" => [1, 3, 6, 1, 2, 1, 2, 2],
|
|
"ifEntry" => [1, 3, 6, 1, 2, 1, 2, 2, 1],
|
|
"ifIndex" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 1],
|
|
"ifDescr" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 2],
|
|
"ifType" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 3],
|
|
"ifMtu" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 4],
|
|
"ifSpeed" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 5],
|
|
"ifPhysAddress" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 6],
|
|
"ifAdminStatus" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 7],
|
|
"ifOperStatus" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 8],
|
|
"ifLastChange" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 9],
|
|
"ifInOctets" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 10],
|
|
"ifInUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 11],
|
|
"ifInNUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 12],
|
|
"ifInDiscards" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 13],
|
|
"ifInErrors" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 14],
|
|
"ifInUnknownProtos" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 15],
|
|
"ifOutOctets" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 16],
|
|
"ifOutUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 17],
|
|
"ifOutNUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 18],
|
|
"ifOutDiscards" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 19],
|
|
"ifOutErrors" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 20],
|
|
"ifOutQLen" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 21],
|
|
"ifSpecific" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 22],
|
|
|
|
# Interface Extensions (ifX) group (1.3.6.1.2.1.31)
|
|
"ifXTable" => [1, 3, 6, 1, 2, 1, 31, 1],
|
|
"ifXEntry" => [1, 3, 6, 1, 2, 1, 31, 1, 1],
|
|
"ifName" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 1],
|
|
"ifInMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 2],
|
|
"ifInBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 3],
|
|
"ifOutMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 4],
|
|
"ifOutBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 5],
|
|
"ifHCInOctets" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 6],
|
|
"ifHCInUcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 7],
|
|
"ifHCInMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 8],
|
|
"ifHCInBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 9],
|
|
"ifHCOutOctets" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 10],
|
|
"ifHCOutUcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 11],
|
|
"ifHCOutMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 12],
|
|
"ifHCOutBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 13],
|
|
"ifLinkUpDownTrapEnable" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 14],
|
|
"ifHighSpeed" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 15],
|
|
"ifPromiscuousMode" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 16],
|
|
"ifConnectorPresent" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 17],
|
|
"ifAlias" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 18],
|
|
"ifCounterDiscontinuityTime" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 19],
|
|
|
|
# IP group (1.3.6.1.2.1.4)
|
|
"ipForwarding" => [1, 3, 6, 1, 2, 1, 4, 1],
|
|
"ipDefaultTTL" => [1, 3, 6, 1, 2, 1, 4, 2],
|
|
"ipInReceives" => [1, 3, 6, 1, 2, 1, 4, 3],
|
|
"ipInHdrErrors" => [1, 3, 6, 1, 2, 1, 4, 4],
|
|
"ipInAddrErrors" => [1, 3, 6, 1, 2, 1, 4, 5],
|
|
|
|
# SNMP group (1.3.6.1.2.1.11)
|
|
"snmpInPkts" => [1, 3, 6, 1, 2, 1, 11, 1],
|
|
"snmpOutPkts" => [1, 3, 6, 1, 2, 1, 11, 2],
|
|
"snmpInBadVersions" => [1, 3, 6, 1, 2, 1, 11, 3],
|
|
"snmpInBadCommunityNames" => [1, 3, 6, 1, 2, 1, 11, 4],
|
|
"snmpInBadCommunityUses" => [1, 3, 6, 1, 2, 1, 11, 5],
|
|
"snmpInASNParseErrs" => [1, 3, 6, 1, 2, 1, 11, 6],
|
|
"snmpInTooBigs" => [1, 3, 6, 1, 2, 1, 11, 8],
|
|
"snmpInNoSuchNames" => [1, 3, 6, 1, 2, 1, 11, 9],
|
|
"snmpInBadValues" => [1, 3, 6, 1, 2, 1, 11, 10],
|
|
"snmpInReadOnlys" => [1, 3, 6, 1, 2, 1, 11, 11],
|
|
"snmpInGenErrs" => [1, 3, 6, 1, 2, 1, 11, 12],
|
|
"snmpInTotalReqVars" => [1, 3, 6, 1, 2, 1, 11, 13],
|
|
"snmpInTotalSetVars" => [1, 3, 6, 1, 2, 1, 11, 14],
|
|
"snmpInGetRequests" => [1, 3, 6, 1, 2, 1, 11, 15],
|
|
"snmpInGetNexts" => [1, 3, 6, 1, 2, 1, 11, 16],
|
|
"snmpInSetRequests" => [1, 3, 6, 1, 2, 1, 11, 17],
|
|
"snmpInGetResponses" => [1, 3, 6, 1, 2, 1, 11, 18],
|
|
"snmpInTraps" => [1, 3, 6, 1, 2, 1, 11, 19],
|
|
"snmpOutTooBigs" => [1, 3, 6, 1, 2, 1, 11, 20],
|
|
"snmpOutNoSuchNames" => [1, 3, 6, 1, 2, 1, 11, 21],
|
|
"snmpOutBadValues" => [1, 3, 6, 1, 2, 1, 11, 22],
|
|
"snmpOutGenErrs" => [1, 3, 6, 1, 2, 1, 11, 24],
|
|
"snmpOutGetRequests" => [1, 3, 6, 1, 2, 1, 11, 25],
|
|
"snmpOutGetNexts" => [1, 3, 6, 1, 2, 1, 11, 26],
|
|
"snmpOutSetRequests" => [1, 3, 6, 1, 2, 1, 11, 27],
|
|
"snmpOutGetResponses" => [1, 3, 6, 1, 2, 1, 11, 28],
|
|
"snmpOutTraps" => [1, 3, 6, 1, 2, 1, 11, 29],
|
|
"snmpEnableAuthenTraps" => [1, 3, 6, 1, 2, 1, 11, 30],
|
|
|
|
# Common group prefixes for bulk walking
|
|
"system" => [1, 3, 6, 1, 2, 1, 1],
|
|
"interfaces" => [1, 3, 6, 1, 2, 1, 2],
|
|
"if" => [1, 3, 6, 1, 2, 1, 2],
|
|
"ifX" => [1, 3, 6, 1, 2, 1, 31],
|
|
"ip" => [1, 3, 6, 1, 2, 1, 4],
|
|
"icmp" => [1, 3, 6, 1, 2, 1, 5],
|
|
"tcp" => [1, 3, 6, 1, 2, 1, 6],
|
|
"udp" => [1, 3, 6, 1, 2, 1, 7],
|
|
"snmp" => [1, 3, 6, 1, 2, 1, 11],
|
|
"mib-2" => [1, 3, 6, 1, 2, 1],
|
|
"mgmt" => [1, 3, 6, 1, 2],
|
|
"internet" => [1, 3, 6, 1],
|
|
|
|
# Common enterprise OIDs
|
|
"enterprises" => [1, 3, 6, 1, 4, 1],
|
|
"cisco" => [1, 3, 6, 1, 4, 1, 9],
|
|
"hp" => [1, 3, 6, 1, 4, 1, 11],
|
|
"3com" => [1, 3, 6, 1, 4, 1, 43],
|
|
"sun" => [1, 3, 6, 1, 4, 1, 42],
|
|
"dec" => [1, 3, 6, 1, 4, 1, 36],
|
|
"ibm" => [1, 3, 6, 1, 4, 1, 2],
|
|
"microsoft" => [1, 3, 6, 1, 4, 1, 311],
|
|
"netapp" => [1, 3, 6, 1, 4, 1, 789],
|
|
"juniper" => [1, 3, 6, 1, 4, 1, 2636],
|
|
"fortinet" => [1, 3, 6, 1, 4, 1, 12_356],
|
|
"paloalto" => [1, 3, 6, 1, 4, 1, 25_461],
|
|
"mikrotik" => [1, 3, 6, 1, 4, 1, 14_988],
|
|
|
|
# Cable/DOCSIS industry OIDs
|
|
"cablelabs" => [1, 3, 6, 1, 4, 1, 4491],
|
|
"docsis" => [1, 3, 6, 1, 2, 1, 127],
|
|
"cableDataPrivateMib" => [1, 3, 6, 1, 4, 1, 4491, 2, 1],
|
|
"arris" => [1, 3, 6, 1, 4, 1, 4115],
|
|
"motorola" => [1, 3, 6, 1, 4, 1, 1166],
|
|
"scientificatlanta" => [1, 3, 6, 1, 4, 1, 1429],
|
|
"broadcom" => [1, 3, 6, 1, 4, 1, 4413]
|
|
}
|
|
|
|
# Curated minimal metadata for high-value IF-MIB objects (stopgap until full compiler integration)
|
|
@curated_syntax %{
|
|
# SNMPv2-MIB system group
|
|
"sysDescr" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"sysObjectID" => %{base: :object_identifier, textual_convention: nil, display_hint: nil},
|
|
"sysUpTime" => %{base: :timeticks, textual_convention: nil, display_hint: nil},
|
|
"sysContact" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"sysName" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"sysLocation" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"sysServices" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
|
|
# IF-MIB ifTable (1.3.6.1.2.1.2.2.1)
|
|
"ifIndex" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ifDescr" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"ifType" => %{base: :integer, textual_convention: "IANAifType", display_hint: nil},
|
|
"ifMtu" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ifSpeed" => %{base: :gauge32, textual_convention: nil, display_hint: nil},
|
|
"ifPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil},
|
|
"ifAdminStatus" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ifOperStatus" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ifLastChange" => %{base: :timeticks, textual_convention: nil, display_hint: nil},
|
|
"ifInOctets" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInNUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInDiscards" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInErrors" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInUnknownProtos" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutOctets" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutNUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutDiscards" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutErrors" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutQLen" => %{base: :gauge32, textual_convention: nil, display_hint: nil},
|
|
"ifSpecific" => %{base: :object_identifier, textual_convention: nil, display_hint: nil},
|
|
|
|
# IF-MIB ifXTable (1.3.6.1.2.1.31.1.1.1)
|
|
"ifName" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"ifInMulticastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifInBroadcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutMulticastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifOutBroadcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil},
|
|
"ifHCInOctets" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCInUcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCInMulticastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCInBroadcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCOutOctets" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCOutUcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCOutMulticastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifHCOutBroadcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil},
|
|
"ifLinkUpDownTrapEnable" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ifHighSpeed" => %{base: :gauge32, textual_convention: nil, display_hint: nil},
|
|
"ifPromiscuousMode" => %{base: :boolean, textual_convention: "TruthValue", display_hint: nil},
|
|
"ifConnectorPresent" => %{base: :boolean, textual_convention: "TruthValue", display_hint: nil},
|
|
"ifAlias" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"},
|
|
"ifCounterDiscontinuityTime" => %{base: :timeticks, textual_convention: "TimeStamp", display_hint: nil},
|
|
|
|
# IP-MIB (ARP table: ipNetToMediaTable 1.3.6.1.2.1.4.22)
|
|
"ipNetToMediaIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ipNetToMediaPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil},
|
|
"ipNetToMediaNetAddress" => %{base: :ip_address, textual_convention: "IpAddress", display_hint: nil},
|
|
"ipNetToMediaType" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
|
|
# BRIDGE-MIB (dot1dTpFdbTable and base)
|
|
"dot1dBaseBridgeAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil},
|
|
"dot1dBaseNumPorts" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"dot1dBasePortIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"dot1dTpFdbAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil},
|
|
"dot1dTpFdbPort" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"dot1dTpFdbStatus" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
|
|
# IP-MIB (RFC 4293) modern ARP replacement: ipNetToPhysicalTable
|
|
# Prefer these over ipNetToMedia*
|
|
"ipNetToPhysicalIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ipNetToPhysicalPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil},
|
|
"ipNetToPhysicalNetAddress" => %{base: :octet_string, textual_convention: "InetAddress", display_hint: nil},
|
|
"ipNetToPhysicalType" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"ipNetToPhysicalLastUpdated" => %{base: :timeticks, textual_convention: "TimeStamp", display_hint: nil},
|
|
|
|
# Q-BRIDGE-MIB (VLAN-aware FDB)
|
|
"dot1qTpFdbAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil},
|
|
"dot1qTpFdbPort" => %{base: :integer, textual_convention: nil, display_hint: nil},
|
|
"dot1qTpFdbStatus" => %{base: :integer, textual_convention: nil, display_hint: nil}
|
|
}
|
|
|
|
## Public API
|
|
|
|
@doc """
|
|
Starts the MIB registry GenServer.
|
|
"""
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Compiles a MIB file using SnmpKit.SnmpLib.MIB pure Elixir implementation.
|
|
|
|
Enhanced to use SnmpKit.SnmpLib.MIB for improved compilation with better error handling.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.compile("SNMPv2-MIB.mib")
|
|
{:ok, "SNMPv2-MIB.bin"}
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.compile("nonexistent.mib")
|
|
{:error, :file_not_found}
|
|
"""
|
|
def compile(mib_file, opts \\ []) do
|
|
# Try SnmpKit.SnmpLib.MIB first for enhanced compilation
|
|
case compile_with_snmp_lib(mib_file, opts) do
|
|
{:ok, result} ->
|
|
{:ok, result}
|
|
|
|
{:error, :snmp_lib_not_available} ->
|
|
{:error, :snmp_lib_not_available}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Compiles all MIB files in a directory using enhanced SnmpKit.SnmpLib.MIB capabilities.
|
|
"""
|
|
def compile_dir(directory, opts \\ []) do
|
|
# Try SnmpKit.SnmpLib.MIB.compile_all first for enhanced batch compilation
|
|
if File.exists?(directory) do
|
|
case compile_all_with_snmp_lib(directory, opts) do
|
|
{:ok, results} ->
|
|
{:ok, results}
|
|
|
|
{:error, :snmp_lib_not_available} ->
|
|
# Fallback to individual file compilation
|
|
compile_dir_fallback(directory, opts)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
else
|
|
{:error, {:directory_error, :enoent}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns enriched MIB metadata for an object by name or OID.
|
|
|
|
Input may be a dotted OID string (with or without instance), an OID list,
|
|
or a base name (optionally with an instance suffix like "ifDescr.6").
|
|
|
|
Returns a map with at least: name (base symbol), base oid, and optional instance
|
|
fields when input includes an instance. Includes curated syntax metadata for a
|
|
subset of high-value IF-MIB objects as a stopgap until full compiler metadata
|
|
is wired in.
|
|
"""
|
|
@spec object_info(String.t() | [integer]) :: {:ok, map()} | {:error, term()}
|
|
def object_info(name_or_oid) do
|
|
with {:ok, input_oid} <- normalize_to_oid_list(name_or_oid),
|
|
{:ok, base_name, _maybe_index} <- base_name_and_index(input_oid),
|
|
{:ok, base_oid} <- name_to_oid(base_name) do
|
|
# Prefer compiled/parsed metadata when available
|
|
compiled_meta = GenServer.call(__MODULE__, {:get_metadata, base_name})
|
|
|
|
syntax =
|
|
case compiled_meta do
|
|
%{syntax_base: base} = m ->
|
|
%{base: base, textual_convention: Map.get(m, :textual_convention), display_hint: Map.get(m, :display_hint)}
|
|
|
|
_ ->
|
|
syntax_for(base_name)
|
|
end
|
|
|
|
base_map = %{
|
|
name: base_name,
|
|
module: module_for(base_name),
|
|
oid: base_oid,
|
|
syntax: syntax
|
|
}
|
|
|
|
enriched = maybe_put_instance(base_map, input_oid, base_oid)
|
|
|
|
# Optionally add access/status/description if we have compiled metadata
|
|
enriched =
|
|
case compiled_meta do
|
|
nil ->
|
|
enriched
|
|
|
|
m ->
|
|
enriched
|
|
|> maybe_put(:access, Map.get(m, :access))
|
|
|> maybe_put(:status, Map.get(m, :status))
|
|
|> maybe_put(:description, Map.get(m, :description))
|
|
end
|
|
|
|
{:ok, enriched}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Alias for object_info/1 to match proposal naming.
|
|
"""
|
|
@spec reverse_lookup_enriched(String.t() | [integer]) :: {:ok, map()} | {:error, term()}
|
|
def reverse_lookup_enriched(name_or_oid), do: object_info(name_or_oid)
|
|
|
|
@doc """
|
|
Batch variant of object_info/1.
|
|
Returns {:ok, list_of_maps} or {:error, reason} if any lookup fails.
|
|
"""
|
|
@spec object_info_many([String.t() | [integer]]) :: {:ok, [map()]} | {:error, term()}
|
|
def object_info_many(list) when is_list(list) do
|
|
results = Enum.map(list, &object_info/1)
|
|
|
|
case Enum.find(results, fn
|
|
{:error, _} -> true
|
|
_ -> false
|
|
end) do
|
|
{:error, reason} -> {:error, reason}
|
|
_ -> {:ok, Enum.map(results, fn {:ok, m} -> m end)}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parses a MIB file to extract object definitions using SnmpKit.SnmpLib.MIB.Parser.
|
|
|
|
This provides enhanced MIB analysis without requiring compilation.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.parse_mib_file("SNMPv2-MIB.mib")
|
|
{:ok, %{objects: [...], imports: [...], exports: [...]}}
|
|
"""
|
|
def parse_mib_file(mib_file, opts \\ []) do
|
|
case File.read(mib_file) do
|
|
{:ok, content} ->
|
|
parse_mib_content(content, opts)
|
|
|
|
{:error, reason} ->
|
|
{:error, {:file_read_error, reason}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parses MIB content string using SnmpKit.SnmpLib.MIB.Parser.
|
|
|
|
## Examples
|
|
|
|
iex> content = "sysDescr OBJECT-TYPE SYNTAX DisplayString ACCESS read-only STATUS mandatory"
|
|
iex> SnmpKit.SnmpMgr.MIB.parse_mib_content(content)
|
|
{:ok, %{tokens: [...], parsed_objects: [...]}}
|
|
"""
|
|
def parse_mib_content(content, opts \\ []) when is_binary(content) do
|
|
# Use SnmpKit.SnmpLib.MIB.Parser for enhanced parsing
|
|
case Parser.tokenize(content) do
|
|
{:ok, tokens} ->
|
|
{:ok, objects} = parse_tokens_to_objects(tokens, opts)
|
|
|
|
{:ok,
|
|
%{
|
|
tokens: tokens,
|
|
parsed_objects: objects,
|
|
parser: :snmp_lib_enhanced
|
|
}}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:tokenization_failed, reason}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Loads a compiled MIB file using SnmpKit.SnmpLib.MIB.load_compiled with fallback.
|
|
"""
|
|
def load(compiled_mib_path) do
|
|
# Try SnmpKit.SnmpLib.MIB.load_compiled first for enhanced loading
|
|
case load_with_snmp_lib(compiled_mib_path) do
|
|
{:ok, result} ->
|
|
GenServer.call(__MODULE__, {:register_loaded_mib, result})
|
|
|
|
{:error, :snmp_lib_not_available} ->
|
|
GenServer.call(__MODULE__, {:load_mib, compiled_mib_path})
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Enhanced MIB object resolution with parsed MIB data integration.
|
|
|
|
Returns enriched object information including OID, syntax, module, and more.
|
|
Leverages both standard MIBs and any loaded/parsed MIB files for comprehensive resolution.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.resolve_enhanced("sysDescr")
|
|
{:ok, %{name: "sysDescr", oid: [1, 3, 6, 1, 2, 1, 1, 1], module: "SNMPv2-MIB", syntax: %{...}}}
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.resolve_enhanced("sysDescr.0")
|
|
{:ok, %{name: "sysDescr", oid: [1, 3, 6, 1, 2, 1, 1, 1], instance_oid: [1, 3, 6, 1, 2, 1, 1, 1, 0], ...}}
|
|
"""
|
|
def resolve_enhanced(name, _opts \\ []) do
|
|
# Use object_info for enriched resolution
|
|
object_info(name)
|
|
end
|
|
|
|
@doc """
|
|
Loads and parses a MIB file, integrating it into the name resolution system.
|
|
|
|
This combines compilation/loading with parsing for comprehensive MIB support.
|
|
"""
|
|
def load_and_integrate_mib(mib_file, opts \\ []) do
|
|
with {:ok, _compiled} <- compile(mib_file, opts),
|
|
{:ok, parsed} <- parse_mib_file(mib_file, opts) do
|
|
# Register both compiled and parsed data
|
|
GenServer.call(__MODULE__, {:integrate_mib_data, mib_file, parsed})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Loads standard MIBs that are built into the library.
|
|
"""
|
|
def load_standard_mibs do
|
|
GenServer.call(__MODULE__, :load_standard_mibs)
|
|
end
|
|
|
|
@doc """
|
|
Resolves a symbolic name to an OID.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.resolve("sysDescr.0")
|
|
{:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]}
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.resolve("sysDescr")
|
|
{:ok, [1, 3, 6, 1, 2, 1, 1, 1]}
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.resolve("unknownName")
|
|
{:error, :not_found}
|
|
"""
|
|
def resolve(name) do
|
|
GenServer.call(__MODULE__, {:resolve, name})
|
|
end
|
|
|
|
@doc """
|
|
Performs reverse lookup from OID to symbolic name.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1, 0])
|
|
{:ok, "sysDescr.0"}
|
|
|
|
iex> SnmpKit.SnmpMgr.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1])
|
|
{:ok, "sysDescr"}
|
|
"""
|
|
def reverse_lookup(oid) when is_list(oid) do
|
|
GenServer.call(__MODULE__, {:reverse_lookup, oid})
|
|
end
|
|
|
|
def reverse_lookup(oid_string) when is_binary(oid_string) do
|
|
case OID.string_to_list(oid_string) do
|
|
{:ok, oid_list} -> reverse_lookup(oid_list)
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets the children of an OID node.
|
|
"""
|
|
def children(oid) do
|
|
GenServer.call(__MODULE__, {:children, oid})
|
|
end
|
|
|
|
@doc """
|
|
Gets the parent of an OID node.
|
|
"""
|
|
def parent([_ | _] = oid) when is_list(oid) do
|
|
{:ok, Enum.drop(oid, -1)}
|
|
end
|
|
|
|
def parent([]), do: {:error, :no_parent}
|
|
|
|
def parent(oid_string) when is_binary(oid_string) do
|
|
case OID.string_to_list(oid_string) do
|
|
{:ok, oid_list} -> parent(oid_list)
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Walks the MIB tree starting from a root OID.
|
|
"""
|
|
def walk_tree(root_oid, opts \\ []) do
|
|
GenServer.call(__MODULE__, {:walk_tree, root_oid, opts})
|
|
end
|
|
|
|
## GenServer Implementation
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Initialize with standard MIBs
|
|
reverse_map = build_reverse_map(@standard_mibs)
|
|
|
|
state = %{
|
|
name_to_oid: @standard_mibs,
|
|
oid_to_name: reverse_map,
|
|
name_to_meta: %{},
|
|
loaded_mibs: [:standard]
|
|
}
|
|
|
|
{:ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:resolve, name}, _from, state) do
|
|
result = resolve_name(name, state.name_to_oid)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:reverse_lookup, oid}, _from, state) do
|
|
result = reverse_lookup_oid(oid, state.oid_to_name)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:children, oid}, _from, state) do
|
|
result = find_children(oid, state.name_to_oid)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:walk_tree, root_oid, _opts}, _from, state) do
|
|
result = walk_tree_from_root(root_oid, state.name_to_oid)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:load_mib, mib_path}, _from, state) do
|
|
case load_mib_file_and_extract_mappings(mib_path) do
|
|
{:ok, mib_data} ->
|
|
new_state = merge_mib_data(state, mib_data)
|
|
{:reply, :ok, new_state}
|
|
|
|
error ->
|
|
{:reply, error, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:load_standard_mibs, _from, state) do
|
|
# Standard MIBs are already loaded in init
|
|
{:reply, :ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:register_loaded_mib, mib_data}, _from, state) do
|
|
# Register MIB data loaded via SnmpKit.SnmpLib.MIB.load_compiled
|
|
new_state = merge_snmp_lib_mib_data(state, mib_data)
|
|
{:reply, :ok, new_state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:get_metadata, base_name}, _from, state) do
|
|
meta = Map.get(state.name_to_meta, base_name)
|
|
{:reply, meta, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:resolve_enhanced, name, _opts}, _from, state) do
|
|
# Enhanced resolution using loaded MIB data
|
|
result = resolve_with_loaded_mibs(name, state)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:integrate_mib_data, mib_file, parsed_data}, _from, state) do
|
|
# Integrate both compiled and parsed MIB data
|
|
new_state = integrate_parsed_mib_data(state, mib_file, parsed_data)
|
|
{:reply, :ok, new_state}
|
|
end
|
|
|
|
## Private Functions
|
|
|
|
defp compile_with_snmp_lib(mib_file, opts) do
|
|
case MIB.compile(mib_file, opts) do
|
|
{:ok, result} -> {:ok, result}
|
|
{:error, reason} -> {:error, {:snmp_lib_compilation_failed, reason}}
|
|
end
|
|
rescue
|
|
UndefinedFunctionError -> {:error, :snmp_lib_not_available}
|
|
end
|
|
|
|
defp compile_all_with_snmp_lib(directory, opts) do
|
|
case File.ls(directory) do
|
|
{:ok, files} ->
|
|
mib_files =
|
|
files
|
|
|> Enum.filter(&String.ends_with?(&1, ".mib"))
|
|
|> Enum.map(&Path.join(directory, &1))
|
|
|
|
case MIB.compile_all(mib_files, opts) do
|
|
{:ok, results} -> {:ok, results}
|
|
{:error, reason} -> {:error, {:snmp_lib_batch_compilation_failed, reason}}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, {:directory_error, reason}}
|
|
end
|
|
rescue
|
|
UndefinedFunctionError -> {:error, :snmp_lib_not_available}
|
|
end
|
|
|
|
defp extract_name_to_oid_from_symbols(symbols) when is_map(symbols) do
|
|
Enum.reduce(symbols, %{}, fn {name, defn}, acc ->
|
|
case defn do
|
|
%{} ->
|
|
case Map.get(defn, :oid) do
|
|
nil ->
|
|
acc
|
|
|
|
oid_any ->
|
|
case normalize_parsed_oid(oid_any) do
|
|
{:ok, oid_list} -> Map.put(acc, name, oid_list)
|
|
_ -> acc
|
|
end
|
|
end
|
|
|
|
_ ->
|
|
acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp extract_meta_from_symbols(symbols) when is_map(symbols) do
|
|
Enum.reduce(symbols, %{}, fn {name, defn}, acc ->
|
|
case defn do
|
|
%{} ->
|
|
case Map.get(defn, :__type__) do
|
|
:object_type ->
|
|
syntax_any = Map.get(defn, :syntax)
|
|
access = Map.get(defn, :max_access)
|
|
status = Map.get(defn, :status)
|
|
description = Map.get(defn, :description)
|
|
|
|
meta = %{
|
|
syntax_base: syntax_base_from(syntax_any),
|
|
textual_convention: textual_convention_from(syntax_any),
|
|
display_hint: nil,
|
|
access: access,
|
|
status: status,
|
|
description: description
|
|
}
|
|
|
|
Map.put(acc, name, meta)
|
|
|
|
_ ->
|
|
acc
|
|
end
|
|
|
|
_ ->
|
|
acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp compile_dir_fallback(directory, opts) do
|
|
case File.ls(directory) do
|
|
{:ok, files} ->
|
|
mib_files = Enum.filter(files, &String.ends_with?(&1, ".mib"))
|
|
|
|
results =
|
|
Enum.map(mib_files, fn file ->
|
|
file_path = Path.join(directory, file)
|
|
{file, compile(file_path, opts)}
|
|
end)
|
|
|
|
{:ok, results}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:directory_error, reason}}
|
|
end
|
|
end
|
|
|
|
defp parse_tokens_to_objects(tokens, _opts) do
|
|
# Extract OBJECT-TYPE definitions from tokens
|
|
objects = extract_object_definitions(tokens)
|
|
{:ok, objects}
|
|
end
|
|
|
|
defp extract_object_definitions(tokens) do
|
|
# Simple object extraction - can be enhanced further
|
|
tokens
|
|
|> Enum.chunk_every(3, 1, :discard)
|
|
|> Enum.filter(fn
|
|
[{:atom, _, name}, {:"OBJECT-TYPE", _}, _] ->
|
|
%{name: name, type: :object}
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|> Enum.map(fn [{:atom, _, name}, {:"OBJECT-TYPE", _}, _] ->
|
|
%{name: name, type: :object_type}
|
|
end)
|
|
end
|
|
|
|
defp load_with_snmp_lib(compiled_mib_path) do
|
|
case MIB.load_compiled(compiled_mib_path) do
|
|
{:ok, result} -> {:ok, result}
|
|
{:error, reason} -> {:error, {:snmp_lib_load_failed, reason}}
|
|
end
|
|
rescue
|
|
UndefinedFunctionError -> {:error, :snmp_lib_not_available}
|
|
end
|
|
|
|
# Derive base syntax from parsed syntax term
|
|
defp syntax_base_from(syntax) do
|
|
case syntax do
|
|
atom when is_atom(atom) ->
|
|
syntax_atom_to_base(atom)
|
|
|
|
{type, _} when type in [:integer, :octet_string, :object_identifier] ->
|
|
type
|
|
|
|
{:type, t} when is_atom(t) ->
|
|
syntax_type_to_base(t)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Map simple syntax atoms to base types
|
|
defp syntax_atom_to_base(atom) do
|
|
base_types = [
|
|
:integer,
|
|
:octet_string,
|
|
:object_identifier,
|
|
:timeticks,
|
|
:counter32,
|
|
:counter64,
|
|
:gauge32,
|
|
:ip_address
|
|
]
|
|
|
|
if atom in base_types, do: atom
|
|
end
|
|
|
|
# Handle typed syntax conversions
|
|
defp syntax_type_to_base(type) do
|
|
case type do
|
|
:"octet string" -> :octet_string
|
|
:"object identifier" -> :object_identifier
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
# Best-effort textual convention detection from syntax term
|
|
defp textual_convention_from(syntax) do
|
|
case syntax do
|
|
{:type, t} when is_atom(t) -> Atom.to_string(t)
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp merge_snmp_lib_mib_data(state, mib_data) do
|
|
# Accept either compiled format with :symbols or a parsed map with name_to_oid/name_to_meta
|
|
{add_map, add_meta} =
|
|
cond do
|
|
is_map(mib_data) and Map.has_key?(mib_data, :symbols) ->
|
|
symbols = Map.get(mib_data, :symbols, %{})
|
|
{extract_name_to_oid_from_symbols(symbols), extract_meta_from_symbols(symbols)}
|
|
|
|
is_map(mib_data) and Map.has_key?(mib_data, :name_to_oid) ->
|
|
raw = Map.get(mib_data, :name_to_oid, %{})
|
|
meta = Map.get(mib_data, :name_to_meta, %{})
|
|
{normalize_name_to_oid(raw), meta}
|
|
|
|
true ->
|
|
{%{}, %{}}
|
|
end
|
|
|
|
merged_name_to_oid = Map.merge(state.name_to_oid, add_map)
|
|
merged_oid_to_name = build_reverse_map(merged_name_to_oid)
|
|
merged_name_to_meta = Map.merge(state.name_to_meta, add_meta)
|
|
|
|
state
|
|
|> Map.put(:name_to_oid, merged_name_to_oid)
|
|
|> Map.put(:oid_to_name, merged_oid_to_name)
|
|
|> Map.put(:name_to_meta, merged_name_to_meta)
|
|
|> Map.update(:snmp_lib_mibs, [mib_data], fn list -> [mib_data | list] end)
|
|
end
|
|
|
|
defp resolve_with_loaded_mibs(name, state) do
|
|
case Map.get(state, :name_to_oid) do
|
|
%{} = m when is_binary(name) ->
|
|
case Map.get(m, name) do
|
|
nil -> {:error, :not_found}
|
|
oid -> {:ok, oid}
|
|
end
|
|
|
|
_ ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp integrate_parsed_mib_data(state, mib_file, parsed_data) do
|
|
# Integrate parsed MIB objects into our name resolution
|
|
integrated_mibs = Map.get(state, :integrated_mibs, %{})
|
|
new_integrated = Map.put(integrated_mibs, mib_file, parsed_data)
|
|
Map.put(state, :integrated_mibs, new_integrated)
|
|
end
|
|
|
|
defp resolve_name(name, name_to_oid_map) do
|
|
cond do
|
|
# Handle nil or invalid names first
|
|
is_nil(name) or not is_binary(name) ->
|
|
{:error, :invalid_name}
|
|
|
|
# Direct match
|
|
Map.has_key?(name_to_oid_map, name) ->
|
|
{:ok, Map.get(name_to_oid_map, name)}
|
|
|
|
# Name with instance (e.g., "sysDescr.0")
|
|
String.contains?(name, ".") ->
|
|
[base_name | instance_parts] = String.split(name, ".")
|
|
|
|
case Map.get(name_to_oid_map, base_name) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
base_oid ->
|
|
case Enum.reduce_while(instance_parts, [], fn part, acc ->
|
|
case Integer.parse(part) do
|
|
{int, ""} -> {:cont, [int | acc]}
|
|
_ -> {:halt, :error}
|
|
end
|
|
end) do
|
|
:error -> {:error, :invalid_instance}
|
|
instance_oids -> {:ok, base_oid ++ Enum.reverse(instance_oids)}
|
|
end
|
|
end
|
|
|
|
true ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp reverse_lookup_oid(oid, oid_to_name_map) do
|
|
case Map.get(oid_to_name_map, oid) do
|
|
nil ->
|
|
# Try to find a partial match
|
|
find_partial_reverse_match(oid, oid_to_name_map)
|
|
|
|
name ->
|
|
# Exact match - return as-is (already includes any suffix in the map)
|
|
{:ok, name}
|
|
end
|
|
end
|
|
|
|
defp find_partial_reverse_match(oid, oid_to_name_map) do
|
|
# Handle case where oid might be a string instead of list
|
|
if is_binary(oid) do
|
|
{:error, :invalid_oid_format}
|
|
else
|
|
# Handle empty list case
|
|
if Enum.empty?(oid) do
|
|
{:error, :empty_oid}
|
|
else
|
|
# Try progressively shorter OIDs to find a base match
|
|
find_partial_match(oid, oid_to_name_map, length(oid) - 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp find_partial_match(_oid, _map, length) when length <= 0, do: {:error, :not_found}
|
|
|
|
defp find_partial_match(oid, oid_to_name_map, length) do
|
|
partial_oid = Enum.take(oid, length)
|
|
|
|
case Map.get(oid_to_name_map, partial_oid) do
|
|
nil ->
|
|
find_partial_match(oid, oid_to_name_map, length - 1)
|
|
|
|
base_name ->
|
|
# Found a base match - append the remaining OID elements as the index suffix
|
|
base = strip_instance_suffix(base_name)
|
|
suffix = Enum.drop(oid, length)
|
|
|
|
case suffix do
|
|
[] -> {:ok, base}
|
|
_ -> {:ok, base <> "." <> Enum.join(suffix, ".")}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp find_children(parent_oid, name_to_oid_map) do
|
|
normalized_oid =
|
|
cond do
|
|
is_nil(parent_oid) ->
|
|
[]
|
|
|
|
is_binary(parent_oid) ->
|
|
case OID.string_to_list(parent_oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
{:error, _} -> []
|
|
end
|
|
|
|
is_list(parent_oid) ->
|
|
parent_oid
|
|
|
|
true ->
|
|
[]
|
|
end
|
|
|
|
# Return error for invalid OIDs
|
|
if normalized_oid == [] and not is_nil(parent_oid) do
|
|
{:error, :invalid_parent_oid}
|
|
else
|
|
children =
|
|
name_to_oid_map
|
|
|> Enum.filter(fn {_name, oid} ->
|
|
is_list(oid) and is_list(normalized_oid) and
|
|
length(oid) == length(normalized_oid) + 1 and
|
|
List.starts_with?(oid, normalized_oid)
|
|
end)
|
|
|> Enum.map(fn {name, _oid} -> name end)
|
|
|> Enum.sort()
|
|
|
|
{:ok, children}
|
|
end
|
|
end
|
|
|
|
defp walk_tree_from_root(root_oid, name_to_oid_map) do
|
|
root_oid =
|
|
cond do
|
|
is_binary(root_oid) ->
|
|
case OID.string_to_list(root_oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
{:error, _} -> []
|
|
end
|
|
|
|
is_list(root_oid) ->
|
|
root_oid
|
|
|
|
is_nil(root_oid) ->
|
|
[]
|
|
|
|
true ->
|
|
[]
|
|
end
|
|
|
|
descendants =
|
|
name_to_oid_map
|
|
|> Enum.filter(fn {_name, oid} ->
|
|
is_list(oid) and List.starts_with?(oid, root_oid)
|
|
end)
|
|
|> Enum.map(fn {name, oid} -> {name, oid} end)
|
|
|> Enum.sort_by(fn {_name, oid} -> oid end)
|
|
|
|
{:ok, descendants}
|
|
end
|
|
|
|
defp build_reverse_map(name_to_oid_map) do
|
|
Map.new(name_to_oid_map, fn {name, oid} -> {oid, name} end)
|
|
end
|
|
|
|
# Normalize any dotted instance suffix from a name like "ifDescr.1" -> "ifDescr"
|
|
defp strip_instance_suffix(name) when is_binary(name) do
|
|
case String.split(name, ".", parts: 2) do
|
|
[base] -> base
|
|
[base, _rest] -> base
|
|
end
|
|
end
|
|
|
|
defp strip_instance_suffix(other), do: other
|
|
|
|
defp normalize_to_oid_list(oid_any) when is_list(oid_any) do
|
|
case OID.valid_oid?(oid_any) do
|
|
:ok -> {:ok, oid_any}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
defp normalize_to_oid_list(oid_any) when is_binary(oid_any) do
|
|
if String.contains?(oid_any, ".") and String.match?(oid_any, ~r/^\.?\d+(?:\.\d+)*$/) do
|
|
OID.string_to_list(oid_any)
|
|
else
|
|
case String.split(oid_any, ".", parts: 2) do
|
|
[base] ->
|
|
case resolve(base) do
|
|
{:ok, base_oid} -> {:ok, base_oid}
|
|
error -> error
|
|
end
|
|
|
|
[base, instance_str] ->
|
|
with {:ok, base_oid} <- resolve(base),
|
|
{:ok, instance_index} <- parse_instance(instance_str) do
|
|
{:ok, base_oid ++ instance_index}
|
|
else
|
|
{:error, _} = err -> err
|
|
_ -> {:error, :invalid_instance}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp normalize_to_oid_list(_), do: {:error, :invalid_input}
|
|
|
|
defp parse_instance(instance_str) do
|
|
parts = String.split(instance_str, ".")
|
|
|
|
try do
|
|
ints =
|
|
Enum.map(parts, fn p ->
|
|
case Integer.parse(p) do
|
|
{i, ""} -> i
|
|
_ -> throw(:bad)
|
|
end
|
|
end)
|
|
|
|
{:ok, ints}
|
|
catch
|
|
:bad -> {:error, :invalid_instance}
|
|
end
|
|
end
|
|
|
|
defp base_name_and_index(oid_list) do
|
|
case reverse_lookup(oid_list) do
|
|
{:ok, name_with_index} ->
|
|
# Strip instance suffix to get the true base name (e.g., "sysDescr.0" -> "sysDescr")
|
|
base_name = strip_instance_suffix(name_with_index)
|
|
|
|
case name_to_oid(base_name) do
|
|
{:ok, base_oid} ->
|
|
base_len = length(base_oid)
|
|
|
|
if length(oid_list) > base_len do
|
|
{:ok, base_name, Enum.drop(oid_list, base_len)}
|
|
else
|
|
{:ok, base_name, nil}
|
|
end
|
|
|
|
{:error, _} = err ->
|
|
err
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp name_to_oid(name) when is_binary(name) do
|
|
case resolve(name) do
|
|
{:ok, oid} -> {:ok, oid}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
defp maybe_put_instance(map, input_oid, base_oid) do
|
|
base_len = length(base_oid)
|
|
|
|
if length(input_oid) > base_len do
|
|
instance = Enum.drop(input_oid, base_len)
|
|
|
|
instance_index =
|
|
case instance do
|
|
[i] -> i
|
|
list -> list
|
|
end
|
|
|
|
map
|
|
|> Map.put(:instance_index, instance_index)
|
|
|> Map.put(:instance_oid, input_oid)
|
|
else
|
|
map
|
|
end
|
|
end
|
|
|
|
defp syntax_for(base_name) do
|
|
Map.get(@curated_syntax, base_name, %{base: nil, textual_convention: nil, display_hint: nil})
|
|
end
|
|
|
|
defp maybe_put(map, _key, nil), do: map
|
|
defp maybe_put(map, key, val), do: Map.put(map, key, val)
|
|
|
|
defp module_for(base_name) do
|
|
cond do
|
|
String.starts_with?(base_name, "sys") -> "SNMPv2-MIB"
|
|
String.starts_with?(base_name, "ifHC") -> "IF-MIB"
|
|
String.starts_with?(base_name, "ifIn") -> "IF-MIB"
|
|
String.starts_with?(base_name, "ifOut") -> "IF-MIB"
|
|
String.starts_with?(base_name, "if") -> "IF-MIB"
|
|
String.starts_with?(base_name, "ipNetToMedia") -> "IP-MIB"
|
|
String.starts_with?(base_name, "ipNetToPhysical") -> "IP-MIB"
|
|
String.starts_with?(base_name, "dot1d") -> "BRIDGE-MIB"
|
|
String.starts_with?(base_name, "dot1q") -> "Q-BRIDGE-MIB"
|
|
true -> nil
|
|
end
|
|
end
|
|
|
|
# Normalize name->oid map from arbitrary representations
|
|
defp normalize_name_to_oid(raw) when is_map(raw) do
|
|
Enum.reduce(raw, %{}, fn {name, oid_any}, acc ->
|
|
case normalize_parsed_oid(oid_any) do
|
|
{:ok, oid_list} -> Map.put(acc, name, oid_list)
|
|
_ -> acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
# Convert parsed OID representation to a flat integer list when possible
|
|
defp normalize_parsed_oid(oid) when is_list(oid) do
|
|
if Enum.all?(oid, &is_integer/1) do
|
|
{:ok, oid}
|
|
else
|
|
# Handle lists like [%{value: 1}, %{value: 3}, ...] possibly with names
|
|
vals =
|
|
Enum.map(oid, fn
|
|
%{value: v} when is_integer(v) ->
|
|
{:ok, v}
|
|
|
|
%{value: v} when is_binary(v) ->
|
|
case Integer.parse(v) do
|
|
{i, ""} -> {:ok, i}
|
|
_ -> :error
|
|
end
|
|
|
|
v when is_integer(v) ->
|
|
{:ok, v}
|
|
|
|
_ ->
|
|
:error
|
|
end)
|
|
|
|
if Enum.any?(vals, &(&1 == :error)) do
|
|
{:error, :unresolved_oid}
|
|
else
|
|
{:ok, Enum.map(vals, fn {:ok, i} -> i end)}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp normalize_parsed_oid(_), do: {:error, :invalid_oid}
|
|
|
|
defp load_mib_file_and_extract_mappings(mib_path) do
|
|
case File.read(mib_path) do
|
|
{:ok, mib_content} ->
|
|
case Parser.parse(mib_content) do
|
|
{:ok, parsed_mib_data} -> {:ok, extract_mib_mappings(parsed_mib_data)}
|
|
{:error, reason} -> {:error, {:mib_parse_failed, reason}}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, {:file_read_failed, reason}}
|
|
end
|
|
end
|
|
|
|
defp extract_mib_mappings(mib_data) do
|
|
# Extract name-to-OID mappings and basic metadata from parsed MIB data
|
|
definitions = Map.get(mib_data, :definitions, [])
|
|
|
|
# Build TC map first
|
|
tc_map =
|
|
definitions
|
|
|> Enum.filter(&(Map.get(&1, :__type__) == :textual_convention))
|
|
|> Enum.reduce(%{}, fn tc, acc ->
|
|
tc_name = Map.get(tc, :name)
|
|
tc_syntax = Map.get(tc, :syntax)
|
|
display_hint = Map.get(tc, :display_hint)
|
|
Map.put(acc, tc_name, %{syntax_base: syntax_base_from(tc_syntax), display_hint: display_hint})
|
|
end)
|
|
|
|
primitives =
|
|
MapSet.new([:integer, :octet_string, :object_identifier, :timeticks, :counter32, :counter64, :gauge32, :ip_address])
|
|
|
|
{name_to_oid_map, name_to_meta} =
|
|
Enum.reduce(definitions, {%{}, %{}}, fn defn, {oid_acc, meta_acc} ->
|
|
case Map.get(defn, :__type__) do
|
|
:object_type ->
|
|
name = Map.get(defn, :name)
|
|
oid_any = Map.get(defn, :oid)
|
|
syntax_any = Map.get(defn, :syntax)
|
|
access = Map.get(defn, :max_access)
|
|
status = Map.get(defn, :status)
|
|
description = Map.get(defn, :description)
|
|
|
|
oid_acc2 =
|
|
case {name, normalize_parsed_oid(oid_any)} do
|
|
{name, {:ok, oid_list}} when is_binary(name) -> Map.put(oid_acc, name, oid_list)
|
|
_ -> oid_acc
|
|
end
|
|
|
|
{syntax_base, textual_convention, display_hint} =
|
|
case syntax_any do
|
|
# Named type referencing a TC like :DisplayString
|
|
t when is_atom(t) ->
|
|
if MapSet.member?(primitives, t) do
|
|
{syntax_base_from(syntax_any), textual_convention_from(syntax_any), nil}
|
|
else
|
|
tc_key = Atom.to_string(t)
|
|
|
|
case Map.get(tc_map, tc_key) do
|
|
%{syntax_base: base, display_hint: hint} -> {base, tc_key, hint}
|
|
_ -> {syntax_base_from(syntax_any), textual_convention_from(syntax_any), nil}
|
|
end
|
|
end
|
|
|
|
_ ->
|
|
{syntax_base_from(syntax_any), textual_convention_from(syntax_any), nil}
|
|
end
|
|
|
|
meta = %{
|
|
syntax_base: syntax_base,
|
|
textual_convention: textual_convention,
|
|
display_hint: display_hint,
|
|
access: access,
|
|
status: status,
|
|
description: description
|
|
}
|
|
|
|
{oid_acc2, Map.put(meta_acc, name, meta)}
|
|
|
|
_ ->
|
|
{oid_acc, meta_acc}
|
|
end
|
|
end)
|
|
|
|
%{name_to_oid: name_to_oid_map, name_to_meta: name_to_meta}
|
|
end
|
|
|
|
defp merge_mib_data(state, _mib_data) do
|
|
# This would merge the new MIB data with existing state
|
|
# For now, just return the current state
|
|
state
|
|
end
|
|
end
|