- 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
155 lines
5.2 KiB
Elixir
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
|