978 lines
33 KiB
Elixir
978 lines
33 KiB
Elixir
defmodule SnmpKit.SnmpLib.Manager do
|
|
@moduledoc """
|
|
High-level SNMP management operations providing a simplified interface for common SNMP tasks.
|
|
|
|
This module builds on the core SnmpLib functionality to provide production-ready SNMP
|
|
management capabilities including GET, GETBULK, SET operations with intelligent error
|
|
handling, connection reuse, and performance optimizations.
|
|
|
|
## Features
|
|
|
|
- **Simple API**: High-level functions for common SNMP operations
|
|
- **Connection Reuse**: Efficient socket management for multiple operations
|
|
- **Error Handling**: Comprehensive error handling with meaningful messages
|
|
- **Performance**: Optimized for bulk operations and large-scale polling
|
|
- **Timeout Management**: Configurable timeouts with sensible defaults
|
|
- **Community Support**: Support for different community strings per device
|
|
|
|
## Quick Start
|
|
|
|
# Simple GET operation
|
|
{:ok, {type, value}} = SnmpKit.SnmpLib.Manager.get("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0])
|
|
|
|
# GET with custom community and timeout
|
|
{:ok, {type, value}} = SnmpKit.SnmpLib.Manager.get("192.168.1.1", "1.3.6.1.2.1.1.1.0",
|
|
community: "private", timeout: 10_000)
|
|
|
|
# Bulk operations for efficiency
|
|
{:ok, results} = SnmpKit.SnmpLib.Manager.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2],
|
|
max_repetitions: 20)
|
|
|
|
# SET operation
|
|
{:ok, :success} = SnmpKit.SnmpLib.Manager.set("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 5, 0],
|
|
{:string, "New System Name"})
|
|
|
|
## Configuration Options
|
|
|
|
- `community`: SNMP community string (default: "public")
|
|
- `version`: SNMP version (:v1, :v2c) (default: :v2c)
|
|
- `timeout`: Operation timeout in milliseconds (default: 5000)
|
|
- `retries`: Number of retry attempts (default: 3)
|
|
- `port`: SNMP port (default: 161)
|
|
- `local_port`: Local source port (default: 0 for random)
|
|
"""
|
|
|
|
alias SnmpKit.SnmpLib.OID
|
|
alias SnmpKit.SnmpLib.PDU
|
|
alias SnmpKit.SnmpLib.Transport
|
|
alias SnmpKit.SnmpLib.Utils
|
|
|
|
require Logger
|
|
|
|
@default_community "public"
|
|
@default_version :v2c
|
|
@default_timeout 5_000
|
|
@default_retries 3
|
|
@default_port 161
|
|
@default_local_port 0
|
|
@default_max_repetitions 30
|
|
@default_non_repeaters 0
|
|
|
|
@type host :: binary() | :inet.ip_address()
|
|
@type oid :: [non_neg_integer()] | binary()
|
|
@type snmp_value :: any()
|
|
@type community :: binary()
|
|
@type version :: :v1 | :v2c
|
|
@type operation_result :: {:ok, snmp_value()} | {:error, atom() | {atom(), any()}}
|
|
@type bulk_result :: {:ok, [varbind()]} | {:error, atom() | {atom(), any()}}
|
|
@type varbind :: {oid(), snmp_value()}
|
|
|
|
@type manager_opts :: [
|
|
community: community(),
|
|
version: version(),
|
|
timeout: pos_integer(),
|
|
retries: non_neg_integer(),
|
|
port: pos_integer(),
|
|
local_port: non_neg_integer()
|
|
]
|
|
|
|
@type bulk_opts :: [
|
|
community: community(),
|
|
version: version(),
|
|
timeout: pos_integer(),
|
|
retries: non_neg_integer(),
|
|
port: pos_integer(),
|
|
local_port: non_neg_integer(),
|
|
max_repetitions: pos_integer(),
|
|
non_repeaters: non_neg_integer()
|
|
]
|
|
|
|
## Public API
|
|
|
|
@doc """
|
|
Performs an SNMP GET operation to retrieve a single value.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `oid`: Object identifier as list or string (e.g., [1,3,6,1,2,1,1,1,0] or "1.3.6.1.2.1.1.1.0")
|
|
- `opts`: Configuration options (see module docs for available options)
|
|
|
|
## Returns
|
|
|
|
- `{:ok, {type, value}}`: Successfully retrieved the value with its SNMP type
|
|
- `{:error, reason}`: Operation failed with reason
|
|
|
|
## Examples
|
|
|
|
# Basic GET operation (would succeed with real device)
|
|
# SnmpKit.SnmpLib.Manager.get("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0])
|
|
# {:ok, {:octet_string, "Cisco IOS Software"}}
|
|
|
|
# GET with custom community and timeout (would succeed with real device)
|
|
# SnmpKit.SnmpLib.Manager.get("192.168.1.1", "1.3.6.1.2.1.1.1.0", community: "private", timeout: 10_000)
|
|
# {:ok, {:octet_string, "Private System Description"}}
|
|
|
|
# Test that function exists and handles invalid input properly
|
|
iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get("invalid.host", [1, 3, 6, 1, 2, 1, 1, 3, 0], timeout: 100))
|
|
true
|
|
"""
|
|
@spec get(host(), oid(), manager_opts()) ::
|
|
{:ok, {atom(), any()}}
|
|
| {:error, atom() | {:network_error, atom()} | {:socket_error, atom()}}
|
|
def get(host, oid, opts \\ []) do
|
|
opts = merge_default_opts(opts)
|
|
normalized_oid = normalize_oid(oid)
|
|
|
|
Logger.debug("Starting GET operation: host=#{inspect(host)}, oid=#{inspect(normalized_oid)}")
|
|
|
|
case create_socket(opts) do
|
|
{:ok, socket} ->
|
|
Logger.debug("Socket created successfully")
|
|
|
|
case perform_get_operation(socket, host, normalized_oid, opts) do
|
|
{:ok, response} ->
|
|
Logger.debug("GET operation completed, extracting result")
|
|
:ok = close_socket(socket)
|
|
result = extract_get_result(response)
|
|
Logger.debug("Final GET result: #{inspect(result)}")
|
|
result
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("GET operation failed: #{inspect(reason)}")
|
|
:ok = close_socket(socket)
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("Socket creation failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Performs an SNMP GETNEXT operation to retrieve the next value in the MIB tree.
|
|
|
|
GETNEXT is used to traverse the MIB tree by retrieving the next available
|
|
object after the specified OID. This is essential for MIB walking operations
|
|
and discovering available objects on SNMP devices.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `oid`: Object identifier to get the next value after
|
|
- `opts`: Configuration options
|
|
|
|
## Implementation Details
|
|
|
|
- **SNMP v1**: Uses proper GETNEXT PDU for compatibility
|
|
- **SNMP v2c+**: Uses optimized GETBULK with max_repetitions=1
|
|
|
|
## Returns
|
|
|
|
- `{:ok, {next_oid, type, value}}`: Next OID and its value as a tuple
|
|
- `{:error, reason}`: Operation failed with reason
|
|
|
|
## Examples
|
|
|
|
# Get next OID after system description
|
|
{:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0")
|
|
|
|
# SNMP v1 compatibility
|
|
{:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0", version: :v1)
|
|
|
|
# With custom community
|
|
{:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0",
|
|
community: "private", timeout: 10_000)
|
|
"""
|
|
@spec get_next(host(), oid(), manager_opts()) ::
|
|
{:ok, {oid(), atom(), any()}} | {:error, atom() | {atom(), any()}}
|
|
def get_next(host, oid, opts \\ []) do
|
|
opts = merge_default_opts(opts)
|
|
normalized_oid = normalize_oid(oid)
|
|
|
|
Logger.debug(
|
|
"Starting GETNEXT operation: host=#{inspect(host)}, oid=#{inspect(normalized_oid)}, version=#{opts[:version]}"
|
|
)
|
|
|
|
case opts[:version] do
|
|
:v1 ->
|
|
# Use proper GETNEXT PDU for SNMP v1 compatibility
|
|
perform_get_next_v1(host, normalized_oid, opts)
|
|
|
|
_ ->
|
|
# Use GETBULK with max_repetitions=1 for v2c+ efficiency
|
|
perform_get_next_v2c(host, normalized_oid, opts)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Performs an SNMP GETBULK operation for efficient bulk data retrieval.
|
|
|
|
GETBULK is more efficient than multiple GET operations when retrieving
|
|
multiple consecutive values, especially for table walking operations.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `base_oid`: Base OID to start the bulk operation
|
|
- `opts`: Configuration options including bulk-specific options
|
|
|
|
## Bulk-Specific Options
|
|
|
|
- `max_repetitions`: Maximum number of repetitions (default: 10)
|
|
- `non_repeaters`: Number of non-repeating variables (default: 0)
|
|
|
|
## Returns
|
|
|
|
- `{:ok, varbinds}`: List of {oid, type, value} tuples
|
|
- `{:error, reason}`: Operation failed with reason
|
|
|
|
## Examples
|
|
|
|
# Test that get_bulk function exists and handles invalid input properly
|
|
iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get_bulk("invalid.host", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100))
|
|
true
|
|
|
|
# High-repetition bulk for large tables
|
|
# SnmpKit.SnmpLib.Manager.get_bulk("192.168.1.1", "1.3.6.1.2.1.2.2", max_repetitions: 50)
|
|
# {:ok, [...]} Returns up to 50 interface entries
|
|
"""
|
|
@spec get_bulk(host(), oid(), bulk_opts()) :: bulk_result()
|
|
def get_bulk(host, base_oid, opts \\ []) do
|
|
opts = merge_bulk_opts(opts)
|
|
normalized_oid = normalize_oid(base_oid)
|
|
|
|
# GETBULK requires SNMPv2c or higher
|
|
if opts[:version] == :v1 do
|
|
{:error, :getbulk_requires_v2c}
|
|
else
|
|
with {:ok, socket} <- create_socket(opts),
|
|
{:ok, response} <- perform_bulk_operation(socket, host, normalized_oid, opts),
|
|
:ok <- close_socket(socket) do
|
|
extract_bulk_result(response)
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Performs an SNMP SET operation to modify a value on the target device.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `oid`: Object identifier to modify
|
|
- `value`: New value as {type, data} tuple (e.g., {:string, "new name"})
|
|
- `opts`: Configuration options
|
|
|
|
## Supported Value Types
|
|
|
|
- `{:string, binary()}`: OCTET STRING
|
|
- `{:integer, integer()}`: INTEGER
|
|
- `{:counter32, non_neg_integer()}`: Counter32
|
|
- `{:gauge32, non_neg_integer()}`: Gauge32
|
|
- `{:timeticks, non_neg_integer()}`: TimeTicks
|
|
- `{:ip_address, binary()}`: IpAddress (4 bytes)
|
|
|
|
## Returns
|
|
|
|
- `{:ok, :success}`: SET operation completed successfully
|
|
- `{:error, reason}`: Operation failed with reason
|
|
|
|
## Examples
|
|
|
|
# Test that SET function exists and handles invalid input properly
|
|
iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.set("invalid.host", [1, 3, 6, 1, 2, 1, 1, 5, 0], {:string, "test"}, timeout: 100))
|
|
true
|
|
"""
|
|
@spec set(host(), oid(), {atom(), any()}, manager_opts()) :: {:ok, :success} | {:error, any()}
|
|
def set(host, oid, {type, value}, opts \\ []) do
|
|
opts = merge_default_opts(opts)
|
|
normalized_oid = normalize_oid(oid)
|
|
|
|
with {:ok, socket} <- create_socket(opts),
|
|
{:ok, response} <-
|
|
perform_set_operation(socket, host, normalized_oid, {type, value}, opts),
|
|
:ok <- close_socket(socket) do
|
|
extract_set_result(response)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Performs multiple GET operations efficiently with connection reuse.
|
|
|
|
More efficient than individual get/3 calls when retrieving multiple values
|
|
from the same device by reusing the same socket connection.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `oids`: List of OIDs to retrieve
|
|
- `opts`: Configuration options
|
|
|
|
## Returns
|
|
|
|
- `{:ok, results}`: List of {oid, type, value} or {oid, {:error, reason}} tuples
|
|
- `{:error, reason}`: Connection or overall operation failed
|
|
|
|
## Examples
|
|
|
|
# Test that get_multi function exists and handles invalid input properly
|
|
iex> oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0", "1.3.6.1.2.1.1.5.0"]
|
|
iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get_multi("invalid.host", oids, timeout: 100))
|
|
true
|
|
"""
|
|
@spec get_multi(host(), [oid()], manager_opts()) ::
|
|
{:ok, [{oid(), atom(), any() | {:error, any()}}]} | {:error, any()}
|
|
def get_multi(host, oids, opts \\ []) when is_list(oids) do
|
|
# Validate input parameters
|
|
case oids do
|
|
[] ->
|
|
{:error, :empty_oids}
|
|
|
|
_ ->
|
|
opts = merge_default_opts(opts)
|
|
normalized_oids = Enum.map(oids, &normalize_oid/1)
|
|
|
|
case create_socket(opts) do
|
|
{:ok, socket} ->
|
|
results = get_multi_with_socket(socket, host, normalized_oids, opts)
|
|
:ok = close_socket(socket)
|
|
|
|
# Check if all operations failed due to network issues
|
|
case check_for_global_failure(results) do
|
|
{:global_failure, reason} -> {:error, reason}
|
|
:mixed_results -> {:ok, results}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Interprets SNMP errors with enhanced semantics for common cases.
|
|
|
|
Provides more specific error interpretation when generic errors like `:gen_err`
|
|
are returned by devices that should return more specific SNMP error codes.
|
|
|
|
## Parameters
|
|
|
|
- `error`: The original error returned by SNMP operations
|
|
- `operation`: The SNMP operation type (`:get`, `:set`, `:get_bulk`)
|
|
- `version`: SNMP version (`:v1`, `:v2c`, `:v3`)
|
|
|
|
## Returns
|
|
|
|
More specific error atom when possible, otherwise the original error.
|
|
|
|
## Examples
|
|
|
|
# Interpret genErr for GET operations
|
|
iex> SnmpKit.SnmpLib.Manager.interpret_error(:gen_err, :get, :v2c)
|
|
:no_such_object
|
|
|
|
iex> SnmpKit.SnmpLib.Manager.interpret_error(:gen_err, :get, :v1)
|
|
:no_such_name
|
|
|
|
iex> SnmpKit.SnmpLib.Manager.interpret_error(:too_big, :get, :v2c)
|
|
:too_big
|
|
"""
|
|
@spec interpret_error(atom(), atom(), atom()) :: atom()
|
|
def interpret_error(:gen_err, :get, :v1) do
|
|
# In SNMPv1, genErr for GET operations commonly means OID doesn't exist
|
|
:no_such_name
|
|
end
|
|
|
|
def interpret_error(:gen_err, :get, version) when version in [:v2c, :v2, :v3] do
|
|
# In SNMPv2c+, genErr for GET operations commonly means object doesn't exist
|
|
:no_such_object
|
|
end
|
|
|
|
def interpret_error(:gen_err, :get_bulk, version) when version in [:v2c, :v2, :v3] do
|
|
# For bulk operations, genErr often indicates end of MIB or missing objects
|
|
:no_such_object
|
|
end
|
|
|
|
def interpret_error(error, _operation, _version) do
|
|
# Return original error for all other cases
|
|
error
|
|
end
|
|
|
|
@doc """
|
|
Checks if a host is reachable via SNMP by performing a basic GET operation.
|
|
|
|
Useful for device discovery and health checking. Attempts to retrieve
|
|
sysUpTime (1.3.6.1.2.1.1.3.0) which should be available on all SNMP devices.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target device IP address or hostname
|
|
- `opts`: Configuration options (typically just community and timeout)
|
|
|
|
## Returns
|
|
|
|
- `{:ok, :reachable}`: Device responded to SNMP request
|
|
- `{:error, reason}`: Device not reachable or SNMP not available
|
|
|
|
## Examples
|
|
|
|
# Test that ping function exists and handles invalid input properly
|
|
iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.ping("invalid.host", timeout: 100))
|
|
true
|
|
"""
|
|
@spec ping(host(), manager_opts()) :: {:ok, :reachable} | {:error, any()}
|
|
def ping(host, opts \\ []) do
|
|
# Use sysUpTime OID as it should be available on all SNMP devices
|
|
sys_uptime_oid = [1, 3, 6, 1, 2, 1, 1, 3, 0]
|
|
|
|
case get(host, sys_uptime_oid, opts) do
|
|
{:ok, {_type, _value}} -> {:ok, :reachable}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
## Private Implementation
|
|
|
|
# Socket management
|
|
defp create_socket(_opts) do
|
|
case Transport.create_client_socket() do
|
|
{:ok, socket} -> {:ok, socket}
|
|
{:error, reason} -> {:error, {:socket_error, reason}}
|
|
end
|
|
end
|
|
|
|
defp close_socket(socket) do
|
|
Transport.close_socket(socket)
|
|
end
|
|
|
|
# Operation implementations
|
|
defp perform_get_operation(socket, host, oid, opts) do
|
|
request_id = generate_request_id()
|
|
pdu = PDU.build_get_request(oid, request_id)
|
|
perform_snmp_request(socket, host, pdu, opts)
|
|
end
|
|
|
|
defp perform_bulk_operation(socket, host, base_oid, opts) do
|
|
request_id = generate_request_id()
|
|
max_reps = opts[:max_repetitions] || @default_max_repetitions
|
|
non_reps = opts[:non_repeaters] || @default_non_repeaters
|
|
|
|
pdu = PDU.build_get_bulk_request(base_oid, request_id, non_reps, max_reps)
|
|
perform_snmp_request(socket, host, pdu, opts)
|
|
end
|
|
|
|
defp perform_set_operation(socket, host, oid, value, opts) do
|
|
request_id = generate_request_id()
|
|
pdu = PDU.build_set_request(oid, value, request_id)
|
|
perform_snmp_request(socket, host, pdu, opts)
|
|
end
|
|
|
|
defp perform_snmp_request(socket, host, pdu, opts) do
|
|
community = opts[:community] || @default_community
|
|
version = opts[:version] || @default_version
|
|
timeout = opts[:timeout] || @default_timeout
|
|
port_option = opts[:port] || @default_port
|
|
|
|
# Parse target to handle both host:port strings and :port option
|
|
{parsed_host, parsed_port} =
|
|
case Utils.parse_target(host) do
|
|
{:ok, %{host: h, port: p}} ->
|
|
# Check if host contained a port specification
|
|
if host_contains_port?(host) do
|
|
# Host:port format - use parsed port (backward compatibility)
|
|
{h, p}
|
|
else
|
|
# Host without port - use :port option
|
|
{h, port_option}
|
|
end
|
|
|
|
{:error, _} ->
|
|
# Parse failed - use original host and :port option
|
|
{host, port_option}
|
|
end
|
|
|
|
message = PDU.build_message(pdu, community, version)
|
|
Logger.debug("Built SNMP message: #{inspect(message)}")
|
|
|
|
case PDU.encode_message(message) do
|
|
{:ok, packet} ->
|
|
Logger.debug("Encoded PDU packet for transmission")
|
|
|
|
case send_and_receive(socket, parsed_host, parsed_port, packet, timeout) do
|
|
{:ok, response_packet} ->
|
|
Logger.debug("Received response packet from network")
|
|
|
|
case PDU.decode_message(response_packet) do
|
|
{:ok, response_message} ->
|
|
Logger.debug("Decoded response message: #{inspect(response_message)}")
|
|
{:ok, response_message}
|
|
|
|
{:error, decode_reason} = decode_error ->
|
|
Logger.error("PDU decode failed: #{inspect(decode_reason)}")
|
|
decode_error
|
|
end
|
|
|
|
{:error, network_reason} = network_error ->
|
|
Logger.error("Network operation failed: #{inspect(network_reason)}")
|
|
network_error
|
|
end
|
|
|
|
{:error, encode_reason} = encode_error ->
|
|
Logger.error("PDU encode failed: #{inspect(encode_reason)}")
|
|
encode_error
|
|
end
|
|
end
|
|
|
|
defp send_and_receive(socket, host, port, packet, timeout) do
|
|
# Normal send-and-receive flow
|
|
case Transport.send_packet(socket, host, port, packet) do
|
|
:ok ->
|
|
Logger.debug("Packet sent successfully, waiting for response (timeout: #{timeout}ms)")
|
|
|
|
case Transport.receive_packet(socket, timeout) do
|
|
{:ok, {response_packet, _from_addr, _from_port}} ->
|
|
Logger.debug("Received response packet: #{byte_size(response_packet)} bytes")
|
|
{:ok, response_packet}
|
|
|
|
{:error, :timeout} = timeout_error ->
|
|
Logger.debug("Timeout waiting for response after #{timeout}ms")
|
|
timeout_error
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("Error receiving response: #{inspect(reason)}")
|
|
{:error, {:network_error, reason}}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("Error sending packet: #{inspect(reason)}")
|
|
{:error, {:network_error, reason}}
|
|
end
|
|
end
|
|
|
|
# Multi-get implementation with connection reuse
|
|
defp get_multi_with_socket(socket, host, oids, opts) do
|
|
Enum.map(oids, fn oid ->
|
|
case perform_get_operation(socket, host, oid, opts) do
|
|
{:ok, response} ->
|
|
case extract_get_result_with_oid(response) do
|
|
{:ok, {oid, type, value}} -> {oid, type, value}
|
|
{:error, reason} -> {oid, {:error, reason}}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{oid, {:error, reason}}
|
|
end
|
|
end)
|
|
end
|
|
|
|
# Result extraction
|
|
defp extract_get_result(%{pdu: %{error_status: error_status}} = response) when error_status != 0 do
|
|
Logger.debug("Extracting error result - error_status: #{error_status}")
|
|
Logger.debug("Full response PDU: #{inspect(response.pdu)}")
|
|
{:error, decode_error_status(error_status)}
|
|
end
|
|
|
|
# Handle responses with one or more varbinds - some devices return extra varbinds
|
|
defp extract_get_result(%{pdu: %{varbinds: [{oid, type, value} | _rest]}} = response) do
|
|
Logger.debug("Extracting successful result - PDU: #{inspect(response.pdu)}")
|
|
|
|
Logger.debug("Varbind details - oid: #{inspect(oid)}, type: #{inspect(type)}, value: #{inspect(value)}")
|
|
|
|
# Check for SNMPv2c exception values in both type and value fields
|
|
case {type, value} do
|
|
# Exception values in type field (from simulator)
|
|
{:no_such_object, _} ->
|
|
Logger.debug("Found exception in type field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{:no_such_instance, _} ->
|
|
Logger.debug("Found exception in type field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{:end_of_mib_view, _} ->
|
|
Logger.debug("Found exception in type field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Exception values in value field (standard format)
|
|
{_, {:no_such_object, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{_, {:no_such_instance, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{_, {:end_of_mib_view, _}} ->
|
|
Logger.debug("Found exception in value field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Normal value - return type and value only (OID is known from input)
|
|
_ ->
|
|
Logger.debug("Returning successful value with type: #{inspect({type, value})}")
|
|
{:ok, {type, value}}
|
|
end
|
|
end
|
|
|
|
defp extract_get_result(response) do
|
|
Logger.error("Invalid response format: #{inspect(response)}")
|
|
{:error, :invalid_response}
|
|
end
|
|
|
|
defp extract_get_result_with_oid(%{pdu: %{error_status: error_status}} = response) when error_status != 0 do
|
|
Logger.debug("Extracting error result - error_status: #{error_status}")
|
|
Logger.debug("Full response PDU: #{inspect(response.pdu)}")
|
|
{:error, decode_error_status(error_status)}
|
|
end
|
|
|
|
# Handle responses with one or more varbinds - some devices return extra varbinds
|
|
defp extract_get_result_with_oid(%{pdu: %{varbinds: [{oid, type, value} | _rest]}} = response) do
|
|
Logger.debug("Extracting successful result - PDU: #{inspect(response.pdu)}")
|
|
|
|
Logger.debug("Varbind details - oid: #{inspect(oid)}, type: #{inspect(type)}, value: #{inspect(value)}")
|
|
|
|
# Check for SNMPv2c exception values in both type and value fields
|
|
case {type, value} do
|
|
# Exception values in type field (from simulator)
|
|
{:no_such_object, _} ->
|
|
Logger.debug("Found exception in type field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{:no_such_instance, _} ->
|
|
Logger.debug("Found exception in type field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{:end_of_mib_view, _} ->
|
|
Logger.debug("Found exception in type field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Exception values in value field (standard format)
|
|
{_, {:no_such_object, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{_, {:no_such_instance, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{_, {:end_of_mib_view, _}} ->
|
|
Logger.debug("Found exception in value field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Normal value - return full 3-tuple for multi operations
|
|
_ ->
|
|
Logger.debug("Returning successful varbind with type: #{inspect({oid, type, value})}")
|
|
{:ok, {oid, type, value}}
|
|
end
|
|
end
|
|
|
|
defp extract_get_result_with_oid(response) do
|
|
Logger.error("Invalid response format: #{inspect(response)}")
|
|
{:error, :invalid_response}
|
|
end
|
|
|
|
defp extract_bulk_result(%{pdu: %{varbinds: varbinds}}) do
|
|
valid_varbinds =
|
|
Enum.filter(varbinds, fn {_oid, type, value} ->
|
|
# Check for SNMPv2c exception values in both type and value fields
|
|
case {type, value} do
|
|
# Exception values in type field (from simulator)
|
|
{:no_such_object, _} -> false
|
|
{:no_such_instance, _} -> false
|
|
{:end_of_mib_view, _} -> false
|
|
# Exception values in value field (standard format)
|
|
{_, {:no_such_object, _}} -> false
|
|
{_, {:no_such_instance, _}} -> false
|
|
{_, {:end_of_mib_view, _}} -> false
|
|
# Valid varbind
|
|
_ -> true
|
|
end
|
|
end)
|
|
|
|
# Return documented 3-tuple varbind format {oid, type, value}
|
|
{:ok, valid_varbinds}
|
|
end
|
|
|
|
defp extract_bulk_result(%{pdu: %{error_status: error_status}}) when error_status != 0 do
|
|
{:error, decode_error_status(error_status)}
|
|
end
|
|
|
|
defp extract_bulk_result(_), do: {:error, :invalid_response}
|
|
|
|
defp extract_set_result(%{pdu: %{error_status: 0}}) do
|
|
{:ok, :success}
|
|
end
|
|
|
|
defp extract_set_result(%{pdu: %{error_status: error_status}}) when error_status != 0 do
|
|
{:error, decode_error_status(error_status)}
|
|
end
|
|
|
|
defp extract_set_result(_), do: {:error, :invalid_response}
|
|
|
|
# GETNEXT implementation for SNMP v1
|
|
defp perform_get_next_v1(host, oid, opts) do
|
|
case create_socket(opts) do
|
|
{:ok, socket} ->
|
|
Logger.debug("Socket created successfully for GETNEXT v1")
|
|
|
|
case perform_get_next_operation(socket, host, oid, opts) do
|
|
{:ok, response} ->
|
|
Logger.debug("GETNEXT v1 operation completed, extracting result")
|
|
:ok = close_socket(socket)
|
|
result = extract_get_next_result(response)
|
|
Logger.debug("Final GETNEXT v1 result: #{inspect(result)}")
|
|
result
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("GETNEXT v1 operation failed: #{inspect(reason)}")
|
|
:ok = close_socket(socket)
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("Socket creation failed for GETNEXT v1: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
# GETNEXT implementation for SNMP v2c+
|
|
defp perform_get_next_v2c(host, oid, opts) do
|
|
# Use get_bulk with max_repetitions=1 for efficiency
|
|
bulk_opts = Keyword.merge(opts, max_repetitions: 1, non_repeaters: 0)
|
|
|
|
case get_bulk(host, oid, bulk_opts) do
|
|
{:ok, [{next_oid, type, value}]} ->
|
|
Logger.debug("GETNEXT v2c+ via GETBULK successful: #{inspect({next_oid, type, value})}")
|
|
{:ok, {next_oid, type, value}}
|
|
|
|
{:ok, []} ->
|
|
Logger.debug("GETNEXT v2c+ reached end of MIB")
|
|
{:error, :end_of_mib_view}
|
|
|
|
{:ok, results} when is_list(results) ->
|
|
# Take the first result if multiple returned
|
|
case List.first(results) do
|
|
{next_oid, type, value} -> {:ok, {next_oid, type, value}}
|
|
_ -> {:error, :invalid_response}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.debug("GETNEXT v2c+ via GETBULK failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp perform_get_next_operation(socket, host, oid, opts) do
|
|
request_id = generate_request_id()
|
|
pdu = PDU.build_get_next_request(oid, request_id)
|
|
perform_snmp_request(socket, host, pdu, opts)
|
|
end
|
|
|
|
# Handle responses with one or more varbinds - some devices return extra varbinds
|
|
defp extract_get_next_result(%{pdu: %{varbinds: [{next_oid, type, value} | _rest]}} = response) do
|
|
Logger.debug("Extracting GETNEXT result - PDU: #{inspect(response.pdu)}")
|
|
|
|
Logger.debug("Varbind details - next_oid: #{inspect(next_oid)}, type: #{inspect(type)}, value: #{inspect(value)}")
|
|
|
|
# Check for SNMPv2c exception values in both type and value fields
|
|
case {type, value} do
|
|
# Exception values in type field (from simulator)
|
|
{:no_such_object, _} ->
|
|
Logger.debug("Found exception in type field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{:no_such_instance, _} ->
|
|
Logger.debug("Found exception in type field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{:end_of_mib_view, _} ->
|
|
Logger.debug("Found exception in type field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Exception values in value field (standard format)
|
|
{_, {:no_such_object, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_object")
|
|
{:error, :no_such_object}
|
|
|
|
{_, {:no_such_instance, _}} ->
|
|
Logger.debug("Found exception in value field: no_such_instance")
|
|
{:error, :no_such_instance}
|
|
|
|
{_, {:end_of_mib_view, _}} ->
|
|
Logger.debug("Found exception in value field: end_of_mib_view")
|
|
{:error, :end_of_mib_view}
|
|
|
|
# Normal value - return both next OID and value
|
|
_ ->
|
|
Logger.debug("Returning successful GETNEXT result: #{inspect({next_oid, type, value})}")
|
|
{:ok, {next_oid, type, value}}
|
|
end
|
|
end
|
|
|
|
defp extract_get_next_result(response) do
|
|
Logger.error("Invalid GETNEXT response format: #{inspect(response)}")
|
|
{:error, :invalid_response}
|
|
end
|
|
|
|
# Helper functions
|
|
defp normalize_oid(oid) when is_list(oid) do
|
|
# Validate the list OID before returning
|
|
case OID.valid_oid?(oid) do
|
|
:ok -> oid
|
|
{:error, :empty_oid} -> [1, 3, 6, 1]
|
|
{:error, _} -> [1, 3, 6, 1]
|
|
end
|
|
end
|
|
|
|
defp normalize_oid(oid) when is_binary(oid) do
|
|
# First try MIB symbolic name resolution
|
|
case SnmpKit.SnmpLib.MIB.Registry.resolve_name(oid) do
|
|
{:ok, oid_list} ->
|
|
oid_list
|
|
|
|
{:error, _} ->
|
|
# Fallback to numeric string parsing
|
|
case OID.string_to_list(oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
# Safe fallback
|
|
{:error, _} -> [1, 3, 6, 1]
|
|
end
|
|
end
|
|
end
|
|
|
|
defp normalize_oid(_), do: [1, 3, 6, 1]
|
|
|
|
defp generate_request_id do
|
|
:rand.uniform(2_147_483_647)
|
|
end
|
|
|
|
defp decode_error_status(0), do: :no_error
|
|
defp decode_error_status(1), do: :too_big
|
|
defp decode_error_status(2), do: :no_such_name
|
|
defp decode_error_status(3), do: :bad_value
|
|
defp decode_error_status(4), do: :read_only
|
|
defp decode_error_status(5), do: :gen_err
|
|
# SNMPv2c additional error codes (RFC 3416)
|
|
defp decode_error_status(6), do: :no_access
|
|
defp decode_error_status(7), do: :wrong_type
|
|
defp decode_error_status(8), do: :wrong_length
|
|
defp decode_error_status(9), do: :wrong_encoding
|
|
defp decode_error_status(10), do: :wrong_value
|
|
defp decode_error_status(11), do: :no_creation
|
|
defp decode_error_status(12), do: :inconsistent_value
|
|
defp decode_error_status(13), do: :resource_unavailable
|
|
defp decode_error_status(14), do: :commit_failed
|
|
defp decode_error_status(15), do: :undo_failed
|
|
defp decode_error_status(16), do: :authorization_error
|
|
defp decode_error_status(17), do: :not_writable
|
|
defp decode_error_status(18), do: :inconsistent_name
|
|
defp decode_error_status(error), do: {:unknown_error, error}
|
|
|
|
defp merge_default_opts(opts) do
|
|
Keyword.merge(
|
|
[
|
|
community: @default_community,
|
|
version: @default_version,
|
|
timeout: @default_timeout,
|
|
retries: @default_retries,
|
|
port: @default_port,
|
|
local_port: @default_local_port
|
|
],
|
|
opts
|
|
)
|
|
end
|
|
|
|
defp merge_bulk_opts(opts) do
|
|
opts
|
|
|> merge_default_opts()
|
|
|> Keyword.merge(
|
|
max_repetitions: @default_max_repetitions,
|
|
non_repeaters: @default_non_repeaters
|
|
)
|
|
|> Keyword.merge(opts)
|
|
end
|
|
|
|
# Helper to determine if host string contains port specification
|
|
defp host_contains_port?(host) when is_binary(host) do
|
|
cond do
|
|
# RFC 3986 bracket notation: [IPv6]:port
|
|
String.starts_with?(host, "[") and String.contains?(host, "]:") ->
|
|
check_bracketed_ipv6_port(host)
|
|
|
|
# Plain IPv6 addresses (contain :: or multiple colons) - no port embedded
|
|
String.contains?(host, "::") ->
|
|
false
|
|
|
|
host |> String.graphemes() |> Enum.count(&(&1 == ":")) > 1 ->
|
|
false
|
|
|
|
# IPv4 or simple hostname with port
|
|
String.contains?(host, ":") ->
|
|
check_simple_host_port(host)
|
|
|
|
# No colon at all
|
|
true ->
|
|
false
|
|
end
|
|
end
|
|
|
|
defp host_contains_port?(_), do: false
|
|
|
|
# Check if bracketed IPv6 address has valid port
|
|
defp check_bracketed_ipv6_port(host) do
|
|
case String.split(host, "]:", parts: 2) do
|
|
[_ipv6_part, port_part] -> valid_port?(port_part)
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
# Check if simple host:port format has valid port
|
|
defp check_simple_host_port(host) do
|
|
case String.split(host, ":", parts: 2) do
|
|
[_host_part, port_part] -> valid_port?(port_part)
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
# Validate port number
|
|
defp valid_port?(port_string) do
|
|
case Integer.parse(port_string) do
|
|
{port, ""} when port > 0 and port <= 65_535 -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
# Check if all results failed with the same network-related error
|
|
defp check_for_global_failure(results) do
|
|
errors =
|
|
Enum.filter(results, fn
|
|
{_oid, {:error, _}} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# If all results are errors, check if they're all network-related
|
|
case {length(errors), length(results)} do
|
|
{same, same} when same > 0 ->
|
|
# All operations failed, check if it's a consistent network error
|
|
network_errors =
|
|
Enum.filter(errors, fn
|
|
{_oid, {:error, {:network_error, _}}} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
case length(network_errors) do
|
|
^same ->
|
|
# All errors are network errors, return the first one as global failure
|
|
{_oid, {:error, reason}} = hd(errors)
|
|
{:global_failure, reason}
|
|
|
|
_ ->
|
|
:mixed_results
|
|
end
|
|
|
|
_ ->
|
|
:mixed_results
|
|
end
|
|
end
|
|
end
|