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_mgr adaptive_walk.ex
Raw

lib/snmpkit/snmp_mgr/adaptive_walk.ex

defmodule SnmpKit.SnmpMgr.AdaptiveWalk do
@moduledoc """
Intelligent SNMP walk operations with adaptive parameter tuning.
This module provides advanced walk operations that automatically adjust
bulk parameters based on device response characteristics for optimal performance.
"""
@default_initial_bulk_size 10
@default_max_entries 1_000
@doc """
Performs an adaptive bulk walk that automatically tunes parameters.
Starts with a conservative bulk size and adapts based on response times
and error rates to find the optimal parameters for the target device.
## Parameters
- `target` - The target device
- `root_oid` - Starting OID for the walk
- `opts` - Options including :adaptive_tuning, :performance_threshold
## Examples
# Note: This function makes actual network calls and is not suitable for doctests
{:ok, results} = SnmpKit.SnmpMgr.AdaptiveWalk.bulk_walk("switch.local", "ifTable")
# Automatically adjusts bulk size for optimal performance:
# [
# {"1.3.6.1.2.1.2.2.1.2.1", :octet_string, "FastEthernet0/1"},
# {"1.3.6.1.2.1.2.2.1.2.2", :octet_string, "FastEthernet0/2"},
# {"1.3.6.1.2.1.2.2.1.2.3", :octet_string, "GigabitEthernet0/1"},
# # ... optimally retrieved with adaptive bulk sizing
# ]
# With custom options:
{:ok, results} = SnmpKit.SnmpMgr.AdaptiveWalk.bulk_walk("router.local", "sysDescr",
adaptive_tuning: true, max_entries: 100, performance_threshold: 50)
# [{"1.3.6.1.2.1.1.1.0", :octet_string, "Cisco IOS Software, Version 15.1"}]
"""
def bulk_walk(target, root_oid, opts \\ []) do
max_entries = Keyword.get(opts, :max_entries, @default_max_entries)
case resolve_oid(root_oid) do
{:ok, start_oid} ->
# Handle empty OID by using standard MIB-II subtree
{actual_start_oid, filter_oid} =
case start_oid do
[] -> {[1, 3], []}
_ -> {start_oid, start_oid}
end
simple_bulk_walk(target, actual_start_oid, filter_oid, [], max_entries, opts)
error ->
error
end
end
@doc """
Performs an adaptive table walk optimized for large tables.
Automatically determines the optimal bulk size for table retrieval
and handles pagination for very large tables.
## Parameters
- `target` - The target device
- `table_oid` - The table OID to walk
- `opts` - Options including :adaptive_tuning, :stream, :max_entries
## Examples
# Note: This function makes actual network calls and is not suitable for doctests
{:ok, table_data} = SnmpKit.SnmpMgr.AdaptiveWalk.table_walk("switch.local", "ifTable", max_entries: 1000)
# Efficiently walks large tables with automatic optimization:
# [
# {"1.3.6.1.2.1.2.2.1.1.1", :integer, 1}, # ifIndex.1
# {"1.3.6.1.2.1.2.2.1.2.1", :octet_string, "Ethernet1"}, # ifDescr.1
# {"1.3.6.1.2.1.2.2.1.3.1", :integer, 6}, # ifType.1 (ethernetCsmacd)
# {"1.3.6.1.2.1.2.2.1.5.1", :gauge32, 1000000000}, # ifSpeed.1 (1 Gbps)
# # ... continues with adaptive pagination for large tables
# ]
# For very large tables with streaming:
{:ok, results} = SnmpKit.SnmpMgr.AdaptiveWalk.table_walk("core-switch", "ipRouteTable",
max_entries: 10000, adaptive_tuning: true)
"""
def table_walk(target, table_oid, opts \\ []) do
adaptive_tuning = Keyword.get(opts, :adaptive_tuning, true)
max_entries = Keyword.get(opts, :max_entries, @default_max_entries)
case resolve_oid(table_oid) do
{:ok, start_oid} ->
if adaptive_tuning do
adaptive_table_walk(target, start_oid, start_oid, [], max_entries, opts)
else
# Fall back to regular table walk
SnmpKit.SnmpMgr.Bulk.get_table_bulk(target, table_oid, opts)
end
error ->
error
end
end
@doc """
Benchmarks a device to determine optimal bulk parameters.
Performs a series of test requests with different bulk sizes
to determine the best parameters for the target device.
## Parameters
- `target` - The target device to benchmark
- `test_oid` - OID to use for testing (should have multiple entries)
- `opts` - Options including :test_sizes, :iterations
## Examples
# Note: This function makes actual network calls and is not suitable for doctests
{:ok, benchmark} = SnmpKit.SnmpMgr.AdaptiveWalk.benchmark_device("switch.local", "ifTable")
# Returns comprehensive performance analysis:
# %{
# optimal_bulk_size: 25,
# avg_response_time: 45,
# error_rate: 0.0,
# all_results: [
# {1, 120}, {5, 85}, {10, 52}, {15, 48},
# {20, 44}, {25, 42}, {30, 45}, {40, 52}, {50, 68}
# ],
# recommendations: %{
# max_repetitions: 25,
# timeout: 3000,
# adaptive_tuning: false # Device has consistent performance
# }
# }
# Custom benchmark with specific test sizes:
{:ok, results} = SnmpKit.SnmpMgr.AdaptiveWalk.benchmark_device("router.local", "ipRouteTable",
test_sizes: [5, 10, 20, 50], iterations: 5)
"""
def benchmark_device(target, test_oid, opts \\ []) do
test_sizes = Keyword.get(opts, :test_sizes, [1, 5, 10, 15, 20, 25, 30, 40, 50])
iterations = Keyword.get(opts, :iterations, 3)
case resolve_oid(test_oid) do
{:ok, oid_list} ->
results =
test_sizes
|> Enum.map(fn size ->
avg_time = benchmark_bulk_size(target, oid_list, size, iterations, opts)
{size, avg_time}
end)
|> Enum.filter(fn {_size, time} -> time != :error end)
if Enum.empty?(results) do
{:error, :no_successful_benchmarks}
else
optimal = find_optimal_parameters(results)
{:ok, optimal}
end
error ->
error
end
end
@doc """
Gets optimal parameters for a previously benchmarked device.
Returns cached optimal parameters or performs a quick benchmark
if no cached data is available.
"""
def get_optimal_params(target, opts \\ []) do
# In a real implementation, this would check a cache/ETS table
# For now, return reasonable defaults based on device type
default_params = %{
max_repetitions: determine_default_bulk_size(target),
timeout: 5000,
adaptive_tuning: true
}
{:ok, Map.merge(default_params, Enum.into(opts, %{}))}
end
# Private functions
# Simple recursive PDU processor
defp simple_bulk_walk(target, current_oid, filter_oid, acc, remaining, opts)
when remaining > 0 do
bulk_size = min(remaining, Keyword.get(opts, :max_repetitions, 10))
bulk_opts =
opts
|> Keyword.put(:max_repetitions, bulk_size)
|> Keyword.put(:version, :v2c)
case SnmpKit.SnmpMgr.Core.send_get_bulk_request(target, current_oid, bulk_opts) do
{:ok, results} ->
# Check for end of MIB conditions first
if has_end_of_mib?(results) do
{:ok, Enum.reverse(acc)}
else
# Filter results based on base OID
filtered_results = filter_results_by_base_oid(results, filter_oid)
# Stop if no more results in scope
if Enum.empty?(filtered_results) do
{:ok, Enum.reverse(acc)}
else
# Get next OID for continuation
next_oid = get_next_oid(results)
# Stop if we can't determine next OID or if it's the same (no progress)
if next_oid == nil or next_oid == current_oid do
{:ok, Enum.reverse(acc)}
else
# Add filtered results to accumulator
new_acc = Enum.reverse(filtered_results) ++ acc
new_remaining = remaining - length(filtered_results)
# Continue recursively
simple_bulk_walk(target, next_oid, filter_oid, new_acc, new_remaining, opts)
end
end
end
{:error, :endOfMibView} ->
{:ok, Enum.reverse(acc)}
{:error, :noSuchName} ->
{:ok, Enum.reverse(acc)}
{:error, _} = error ->
error
end
end
defp simple_bulk_walk(_target, _current_oid, _filter_oid, acc, 0, _opts) do
{:ok, Enum.reverse(acc)}
end
# Check if any result contains end-of-MIB indicators
defp has_end_of_mib?(results) do
Enum.any?(results, fn result ->
case result do
{_oid, :endOfMibView, _} -> true
{_oid, :noSuchObject, _} -> true
{_oid, :noSuchInstance, _} -> true
_ -> false
end
end)
end
# Filter results to only include those within the base OID scope
defp filter_results_by_base_oid(results, filter_oid) do
Enum.filter(results, fn result ->
case result do
{oid_list, _type, _value} when is_list(oid_list) ->
oid_in_scope?(oid_list, filter_oid)
{oid_string, _type, _value} when is_binary(oid_string) ->
case SnmpKit.SnmpLib.OID.string_to_list(oid_string) do
{:ok, oid_list} -> oid_in_scope?(oid_list, filter_oid)
_ -> false
end
_ ->
false
end
end)
end
# Get the next OID for continuing the walk
defp get_next_oid(results) do
case List.last(results) do
{oid_list, _type, _value} when is_list(oid_list) ->
oid_list
{oid_string, _type, _value} when is_binary(oid_string) ->
case SnmpKit.SnmpLib.OID.string_to_list(oid_string) do
{:ok, oid_list} -> oid_list
_ -> nil
end
_ ->
nil
end
end
# Check if OID is within scope of the filter OID
defp oid_in_scope?(oid_list, filter_oid) do
case filter_oid do
[] ->
# Empty filter means accept everything
true
_ ->
# Check if OID starts with filter OID
List.starts_with?(oid_list, filter_oid)
end
end
defp adaptive_table_walk(target, current_oid, root_oid, acc, remaining, opts) do
# Use simple bulk walk for table operations too
simple_bulk_walk(target, current_oid, root_oid, acc, remaining, opts)
end
defp benchmark_bulk_size(target, oid_list, bulk_size, iterations, opts) do
times =
1..iterations
|> Enum.map(fn _ ->
start_time = System.monotonic_time(:millisecond)
bulk_opts =
opts
|> Keyword.put(:max_repetitions, bulk_size)
|> Keyword.put(:version, :v2c)
case SnmpKit.SnmpMgr.Core.send_get_bulk_request(target, oid_list, bulk_opts) do
{:ok, _results} ->
end_time = System.monotonic_time(:millisecond)
end_time - start_time
{:error, _} ->
:error
end
end)
|> Enum.filter(&(&1 != :error))
if Enum.empty?(times) do
:error
else
Enum.sum(times) / length(times)
end
end
defp find_optimal_parameters(results) do
# Find the bulk size with best performance (lowest average time)
{optimal_size, optimal_time} =
results
|> Enum.min_by(fn {_size, time} -> time end)
# Calculate statistics (avg_time not currently used but available for future enhancements)
_avg_time =
results |> Enum.map(fn {_size, time} -> time end) |> Enum.sum() |> div(length(results))
%{
optimal_bulk_size: optimal_size,
avg_response_time: optimal_time,
# We filtered out errors above
error_rate: 0.0,
all_results: results,
recommendations: %{
max_repetitions: optimal_size,
# 3x response time for timeout
timeout: max(3000, trunc(optimal_time * 3)),
# Enable adaptive tuning for slow devices
adaptive_tuning: optimal_time > 100
}
}
end
defp determine_default_bulk_size(target) do
# Simple heuristic based on target type
cond do
String.contains?(target, "switch") -> 25
String.contains?(target, "router") -> 15
String.contains?(target, "server") -> 10
true -> @default_initial_bulk_size
end
end
defp resolve_oid(oid) when is_binary(oid) do
case oid do
"" ->
# Empty string means start from MIB root
{:ok, []}
_ ->
case SnmpKit.SnmpLib.OID.string_to_list(oid) do
{:ok, oid_list} ->
{:ok, oid_list}
{:error, _} ->
# Try as symbolic name
case SnmpKit.SnmpMgr.MIB.resolve(oid) do
{:ok, resolved_oid} -> {:ok, resolved_oid}
error -> error
end
end
end
end
defp resolve_oid(oid) when is_list(oid), do: {:ok, oid}
defp resolve_oid(_), do: {:error, :invalid_oid_format}
end