towerops/lib/snmpkit/snmp_mgr/walk.ex
Graham McIntire d1403c8069 Fix failing tests and clean up code
- Fix doctests for Accounts, Agents.Stats, Snmp to match actual behavior
- Fix dynamic_extra_test vendor post-processing tests to seed sensor data
- Fix activity_controller_test to seed devices for feed data
- Fix session_manager_test to create browser session for test
- Fix topology_test link creation for connection test
- Fix device_monitor/driver_worker tests for unique job constraints
- Fix accounts_test expired_tokens assertion (magic link token is expired)
- Fix happy_path_test and show_events_test to seed monitor data
- Fix admin user_live_test user.name -> user.email (no name field)
- Fix schema_test to seed activity data
- Fix mobile_qr_live_test to match actual template text
- Fix SnmpKit.MIB doctests and tests for enriched return values
- Fix onboarding_live, mobile_controller, mib_test weak assertions
- Remove dead code and fix credo warnings
2026-06-16 14:54:34 -05:00

155 lines
5.2 KiB
Elixir

defmodule SnmpKit.SnmpMgr.Walk do
@moduledoc """
SNMP walk operations using iterative GETNEXT requests.
This module provides efficient walking of SNMP trees and tables
using the GETNEXT operation repeatedly until the end of the subtree.
"""
alias SnmpKit.SnmpLib.OID
alias SnmpKit.SnmpMgr.Bulk
alias SnmpKit.SnmpMgr.Core
@default_max_iterations 100
@default_timeout 5000
@doc """
Performs a walk starting from the given root OID.
Automatically chooses between GETNEXT (SNMPv1) and GETBULK (SNMPv2c)
based on the version specified in options.
## Parameters
- `target` - The target device
- `root_oid` - Starting OID for the walk
- `opts` - Options including :version, :max_repetitions, :timeout, :community
"""
def walk(target, root_oid, opts \\ []) do
version = Keyword.get(opts, :version, :v2c)
case version do
:v2c ->
# Use bulk walk for better performance
Bulk.walk_bulk(target, root_oid, opts)
_ ->
# Fall back to traditional GETNEXT walk (SNMPv1)
# Use max_iterations instead of max_repetitions for v1 (which doesn't support bulk operations)
max_iterations = Keyword.get(opts, :max_iterations, @default_max_iterations)
_timeout = Keyword.get(opts, :timeout, @default_timeout)
# Remove max_repetitions from opts for v1 operations since it's not supported
v1_opts = Keyword.delete(opts, :max_repetitions)
case resolve_oid(root_oid) do
{:ok, start_oid} ->
walk_from_oid(target, start_oid, start_oid, [], max_iterations, v1_opts)
error ->
error
end
end
end
@doc """
Walks an SNMP table starting from the table OID.
Automatically chooses between GETNEXT and GETBULK based on version.
GETBULK provides significantly better performance for large tables.
## Parameters
- `target` - The target device
- `table_oid` - The table OID to walk
- `opts` - Options including :version, :max_repetitions, :timeout, :community
"""
def walk_table(target, table_oid, opts \\ []) do
version = Keyword.get(opts, :version, :v2c)
case version do
:v2c ->
# Use bulk table walk for better performance
Bulk.get_table_bulk(target, table_oid, opts)
_ ->
# Fall back to traditional GETNEXT walk (SNMPv1)
# Use max_iterations instead of max_repetitions for v1
max_iterations = Keyword.get(opts, :max_iterations, @default_max_iterations)
v1_opts = Keyword.delete(opts, :max_repetitions)
case resolve_oid(table_oid) do
{:ok, start_oid} ->
walk_from_oid(target, start_oid, start_oid, [], max_iterations, v1_opts)
error ->
error
end
end
end
@doc """
Walks a specific table column.
## Parameters
- `target` - The target device
- `column_oid` - The full column OID (table + entry + column)
- `opts` - Options
"""
def walk_column(target, column_oid, opts \\ []) do
case resolve_oid(column_oid) do
{:ok, start_oid} ->
max_iterations = Keyword.get(opts, :max_iterations, @default_max_iterations)
v1_opts = Keyword.delete(opts, :max_repetitions)
walk_from_oid(target, start_oid, start_oid, [], max_iterations, v1_opts)
error ->
error
end
end
# Private functions
defp walk_from_oid(target, current_oid, root_oid, acc, remaining, opts) when remaining > 0 do
case Core.send_get_next_request(target, current_oid, opts) do
{:ok, {next_oid_string, type, value}} ->
handle_walk_response(target, root_oid, acc, remaining, opts, next_oid_string, type, value)
{:error, error} ->
handle_walk_error(error, acc)
end
end
defp walk_from_oid(_target, _current_oid, _root_oid, acc, 0, _opts) do
# Hit max repetitions limit
{:ok, Enum.reverse(acc)}
end
defp handle_walk_response(target, root_oid, acc, remaining, opts, next_oid_string, type, value) do
with {:ok, next_oid} <- OID.string_to_list(next_oid_string),
true <- still_in_scope?(next_oid, root_oid) do
new_acc = [{next_oid_string, type, value} | acc]
walk_from_oid(target, next_oid, root_oid, new_acc, remaining - 1, opts)
else
{:error, _} -> {:ok, Enum.reverse(acc)}
false -> {:ok, Enum.reverse(acc)}
end
end
defp handle_walk_error({:snmp_error, :endOfMibView}, acc), do: {:ok, Enum.reverse(acc)}
defp handle_walk_error({:snmp_error, :noSuchName}, acc), do: {:ok, Enum.reverse(acc)}
defp handle_walk_error(:end_of_mib_view, acc), do: {:ok, Enum.reverse(acc)}
defp handle_walk_error(:no_such_name, acc), do: {:ok, Enum.reverse(acc)}
defp handle_walk_error(error, _acc), do: {:error, error}
defp still_in_scope?(current_oid, root_oid) do
# Check if current OID is still within the root OID scope
List.starts_with?(current_oid, root_oid)
end
# OID resolution helper - delegates to canonical Core.parse_oid
defp resolve_oid(oid), do: Core.parse_oid(oid)
# Type information must never be inferred - it must be preserved from SNMP responses
# Removing type inference functions to prevent loss of critical type information
# All operations must reject responses with type_information_lost to maintain data integrity
end