Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_sim walk_parser.ex
Raw

lib/snmpkit/snmp_sim/walk_parser.ex

defmodule SnmpKit.SnmpSim.WalkParser do
@moduledoc """
Parse both named MIB and numeric OID walk file formats.
Handle different snmpwalk output variations automatically.
"""
@doc """
Parse a walk file and return a map of OID -> value mappings.
Supports both named MIB format and numeric OID format:
- Named: "IF-MIB::ifInOctets.2 = Counter32: 1234567890"
- Numeric: ".1.3.6.1.2.1.2.2.1.10.2 = Counter32: 1234567890"
## Examples
{:ok, oid_map} = SnmpKit.SnmpSim.WalkParser.parse_walk_file("priv/walks/cable_modem.walk")
"""
def parse_walk_file(file_path) do
case File.read(file_path) do
{:ok, content} ->
oid_map =
content
|> String.split("\n")
|> Enum.map(&parse_walk_line/1)
|> Enum.reject(&is_nil/1)
|> Map.new()
{:ok, oid_map}
{:error, reason} ->
{:error, {:file_read_error, reason}}
end
end
@doc """
Parse a single line from a walk file.
## Examples
# Named MIB format
result = SnmpKit.SnmpSim.WalkParser.parse_walk_line("IF-MIB::ifInOctets.2 = Counter32: 1234567890")
# => {"1.3.6.1.2.1.2.2.1.10.2", %{type: "Counter32", value: 1234567890, mib_name: "IF-MIB::ifInOctets.2"}}
# Numeric OID format
result = SnmpKit.SnmpSim.WalkParser.parse_walk_line(".1.3.6.1.2.1.2.2.1.10.2 = Counter32: 1234567890")
# => {"1.3.6.1.2.1.2.2.1.10.2", %{type: "Counter32", value: 1234567890}}
"""
def parse_walk_line(line) do
line = String.trim(line)
cond do
# Named MIB format: "IF-MIB::ifInOctets.2 = Counter32: 1234567890"
String.contains?(line, "::") ->
parse_named_mib_line(line)
# Numeric OID format: ".1.3.6.1.2.1.2.2.1.10.2 = Counter32: 1234567890"
String.starts_with?(line, ".") ->
parse_numeric_oid_line(line)
# Skip comments and empty lines
String.starts_with?(line, "#") or line == "" ->
nil
# Try generic parsing for other formats
true ->
parse_generic_line(line)
end
end
# Parse named MIB format lines
defp parse_named_mib_line(line) do
case Regex.run(~r/^(.+?)::(.+?)\s*=\s*([\w-]+):\s*(.+)$/, line) do
[_, mib_name, oid_suffix, data_type, value] ->
numeric_oid = resolve_mib_name(mib_name, oid_suffix)
parsed_value = parse_typed_value(value, data_type)
{numeric_oid,
%{
type: data_type,
value: parsed_value,
mib_name: "#{mib_name}::#{oid_suffix}"
}}
_ ->
nil
end
end
# Parse numeric OID format lines
defp parse_numeric_oid_line(line) do
case Regex.run(~r/^(\.[\d\.]+)\s*=\s*([\w-]+):\s*(.+)$/, line) do
[_, oid, data_type, value] ->
clean_oid = String.trim_leading(oid, ".")
parsed_value = parse_typed_value(value, data_type)
{clean_oid, %{type: data_type, value: parsed_value}}
_ ->
nil
end
end
# Parse other potential formats
defp parse_generic_line(line) do
case Regex.run(~r/^(.+?)\s*=\s*([\w-]+):\s*(.+)$/, line) do
[_, oid_part, data_type, value] ->
oid = normalize_oid(oid_part)
parsed_value = parse_typed_value(value, data_type)
{oid, %{type: data_type, value: parsed_value}}
_ ->
nil
end
end
# Parse and convert values based on their SNMP data type
defp parse_typed_value(value, data_type) do
cleaned_value = clean_value(value)
case String.upcase(data_type) do
"INTEGER" -> parse_integer(cleaned_value)
"COUNTER32" -> parse_integer(cleaned_value)
"COUNTER64" -> parse_integer(cleaned_value)
"GAUGE32" -> parse_integer(cleaned_value)
"GAUGE" -> parse_integer(cleaned_value)
"TIMETICKS" -> parse_timeticks(cleaned_value)
"STRING" -> cleaned_value
"OCTET" -> cleaned_value
"HEX-STRING" -> parse_hex_string(cleaned_value)
"OID" -> parse_oid_value(cleaned_value)
"IPADDRESS" -> parse_ip_address(cleaned_value)
_ -> cleaned_value
end
end
# Clean up raw value strings
defp clean_value(value) do
value
|> String.trim()
# Remove quotes from strings
|> String.trim("\"")
end
# Parse integer values, including named enums like "ethernetCsmacd(6)"
defp parse_integer(value) do
# First try to extract number from parentheses (for named enums)
case Regex.run(~r/\((\d+)\)/, value) do
[_, number_str] ->
case Integer.parse(number_str) do
{int_val, _} -> int_val
:error -> 0
end
nil ->
# Try to parse as regular integer
case Integer.parse(value) do
{int_val, _} -> int_val
:error -> 0
end
end
end
# Parse timeticks with format like "(12345600) 1 day, 10:17:36.00"
defp parse_timeticks(value) do
case Regex.run(~r/^\((\d+)\)/, value) do
[_, ticks] -> parse_integer(ticks)
_ -> parse_integer(value)
end
end
# Parse hex strings like "00 1A 2B 3C 4D 5E"
defp parse_hex_string(value) do
if Regex.match?(~r/^[0-9A-Fa-f\s]+$/, value) do
value
|> String.replace(" ", "")
|> String.upcase()
else
value
end
end
# Parse OID values that may start with dots or contain MIB names
defp parse_oid_value(value) do
cleaned_value = String.trim_leading(value, ".")
# Handle SNMPv2-SMI::enterprises prefix
case String.starts_with?(cleaned_value, "SNMPv2-SMI::enterprises.") do
true ->
# Replace SNMPv2-SMI::enterprises with 1.3.6.1.4.1
suffix = String.replace_prefix(cleaned_value, "SNMPv2-SMI::enterprises.", "")
oid_string = "1.3.6.1.4.1.#{suffix}"
# Convert to OID list for object_identifier types
oid_string
|> String.split(".")
|> Enum.map(&String.to_integer/1)
false ->
# For other OID values, just clean and return as string
cleaned_value
end
end
# Parse IP addresses
defp parse_ip_address(value) do
# Remove any surrounding formatting and return the IP
value
|> String.trim()
|> String.trim("\"")
end
# Normalize OID format (remove leading dots, handle different formats)
defp normalize_oid(oid_part) do
oid_part
|> String.trim()
|> String.trim_leading(".")
end
# Basic MIB name resolution (can be extended with more comprehensive mapping)
defp resolve_mib_name(mib_name, oid_suffix) do
base_oids = %{
"SNMPv2-MIB" => "1.3.6.1.2.1.1",
"IF-MIB" => "1.3.6.1.2.1.2",
"IP-MIB" => "1.3.6.1.2.1.4",
"TCP-MIB" => "1.3.6.1.2.1.6",
"UDP-MIB" => "1.3.6.1.2.1.7",
"HOST-RESOURCES-MIB" => "1.3.6.1.2.1.25",
"BRIDGE-MIB" => "1.3.6.1.2.1.17",
"ENTITY-MIB" => "1.3.6.1.2.1.47",
"CISCO-SMI" => "1.3.6.1.4.1.9",
"DOCS-CABLE-DEVICE-MIB" => "1.3.6.1.2.1.69",
"DOCS-IF-MIB" => "1.3.6.1.2.1.10.127"
}
# Try to resolve known MIB names
case Map.get(base_oids, mib_name) do
nil ->
# Unknown MIB - try to extract numeric portion from suffix
extract_numeric_oid(oid_suffix)
base_oid ->
# Combine base OID with suffix
combine_oid_parts(base_oid, oid_suffix)
end
end
# Extract numeric OID from suffix like "sysDescr.0" -> "1.0" or "ifInOctets.2" -> "10.2"
defp extract_numeric_oid(oid_suffix) do
# Common object mappings within standard MIBs
object_mappings = %{
"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",
"ifNumber" => "1.3.6.1.2.1.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"
}
case Regex.run(~r/^([^.]+)\.(.+)$/, oid_suffix) do
[_, object_name, instance] ->
case Map.get(object_mappings, object_name) do
# Return as-is if unknown
nil -> oid_suffix
base_oid -> "#{base_oid}.#{instance}"
end
_ ->
# Return as-is if no instance part
oid_suffix
end
end
# Combine base OID with suffix intelligently
defp combine_oid_parts(base_oid, oid_suffix) do
# Handle different suffix formats
case Regex.run(~r/^([^.]+)\.(.+)$/, oid_suffix) do
[_, object_name, instance] ->
# Map object names to their sub-OIDs within the MIB
object_oid =
case object_name do
"sysDescr" -> "#{base_oid}.1"
"sysObjectID" -> "#{base_oid}.2"
"sysUpTime" -> "#{base_oid}.3"
"sysContact" -> "#{base_oid}.4"
"sysName" -> "#{base_oid}.5"
"sysLocation" -> "#{base_oid}.6"
"sysServices" -> "#{base_oid}.7"
"ifNumber" -> "#{base_oid}.1"
"ifIndex" -> "#{base_oid}.2.1.1"
"ifDescr" -> "#{base_oid}.2.1.2"
"ifType" -> "#{base_oid}.2.1.3"
"ifMtu" -> "#{base_oid}.2.1.4"
"ifSpeed" -> "#{base_oid}.2.1.5"
"ifPhysAddress" -> "#{base_oid}.2.1.6"
"ifAdminStatus" -> "#{base_oid}.2.1.7"
"ifOperStatus" -> "#{base_oid}.2.1.8"
"ifLastChange" -> "#{base_oid}.2.1.9"
"ifInOctets" -> "#{base_oid}.2.1.10"
"ifInUcastPkts" -> "#{base_oid}.2.1.11"
"ifInNUcastPkts" -> "#{base_oid}.2.1.12"
"ifInDiscards" -> "#{base_oid}.2.1.13"
"ifInErrors" -> "#{base_oid}.2.1.14"
"ifInUnknownProtos" -> "#{base_oid}.2.1.15"
"ifOutOctets" -> "#{base_oid}.2.1.16"
"ifOutUcastPkts" -> "#{base_oid}.2.1.17"
"ifOutNUcastPkts" -> "#{base_oid}.2.1.18"
"ifOutDiscards" -> "#{base_oid}.2.1.19"
"ifOutErrors" -> "#{base_oid}.2.1.20"
"ifOutQLen" -> "#{base_oid}.2.1.21"
# Default
_ -> "#{base_oid}.1"
end
"#{object_oid}.#{instance}"
_ ->
# Simple scalar object
"#{base_oid}.0"
end
end
end