553 lines
14 KiB
Elixir
553 lines
14 KiB
Elixir
defmodule SnmpKit.SnmpLib.Error do
|
|
@moduledoc """
|
|
Standard SNMP error handling and error code utilities.
|
|
|
|
Provides standardized error codes, error handling utilities, and error response
|
|
generation for SNMP operations. This module centralizes all SNMP-specific error
|
|
handling to ensure consistent error reporting across the library.
|
|
|
|
## SNMP Error Codes
|
|
|
|
Standard SNMP error status values as defined in RFC 1157 and RFC 3416:
|
|
|
|
- `no_error` (0) - No error occurred
|
|
- `too_big` (1) - Response message would be too large
|
|
- `no_such_name` (2) - Requested OID does not exist
|
|
- `bad_value` (3) - Invalid value for SET operation
|
|
- `read_only` (4) - Attempted to set read-only variable
|
|
- `gen_err` (5) - General error
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Error Handling
|
|
|
|
# Check if an error is retriable
|
|
if SnmpKit.SnmpLib.Error.retriable_error?(error_code) do
|
|
retry_operation()
|
|
end
|
|
|
|
# Format error for logging
|
|
error_msg = SnmpKit.SnmpLib.Error.format_error(3, 2, varbinds)
|
|
Logger.error(error_msg)
|
|
|
|
### Error Response Generation
|
|
|
|
# Create error response for invalid request
|
|
{:ok, error_response} = SnmpKit.SnmpLib.Error.create_error_response(
|
|
request_pdu,
|
|
:no_such_name,
|
|
error_index
|
|
)
|
|
"""
|
|
|
|
@type error_status ::
|
|
:no_error
|
|
| :too_big
|
|
| :no_such_name
|
|
| :bad_value
|
|
| :read_only
|
|
| :gen_err
|
|
| non_neg_integer()
|
|
|
|
@type error_index :: non_neg_integer()
|
|
@type varbind :: {list(), any()}
|
|
@type varbinds :: [varbind()]
|
|
|
|
# Standard SNMP error status codes (RFC 1157, RFC 3416)
|
|
@no_error 0
|
|
@too_big 1
|
|
@no_such_name 2
|
|
@bad_value 3
|
|
@read_only 4
|
|
@gen_err 5
|
|
|
|
# Additional SNMPv2c error codes
|
|
@no_access 6
|
|
@wrong_type 7
|
|
@wrong_length 8
|
|
@wrong_encoding 9
|
|
@wrong_value 10
|
|
@no_creation 11
|
|
@inconsistent_value 12
|
|
@resource_unavailable 13
|
|
@commit_failed 14
|
|
@undo_failed 15
|
|
@authorization_error 16
|
|
@not_writable 17
|
|
@inconsistent_name 18
|
|
|
|
# Errors that are typically transient and worth retrying
|
|
@retriable_errors MapSet.new([:too_big, :gen_err, :resource_unavailable])
|
|
|
|
## Standard Error Codes
|
|
|
|
@doc """
|
|
Returns the numeric code for 'no error' status.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.no_error()
|
|
0
|
|
"""
|
|
@spec no_error() :: 0
|
|
def no_error, do: @no_error
|
|
|
|
@doc """
|
|
Returns the numeric code for 'too big' error status.
|
|
|
|
The response message would be too large to fit in a single SNMP message.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.too_big()
|
|
1
|
|
"""
|
|
@spec too_big() :: 1
|
|
def too_big, do: @too_big
|
|
|
|
@doc """
|
|
Returns the numeric code for 'no such name' error status.
|
|
|
|
The requested OID does not exist on the agent.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.no_such_name()
|
|
2
|
|
"""
|
|
@spec no_such_name() :: 2
|
|
def no_such_name, do: @no_such_name
|
|
|
|
@doc """
|
|
Returns the numeric code for 'bad value' error status.
|
|
|
|
The value provided in a SET operation is invalid for the variable.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.bad_value()
|
|
3
|
|
"""
|
|
@spec bad_value() :: 3
|
|
def bad_value, do: @bad_value
|
|
|
|
@doc """
|
|
Returns the numeric code for 'read only' error status.
|
|
|
|
Attempted to set a read-only variable.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.read_only()
|
|
4
|
|
"""
|
|
@spec read_only() :: 4
|
|
def read_only, do: @read_only
|
|
|
|
@doc """
|
|
Returns the numeric code for 'general error' status.
|
|
|
|
A general error occurred that doesn't fit other categories.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.gen_err()
|
|
5
|
|
"""
|
|
@spec gen_err() :: 5
|
|
def gen_err, do: @gen_err
|
|
|
|
## Error Utilities
|
|
|
|
@doc """
|
|
Returns the human-readable name for an error status code.
|
|
|
|
## Parameters
|
|
|
|
- `code`: Numeric error status code or atom
|
|
|
|
## Returns
|
|
|
|
- String name of the error status
|
|
- "unknown_error" for unrecognized codes
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(0)
|
|
"no_error"
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(:too_big)
|
|
"too_big"
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(999)
|
|
"unknown_error"
|
|
"""
|
|
@spec error_name(error_status()) :: String.t()
|
|
def error_name(0), do: "no_error"
|
|
def error_name(:no_error), do: "no_error"
|
|
|
|
def error_name(1), do: "too_big"
|
|
def error_name(:too_big), do: "too_big"
|
|
|
|
def error_name(2), do: "no_such_name"
|
|
def error_name(:no_such_name), do: "no_such_name"
|
|
|
|
def error_name(3), do: "bad_value"
|
|
def error_name(:bad_value), do: "bad_value"
|
|
|
|
def error_name(4), do: "read_only"
|
|
def error_name(:read_only), do: "read_only"
|
|
|
|
def error_name(5), do: "gen_err"
|
|
def error_name(:gen_err), do: "gen_err"
|
|
|
|
# SNMPv2c additional error codes
|
|
def error_name(6), do: "no_access"
|
|
def error_name(:no_access), do: "no_access"
|
|
def error_name(7), do: "wrong_type"
|
|
def error_name(:wrong_type), do: "wrong_type"
|
|
def error_name(8), do: "wrong_length"
|
|
def error_name(:wrong_length), do: "wrong_length"
|
|
def error_name(9), do: "wrong_encoding"
|
|
def error_name(:wrong_encoding), do: "wrong_encoding"
|
|
def error_name(10), do: "wrong_value"
|
|
def error_name(:wrong_value), do: "wrong_value"
|
|
def error_name(11), do: "no_creation"
|
|
def error_name(:no_creation), do: "no_creation"
|
|
def error_name(12), do: "inconsistent_value"
|
|
def error_name(:inconsistent_value), do: "inconsistent_value"
|
|
def error_name(13), do: "resource_unavailable"
|
|
def error_name(:resource_unavailable), do: "resource_unavailable"
|
|
def error_name(14), do: "commit_failed"
|
|
def error_name(:commit_failed), do: "commit_failed"
|
|
def error_name(15), do: "undo_failed"
|
|
def error_name(:undo_failed), do: "undo_failed"
|
|
def error_name(16), do: "authorization_error"
|
|
def error_name(:authorization_error), do: "authorization_error"
|
|
def error_name(17), do: "not_writable"
|
|
def error_name(:not_writable), do: "not_writable"
|
|
def error_name(18), do: "inconsistent_name"
|
|
def error_name(:inconsistent_name), do: "inconsistent_name"
|
|
|
|
def error_name(_), do: "unknown_error"
|
|
|
|
@doc """
|
|
Converts error status code to atom representation.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_atom(2)
|
|
:no_such_name
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_atom(999)
|
|
:unknown_error
|
|
"""
|
|
@spec error_atom(error_status()) :: atom()
|
|
def error_atom(code) when is_integer(code) do
|
|
code |> error_name() |> String.to_atom()
|
|
end
|
|
|
|
def error_atom(atom) when is_atom(atom), do: atom
|
|
|
|
@doc """
|
|
Converts error atom or name to numeric code.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code(:no_such_name)
|
|
2
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code("bad_value")
|
|
3
|
|
"""
|
|
@spec error_code(atom() | String.t()) :: non_neg_integer()
|
|
def error_code(:no_error), do: @no_error
|
|
def error_code(:too_big), do: @too_big
|
|
def error_code(:no_such_name), do: @no_such_name
|
|
def error_code(:bad_value), do: @bad_value
|
|
def error_code(:read_only), do: @read_only
|
|
def error_code(:gen_err), do: @gen_err
|
|
def error_code(:no_access), do: @no_access
|
|
def error_code(:wrong_type), do: @wrong_type
|
|
def error_code(:wrong_length), do: @wrong_length
|
|
def error_code(:wrong_encoding), do: @wrong_encoding
|
|
def error_code(:wrong_value), do: @wrong_value
|
|
def error_code(:no_creation), do: @no_creation
|
|
def error_code(:inconsistent_value), do: @inconsistent_value
|
|
def error_code(:resource_unavailable), do: @resource_unavailable
|
|
def error_code(:commit_failed), do: @commit_failed
|
|
def error_code(:undo_failed), do: @undo_failed
|
|
def error_code(:authorization_error), do: @authorization_error
|
|
def error_code(:not_writable), do: @not_writable
|
|
def error_code(:inconsistent_name), do: @inconsistent_name
|
|
|
|
def error_code(name) when is_binary(name) do
|
|
name |> String.to_atom() |> error_code()
|
|
end
|
|
|
|
def error_code(_), do: @gen_err
|
|
|
|
@doc """
|
|
Formats an SNMP error for human-readable display.
|
|
|
|
## Parameters
|
|
|
|
- `error_status`: Error status code (integer or atom)
|
|
- `error_index`: Index of the varbind that caused the error (1-based)
|
|
- `varbinds`: List of varbinds from the request (optional)
|
|
|
|
## Returns
|
|
|
|
Formatted error string suitable for logging or display.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.format_error(2, 1, [])
|
|
"SNMP Error: no_such_name (2) at index 1"
|
|
|
|
iex> varbinds = [{[1,3,6,1,2,1,1,1,0], "test"}]
|
|
iex> SnmpKit.SnmpLib.Error.format_error(:bad_value, 1, varbinds)
|
|
"SNMP Error: bad_value (3) at index 1 - OID: 1.3.6.1.2.1.1.1.0"
|
|
"""
|
|
@spec format_error(error_status(), error_index(), varbinds()) :: String.t()
|
|
def format_error(error_status, error_index, varbinds \\ []) do
|
|
error_name_str = error_name(error_status)
|
|
error_code_num = if is_integer(error_status), do: error_status, else: error_code(error_status)
|
|
|
|
base_msg = "SNMP Error: #{error_name_str} (#{error_code_num}) at index #{error_index}"
|
|
|
|
case get_error_varbind(varbinds, error_index) do
|
|
nil ->
|
|
base_msg
|
|
|
|
{oid, _value} ->
|
|
oid_str = Enum.join(oid, ".")
|
|
"#{base_msg} - OID: #{oid_str}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Determines if an error status indicates a retriable condition.
|
|
|
|
Some SNMP errors are temporary and operations can be retried, while others
|
|
indicate permanent failures.
|
|
|
|
## Retriable Errors
|
|
- `too_big` - Can retry with smaller request
|
|
- `gen_err` - General error, may be temporary
|
|
- `resource_unavailable` - Temporary resource constraint
|
|
|
|
## Non-Retriable Errors
|
|
- `no_such_name` - OID doesn't exist
|
|
- `bad_value` - Invalid value provided
|
|
- `read_only` - Attempted to write read-only variable
|
|
- `no_access` - Access denied
|
|
- Most SNMPv2c specific errors
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.retriable_error?(:too_big)
|
|
true
|
|
|
|
iex> SnmpKit.SnmpLib.Error.retriable_error?(:no_such_name)
|
|
false
|
|
"""
|
|
@spec retriable_error?(error_status()) :: boolean()
|
|
def retriable_error?(error_status) do
|
|
error_status
|
|
|> error_atom()
|
|
|> then(&MapSet.member?(@retriable_errors, &1))
|
|
end
|
|
|
|
@doc """
|
|
Creates an SNMP error response PDU.
|
|
|
|
Generates a properly formatted error response based on the original request
|
|
and the error condition that occurred.
|
|
|
|
## Parameters
|
|
|
|
- `request_pdu`: Original request PDU
|
|
- `error_status`: Error status code or atom
|
|
- `error_index`: Index of the varbind that caused the error (1-based)
|
|
|
|
## Returns
|
|
|
|
- `{:ok, error_pdu}`: Successfully created error response
|
|
- `{:error, reason}`: Failed to create error response
|
|
|
|
## Examples
|
|
|
|
request = %{type: :get_request, request_id: 123, varbinds: [...]}
|
|
{:ok, error_response} = SnmpKit.SnmpLib.Error.create_error_response(
|
|
request,
|
|
:no_such_name,
|
|
1
|
|
)
|
|
"""
|
|
@spec create_error_response(map(), error_status(), error_index()) ::
|
|
{:ok, map()} | {:error, atom()}
|
|
def create_error_response(request_pdu, error_status, error_index) do
|
|
case validate_request_pdu(request_pdu) do
|
|
:ok ->
|
|
error_code_num =
|
|
if is_integer(error_status), do: error_status, else: error_code(error_status)
|
|
|
|
error_response = %{
|
|
type: :get_response,
|
|
request_id: Map.get(request_pdu, :request_id, 0),
|
|
error_status: error_code_num,
|
|
error_index: error_index,
|
|
varbinds: Map.get(request_pdu, :varbinds, [])
|
|
}
|
|
|
|
{:ok, error_response}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates an error status code.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.valid_error_status?(2)
|
|
true
|
|
|
|
iex> SnmpKit.SnmpLib.Error.valid_error_status?(:no_such_name)
|
|
true
|
|
|
|
iex> SnmpKit.SnmpLib.Error.valid_error_status?(999)
|
|
false
|
|
"""
|
|
@spec valid_error_status?(any()) :: boolean()
|
|
def valid_error_status?(status) when is_integer(status) do
|
|
status >= 0 and status <= 18
|
|
end
|
|
|
|
def valid_error_status?(status) when is_atom(status) do
|
|
error_name(status) != "unknown_error"
|
|
end
|
|
|
|
def valid_error_status?(_), do: false
|
|
|
|
@doc """
|
|
Returns a list of all standard SNMP error codes.
|
|
|
|
## Examples
|
|
|
|
iex> codes = SnmpKit.SnmpLib.Error.all_error_codes()
|
|
iex> 0 in codes
|
|
true
|
|
iex> 5 in codes
|
|
true
|
|
"""
|
|
@spec all_error_codes() :: [non_neg_integer()]
|
|
def all_error_codes do
|
|
Enum.to_list(0..18)
|
|
end
|
|
|
|
@doc """
|
|
Returns a list of all standard SNMP error atoms.
|
|
|
|
## Examples
|
|
|
|
iex> atoms = SnmpKit.SnmpLib.Error.all_error_atoms()
|
|
iex> :no_error in atoms
|
|
true
|
|
iex> :gen_err in atoms
|
|
true
|
|
"""
|
|
@spec all_error_atoms() :: [
|
|
:no_error
|
|
| :too_big
|
|
| :no_such_name
|
|
| :bad_value
|
|
| :read_only
|
|
| :gen_err
|
|
| :no_access
|
|
| :wrong_type
|
|
| :wrong_length
|
|
| :wrong_encoding
|
|
| :wrong_value
|
|
| :no_creation
|
|
| :inconsistent_value
|
|
| :resource_unavailable
|
|
| :commit_failed
|
|
| :undo_failed
|
|
| :authorization_error
|
|
| :not_writable
|
|
| :inconsistent_name
|
|
]
|
|
def all_error_atoms do
|
|
[
|
|
:no_error,
|
|
:too_big,
|
|
:no_such_name,
|
|
:bad_value,
|
|
:read_only,
|
|
:gen_err,
|
|
:no_access,
|
|
:wrong_type,
|
|
:wrong_length,
|
|
:wrong_encoding,
|
|
:wrong_value,
|
|
:no_creation,
|
|
:inconsistent_value,
|
|
:resource_unavailable,
|
|
:commit_failed,
|
|
:undo_failed,
|
|
:authorization_error,
|
|
:not_writable,
|
|
:inconsistent_name
|
|
]
|
|
end
|
|
|
|
@doc """
|
|
Categorizes error by severity level.
|
|
|
|
## Returns
|
|
|
|
- `:info` - No error
|
|
- `:warning` - Retriable errors
|
|
- `:error` - Non-retriable errors
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_severity(:no_error)
|
|
:info
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_severity(:too_big)
|
|
:warning
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_severity(:no_such_name)
|
|
:error
|
|
"""
|
|
@spec error_severity(error_status()) :: :info | :warning | :error
|
|
def error_severity(error_status) do
|
|
case error_atom(error_status) do
|
|
:no_error -> :info
|
|
status when status in [:too_big, :gen_err, :resource_unavailable] -> :warning
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
## Private Helper Functions
|
|
|
|
defp validate_request_pdu(request_pdu) when is_map(request_pdu) do
|
|
:ok
|
|
end
|
|
|
|
defp validate_request_pdu(_), do: {:error, :invalid_request_pdu}
|
|
|
|
defp get_error_varbind(varbinds, error_index) when is_list(varbinds) do
|
|
# SNMP error_index is 1-based
|
|
if error_index > 0 and error_index <= length(varbinds) do
|
|
Enum.at(varbinds, error_index - 1)
|
|
end
|
|
end
|
|
|
|
defp get_error_varbind(_, _), do: nil
|
|
end
|