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