towerops/lib/snmpkit/snmp_mgr/errors.ex
Graham McIntire d1403c8069 Fix failing tests and clean up code
- 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
2026-06-16 14:54:34 -05:00

674 lines
20 KiB
Elixir

defmodule SnmpKit.SnmpMgr.Errors do
@moduledoc """
SNMP error handling and error code translation.
Provides functions to handle both SNMPv1 and SNMPv2c error conditions
and translate error codes to human-readable messages.
"""
alias SnmpKit.SnmpLib.Error
# SNMPv1 and SNMPv2c error codes
@error_codes %{
0 => :no_error,
1 => :too_big,
2 => :no_such_name,
3 => :bad_value,
4 => :read_only,
5 => :gen_err,
# SNMPv2c additional error codes
6 => :no_access,
7 => :wrong_type,
8 => :wrong_length,
9 => :wrong_encoding,
10 => :wrong_value,
11 => :no_creation,
12 => :inconsistent_value,
13 => :resource_unavailable,
14 => :commit_failed,
15 => :undo_failed,
16 => :authorization_error,
17 => :not_writable,
18 => :inconsistent_name
}
@error_descriptions %{
:no_error => "No error occurred",
:too_big => "Response too big to fit in message",
:no_such_name => "Variable name not found",
:bad_value => "Invalid value for variable",
:read_only => "Variable is read-only",
:gen_err => "General error",
# SNMPv2c additional errors
:no_access => "Access denied",
:wrong_type => "Wrong data type",
:wrong_length => "Wrong data length",
:wrong_encoding => "Wrong encoding",
:wrong_value => "Wrong value",
:no_creation => "Cannot create variable",
:inconsistent_value => "Inconsistent value",
:resource_unavailable => "Resource unavailable",
:commit_failed => "Commit failed",
:undo_failed => "Undo failed",
:authorization_error => "Authorization failed",
:not_writable => "Variable not writable",
:inconsistent_name => "Inconsistent name"
}
@doc """
Translates an SNMP error code to an atom.
Uses SnmpKit.SnmpLib.Error for validation and standardization of RFC-compliant error codes.
## Examples
iex> SnmpKit.SnmpMgr.Errors.code_to_atom(2)
:no_such_name
iex> SnmpKit.SnmpMgr.Errors.code_to_atom(0)
:no_error
iex> SnmpKit.SnmpMgr.Errors.code_to_atom(999)
:unknown_error
"""
def code_to_atom(error_code) when is_integer(error_code) do
# Use SnmpKit.SnmpLib.Error for validation and RFC compliance, fall back to our mapping
if Error.valid_error_status?(error_code) do
Error.error_atom(error_code)
else
Map.get(@error_codes, error_code, :unknown_error)
end
end
@doc """
Translates an SNMP error atom to a human-readable description.
## Examples
iex> SnmpKit.SnmpMgr.Errors.description(:no_such_name)
"Variable name not found"
iex> SnmpKit.SnmpMgr.Errors.description(:too_big)
"Response too big to fit in message"
iex> SnmpKit.SnmpMgr.Errors.description(:unknown_error)
"Unknown error"
"""
def description(error_atom) when is_atom(error_atom) do
Map.get(@error_descriptions, error_atom, "Unknown error")
end
@doc """
Translates an error code directly to a description.
## Examples
iex> SnmpKit.SnmpMgr.Errors.code_to_description(2)
"Variable name not found"
iex> SnmpKit.SnmpMgr.Errors.code_to_description(18)
"Inconsistent name"
"""
def code_to_description(error_code) when is_integer(error_code) do
error_code
|> code_to_atom()
|> description()
end
@doc """
Determines if an error is version-specific.
## Examples
iex> SnmpKit.SnmpMgr.Errors.v2c_error?(:no_access)
true
iex> SnmpKit.SnmpMgr.Errors.v2c_error?(:no_such_name)
false
"""
def v2c_error?(error_atom) do
v2c_errors = [
: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
]
error_atom in v2c_errors
end
@doc """
Formats an SNMP error for display.
## Examples
iex> SnmpKit.SnmpMgr.Errors.format_error({:snmp_error, 2})
"SNMP Error (2): Variable name not found"
iex> SnmpKit.SnmpMgr.Errors.format_error({:snmp_error, :no_such_name})
"SNMP Error: Variable name not found"
iex> SnmpKit.SnmpMgr.Errors.format_error({:v2c_error, :no_access, oid: "1.2.3.4"})
"SNMPv2c Error: Access denied (OID: 1.2.3.4)"
"""
def format_error({:snmp_error, error_code}) when is_integer(error_code) do
error_atom = code_to_atom(error_code)
desc = description(error_atom)
"SNMP Error (#{error_code}): #{desc}"
end
def format_error({:snmp_error, error_atom}) when is_atom(error_atom) do
desc = description(error_atom)
"SNMP Error: #{desc}"
end
def format_error({:v2c_error, error_atom}) when is_atom(error_atom) do
desc = description(error_atom)
"SNMPv2c Error: #{desc}"
end
def format_error({:v2c_error, error_atom, details}) when is_atom(error_atom) do
desc = description(error_atom)
detail_str = format_error_details(details)
"SNMPv2c Error: #{desc}#{detail_str}"
end
def format_error({:network_error, reason}) do
"Network Error: #{inspect(reason)}"
end
def format_error({:timeout, _}) do
"Timeout Error: Request timed out"
end
def format_error({:encoding_error, reason}) do
"Encoding Error: #{inspect(reason)}"
end
def format_error({:decoding_error, reason}) do
"Decoding Error: #{inspect(reason)}"
end
def format_error(error) do
"Unknown Error: #{inspect(error)}"
end
@doc """
Checks if an error is recoverable (can be retried).
## Examples
iex> SnmpKit.SnmpMgr.Errors.recoverable?({:network_error, :host_unreachable})
false
iex> SnmpKit.SnmpMgr.Errors.recoverable?({:snmp_error, :too_big})
true
iex> SnmpKit.SnmpMgr.Errors.recoverable?(:timeout)
true
"""
def recoverable?({:network_error, :host_unreachable}), do: false
def recoverable?({:network_error, :network_unreachable}), do: false
def recoverable?({:snmp_error, :no_such_name}), do: false
def recoverable?({:snmp_error, :bad_value}), do: false
def recoverable?({:snmp_error, :read_only}), do: false
def recoverable?({:v2c_error, :no_access}), do: false
def recoverable?({:v2c_error, :not_writable}), do: false
def recoverable?({:v2c_error, :wrong_type}), do: false
def recoverable?(:timeout), do: true
def recoverable?({:snmp_error, :too_big}), do: true
def recoverable?({:snmp_error, :gen_err}), do: true
def recoverable?(_), do: false
@doc """
Enhanced error analysis using SnmpKit.SnmpLib.Error for SNMP protocol errors.
Provides detailed error information including severity and RFC compliance
for SNMP protocol errors while preserving our comprehensive network error handling.
## Examples
iex> SnmpKit.SnmpMgr.Errors.analyze_error({:snmp_error, 2})
%{
type: :snmp_protocol,
atom: :no_such_name,
code: 2,
severity: :error,
retriable: false,
rfc_compliant: true,
category: :user_error,
description: "Variable name not found"
}
iex> SnmpKit.SnmpMgr.Errors.analyze_error(:timeout)
%{
type: :network,
atom: :timeout,
retriable: true,
category: :transient_error,
description: "Operation timed out"
}
"""
def analyze_error(error) do
case error do
# SNMP protocol errors - use SnmpKit.SnmpLib.Error for detailed analysis
{:snmp_error, code} when is_integer(code) ->
if Error.valid_error_status?(code) do
atom = Error.error_atom(code)
%{
type: :snmp_protocol,
atom: atom,
code: code,
severity: Error.error_severity(atom),
retriable: Error.retriable_error?(atom),
rfc_compliant: true,
category: classify_snmp_error(atom),
description: description(atom)
}
else
%{
type: :snmp_protocol,
atom: code_to_atom(code),
code: code,
retriable: false,
rfc_compliant: false,
category: :unknown_error,
description: "Unknown SNMP error code"
}
end
{:snmp_error, atom} when is_atom(atom) ->
%{
type: :snmp_protocol,
atom: atom,
code: Error.error_code(atom),
severity: Error.error_severity(atom),
retriable: Error.retriable_error?(atom),
rfc_compliant: true,
category: classify_snmp_error(atom),
description: description(atom)
}
# Network errors - our superior handling
error_atom
when error_atom in [:timeout, :host_unreachable, :network_unreachable, :connection_refused] ->
%{
type: :network,
atom: error_atom,
retriable: recoverable?(error_atom),
category: classify_network_error(error_atom),
description: get_network_error_description(error_atom)
}
# Other errors
_ ->
{category, subcategory} = classify_error(error)
%{
type: :other,
atom: error,
retriable: recoverable?(error),
category: category,
subcategory: subcategory,
description: format_error(error)
}
end
end
defp classify_snmp_error(atom) do
case atom do
atom when atom in [:no_such_name, :bad_value, :wrong_type, :wrong_value] ->
:user_error
atom when atom in [:read_only, :not_writable, :no_access, :authorization_error] ->
:security_error
atom when atom in [:too_big, :resource_unavailable] ->
:resource_error
atom when atom in [:gen_err, :commit_failed, :undo_failed] ->
:device_error
_ ->
:protocol_error
end
end
defp classify_network_error(atom) do
case atom do
:timeout -> :transient_error
:host_unreachable -> :configuration_error
:network_unreachable -> :configuration_error
:connection_refused -> :service_error
end
end
defp get_network_error_description(atom) do
case atom do
:timeout -> "Operation timed out"
:host_unreachable -> "Host is unreachable"
:network_unreachable -> "Network is unreachable"
:connection_refused -> "Connection refused by target"
end
end
# Private functions
defp format_error_details(details) when is_list(details) do
formatted =
Enum.map_join(details, ", ", fn
{:oid, oid} -> "OID: #{oid}"
{:index, index} -> "Index: #{index}"
{:value, value} -> "Value: #{inspect(value)}"
{key, value} -> "#{key}: #{value}"
end)
if formatted == "", do: "", else: " (#{formatted})"
end
defp format_error_details(_), do: ""
@doc """
Classifies an error into a category for better handling.
## Examples
iex> SnmpKit.SnmpMgr.Errors.classify_error({:snmp_error, :no_such_name}, "Get request")
{:user_error, :invalid_oid}
iex> SnmpKit.SnmpMgr.Errors.classify_error({:network_error, :timeout}, "Network operation")
{:transient_error, :network_timeout}
iex> SnmpKit.SnmpMgr.Errors.classify_error({:snmp_error, :too_big}, "Bulk request")
{:recoverable_error, :response_too_large}
"""
def classify_error(error, context \\ nil)
def classify_error({:snmp_error, :no_such_name}, _context) do
{:user_error, :invalid_oid}
end
def classify_error({:snmp_error, :bad_value}, _context) do
{:user_error, :invalid_value}
end
def classify_error({:snmp_error, :read_only}, _context) do
{:user_error, :write_to_readonly}
end
def classify_error({:snmp_error, :too_big}, _context) do
{:recoverable_error, :response_too_large}
end
def classify_error({:snmp_error, :gen_err}, _context) do
{:device_error, :general_failure}
end
def classify_error({:v2c_error, :no_access}, _context) do
{:security_error, :access_denied}
end
def classify_error({:v2c_error, :authorization_error}, _context) do
{:security_error, :authorization_failed}
end
def classify_error({:v2c_error, :wrong_type}, _context) do
{:user_error, :type_mismatch}
end
def classify_error({:v2c_error, :resource_unavailable}, _context) do
{:device_error, :resource_exhausted}
end
def classify_error({:network_error, :timeout}, _context) do
{:transient_error, :network_timeout}
end
def classify_error({:network_error, :host_unreachable}, _context) do
{:configuration_error, :unreachable_host}
end
def classify_error({:network_error, :network_unreachable}, _context) do
{:configuration_error, :network_unavailable}
end
def classify_error({:encoding_error, _reason}, _context) do
{:protocol_error, :message_encoding_failed}
end
def classify_error({:decoding_error, _reason}, _context) do
{:protocol_error, :message_decoding_failed}
end
def classify_error(:timeout, _context) do
{:transient_error, :operation_timeout}
end
def classify_error(:invalid_oid_values, _context) do
:validation_error
end
def classify_error({:snmp_encoding_error, _reason}, _context) do
:validation_error
end
def classify_error(:encoding_failed, _context) do
:validation_error
end
def classify_error(:authentication_error, _context) do
:authentication_error
end
def classify_error(:invalid_community, _context) do
:invalid_community
end
def classify_error(:bad_community, _context) do
:invalid_community
end
def classify_error(:snmp_modules_not_available, _context) do
:system_error
end
def classify_error(error, _context) do
{:unknown_error, error}
end
@doc """
Formats an error message in a user-friendly way with context.
## Examples
iex> SnmpKit.SnmpMgr.Errors.format_user_friendly_error({:snmp_error, :no_such_name}, "Getting system description")
"Getting system description failed: The requested OID does not exist on the device"
iex> SnmpKit.SnmpMgr.Errors.format_user_friendly_error({:network_error, :timeout}, "Contacting device")
"Contacting device failed: The device did not respond within the timeout period"
"""
def format_user_friendly_error(error, context \\ "Operation")
def format_user_friendly_error({:snmp_error, :no_such_name}, context) do
"#{context} failed: The requested OID does not exist on the device"
end
def format_user_friendly_error({:snmp_error, :bad_value}, context) do
"#{context} failed: The provided value is invalid for this OID"
end
def format_user_friendly_error({:snmp_error, :read_only}, context) do
"#{context} failed: This OID is read-only and cannot be modified"
end
def format_user_friendly_error({:snmp_error, :too_big}, context) do
"#{context} failed: The response is too large. Try requesting fewer OIDs or use BULK operations"
end
def format_user_friendly_error({:snmp_error, :gen_err}, context) do
"#{context} failed: The device reported a general error"
end
def format_user_friendly_error({:v2c_error, :no_access}, context) do
"#{context} failed: Access denied. Check your community string and device configuration"
end
def format_user_friendly_error({:v2c_error, :authorization_error}, context) do
"#{context} failed: Authorization failed. Verify your credentials"
end
def format_user_friendly_error({:v2c_error, :wrong_type}, context) do
"#{context} failed: The value type does not match the expected type for this OID"
end
def format_user_friendly_error({:network_error, :timeout}, context) do
"#{context} failed: The device did not respond within the timeout period"
end
def format_user_friendly_error({:network_error, :host_unreachable}, context) do
"#{context} failed: Cannot reach the device. Check the IP address and network connectivity"
end
def format_user_friendly_error({:network_error, :network_unreachable}, context) do
"#{context} failed: Network is unreachable. Check your network configuration"
end
def format_user_friendly_error(:timeout, context) do
"#{context} timed out: The operation took too long to complete"
end
def format_user_friendly_error({:encoding_error, _reason}, context) do
"#{context} failed: Unable to encode the SNMP message"
end
def format_user_friendly_error({:decoding_error, _reason}, context) do
"#{context} failed: Unable to decode the SNMP response"
end
def format_user_friendly_error(error, context) do
"#{context} failed: #{inspect(error)}"
end
@doc """
Provides recovery suggestions for common errors.
## Examples
iex> SnmpKit.SnmpMgr.Errors.get_recovery_suggestions({:snmp_error, :no_such_name})
["Verify the OID is correct", "Check if the OID is supported by this device", "Try using MIB browser to explore available OIDs", "Ensure you're using the correct SNMP version"]
iex> SnmpKit.SnmpMgr.Errors.get_recovery_suggestions({:network_error, :timeout})
["Increase timeout value", "Check network connectivity to the device", "Verify device is responding to ping", "Check for network congestion or packet loss", "Ensure device SNMP service is running"]
"""
def get_recovery_suggestions({:snmp_error, :no_such_name}) do
[
"Verify the OID is correct",
"Check if the OID is supported by this device",
"Try using MIB browser to explore available OIDs",
"Ensure you're using the correct SNMP version"
]
end
def get_recovery_suggestions({:snmp_error, :bad_value}) do
[
"Check the value format and type",
"Verify the value is within acceptable range",
"Ensure the value matches the OID's expected data type",
"Check device documentation for valid values"
]
end
def get_recovery_suggestions({:snmp_error, :read_only}) do
[
"This OID cannot be modified",
"Use GET operation instead of SET",
"Check device documentation for writable OIDs",
"Verify you have the correct OID for writing"
]
end
def get_recovery_suggestions({:snmp_error, :too_big}) do
[
"Reduce the number of OIDs in the request",
"Use GETBULK operation with smaller max-repetitions",
"Split the request into multiple smaller requests",
"Check device's maximum PDU size settings"
]
end
def get_recovery_suggestions({:v2c_error, :no_access}) do
[
"Verify the community string is correct",
"Check device SNMP access configuration",
"Ensure the device allows SNMP access from your IP",
"Verify SNMP version compatibility (v1/v2c)"
]
end
def get_recovery_suggestions({:network_error, :timeout}) do
[
"Increase timeout value",
"Check network connectivity to the device",
"Verify device is responding to ping",
"Check for network congestion or packet loss",
"Ensure device SNMP service is running"
]
end
def get_recovery_suggestions({:network_error, :host_unreachable}) do
[
"Verify the IP address is correct",
"Check network routing",
"Ensure device is powered on and connected",
"Test basic connectivity with ping",
"Check firewall rules"
]
end
def get_recovery_suggestions({:encoding_error, _reason}) do
[
"Check OID format",
"Verify value types are supported",
"Ensure SNMP version compatibility",
"Check for invalid characters in community string"
]
end
def get_recovery_suggestions({:decoding_error, _reason}) do
[
"Device may not support SNMP",
"Check SNMP version compatibility",
"Verify device is not sending corrupted responses",
"Try different SNMP version (v1/v2c)"
]
end
def get_recovery_suggestions(:timeout) do
[
"Increase operation timeout",
"Check if operation is too complex",
"Verify device resources are available",
"Try simpler operations first"
]
end
def get_recovery_suggestions(_error) do
[
"Check device documentation",
"Verify SNMP configuration",
"Try basic connectivity tests",
"Contact device vendor support"
]
end
end