towerops/lib/snmpkit/snmp_mgr/mib.ex
2026-01-23 14:01:52 -06:00

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