Removes unreachable catch-all clauses, drops unused `require Logger` lines, adds pin operators to bitstring size patterns, reorders function heads for correct dispatch, and replaces deprecated LoggerBackends calls.
511 lines
16 KiB
Elixir
511 lines
16 KiB
Elixir
defmodule SnmpKit.SnmpMgr.Core do
|
|
@moduledoc """
|
|
Core SNMP operations using Erlang's SNMP PDU functions directly.
|
|
|
|
This module handles the low-level SNMP PDU encoding/decoding and UDP communication
|
|
without requiring the heavyweight :snmpm manager process.
|
|
|
|
## Timeout Behavior
|
|
|
|
All functions in this module use a single timeout parameter that controls
|
|
the SNMP PDU timeout - how long to wait for a response to each individual
|
|
SNMP packet sent to the target device.
|
|
|
|
- **Default timeout**: 10 seconds (10,000 milliseconds)
|
|
- **Timeout applies to**: Each individual SNMP PDU (GET, SET, GETBULK, etc.)
|
|
- **Not applicable to**: Multi-PDU operations (use walk functions for those)
|
|
|
|
For operations that may require multiple PDUs (like walking large tables),
|
|
consider using the higher-level walk functions in `SnmpKit.SnmpMgr.MultiV2`
|
|
which handle multi-PDU timeouts appropriately.
|
|
"""
|
|
|
|
alias SnmpKit.SnmpLib.Manager
|
|
alias SnmpKit.SnmpLib.OID
|
|
alias SnmpKit.SnmpMgr.MIB
|
|
alias SnmpKit.SnmpMgr.Target
|
|
|
|
@type snmp_result :: {:ok, term()} | {:error, atom() | tuple()}
|
|
@type target :: binary() | tuple() | map()
|
|
@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.
|
|
|
|
## Parameters
|
|
- `target` - SNMP target (host, "host:port", or target map)
|
|
- `oid` - Object identifier (string or list format)
|
|
- `opts` - Request options
|
|
- `:timeout` - SNMP PDU timeout in milliseconds (default: 10000)
|
|
- `:community` - SNMP community string (default: "public")
|
|
- `:version` - SNMP version (:v1, :v2c) (default: :v2c)
|
|
- `:port` - SNMP port (default: 161)
|
|
"""
|
|
@spec send_get_request(target(), oid(), opts()) :: snmp_result()
|
|
def send_get_request(target, oid, opts \\ []) do
|
|
# Parse target to extract host and port
|
|
{host, updated_opts} =
|
|
case Target.parse(target) do
|
|
{:ok, %{host: host, port: port}} ->
|
|
# Only use parsed port if the target actually contained a port specification
|
|
if target_contains_port?(target) do
|
|
# Target contained port - use parsed port
|
|
opts_with_port = Keyword.put(opts, :port, port)
|
|
{host, opts_with_port}
|
|
else
|
|
# Target didn't contain port - preserve user's port option
|
|
{host, opts}
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
# Failed to parse, use as-is
|
|
{target, opts}
|
|
end
|
|
|
|
# Convert oid to proper format
|
|
oid_parsed =
|
|
case parse_oid(oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
{:error, _} -> oid
|
|
end
|
|
|
|
# Map options to snmp_lib format
|
|
snmp_lib_opts = map_options_to_snmp_lib(updated_opts)
|
|
|
|
# Use SnmpKit.SnmpLib.Manager for the actual operation
|
|
case Manager.get(host, oid_parsed, snmp_lib_opts) do
|
|
{:ok, {type, value}} ->
|
|
{:ok, {type, value}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sends a GET request and returns the result in 3-tuple format.
|
|
|
|
This function returns `{oid_string, type, value}` for consistency with
|
|
other operations like walk, bulk, etc.
|
|
"""
|
|
@spec send_get_request_with_type(target(), oid(), opts()) ::
|
|
{:ok, {String.t(), atom(), any()}} | {:error, any()}
|
|
def send_get_request_with_type(target, oid, opts \\ []) do
|
|
# Parse target to extract host and port
|
|
{host, updated_opts} =
|
|
case Target.parse(target) do
|
|
{:ok, %{host: host, port: port}} ->
|
|
# Use parsed port, overriding any default
|
|
opts_with_port = Keyword.put(opts, :port, port)
|
|
{host, opts_with_port}
|
|
|
|
{:error, _reason} ->
|
|
# Failed to parse, use as-is
|
|
{target, opts}
|
|
end
|
|
|
|
# Convert oid to proper format - always work with lists internally
|
|
oid_parsed =
|
|
case parse_oid(oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
# Safe fallback
|
|
{:error, _} -> [1, 3]
|
|
end
|
|
|
|
# Generate string representation only for final response
|
|
oid_string = Enum.join(oid_parsed, ".")
|
|
|
|
# Map options to snmp_lib format
|
|
snmp_lib_opts = map_options_to_snmp_lib(updated_opts)
|
|
|
|
case Manager.get(host, oid_parsed, snmp_lib_opts) do
|
|
{:ok, {type, value}} ->
|
|
{:ok, {oid_string, type, value}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sends a GETNEXT request to retrieve the next OID in the MIB tree.
|
|
|
|
Now uses the proper SnmpKit.SnmpLib.Manager.get_next/3 function which handles
|
|
version-specific logic (GETNEXT for v1, GETBULK for v2c+) correctly.
|
|
"""
|
|
@spec send_get_next_request(target(), oid(), opts()) :: snmp_result()
|
|
def send_get_next_request(target, oid, opts \\ []) do
|
|
# Parse target to extract host and port
|
|
{host, updated_opts} =
|
|
case Target.parse(target) do
|
|
{:ok, %{host: host, port: port}} ->
|
|
# Use parsed port, overriding any default
|
|
opts_with_port = Keyword.put(opts, :port, port)
|
|
{host, opts_with_port}
|
|
|
|
{:error, _reason} ->
|
|
# Failed to parse, use as-is
|
|
{target, opts}
|
|
end
|
|
|
|
# Convert oid to proper format
|
|
oid_parsed =
|
|
case parse_oid(oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
{:error, _} -> oid
|
|
end
|
|
|
|
# Map options to snmp_lib format
|
|
snmp_lib_opts = map_options_to_snmp_lib(updated_opts)
|
|
|
|
# Use the new SnmpKit.SnmpLib.Manager.get_next function which properly handles version logic
|
|
case Manager.get_next(host, oid_parsed, snmp_lib_opts) do
|
|
{:ok, {next_oid, type, value}} ->
|
|
# ALWAYS preserve type information - this is critical for SNMP
|
|
# Convert OID to string only for final response format
|
|
next_oid_string =
|
|
if is_list(next_oid) do
|
|
Enum.join(next_oid, ".")
|
|
else
|
|
# Should not happen, but handle gracefully
|
|
to_string(next_oid)
|
|
end
|
|
|
|
{:ok, {next_oid_string, type, value}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sends an SNMP SET request and returns the response.
|
|
|
|
## Parameters
|
|
- `target` - SNMP target (host, "host:port", or target map)
|
|
- `oid` - Object identifier to set
|
|
- `value` - Value to set (will be encoded based on type)
|
|
- `opts` - Request options
|
|
- `:timeout` - SNMP PDU timeout in milliseconds (default: 10000)
|
|
- `:community` - SNMP community string (default: "public")
|
|
- `:version` - SNMP version (:v1, :v2c) (default: :v2c)
|
|
"""
|
|
@spec send_set_request(target(), oid(), term(), opts()) :: snmp_result()
|
|
def send_set_request(target, oid, value, opts \\ []) do
|
|
{host, updated_opts} = parse_target_with_port(target, opts)
|
|
oid_parsed = parse_oid_or_fallback(oid)
|
|
typed_value = convert_value_to_typed(value)
|
|
snmp_lib_opts = map_options_to_snmp_lib(updated_opts)
|
|
|
|
case Manager.set(host, oid_parsed, typed_value, snmp_lib_opts) do
|
|
{:ok, result} -> {:ok, result}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_target_with_port(target, opts) do
|
|
case Target.parse(target) do
|
|
{:ok, %{host: host, port: port}} ->
|
|
if target_contains_port?(target) do
|
|
{host, Keyword.put(opts, :port, port)}
|
|
else
|
|
{host, opts}
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
{target, opts}
|
|
end
|
|
end
|
|
|
|
defp parse_oid_or_fallback(oid) do
|
|
case parse_oid(oid) do
|
|
{:ok, oid_list} -> oid_list
|
|
{:error, _} -> oid
|
|
end
|
|
end
|
|
|
|
defp convert_value_to_typed(value) do
|
|
cond do
|
|
match?({t, _v} when is_atom(t), value) -> normalize_manager_typed(value)
|
|
is_binary(value) -> {:string, value}
|
|
is_integer(value) -> {:integer, value}
|
|
ipv4_tuple?(value) -> {:ip_address, value}
|
|
true -> {:opaque, value}
|
|
end
|
|
end
|
|
|
|
defp ipv4_tuple?({a, b, c, d}) when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d), do: true
|
|
|
|
defp ipv4_tuple?(_), do: false
|
|
|
|
# Normalize typed values to the atoms that SnmpKit.SnmpLib.Manager expects
|
|
defp normalize_manager_typed({type, val}) when is_atom(type) do
|
|
mapped_type = Map.get(@type_normalization, type, type)
|
|
{mapped_type, val}
|
|
end
|
|
|
|
# Extract varbinds from get_bulk results
|
|
defp extract_varbinds(%{"varbinds" => varbinds}) when is_list(varbinds), do: varbinds
|
|
defp extract_varbinds(results) when is_list(results), do: results
|
|
defp extract_varbinds(_), do: []
|
|
|
|
@doc """
|
|
Sends an SNMP GETBULK request (SNMPv2c only).
|
|
|
|
GETBULK is more efficient than multiple GETNEXT operations for retrieving
|
|
multiple consecutive OIDs.
|
|
|
|
## Parameters
|
|
- `target` - SNMP target (host, "host:port", or target map)
|
|
- `oid` - Starting OID for bulk retrieval
|
|
- `opts` - Request options
|
|
- `:timeout` - SNMP PDU timeout in milliseconds (default: 10000)
|
|
- `:max_repetitions` - Maximum number of OIDs to retrieve (default: 30)
|
|
- `:community` - SNMP community string (default: "public")
|
|
- `:version` - SNMP version (must be :v2c) (default: :v2c)
|
|
"""
|
|
@spec send_get_bulk_request(target(), oid(), opts()) :: snmp_result()
|
|
def send_get_bulk_request(target, oid, opts \\ []) do
|
|
version = Keyword.get(opts, :version, :v2c)
|
|
|
|
case version do
|
|
:v2c ->
|
|
{host, updated_opts} = parse_target_with_port(target, opts)
|
|
oid_parsed = parse_oid_or_fallback(oid)
|
|
snmp_lib_opts = map_options_to_snmp_lib(updated_opts)
|
|
|
|
case Manager.get_bulk(host, oid_parsed, snmp_lib_opts) do
|
|
{:ok, results} ->
|
|
{:ok, extract_varbinds(results)}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
|
|
_ ->
|
|
{:error, :getbulk_requires_v2c}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sends an asynchronous SNMP GET request.
|
|
|
|
Returns immediately with a reference. The calling process will receive
|
|
a message with the result.
|
|
|
|
## Parameters
|
|
- `target` - SNMP target (host, "host:port", or target map)
|
|
- `oid` - Object identifier (string or list format)
|
|
- `opts` - Request options
|
|
- `:timeout` - SNMP PDU timeout in milliseconds (default: 10000)
|
|
- `:community` - SNMP community string (default: "public")
|
|
- `:version` - SNMP version (:v1, :v2c) (default: :v2c)
|
|
|
|
## Returns
|
|
Reference that will be included in the response message.
|
|
"""
|
|
@spec send_get_request_async(target(), oid(), opts()) :: reference()
|
|
def send_get_request_async(target, oid, opts \\ []) do
|
|
caller = self()
|
|
ref = make_ref()
|
|
|
|
spawn(fn ->
|
|
result = send_get_request(target, oid, opts)
|
|
send(caller, {ref, result})
|
|
end)
|
|
|
|
ref
|
|
end
|
|
|
|
@doc """
|
|
Sends an asynchronous SNMP GETBULK request.
|
|
|
|
Returns immediately with a reference. The calling process will receive
|
|
a message with the result.
|
|
|
|
## Parameters
|
|
- `target` - SNMP target (host, "host:port", or target map)
|
|
- `oid` - Starting OID for bulk retrieval
|
|
- `opts` - Request options
|
|
- `:timeout` - SNMP PDU timeout in milliseconds (default: 10000)
|
|
- `:max_repetitions` - Maximum number of OIDs to retrieve (default: 30)
|
|
- `:community` - SNMP community string (default: "public")
|
|
- `:version` - SNMP version (must be :v2c) (default: :v2c)
|
|
|
|
## Returns
|
|
Reference that will be included in the response message.
|
|
"""
|
|
@spec send_get_bulk_request_async(target(), oid(), opts()) :: reference()
|
|
def send_get_bulk_request_async(target, oid, opts \\ []) do
|
|
caller = self()
|
|
ref = make_ref()
|
|
|
|
spawn(fn ->
|
|
result = send_get_bulk_request(target, oid, opts)
|
|
send(caller, {ref, result})
|
|
end)
|
|
|
|
ref
|
|
end
|
|
|
|
# Private functions for snmp_lib integration
|
|
|
|
@spec map_options_to_snmp_lib(opts()) :: list()
|
|
defp map_options_to_snmp_lib(opts) do
|
|
# Map SnmpMgr options to SnmpKit.SnmpLib.Manager options
|
|
mapped = []
|
|
|
|
mapped =
|
|
if community = Keyword.get(opts, :community),
|
|
do: [{:community, community} | mapped],
|
|
else: mapped
|
|
|
|
mapped =
|
|
if timeout = Keyword.get(opts, :timeout), do: [{:timeout, timeout} | mapped], else: mapped
|
|
|
|
mapped =
|
|
if retries = Keyword.get(opts, :retries), do: [{:retries, retries} | mapped], else: mapped
|
|
|
|
mapped =
|
|
if version = Keyword.get(opts, :version), do: [{:version, version} | mapped], else: mapped
|
|
|
|
mapped = if port = Keyword.get(opts, :port), do: [{:port, port} | mapped], else: mapped
|
|
|
|
mapped =
|
|
if max_repetitions = Keyword.get(opts, :max_repetitions),
|
|
do: [{:max_repetitions, max_repetitions} | mapped],
|
|
else: mapped
|
|
|
|
mapped =
|
|
if non_repeaters = Keyword.get(opts, :non_repeaters),
|
|
do: [{:non_repeaters, non_repeaters} | mapped],
|
|
else: mapped
|
|
|
|
mapped
|
|
end
|
|
|
|
@doc """
|
|
Parses and normalizes an OID to internal list format.
|
|
|
|
Converts external OID input (string or list) to internal list of integers format.
|
|
This function establishes the API boundary - all external input is converted to
|
|
internal list format here.
|
|
"""
|
|
@spec parse_oid(oid()) :: {:ok, list(non_neg_integer())} | {:error, term()}
|
|
def parse_oid(oid) when is_list(oid) do
|
|
# Already a list - validate and return
|
|
case OID.valid_oid?(oid) do
|
|
:ok ->
|
|
{:ok, oid}
|
|
|
|
{:error, :empty_oid} ->
|
|
# Empty list fallback
|
|
{:ok, [1, 3]}
|
|
|
|
{:error, _reason} ->
|
|
case oid do
|
|
# Single [1] is invalid, use [1,3]
|
|
[1] -> {:ok, [1, 3]}
|
|
_ -> {:error, :invalid_oid_list}
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_oid(oid) when is_binary(oid) do
|
|
case String.trim(oid) do
|
|
"" -> {:ok, [1, 3]}
|
|
trimmed -> resolve_oid_string(trimmed)
|
|
end
|
|
end
|
|
|
|
def parse_oid(_), do: {:error, :invalid_oid_input}
|
|
|
|
defp resolve_oid_string(trimmed) do
|
|
with {:error, _} <- SnmpKit.SnmpLib.MIB.Registry.resolve_name(trimmed),
|
|
{:error, _} <- parse_numeric_oid(trimmed) do
|
|
MIB.resolve(trimmed)
|
|
end
|
|
end
|
|
|
|
defp parse_numeric_oid(oid_string) do
|
|
case OID.string_to_list(oid_string) do
|
|
{:ok, [_ | _] = oid_list} -> {:ok, oid_list}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
# Type information must never be inferred - it must be preserved from SNMP responses
|
|
# Removing type inference functions to prevent loss of critical type information
|
|
# Any SNMP operation that does not preserve type information should fail with an error
|
|
|
|
# Private helper to check if target contains port specification
|
|
defp target_contains_port?(target) when is_binary(target) do
|
|
cond do
|
|
# RFC 3986 bracket notation: [IPv6]:port
|
|
String.starts_with?(target, "[") and String.contains?(target, "]:") ->
|
|
target |> extract_bracket_port() |> valid_port_string?()
|
|
|
|
# Plain IPv6 addresses (contain :: or multiple colons) - no port embedded
|
|
String.contains?(target, "::") ->
|
|
false
|
|
|
|
target |> String.graphemes() |> Enum.count(&(&1 == ":")) > 1 ->
|
|
false
|
|
|
|
# IPv4 or simple hostname with port
|
|
String.contains?(target, ":") ->
|
|
target |> extract_simple_port() |> valid_port_string?()
|
|
|
|
# No colon at all
|
|
true ->
|
|
false
|
|
end
|
|
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
|