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 oid_tree.ex
Raw

lib/snmpkit/snmp_sim/oid_tree.ex

defmodule SnmpKit.SnmpSim.OIDTree do
@moduledoc """
Optimized OID tree for fast lookups and lexicographic traversal.
Supports GETNEXT operations and GETBULK bulk retrieval.
Uses a radix tree structure optimized for OID operations with:
- Fast O(log n) lookups and insertions
- Efficient lexicographic traversal for GETNEXT
- Memory-efficient storage for 10K+ OIDs
- Optimized bulk operations
"""
defstruct [
# Root node of the OID tree
:root,
# Number of OIDs in the tree
:size,
# Cached sorted OID list for traversal
:sorted_oids
]
# Tree node structure
defmodule Node do
@moduledoc false
defstruct [
# This part of the OID (integer)
:oid_part,
# Value for this OID (if it's a leaf)
:value,
# Behavior configuration for this OID
:behavior,
# Map of child nodes
:children,
# Whether this node represents a complete OID
:is_leaf
]
end
@doc """
Create a new empty OID tree.
## Examples
tree = SnmpKit.SnmpSim.OIDTree.new()
"""
def new() do
%__MODULE__{
root: %Node{oid_part: nil, children: %{}, is_leaf: false},
size: 0,
sorted_oids: []
}
end
@doc """
Insert an OID with its value and behavior information into the tree.
Maintains lexicographic ordering for efficient GETNEXT operations.
## Examples
tree = SnmpKit.SnmpSim.OIDTree.new()
tree = SnmpKit.SnmpSim.OIDTree.insert(tree, "1.3.6.1.2.1.1.1.0", "System Description", nil)
"""
def insert(%__MODULE__{} = tree, oid_string, value, behavior_info \\ nil) do
oid_parts = parse_oid(oid_string)
{new_root, inserted} = insert_node(tree.root, oid_parts, value, behavior_info)
new_size = if inserted, do: tree.size + 1, else: tree.size
# Mark sorted OIDs as stale (will be rebuilt on next traversal)
%{tree | root: new_root, size: new_size, sorted_oids: nil}
end
@doc """
Get the value for an exact OID match.
## Examples
{:ok, value, behavior} = SnmpKit.SnmpSim.OIDTree.get(tree, "1.3.6.1.2.1.1.1.0")
:not_found = SnmpKit.SnmpSim.OIDTree.get(tree, "1.3.6.1.2.1.1.1.999")
"""
def get(%__MODULE__{} = tree, oid_string) do
oid_parts = parse_oid(oid_string)
get_node(tree.root, oid_parts)
end
@doc """
Get the next OID in lexicographic order (GETNEXT operation).
Returns the next OID after the given OID, or :end_of_mib if no more OIDs exist.
## Examples
{:ok, next_oid, value, behavior} = SnmpKit.SnmpSim.OIDTree.get_next(tree, "1.3.6.1.2.1.1.1.0")
:end_of_mib = SnmpKit.SnmpSim.OIDTree.get_next(tree, "1.3.6.1.9.9.9.9.9")
"""
def get_next(%__MODULE__{} = tree, oid_string) do
# Ensure we have a sorted OID list for traversal
tree = ensure_sorted_oids(tree)
target_oid_parts = parse_oid(oid_string)
case find_next_oid(tree.sorted_oids, target_oid_parts) do
nil ->
:end_of_mib
next_oid_string ->
case get(tree, next_oid_string) do
{:ok, value, behavior} -> {:ok, next_oid_string, value, behavior}
:not_found -> :end_of_mib
end
end
end
@doc """
Perform a bulk walk operation starting from the given OID.
Used for GETBULK operations with proper handling of non-repeaters and max-repetitions.
## Examples
results = SnmpKit.SnmpSim.OIDTree.bulk_walk(tree, "1.3.6.1.2.1.2.2.1", 10, 0)
"""
def bulk_walk(%__MODULE__{} = tree, start_oid, max_repetitions, non_repeaters \\ 0) do
# Ensure we have a sorted OID list for traversal
tree = ensure_sorted_oids(tree)
start_oid_parts = parse_oid(start_oid)
# Find starting position in sorted list
start_index = find_start_index(tree.sorted_oids, start_oid_parts)
# Collect results up to max_repetitions
collect_bulk_results(tree, start_index, max_repetitions, non_repeaters)
end
@doc """
Get all OIDs in the tree in lexicographic order.
Useful for debugging and full tree traversal.
## Examples
oids = SnmpKit.SnmpSim.OIDTree.list_oids(tree)
"""
def list_oids(%__MODULE__{} = tree) do
tree = ensure_sorted_oids(tree)
tree.sorted_oids
end
@doc """
Get the size of the tree (number of OIDs).
## Examples
size = SnmpKit.SnmpSim.OIDTree.size(tree)
"""
def size(%__MODULE__{} = tree), do: tree.size
@doc """
Check if the tree is empty.
## Examples
empty? = SnmpKit.SnmpSim.OIDTree.empty?(tree)
"""
def empty?(%__MODULE__{} = tree), do: tree.size == 0
# Private helper functions
defp parse_oid(oid_string) when is_binary(oid_string) do
oid_string
|> String.trim_leading(".")
|> String.split(".")
|> Enum.map(&String.to_integer/1)
end
defp insert_node(%Node{} = node, [], value, behavior) do
# Reached the end of OID parts - this node becomes a leaf
inserted = not node.is_leaf
updated_node = %{node | value: value, behavior: behavior, is_leaf: true}
{updated_node, inserted}
end
defp insert_node(%Node{} = node, [part | rest], value, behavior) do
child = Map.get(node.children, part, %Node{oid_part: part, children: %{}, is_leaf: false})
{updated_child, inserted} = insert_node(child, rest, value, behavior)
updated_children = Map.put(node.children, part, updated_child)
updated_node = %{node | children: updated_children}
{updated_node, inserted}
end
defp get_node(%Node{} = node, []) do
if node.is_leaf do
{:ok, node.value, node.behavior}
else
:not_found
end
end
defp get_node(%Node{} = node, [part | rest]) do
case Map.get(node.children, part) do
nil -> :not_found
child_node -> get_node(child_node, rest)
end
end
defp ensure_sorted_oids(%__MODULE__{sorted_oids: nil} = tree) do
sorted_oids = build_sorted_oids(tree.root, [])
%{tree | sorted_oids: sorted_oids}
end
defp ensure_sorted_oids(%__MODULE__{} = tree), do: tree
defp build_sorted_oids(%Node{} = node, current_path) do
# Collect this node if it's a leaf
this_oids =
if node.is_leaf do
[Enum.join(current_path, ".")]
else
[]
end
# Collect all child OIDs in sorted order
child_oids =
node.children
# Sort by OID part numerically
|> Enum.sort_by(fn {part, _} -> part end)
|> Enum.flat_map(fn {part, child_node} ->
build_sorted_oids(child_node, current_path ++ [part])
end)
this_oids ++ child_oids
end
defp find_next_oid(sorted_oids, target_oid_parts) do
_target_oid_string = Enum.join(target_oid_parts, ".")
sorted_oids
|> Enum.find(fn oid_string ->
compare_oids(parse_oid(oid_string), target_oid_parts) == :gt
end)
end
defp find_start_index(sorted_oids, start_oid_parts) do
sorted_oids
|> Enum.find_index(fn oid_string ->
compare_oids(parse_oid(oid_string), start_oid_parts) == :gt
end) || length(sorted_oids)
end
defp collect_bulk_results(%__MODULE__{} = tree, start_index, max_repetitions, _non_repeaters) do
available_oids = Enum.drop(tree.sorted_oids, start_index)
# Take up to max_repetitions OIDs (considering non_repeaters)
# For now, simplified to just take max_repetitions
oids_to_fetch = Enum.take(available_oids, max_repetitions)
# Fetch values for each OID
Enum.map(oids_to_fetch, fn oid_string ->
case get(tree, oid_string) do
{:ok, value, behavior} -> {oid_string, value, behavior}
:not_found -> {oid_string, nil, nil}
end
end)
end
# Compare two OID part lists lexicographically
defp compare_oids([], []), do: :eq
defp compare_oids([], _), do: :lt
defp compare_oids(_, []), do: :gt
defp compare_oids([a | rest_a], [b | rest_b]) when a == b do
compare_oids(rest_a, rest_b)
end
defp compare_oids([a | _], [b | _]) when a < b, do: :lt
defp compare_oids([a | _], [b | _]) when a > b, do: :gt
end