credo improvements
This commit is contained in:
parent
a0cee485a4
commit
7fcfdbf2e9
13 changed files with 252 additions and 285 deletions
|
|
@ -144,7 +144,8 @@
|
|||
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||
{Credo.Check.Warning.IExPry, []},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||
# Disabled: We use structured logging with dynamic metadata keys
|
||||
# {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ defmodule SnmpKit.SnmpLib.Error do
|
|||
@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 """
|
||||
|
|
@ -351,28 +354,9 @@ defmodule SnmpKit.SnmpLib.Error do
|
|||
"""
|
||||
@spec retriable_error?(error_status()) :: boolean()
|
||||
def retriable_error?(error_status) do
|
||||
case error_atom(error_status) do
|
||||
:no_error -> false
|
||||
:too_big -> true
|
||||
:no_such_name -> false
|
||||
:bad_value -> false
|
||||
:read_only -> false
|
||||
:gen_err -> true
|
||||
:no_access -> false
|
||||
:wrong_type -> false
|
||||
:wrong_length -> false
|
||||
:wrong_encoding -> false
|
||||
:wrong_value -> false
|
||||
:no_creation -> false
|
||||
:inconsistent_value -> false
|
||||
:resource_unavailable -> true
|
||||
:commit_failed -> false
|
||||
:undo_failed -> false
|
||||
:authorization_error -> false
|
||||
:not_writable -> false
|
||||
:inconsistent_name -> false
|
||||
_ -> false
|
||||
end
|
||||
error_status
|
||||
|> error_atom()
|
||||
|> then(&MapSet.member?(@retriable_errors, &1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -111,6 +111,30 @@ defmodule SnmpKit.SnmpLib.ErrorHandler do
|
|||
quarantine_until: integer() | nil
|
||||
}
|
||||
|
||||
# Error classification map for O(1) lookup
|
||||
@error_classifications %{
|
||||
# Network-related transient errors
|
||||
:timeout => :transient,
|
||||
:nxdomain => :transient,
|
||||
:network_unreachable => :transient,
|
||||
:connection_refused => :transient,
|
||||
# Device overload (transient)
|
||||
:device_busy => :transient,
|
||||
:too_big => :transient,
|
||||
:resource_unavailable => :transient,
|
||||
# Permanent configuration errors
|
||||
:authentication_failed => :permanent,
|
||||
:community_mismatch => :permanent,
|
||||
:unsupported_version => :permanent,
|
||||
:no_such_name => :permanent,
|
||||
:bad_value => :permanent,
|
||||
:read_only => :permanent,
|
||||
# Performance degradation
|
||||
:slow_response => :degraded,
|
||||
:partial_failure => :degraded,
|
||||
:high_error_rate => :degraded
|
||||
}
|
||||
|
||||
defstruct device_stats: %{},
|
||||
global_stats: %{
|
||||
total_operations: 0,
|
||||
|
|
@ -351,33 +375,8 @@ defmodule SnmpKit.SnmpLib.ErrorHandler do
|
|||
:degraded = SnmpKit.SnmpLib.ErrorHandler.classify_error(:slow_response)
|
||||
"""
|
||||
@spec classify_error(any()) :: error_class()
|
||||
def classify_error(error) do
|
||||
case error do
|
||||
# Network-related transient errors
|
||||
:timeout -> :transient
|
||||
:nxdomain -> :transient
|
||||
:network_unreachable -> :transient
|
||||
:connection_refused -> :transient
|
||||
{:network_error, _} -> :transient
|
||||
# Device overload (transient)
|
||||
:device_busy -> :transient
|
||||
:too_big -> :transient
|
||||
:resource_unavailable -> :transient
|
||||
# Permanent configuration errors
|
||||
:authentication_failed -> :permanent
|
||||
:community_mismatch -> :permanent
|
||||
:unsupported_version -> :permanent
|
||||
:no_such_name -> :permanent
|
||||
:bad_value -> :permanent
|
||||
:read_only -> :permanent
|
||||
# Performance degradation
|
||||
:slow_response -> :degraded
|
||||
:partial_failure -> :degraded
|
||||
:high_error_rate -> :degraded
|
||||
# Default to unknown for unclassified errors
|
||||
_ -> :unknown
|
||||
end
|
||||
end
|
||||
def classify_error({:network_error, _}), do: :transient
|
||||
def classify_error(error), do: Map.get(@error_classifications, error, :unknown)
|
||||
|
||||
@doc """
|
||||
Puts a device into quarantine for a specified duration.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,94 @@ defmodule SnmpKit.SnmpLib.MIB.SnmpTokenizer do
|
|||
# State record equivalent
|
||||
defstruct line: 1, chars: [], get_line_fun: nil
|
||||
|
||||
# Reserved words from SNMP/SMI - complete list from Erlang tokenizer
|
||||
# Using a Map for O(1) lookup and reduced cyclomatic complexity
|
||||
@reserved_words %{
|
||||
"DEFINITIONS" => :DEFINITIONS,
|
||||
"BEGIN" => :BEGIN,
|
||||
"END" => :END,
|
||||
"IMPORTS" => :IMPORTS,
|
||||
"FROM" => :FROM,
|
||||
"EXPORTS" => :EXPORTS,
|
||||
"OBJECT" => :OBJECT,
|
||||
"IDENTIFIER" => :IDENTIFIER,
|
||||
"OBJECT-TYPE" => :"OBJECT-TYPE",
|
||||
"SYNTAX" => :SYNTAX,
|
||||
"ACCESS" => :ACCESS,
|
||||
"MAX-ACCESS" => :"MAX-ACCESS",
|
||||
"STATUS" => :STATUS,
|
||||
"DESCRIPTION" => :DESCRIPTION,
|
||||
"REFERENCE" => :REFERENCE,
|
||||
"INDEX" => :INDEX,
|
||||
"AUGMENTS" => :AUGMENTS,
|
||||
"DEFVAL" => :DEFVAL,
|
||||
"UNITS" => :UNITS,
|
||||
"SEQUENCE" => :SEQUENCE,
|
||||
"OF" => :OF,
|
||||
"CHOICE" => :CHOICE,
|
||||
"SIZE" => :SIZE,
|
||||
"INTEGER" => :INTEGER,
|
||||
"OCTET" => :OCTET,
|
||||
"STRING" => :STRING,
|
||||
"NULL" => :NULL,
|
||||
"IpAddress" => :IpAddress,
|
||||
"Counter" => :Counter,
|
||||
"Counter32" => :Counter32,
|
||||
"Counter64" => :Counter64,
|
||||
"Gauge" => :Gauge,
|
||||
"Gauge32" => :Gauge32,
|
||||
"TimeTicks" => :TimeTicks,
|
||||
"Unsigned32" => :Unsigned32,
|
||||
"Integer32" => :Integer32,
|
||||
"Opaque" => :Opaque,
|
||||
"BITS" => :BITS,
|
||||
"MODULE-IDENTITY" => :"MODULE-IDENTITY",
|
||||
"OBJECT-IDENTITY" => :"OBJECT-IDENTITY",
|
||||
"TEXTUAL-CONVENTION" => :"TEXTUAL-CONVENTION",
|
||||
"OBJECT-GROUP" => :"OBJECT-GROUP",
|
||||
"NOTIFICATION-GROUP" => :"NOTIFICATION-GROUP",
|
||||
"MODULE-COMPLIANCE" => :"MODULE-COMPLIANCE",
|
||||
"AGENT-CAPABILITIES" => :"AGENT-CAPABILITIES",
|
||||
"NOTIFICATION-TYPE" => :"NOTIFICATION-TYPE",
|
||||
"TRAP-TYPE" => :"TRAP-TYPE",
|
||||
"LAST-UPDATED" => :"LAST-UPDATED",
|
||||
"ORGANIZATION" => :ORGANIZATION,
|
||||
"CONTACT-INFO" => :"CONTACT-INFO",
|
||||
"REVISION" => :REVISION,
|
||||
"DISPLAY-HINT" => :"DISPLAY-HINT",
|
||||
"IMPLIED" => :IMPLIED,
|
||||
"OBJECTS" => :OBJECTS,
|
||||
"NOTIFICATIONS" => :NOTIFICATIONS,
|
||||
"MANDATORY-GROUPS" => :"MANDATORY-GROUPS",
|
||||
"GROUP" => :GROUP,
|
||||
"MODULE" => :MODULE,
|
||||
"WRITE-SYNTAX" => :"WRITE-SYNTAX",
|
||||
"MIN-ACCESS" => :"MIN-ACCESS",
|
||||
"PRODUCT-RELEASE" => :"PRODUCT-RELEASE",
|
||||
"SUPPORTS" => :SUPPORTS,
|
||||
"INCLUDES" => :INCLUDES,
|
||||
"VARIATION" => :VARIATION,
|
||||
"CREATION-REQUIRES" => :"CREATION-REQUIRES",
|
||||
"ENTERPRISE" => :ENTERPRISE,
|
||||
"VARIABLES" => :VARIABLES,
|
||||
"APPLICATION" => :APPLICATION,
|
||||
"IMPLICIT" => :IMPLICIT,
|
||||
"EXPLICIT" => :EXPLICIT,
|
||||
"UNIVERSAL" => :UNIVERSAL,
|
||||
"PRIVATE" => :PRIVATE,
|
||||
"MACRO" => :MACRO,
|
||||
"TYPE" => :TYPE,
|
||||
"NOTATION" => :NOTATION,
|
||||
"VALUE" => :VALUE,
|
||||
# Access values
|
||||
"read-only" => :"read-only",
|
||||
"read-write" => :"read-write",
|
||||
"write-only" => :"write-only",
|
||||
"not-accessible" => :"not-accessible",
|
||||
"accessible-for-notify" => :"accessible-for-notify",
|
||||
"read-create" => :"read-create"
|
||||
}
|
||||
|
||||
@type state() :: %__MODULE__{
|
||||
line: pos_integer(),
|
||||
chars: charlist(),
|
||||
|
|
@ -590,100 +678,5 @@ defmodule SnmpKit.SnmpLib.MIB.SnmpTokenizer do
|
|||
end
|
||||
end
|
||||
|
||||
# Reserved words from SNMP/SMI - complete list from Erlang tokenizer
|
||||
defp reserved_word(word) do
|
||||
case word do
|
||||
"DEFINITIONS" -> :DEFINITIONS
|
||||
"BEGIN" -> :BEGIN
|
||||
"END" -> :END
|
||||
"IMPORTS" -> :IMPORTS
|
||||
"FROM" -> :FROM
|
||||
"EXPORTS" -> :EXPORTS
|
||||
"OBJECT" -> :OBJECT
|
||||
"IDENTIFIER" -> :IDENTIFIER
|
||||
"OBJECT-TYPE" -> :"OBJECT-TYPE"
|
||||
"SYNTAX" -> :SYNTAX
|
||||
"ACCESS" -> :ACCESS
|
||||
"MAX-ACCESS" -> :"MAX-ACCESS"
|
||||
"STATUS" -> :STATUS
|
||||
"DESCRIPTION" -> :DESCRIPTION
|
||||
"REFERENCE" -> :REFERENCE
|
||||
"INDEX" -> :INDEX
|
||||
"AUGMENTS" -> :AUGMENTS
|
||||
"DEFVAL" -> :DEFVAL
|
||||
"UNITS" -> :UNITS
|
||||
"SEQUENCE" -> :SEQUENCE
|
||||
"OF" -> :OF
|
||||
"CHOICE" -> :CHOICE
|
||||
"SIZE" -> :SIZE
|
||||
"INTEGER" -> :INTEGER
|
||||
"OCTET" -> :OCTET
|
||||
"STRING" -> :STRING
|
||||
"NULL" -> :NULL
|
||||
"IpAddress" -> :IpAddress
|
||||
"Counter" -> :Counter
|
||||
"Counter32" -> :Counter32
|
||||
"Counter64" -> :Counter64
|
||||
"Gauge" -> :Gauge
|
||||
"Gauge32" -> :Gauge32
|
||||
"TimeTicks" -> :TimeTicks
|
||||
"Unsigned32" -> :Unsigned32
|
||||
"Integer32" -> :Integer32
|
||||
"Opaque" -> :Opaque
|
||||
"BITS" -> :BITS
|
||||
"MODULE-IDENTITY" -> :"MODULE-IDENTITY"
|
||||
"OBJECT-IDENTITY" -> :"OBJECT-IDENTITY"
|
||||
"TEXTUAL-CONVENTION" -> :"TEXTUAL-CONVENTION"
|
||||
"OBJECT-GROUP" -> :"OBJECT-GROUP"
|
||||
"NOTIFICATION-GROUP" -> :"NOTIFICATION-GROUP"
|
||||
"MODULE-COMPLIANCE" -> :"MODULE-COMPLIANCE"
|
||||
"AGENT-CAPABILITIES" -> :"AGENT-CAPABILITIES"
|
||||
"NOTIFICATION-TYPE" -> :"NOTIFICATION-TYPE"
|
||||
"TRAP-TYPE" -> :"TRAP-TYPE"
|
||||
"LAST-UPDATED" -> :"LAST-UPDATED"
|
||||
"ORGANIZATION" -> :ORGANIZATION
|
||||
"CONTACT-INFO" -> :"CONTACT-INFO"
|
||||
"REVISION" -> :REVISION
|
||||
"DISPLAY-HINT" -> :"DISPLAY-HINT"
|
||||
"IMPLIED" -> :IMPLIED
|
||||
"OBJECTS" -> :OBJECTS
|
||||
"NOTIFICATIONS" -> :NOTIFICATIONS
|
||||
"MANDATORY-GROUPS" -> :"MANDATORY-GROUPS"
|
||||
"GROUP" -> :GROUP
|
||||
"MODULE" -> :MODULE
|
||||
"WRITE-SYNTAX" -> :"WRITE-SYNTAX"
|
||||
"MIN-ACCESS" -> :"MIN-ACCESS"
|
||||
"PRODUCT-RELEASE" -> :"PRODUCT-RELEASE"
|
||||
"SUPPORTS" -> :SUPPORTS
|
||||
"INCLUDES" -> :INCLUDES
|
||||
"VARIATION" -> :VARIATION
|
||||
"CREATION-REQUIRES" -> :"CREATION-REQUIRES"
|
||||
"ENTERPRISE" -> :ENTERPRISE
|
||||
"VARIABLES" -> :VARIABLES
|
||||
"APPLICATION" -> :APPLICATION
|
||||
"IMPLICIT" -> :IMPLICIT
|
||||
"EXPLICIT" -> :EXPLICIT
|
||||
"UNIVERSAL" -> :UNIVERSAL
|
||||
"PRIVATE" -> :PRIVATE
|
||||
"MACRO" -> :MACRO
|
||||
"TYPE" -> :TYPE
|
||||
"NOTATION" -> :NOTATION
|
||||
"VALUE" -> :VALUE
|
||||
# Status values - removed to let grammar handle context
|
||||
# "current" -> :'current'
|
||||
# "deprecated" -> :'deprecated'
|
||||
# "obsolete" -> :'obsolete'
|
||||
# Access values
|
||||
"read-only" -> :"read-only"
|
||||
"read-write" -> :"read-write"
|
||||
"write-only" -> :"write-only"
|
||||
"not-accessible" -> :"not-accessible"
|
||||
"accessible-for-notify" -> :"accessible-for-notify"
|
||||
"read-create" -> :"read-create"
|
||||
# Special values - removed to let grammar handle context
|
||||
# "mandatory" -> :'mandatory'
|
||||
# "optional" -> :'optional'
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
defp reserved_word(word), do: Map.get(@reserved_words, word)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -344,42 +344,39 @@ defmodule SnmpKit.SnmpLib.Security do
|
|||
## Private Implementation
|
||||
|
||||
defp validate_user_config(config) do
|
||||
# Validate authentication protocol
|
||||
auth_protocol = config[:auth_protocol] || :none
|
||||
priv_protocol = config[:priv_protocol] || :none
|
||||
|
||||
if auth_protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512] do
|
||||
# Validate privacy protocol
|
||||
priv_protocol = config[:priv_protocol] || :none
|
||||
|
||||
if priv_protocol in [:none, :des, :aes128, :aes192, :aes256] do
|
||||
# Validate protocol compatibility
|
||||
if priv_protocol != :none and auth_protocol == :none do
|
||||
{:error, :priv_requires_auth}
|
||||
else
|
||||
# Validate required passwords
|
||||
if auth_protocol != :none and is_nil(config[:auth_password]) do
|
||||
{:error, :missing_auth_password}
|
||||
else
|
||||
if priv_protocol != :none and is_nil(config[:priv_password]) do
|
||||
{:error, :missing_priv_password}
|
||||
else
|
||||
# Validate engine ID
|
||||
if is_nil(config[:engine_id]) do
|
||||
{:error, :missing_engine_id}
|
||||
else
|
||||
{:ok, config}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, :invalid_priv_protocol}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_auth_protocol}
|
||||
with :ok <- validate_auth_protocol(auth_protocol),
|
||||
:ok <- validate_priv_protocol(priv_protocol),
|
||||
:ok <- validate_protocol_compatibility(auth_protocol, priv_protocol),
|
||||
:ok <- validate_required_passwords(config, auth_protocol, priv_protocol),
|
||||
:ok <- validate_engine_id(config) do
|
||||
{:ok, config}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_auth_protocol(protocol) when protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512], do: :ok
|
||||
defp validate_auth_protocol(_), do: {:error, :invalid_auth_protocol}
|
||||
|
||||
defp validate_priv_protocol(protocol) when protocol in [:none, :des, :aes128, :aes192, :aes256], do: :ok
|
||||
defp validate_priv_protocol(_), do: {:error, :invalid_priv_protocol}
|
||||
|
||||
defp validate_protocol_compatibility(:none, priv) when priv != :none, do: {:error, :priv_requires_auth}
|
||||
defp validate_protocol_compatibility(_, _), do: :ok
|
||||
|
||||
defp validate_required_passwords(config, auth_protocol, priv_protocol) do
|
||||
cond do
|
||||
auth_protocol != :none and is_nil(config[:auth_password]) -> {:error, :missing_auth_password}
|
||||
priv_protocol != :none and is_nil(config[:priv_password]) -> {:error, :missing_priv_password}
|
||||
true -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_engine_id(config) do
|
||||
if is_nil(config[:engine_id]), do: {:error, :missing_engine_id}, else: :ok
|
||||
end
|
||||
|
||||
defp derive_auth_key(config) do
|
||||
case config[:auth_protocol] do
|
||||
:none ->
|
||||
|
|
|
|||
|
|
@ -583,18 +583,22 @@ defmodule SnmpKit.SnmpLib.Utils do
|
|||
|
||||
# Standard host:port parsing for IPv4 and simple hostnames
|
||||
true ->
|
||||
case String.split(target_str, ":", parts: 2) do
|
||||
[host_str] ->
|
||||
{host_str, nil}
|
||||
parse_standard_host_port(target_str)
|
||||
end
|
||||
end
|
||||
|
||||
[host_str, port_str] ->
|
||||
# Check if host part looks like IPv4 or simple hostname
|
||||
if ipv4_or_simple_hostname?(host_str) do
|
||||
{host_str, port_str}
|
||||
else
|
||||
# Complex hostname with colons, treat as hostname without port
|
||||
{target_str, nil}
|
||||
end
|
||||
defp parse_standard_host_port(target_str) do
|
||||
case String.split(target_str, ":", parts: 2) do
|
||||
[host_str] ->
|
||||
{host_str, nil}
|
||||
|
||||
[host_str, port_str] ->
|
||||
# Check if host part looks like IPv4 or simple hostname
|
||||
if ipv4_or_simple_hostname?(host_str) do
|
||||
{host_str, port_str}
|
||||
else
|
||||
# Complex hostname with colons, treat as hostname without port
|
||||
{target_str, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -667,58 +671,46 @@ defmodule SnmpKit.SnmpLib.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
defp valid_ip_tuple?({a, b, c, d}) when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do
|
||||
a >= 0 and a <= 255 and
|
||||
b >= 0 and b <= 255 and
|
||||
c >= 0 and c <= 255 and
|
||||
d >= 0 and d <= 255
|
||||
defp valid_ip_tuple?({a, b, c, d}) do
|
||||
Enum.all?([a, b, c, d], &valid_ipv4_octet?/1)
|
||||
end
|
||||
|
||||
defp valid_ip_tuple?(_), do: false
|
||||
|
||||
defp valid_ipv6_tuple?({a, b, c, d, e, f, g, h})
|
||||
when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) and is_integer(e) and is_integer(f) and
|
||||
is_integer(g) and is_integer(h) do
|
||||
a >= 0 and a <= 65_535 and
|
||||
b >= 0 and b <= 65_535 and
|
||||
c >= 0 and c <= 65_535 and
|
||||
d >= 0 and d <= 65_535 and
|
||||
e >= 0 and e <= 65_535 and
|
||||
f >= 0 and f <= 65_535 and
|
||||
g >= 0 and g <= 65_535 and
|
||||
h >= 0 and h <= 65_535
|
||||
defp valid_ipv4_octet?(n), do: is_integer(n) and n >= 0 and n <= 255
|
||||
|
||||
defp valid_ipv6_tuple?({a, b, c, d, e, f, g, h}) do
|
||||
Enum.all?([a, b, c, d, e, f, g, h], &valid_ipv6_segment?/1)
|
||||
end
|
||||
|
||||
defp valid_ipv6_tuple?(_), do: false
|
||||
|
||||
defp valid_ipv6_segment?(n), do: is_integer(n) and n >= 0 and n <= 65_535
|
||||
|
||||
defp ipv4_or_simple_hostname?(host_str) do
|
||||
# Check if it looks like IPv4 (has 3 dots and only digits/dots)
|
||||
case String.split(host_str, ".") do
|
||||
[a, b, c, d] ->
|
||||
# Looks like IPv4, check if all parts are numeric
|
||||
Enum.all?([a, b, c, d], fn part ->
|
||||
case Integer.parse(part) do
|
||||
{num, ""} when num >= 0 and num <= 255 -> true
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
|
||||
_ ->
|
||||
# Not IPv4 format, check if it's a simple hostname
|
||||
# Exclude anything that looks like IPv6 (contains :: or multiple colons)
|
||||
cond do
|
||||
# IPv6
|
||||
String.contains?(host_str, "::") -> false
|
||||
# IPv6 or complex
|
||||
host_str |> String.graphemes() |> Enum.count(&(&1 == ":")) > 1 -> false
|
||||
# Contains colon, not simple
|
||||
String.contains?(host_str, ":") -> false
|
||||
# Simple hostname pattern
|
||||
true -> String.match?(host_str, ~r/^[a-zA-Z0-9\-\.\_]+$/)
|
||||
end
|
||||
[a, b, c, d] -> Enum.all?([a, b, c, d], &valid_ipv4_octet_string?/1)
|
||||
_ -> simple_hostname?(host_str)
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_ipv4_octet_string?(part) do
|
||||
case Integer.parse(part) do
|
||||
{num, ""} when num >= 0 and num <= 255 -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp simple_hostname?(host_str) do
|
||||
# Not IPv4 format, check if it's a simple hostname
|
||||
# Exclude anything that looks like IPv6 (contains :: or multiple colons)
|
||||
not String.contains?(host_str, "::") and
|
||||
count_colons(host_str) <= 1 and
|
||||
not String.contains?(host_str, ":") and
|
||||
String.match?(host_str, ~r/^[a-zA-Z0-9\-\.\_]+$/)
|
||||
end
|
||||
|
||||
defp format_pdu_type(:get_request), do: "GET Request"
|
||||
defp format_pdu_type(:get_next_request), do: "GET-NEXT Request"
|
||||
defp format_pdu_type(:get_bulk_request), do: "GET-BULK Request"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,27 @@ defmodule SnmpKit.SnmpMgr.Core do
|
|||
@type oid :: binary() | list(non_neg_integer())
|
||||
@type opts :: keyword()
|
||||
|
||||
# Type normalization map for manager-expected atoms
|
||||
@type_normalization %{
|
||||
:string => :string,
|
||||
:octet_string => :string,
|
||||
:octetString => :string,
|
||||
:integer => :integer,
|
||||
:unsigned32 => :integer,
|
||||
:gauge32 => :gauge32,
|
||||
:counter32 => :counter32,
|
||||
:counter64 => :counter64,
|
||||
:timeticks => :timeticks,
|
||||
:timeTicks => :timeticks,
|
||||
:ip_address => :ip_address,
|
||||
:ipAddress => :ip_address,
|
||||
:object_identifier => :object_identifier,
|
||||
:objectId => :object_identifier,
|
||||
:oid => :object_identifier,
|
||||
:null => :null,
|
||||
:opaque => :opaque
|
||||
}
|
||||
|
||||
@doc """
|
||||
Sends an SNMP GET request and returns the response.
|
||||
|
||||
|
|
@ -266,28 +287,7 @@ defmodule SnmpKit.SnmpMgr.Core do
|
|||
|
||||
# Normalize typed values to the atoms that SnmpKit.SnmpLib.Manager expects
|
||||
defp normalize_manager_typed({type, val}) when is_atom(type) do
|
||||
mapped_type =
|
||||
case type do
|
||||
:string -> :string
|
||||
:octet_string -> :string
|
||||
:octetString -> :string
|
||||
:integer -> :integer
|
||||
:unsigned32 -> :integer
|
||||
:gauge32 -> :gauge32
|
||||
:counter32 -> :counter32
|
||||
:counter64 -> :counter64
|
||||
:timeticks -> :timeticks
|
||||
:timeTicks -> :timeticks
|
||||
:ip_address -> :ip_address
|
||||
:ipAddress -> :ip_address
|
||||
:object_identifier -> :object_identifier
|
||||
:objectId -> :object_identifier
|
||||
:oid -> :object_identifier
|
||||
:null -> :null
|
||||
:opaque -> :opaque
|
||||
other -> other
|
||||
end
|
||||
|
||||
mapped_type = Map.get(@type_normalization, type, type)
|
||||
{mapped_type, val}
|
||||
end
|
||||
|
||||
|
|
@ -539,16 +539,7 @@ defmodule SnmpKit.SnmpMgr.Core do
|
|||
cond do
|
||||
# RFC 3986 bracket notation: [IPv6]:port
|
||||
String.starts_with?(target, "[") and String.contains?(target, "]:") ->
|
||||
case String.split(target, "]:", parts: 2) do
|
||||
[_ipv6_part, port_part] ->
|
||||
case Integer.parse(port_part) do
|
||||
{port, ""} when port > 0 and port <= 65_535 -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
target |> extract_bracket_port() |> valid_port_string?()
|
||||
|
||||
# Plain IPv6 addresses (contain :: or multiple colons) - no port embedded
|
||||
String.contains?(target, "::") ->
|
||||
|
|
@ -559,16 +550,7 @@ defmodule SnmpKit.SnmpMgr.Core do
|
|||
|
||||
# IPv4 or simple hostname with port
|
||||
String.contains?(target, ":") ->
|
||||
case String.split(target, ":", parts: 2) do
|
||||
[_host_part, port_part] ->
|
||||
case Integer.parse(port_part) do
|
||||
{port, ""} when port > 0 and port <= 65_535 -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
target |> extract_simple_port() |> valid_port_string?()
|
||||
|
||||
# No colon at all
|
||||
true ->
|
||||
|
|
@ -577,4 +559,27 @@ defmodule SnmpKit.SnmpMgr.Core do
|
|||
end
|
||||
|
||||
defp target_contains_port?(_), do: false
|
||||
|
||||
defp extract_bracket_port(target) do
|
||||
case String.split(target, "]:", parts: 2) do
|
||||
[_ipv6_part, port_part] -> port_part
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_simple_port(target) do
|
||||
case String.split(target, ":", parts: 2) do
|
||||
[_host_part, port_part] -> port_part
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_port_string?(nil), do: false
|
||||
|
||||
defp valid_port_string?(port_str) do
|
||||
case Integer.parse(port_str) do
|
||||
{port, ""} when port > 0 and port <= 65_535 -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ defmodule SnmpKit.SnmpLib.MIB.CompilerTest do
|
|||
|
||||
assert {:error, errors} = Compiler.compile_string(mib_content)
|
||||
assert is_list(errors)
|
||||
assert length(errors) > 0
|
||||
assert errors != []
|
||||
end
|
||||
|
||||
test "respects compile options" do
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule SnmpKit.SnmpLib.MIB.ComprehensiveMibTest do
|
|||
{:ok, files} ->
|
||||
mib_files = filter_mib_files(files)
|
||||
|
||||
assert length(mib_files) > 0, "No MIB files found in #{unquote(dir_name)} directory"
|
||||
assert mib_files != [], "No MIB files found in #{unquote(dir_name)} directory"
|
||||
|
||||
results = test_mib_files(dir_path, mib_files)
|
||||
|
||||
|
|
@ -34,9 +34,9 @@ defmodule SnmpKit.SnmpLib.MIB.ComprehensiveMibTest do
|
|||
failed = Enum.filter(results, fn {status, _} -> status == :error end)
|
||||
|
||||
# Log results for visibility
|
||||
IO.puts("\n#{String.upcase(unquote(dir_name))} MIBs: #{successful}/#{length(mib_files)} successful")
|
||||
IO.puts("\n#{String.upcase(unquote(dir_name))} MIBs: #{successful}/#{Enum.count(mib_files)} successful")
|
||||
|
||||
if length(failed) > 0 do
|
||||
if failed != [] do
|
||||
IO.puts("Failed files:")
|
||||
|
||||
for {:error, {file, reason}} <- failed do
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
|
|||
not match?({:ok, _}, result) and not match?({:warning, _, _}, result)
|
||||
end)
|
||||
|
||||
if length(failed_mibs) > 0 do
|
||||
if failed_mibs != [] do
|
||||
error_details =
|
||||
Enum.map(failed_mibs, fn
|
||||
{name, {:error, errors}} when is_list(errors) ->
|
||||
|
|
@ -72,13 +72,13 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
|
|||
{:ok, mib} ->
|
||||
assert mib.name != nil, "#{name} should have a valid MIB name"
|
||||
assert is_list(mib.definitions), "#{name} should have definitions list"
|
||||
assert length(mib.definitions) > 0, "#{name} should have at least one definition"
|
||||
assert mib.definitions != [], "#{name} should have at least one definition"
|
||||
|
||||
{:warning, mib, warnings} ->
|
||||
assert mib.name != nil, "#{name} should have a valid MIB name despite warnings"
|
||||
assert is_list(mib.definitions), "#{name} should have definitions list"
|
||||
|
||||
assert length(warnings) > 0,
|
||||
assert warnings != [],
|
||||
"#{name} should have warnings if returning warning result"
|
||||
|
||||
{:error, _} ->
|
||||
|
|
@ -136,7 +136,7 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
|
|||
# Should contain MODULE-COMPLIANCE for QoS conformance
|
||||
# Note: Some MIBs might not have module compliance definitions
|
||||
# Just verify parsing succeeded
|
||||
assert length(mib.definitions) > 0, "Should have parsed at least some definitions"
|
||||
assert mib.definitions != [], "Should have parsed at least some definitions"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ defmodule SnmpKit.SnmpLib.MIB.ParserTest do
|
|||
assert {:ok, mib} = Parser.parse(mib_content)
|
||||
|
||||
assert %{__type__: :mib, name: "TEST-MIB"} = mib
|
||||
assert length(mib.imports) == 1
|
||||
assert length(mib.definitions) >= 1
|
||||
assert match?([_], mib.imports)
|
||||
assert mib.definitions != []
|
||||
end
|
||||
|
||||
test "parses simple object identifier assignment" do
|
||||
|
|
@ -145,8 +145,8 @@ defmodule SnmpKit.SnmpLib.MIB.ParserTest do
|
|||
assert {:ok, mib} = Parser.parse(mib_content)
|
||||
|
||||
assert %{__type__: :mib, name: "TEST-MIB"} = mib
|
||||
assert length(mib.imports) >= 1
|
||||
assert length(mib.definitions) >= 1
|
||||
assert mib.imports != []
|
||||
assert mib.definitions != []
|
||||
end
|
||||
|
||||
test "handles comments in MIB content" do
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ defmodule SnmpKit.SnmpLib.SNMPv3EdgeCasesTest do
|
|||
case V3Encoder.decode_message(encoded, user) do
|
||||
{:ok, decoded} ->
|
||||
# Verify we have at least one varbind and it's reasonably long
|
||||
assert length(decoded.msg_data.pdu.varbinds) >= 1
|
||||
assert decoded.msg_data.pdu.varbinds != []
|
||||
[{decoded_oid, _, _} | _] = decoded.msg_data.pdu.varbinds
|
||||
# Allow some tolerance for encoding limits
|
||||
assert length(decoded_oid) >= 50
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ defmodule SnmpKit.SnmpLib.SNMPv3IntegrationTest do
|
|||
use ExUnit.Case, async: false
|
||||
|
||||
alias SnmpKit.SnmpLib.PDU.V3Encoder
|
||||
alias SnmpKit.SnmpLib.Security
|
||||
alias SnmpKit.SnmpLib.Security.Keys
|
||||
alias SnmpKit.SnmpLib.Security.Priv
|
||||
alias SnmpKit.SnmpLib.Security.USM
|
||||
|
||||
@moduletag :snmpv3
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue