Phase 1: Foundation Types (100% complete)
- query_helpers: SQL LIKE sanitization with pipe operators
- numeric: Integer parsing with pattern matching guards
- result: Pure Elixir Result monad (map, and_then, unwrap_or)
Phase 2: Ecto Domain Types (100% complete)
- ip_address: IPv4/IPv6 validation using :inet directly
- mac_address: Multi-format MAC parsing (colon/hyphen/dot/compact)
- snmp_oid: OID parsing/manipulation with recursive pattern matching
All 198 tests passing across converted modules.
API changed from Gleam-style {:some/:none to idiomatic {:ok/:error.
Refactored parse_numeric_oid to use with statement, reducing nesting depth.
Reviewed-on: graham/towerops-web#196
343 lines
11 KiB
Elixir
343 lines
11 KiB
Elixir
defmodule SnmpKit.SnmpLib.Error do
|
|
@moduledoc """
|
|
SNMP error handling and error code utilities.
|
|
|
|
Provides standardized error codes, retriability classification, severity levels,
|
|
and error formatting for SNMP operations (RFC 1157, RFC 3416).
|
|
"""
|
|
|
|
@type error_status ::
|
|
: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
|
|
| :unknown_error
|
|
| non_neg_integer()
|
|
|
|
@type error_index :: non_neg_integer()
|
|
@type varbind :: {list(), any()}
|
|
@type varbinds :: [varbind()]
|
|
|
|
## Standard Error Code Constants
|
|
|
|
@spec no_error() :: 0
|
|
def no_error, do: 0
|
|
|
|
@spec too_big() :: 1
|
|
def too_big, do: 1
|
|
|
|
@spec no_such_name() :: 2
|
|
def no_such_name, do: 2
|
|
|
|
@spec bad_value() :: 3
|
|
def bad_value, do: 3
|
|
|
|
@spec read_only() :: 4
|
|
def read_only, do: 4
|
|
|
|
@spec gen_err() :: 5
|
|
def gen_err, do: 5
|
|
|
|
## Error Utilities
|
|
|
|
@doc """
|
|
Returns the human-readable name for an error status.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(:no_error)
|
|
"no_error"
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(0)
|
|
"no_error"
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_name(:too_big)
|
|
"too_big"
|
|
"""
|
|
@spec error_name(error_status()) :: String.t()
|
|
def error_name(:no_error), do: "no_error"
|
|
def error_name(:too_big), do: "too_big"
|
|
def error_name(:no_such_name), do: "no_such_name"
|
|
def error_name(:bad_value), do: "bad_value"
|
|
def error_name(:read_only), do: "read_only"
|
|
def error_name(:gen_err), do: "gen_err"
|
|
def error_name(:no_access), do: "no_access"
|
|
def error_name(:wrong_type), do: "wrong_type"
|
|
def error_name(:wrong_length), do: "wrong_length"
|
|
def error_name(:wrong_encoding), do: "wrong_encoding"
|
|
def error_name(:wrong_value), do: "wrong_value"
|
|
def error_name(:no_creation), do: "no_creation"
|
|
def error_name(:inconsistent_value), do: "inconsistent_value"
|
|
def error_name(:resource_unavailable), do: "resource_unavailable"
|
|
def error_name(:commit_failed), do: "commit_failed"
|
|
def error_name(:undo_failed), do: "undo_failed"
|
|
def error_name(:authorization_error), do: "authorization_error"
|
|
def error_name(:not_writable), do: "not_writable"
|
|
def error_name(:inconsistent_name), do: "inconsistent_name"
|
|
def error_name(:unknown_error), do: "unknown_error"
|
|
def error_name(code) when is_integer(code), do: code |> from_code() |> error_name()
|
|
def error_name(_), do: "unknown_error"
|
|
|
|
@doc """
|
|
Returns the error status atom for a code or name.
|
|
"""
|
|
@spec error_atom(error_status()) :: atom()
|
|
def error_atom(atom) when is_atom(atom), do: atom
|
|
def error_atom(code) when is_integer(code), do: from_code(code)
|
|
def error_atom(name) when is_binary(name), do: from_name(name)
|
|
|
|
@doc """
|
|
Returns the numeric code for an error status.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code(:no_error)
|
|
0
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code(:too_big)
|
|
1
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code("no_error")
|
|
0
|
|
|
|
iex> SnmpKit.SnmpLib.Error.error_code(:unknown)
|
|
5
|
|
"""
|
|
@spec error_code(atom() | String.t()) :: non_neg_integer()
|
|
def error_code(:no_error), do: 0
|
|
def error_code(:too_big), do: 1
|
|
def error_code(:no_such_name), do: 2
|
|
def error_code(:bad_value), do: 3
|
|
def error_code(:read_only), do: 4
|
|
def error_code(:gen_err), do: 5
|
|
def error_code(:no_access), do: 6
|
|
def error_code(:wrong_type), do: 7
|
|
def error_code(:wrong_length), do: 8
|
|
def error_code(:wrong_encoding), do: 9
|
|
def error_code(:wrong_value), do: 10
|
|
def error_code(:no_creation), do: 11
|
|
def error_code(:inconsistent_value), do: 12
|
|
def error_code(:resource_unavailable), do: 13
|
|
def error_code(:commit_failed), do: 14
|
|
def error_code(:undo_failed), do: 15
|
|
def error_code(:authorization_error), do: 16
|
|
def error_code(:not_writable), do: 17
|
|
def error_code(:inconsistent_name), do: 18
|
|
def error_code(:unknown_error), do: 5
|
|
def error_code(name) when is_binary(name), do: name |> from_name() |> error_code()
|
|
def error_code(_), do: 5
|
|
|
|
@doc """
|
|
Converts a numeric code to an error status atom.
|
|
|
|
Unknown codes return :unknown_error.
|
|
"""
|
|
@spec from_code(integer()) :: atom()
|
|
def from_code(0), do: :no_error
|
|
def from_code(1), do: :too_big
|
|
def from_code(2), do: :no_such_name
|
|
def from_code(3), do: :bad_value
|
|
def from_code(4), do: :read_only
|
|
def from_code(5), do: :gen_err
|
|
def from_code(6), do: :no_access
|
|
def from_code(7), do: :wrong_type
|
|
def from_code(8), do: :wrong_length
|
|
def from_code(9), do: :wrong_encoding
|
|
def from_code(10), do: :wrong_value
|
|
def from_code(11), do: :no_creation
|
|
def from_code(12), do: :inconsistent_value
|
|
def from_code(13), do: :resource_unavailable
|
|
def from_code(14), do: :commit_failed
|
|
def from_code(15), do: :undo_failed
|
|
def from_code(16), do: :authorization_error
|
|
def from_code(17), do: :not_writable
|
|
def from_code(18), do: :inconsistent_name
|
|
def from_code(_), do: :unknown_error
|
|
|
|
@doc """
|
|
Converts a string name to an error status atom.
|
|
|
|
Unknown names return :unknown_error.
|
|
"""
|
|
@spec from_name(String.t()) :: atom()
|
|
def from_name("no_error"), do: :no_error
|
|
def from_name("too_big"), do: :too_big
|
|
def from_name("no_such_name"), do: :no_such_name
|
|
def from_name("bad_value"), do: :bad_value
|
|
def from_name("read_only"), do: :read_only
|
|
def from_name("gen_err"), do: :gen_err
|
|
def from_name("no_access"), do: :no_access
|
|
def from_name("wrong_type"), do: :wrong_type
|
|
def from_name("wrong_length"), do: :wrong_length
|
|
def from_name("wrong_encoding"), do: :wrong_encoding
|
|
def from_name("wrong_value"), do: :wrong_value
|
|
def from_name("no_creation"), do: :no_creation
|
|
def from_name("inconsistent_value"), do: :inconsistent_value
|
|
def from_name("resource_unavailable"), do: :resource_unavailable
|
|
def from_name("commit_failed"), do: :commit_failed
|
|
def from_name("undo_failed"), do: :undo_failed
|
|
def from_name("authorization_error"), do: :authorization_error
|
|
def from_name("not_writable"), do: :not_writable
|
|
def from_name("inconsistent_name"), do: :inconsistent_name
|
|
def from_name(_), do: :unknown_error
|
|
|
|
@doc """
|
|
Formats an SNMP error for human-readable display.
|
|
|
|
Produces a string like "SNMP Error: no_such_name (2) at index 1".
|
|
If the varbind at error_index exists, appends " - OID: 1.3.6.1.2.1.1.1.0".
|
|
|
|
The varbinds parameter is a list of {oid_list, value} tuples where
|
|
the first element is the OID as a list of integers.
|
|
"""
|
|
@spec format_error(error_status(), error_index(), varbinds()) :: String.t()
|
|
def format_error(error_status, error_index, varbinds \\ []) do
|
|
status = error_atom(error_status)
|
|
name = error_name(status)
|
|
code = error_code(status)
|
|
|
|
base_msg = "SNMP Error: #{name} (#{code}) at index #{error_index}"
|
|
|
|
case get_varbind_at(varbinds, error_index) do
|
|
{:ok, {oid, _value}} ->
|
|
oid_str = Enum.map_join(oid, ".", &Integer.to_string/1)
|
|
"#{base_msg} - OID: #{oid_str}"
|
|
|
|
{:error, _} ->
|
|
base_msg
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns true if the error is typically transient and worth retrying.
|
|
|
|
Retriable: :too_big, :gen_err, :resource_unavailable.
|
|
"""
|
|
@spec retriable_error?(error_status()) :: boolean()
|
|
def retriable_error?(:too_big), do: true
|
|
def retriable_error?(:gen_err), do: true
|
|
def retriable_error?(:resource_unavailable), do: true
|
|
def retriable_error?(1), do: true
|
|
def retriable_error?(5), do: true
|
|
def retriable_error?(13), do: true
|
|
def retriable_error?(code) when is_integer(code), do: code |> from_code() |> retriable_error?()
|
|
def retriable_error?(_), do: false
|
|
|
|
@doc """
|
|
Categorizes error by severity level.
|
|
|
|
:no_error -> :info, retriable errors -> :warning, everything else -> :error.
|
|
"""
|
|
@spec error_severity(error_status()) :: :info | :warning | :error
|
|
def error_severity(:no_error), do: :info
|
|
def error_severity(:too_big), do: :warning
|
|
def error_severity(:gen_err), do: :warning
|
|
def error_severity(:resource_unavailable), do: :warning
|
|
def error_severity(0), do: :info
|
|
def error_severity(1), do: :warning
|
|
def error_severity(5), do: :warning
|
|
def error_severity(13), do: :warning
|
|
def error_severity(code) when is_integer(code), do: code |> from_code() |> error_severity()
|
|
def error_severity(_), do: :error
|
|
|
|
@doc """
|
|
Returns true if the code is a valid SNMP error code (0-18 inclusive).
|
|
"""
|
|
@spec valid_error_status?(any()) :: boolean()
|
|
def valid_error_status?(code) when is_integer(code) and code >= 0 and code <= 18, do: true
|
|
def valid_error_status?(atom) when is_atom(atom), do: error_name(atom) != "unknown_error"
|
|
def valid_error_status?(_), do: false
|
|
|
|
@doc """
|
|
Returns all standard SNMP error codes (0 through 18).
|
|
"""
|
|
@spec all_error_codes() :: [non_neg_integer()]
|
|
def all_error_codes do
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
|
|
end
|
|
|
|
@doc """
|
|
Returns all standard SNMP error status atoms.
|
|
"""
|
|
@spec all_error_atoms() :: [atom()]
|
|
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 """
|
|
Creates an error response PDU from a request PDU.
|
|
"""
|
|
@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
|
|
|
|
## Private Helpers
|
|
|
|
defp get_varbind_at(varbinds, error_index) when error_index > 0 do
|
|
zero_index = error_index - 1
|
|
|
|
case Enum.drop(varbinds, zero_index) do
|
|
[varbind | _] -> {:ok, varbind}
|
|
[] -> {:error, nil}
|
|
end
|
|
end
|
|
|
|
defp get_varbind_at(_varbinds, _error_index), do: {:error, nil}
|
|
|
|
defp validate_request_pdu(request_pdu) when is_map(request_pdu), do: :ok
|
|
defp validate_request_pdu(_), do: {:error, :invalid_request_pdu}
|
|
end
|