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

lib/snmpkit/snmp_sim/bulk_operations.ex

defmodule SnmpKit.SnmpSim.BulkOperations do
@moduledoc """
Efficient GETBULK implementation for SNMPv2c.
Handles non-repeaters, max-repetitions, and response size management.
GETBULK is a powerful SNMP operation that retrieves multiple variables in a single request.
It's particularly useful for retrieving large tables like interface statistics.
## Algorithm
1. Process first N variable bindings as non-repeaters (like GETNEXT)
2. For remaining variable bindings, repeat up to max-repetitions times
3. Respect UDP packet size limits (typically 1472 bytes for Ethernet)
4. Return tooBig error if response would exceed limits
"""
alias SnmpKit.SnmpSim.OIDTree
# Conservative UDP payload size
@max_udp_size 1400
@doc """
Handle a GETBULK request with proper non-repeaters and max-repetitions processing.
## Parameters
- `oid_tree`: The OID tree to query
- `non_repeaters`: Number of variables to treat as non-repeating (GETNEXT only)
- `max_repetitions`: Maximum number of repetitions for repeating variables
- `varbinds`: List of starting OIDs for the bulk operation
## Returns
- `{:ok, result_varbinds}`: Successful bulk operation results
- `{:error, :too_big}`: Response would exceed UDP size limits
- `{:error, reason}`: Other error conditions
## Examples
varbinds = [{"1.3.6.1.2.1.2.2.1.1", nil}, {"1.3.6.1.2.1.2.2.1.2", nil}]
{:ok, results} = SnmpKit.SnmpSim.BulkOperations.handle_bulk_request(
tree, 0, 10, varbinds
)
"""
def handle_bulk_request(%OIDTree{} = oid_tree, non_repeaters, max_repetitions, varbinds)
when is_integer(non_repeaters) and is_integer(max_repetitions) and is_list(varbinds) do
# Validate parameters
cond do
non_repeaters < 0 -> {:error, :invalid_non_repeaters}
max_repetitions < 0 -> {:error, :invalid_max_repetitions}
non_repeaters > length(varbinds) -> {:error, :non_repeaters_exceeds_varbinds}
true -> process_bulk_request(oid_tree, non_repeaters, max_repetitions, varbinds)
end
end
@doc """
Optimize bulk response to fit within UDP packet size limits.
Estimates response size and truncates if necessary.
## Parameters
- `results`: List of {oid, value, behavior} tuples
- `max_size`: Maximum response size in bytes (default: 1400)
## Returns
- `{:ok, optimized_results}`: Results that fit within size limit
- `{:error, :too_big}`: Even minimal response exceeds size limit
## Examples
{:ok, optimized} = SnmpKit.SnmpSim.BulkOperations.optimize_bulk_response(results, 1400)
"""
def optimize_bulk_response(results, max_size \\ @max_udp_size) when is_list(results) do
estimate_and_truncate(results, max_size, [], 0)
end
@doc """
Calculate estimated response size for a list of variable bindings.
Used for response size management.
## Examples
size = SnmpKit.SnmpSim.BulkOperations.estimate_response_size(varbinds)
"""
def estimate_response_size(varbinds) when is_list(varbinds) do
# SNMP message overhead
base_overhead = 50
varbind_size =
varbinds
|> Enum.reduce(0, fn {oid, value, _behavior}, acc ->
# OID encoding overhead
oid_size = byte_size(oid) + 10
value_size = estimate_value_size(value)
# Variable binding overhead
acc + oid_size + value_size + 8
end)
base_overhead + varbind_size
end
@doc """
Process an interface table bulk request efficiently.
Optimized for common SNMP table walking operations.
## Examples
{:ok, results} = SnmpKit.SnmpSim.BulkOperations.process_interface_table(
tree, "1.3.6.1.2.1.2.2.1", 10
)
"""
def process_interface_table(%OIDTree{} = oid_tree, table_oid, max_repetitions) do
# Find all OIDs under the table
all_oids = OIDTree.list_oids(oid_tree)
table_oids =
all_oids
|> Enum.filter(&String.starts_with?(&1, table_oid))
|> Enum.take(max_repetitions)
# Fetch values for table OIDs
results =
table_oids
|> Enum.map(fn oid ->
case OIDTree.get(oid_tree, oid) do
{:ok, value, behavior} -> {oid, value, behavior}
:not_found -> {oid, nil, nil}
end
end)
{:ok, results}
end
# Private implementation functions
defp process_bulk_request(oid_tree, non_repeaters, max_repetitions, varbinds) do
{non_repeating_vbs, repeating_vbs} = Enum.split(varbinds, non_repeaters)
# Process non-repeating variables (like GETNEXT)
non_repeating_results = process_non_repeaters(oid_tree, non_repeating_vbs)
# Process repeating variables up to max_repetitions
repeating_results = process_repeaters(oid_tree, repeating_vbs, max_repetitions)
all_results = non_repeating_results ++ repeating_results
# Check if response fits in UDP packet
case optimize_bulk_response(all_results) do
{:ok, optimized_results} -> {:ok, optimized_results}
{:error, :too_big} -> {:error, :too_big}
end
end
defp process_non_repeaters(oid_tree, varbinds) do
Enum.map(varbinds, fn {oid, _value} ->
case OIDTree.get_next(oid_tree, oid) do
{:ok, next_oid, value, behavior} -> {next_oid, value, behavior}
:end_of_mib -> {oid, :end_of_mib_view, nil}
end
end)
end
defp process_repeaters(oid_tree, varbinds, max_repetitions) do
# For each repeating variable, collect up to max_repetitions values
varbinds
|> Enum.flat_map(fn {start_oid, _value} ->
collect_repetitions(oid_tree, start_oid, max_repetitions, [])
end)
end
defp collect_repetitions(_oid_tree, _current_oid, 0, acc), do: Enum.reverse(acc)
defp collect_repetitions(oid_tree, current_oid, remaining, acc) do
# Use efficient bulk walk instead of individual get_next calls
results = OIDTree.bulk_walk(oid_tree, current_oid, remaining, 0)
case results do
[] ->
# No more OIDs in this branch
Enum.reverse(acc)
results when is_list(results) ->
# Convert to the expected format and append to accumulator
formatted_results =
Enum.map(results, fn {oid, value, behavior} -> {oid, value, behavior} end)
Enum.reverse(acc) ++ formatted_results
end
end
defp estimate_and_truncate([], _max_size, acc, _current_size) do
{:ok, Enum.reverse(acc)}
end
defp estimate_and_truncate([result | rest], max_size, acc, current_size) do
result_size = estimate_result_size(result)
new_size = current_size + result_size
cond do
new_size <= max_size ->
# This result fits, continue
estimate_and_truncate(rest, max_size, [result | acc], new_size)
current_size == 0 ->
# Even the first result is too big
{:error, :too_big}
true ->
# This result would exceed limit, stop here
{:ok, Enum.reverse(acc)}
end
end
defp estimate_result_size({oid, value, _behavior}) do
# OID encoding overhead
oid_size = byte_size(oid) + 10
value_size = estimate_value_size(value)
# Variable binding overhead
oid_size + value_size + 8
end
defp estimate_value_size(value) when is_binary(value), do: byte_size(value) + 4
defp estimate_value_size(value) when is_integer(value), do: 8
defp estimate_value_size({:counter32, _}), do: 8
defp estimate_value_size({:counter64, _}), do: 12
defp estimate_value_size({:gauge32, _}), do: 8
defp estimate_value_size({:timeticks, _}), do: 8
defp estimate_value_size({:ipaddress, _}), do: 8
defp estimate_value_size({:objectid, oid}), do: byte_size(oid) + 4
defp estimate_value_size(:end_of_mib_view), do: 4
defp estimate_value_size(:no_such_object), do: 4
defp estimate_value_size(:no_such_instance), do: 4
defp estimate_value_size(nil), do: 4
# Default estimate
defp estimate_value_size(_), do: 8
end