445 lines
13 KiB
Elixir
445 lines
13 KiB
Elixir
defmodule SnmpKit.SnmpLib.MIB.Utilities do
|
|
@moduledoc """
|
|
Direct port of Erlang snmpc_lib.erl utility functions to Elixir.
|
|
|
|
This is a 1:1 port of the utility functions from the official Erlang SNMP compiler
|
|
from OTP lib/snmp/src/compile/snmpc_lib.erl
|
|
|
|
Original copyright: Ericsson AB 1996-2025 (Apache License 2.0)
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@type oid() :: [integer()]
|
|
@type oid_status() :: :resolved | :unresolved
|
|
@type verbosity() :: :silent | :warning | :info | :debug
|
|
|
|
# OID and Name Resolution
|
|
|
|
@doc """
|
|
Register an OID entry in the OID table.
|
|
Port of register_oid/4 from snmpc_lib.erl
|
|
"""
|
|
@spec register_oid(binary(), oid(), atom(), map()) :: map()
|
|
def register_oid(name, oid, status, oid_table) do
|
|
entry = %{
|
|
name: name,
|
|
oid: oid,
|
|
status: status,
|
|
parent: get_parent_oid(oid),
|
|
children: []
|
|
}
|
|
|
|
Map.put(oid_table, name, entry)
|
|
end
|
|
|
|
@doc """
|
|
Resolve symbolic OID references to numeric OIDs.
|
|
Port of resolve_oids/1 from snmpc_lib.erl
|
|
"""
|
|
@spec resolve_oids(map()) :: {:ok, map()} | {:error, [binary()]}
|
|
def resolve_oids(oid_table) do
|
|
# Start with root OIDs (those without parents)
|
|
root_oids = find_root_oids(oid_table)
|
|
|
|
case resolve_oid_tree(root_oids, oid_table, MapSet.new()) do
|
|
{:ok, resolved_table} -> {:ok, resolved_table}
|
|
{:error, reason} when is_binary(reason) -> {:error, [reason]}
|
|
{:error, unresolved} when is_list(unresolved) -> {:error, unresolved}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Translate symbolic name to numeric OID.
|
|
Port of tr_oid/2 from snmpc_lib.erl
|
|
"""
|
|
@spec tr_oid(binary(), map()) :: {:ok, oid()} | {:error, :not_found}
|
|
def tr_oid(name, oid_table) do
|
|
case Map.get(oid_table, name) do
|
|
%{oid: oid, status: :resolved} -> {:ok, oid}
|
|
%{status: :unresolved} -> {:error, :unresolved}
|
|
nil -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Update MIB entries with resolved OIDs.
|
|
Port of update_me_oids/3 from snmpc_lib.erl
|
|
"""
|
|
@spec update_me_oids([map()], map(), verbosity()) :: [map()]
|
|
def update_me_oids(mib_entries, oid_table, verbosity) do
|
|
Enum.map(mib_entries, fn entry ->
|
|
update_single_entry_oid(entry, oid_table, verbosity)
|
|
end)
|
|
end
|
|
|
|
# Type Checking and Validation
|
|
|
|
@doc """
|
|
Validate and transform ASN.1 type definition.
|
|
Port of make_ASN1type/1 from snmpc_lib.erl
|
|
"""
|
|
@spec make_asn1_type(term()) :: {:ok, map()} | {:error, binary()}
|
|
def make_asn1_type(type_def) do
|
|
case normalize_type(type_def) do
|
|
{:ok, normalized} -> validate_type_constraints(normalized)
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validate bit definitions for BITS syntax.
|
|
Port of test_kibbles/2 from snmpc_lib.erl
|
|
"""
|
|
@spec test_kibbles([map()], verbosity()) :: :ok | {:error, binary()}
|
|
def test_kibbles(bit_definitions, verbosity) do
|
|
case validate_bit_names(bit_definitions) do
|
|
:ok -> validate_bit_values(bit_definitions, verbosity)
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Check if size constraint is allowed for given type (RFC 1902 compliance).
|
|
Port of allow_size_rfc1902/1 from snmpc_lib.erl
|
|
"""
|
|
@spec allow_size_rfc1902(atom()) :: boolean()
|
|
def allow_size_rfc1902(:octet_string), do: true
|
|
def allow_size_rfc1902(_), do: false
|
|
|
|
@doc """
|
|
Validate sub-identifier ranges.
|
|
Port of check_sub_ids/3 from snmpc_lib.erl
|
|
"""
|
|
@spec check_sub_ids(oid(), integer(), integer()) :: :ok | {:error, binary()}
|
|
def check_sub_ids(oid, min_value, max_value) do
|
|
case Enum.find(oid, &(&1 < min_value or &1 > max_value)) do
|
|
nil ->
|
|
:ok
|
|
|
|
invalid_id ->
|
|
{:error, "Sub-identifier #{invalid_id} out of range [#{min_value}..#{max_value}]"}
|
|
end
|
|
end
|
|
|
|
# Error Handling and Reporting
|
|
|
|
@doc """
|
|
Print error message with formatting.
|
|
Port of print_error/2 and print_error/3 from snmpc_lib.erl
|
|
"""
|
|
@spec print_error(binary(), verbosity()) :: :ok
|
|
def print_error(message, verbosity) when verbosity != :silent do
|
|
Logger.error("Error: #{message}")
|
|
:ok
|
|
end
|
|
|
|
def print_error(_message, :silent), do: :ok
|
|
|
|
@spec print_error(binary(), term(), verbosity()) :: :ok
|
|
def print_error(format, args, verbosity) when verbosity != :silent do
|
|
message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
|
|
print_error(message, verbosity)
|
|
end
|
|
|
|
def print_error(_format, _args, :silent), do: :ok
|
|
|
|
@doc """
|
|
Terminate compilation with error.
|
|
Port of error/2 and error/3 from snmpc_lib.erl
|
|
"""
|
|
@spec compilation_error(binary()) :: no_return()
|
|
def compilation_error(message) do
|
|
throw({:error, message})
|
|
end
|
|
|
|
@spec compilation_error(binary(), term()) :: no_return()
|
|
def compilation_error(format, args) do
|
|
message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
|
|
compilation_error(message)
|
|
end
|
|
|
|
# Debugging Utilities
|
|
|
|
@doc """
|
|
Configurable verbose printing.
|
|
Port of vprint/6 from snmpc_lib.erl
|
|
"""
|
|
@spec vprint(verbosity(), verbosity(), binary(), binary(), binary(), [term()]) :: :ok
|
|
def vprint(current_verbosity, required_verbosity, module, function, format, args) do
|
|
if printable?(current_verbosity, required_verbosity) do
|
|
message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
|
|
Logger.debug("[#{module}:#{function}] #{message}")
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Determine if message should be printed based on verbosity.
|
|
Port of printable/2 from snmpc_lib.erl
|
|
"""
|
|
@spec printable?(verbosity(), verbosity()) :: boolean()
|
|
def printable?(current_verbosity, required_verbosity) do
|
|
verbosity_level(current_verbosity) >= verbosity_level(required_verbosity)
|
|
end
|
|
|
|
@doc """
|
|
Validate verbosity level.
|
|
Port of vvalidate/1 from snmpc_lib.erl
|
|
"""
|
|
@spec vvalidate(term()) :: {:ok, verbosity()} | {:error, binary()}
|
|
def vvalidate(verbosity) when verbosity in [:silent, :warning, :info, :debug] do
|
|
{:ok, verbosity}
|
|
end
|
|
|
|
def vvalidate(invalid) do
|
|
{:error, "Invalid verbosity level: #{inspect(invalid)}"}
|
|
end
|
|
|
|
# Miscellaneous Helpers
|
|
|
|
@doc """
|
|
Safe key lookup in list of tuples/maps.
|
|
Port of key1search/2 and key1search/3 from snmpc_lib.erl
|
|
"""
|
|
@spec key1search(term(), [tuple()]) :: {:value, term()} | false
|
|
def key1search(key, list) do
|
|
case Enum.find(list, &(elem(&1, 0) == key)) do
|
|
nil -> false
|
|
tuple -> {:value, tuple}
|
|
end
|
|
end
|
|
|
|
@spec key1search(term(), [tuple()], term()) :: term()
|
|
def key1search(key, list, default) do
|
|
case key1search(key, list) do
|
|
{:value, tuple} -> tuple
|
|
false -> default
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Set directory path helper.
|
|
Port of set_dir/2 from snmpc_lib.erl
|
|
"""
|
|
@spec set_dir(binary(), binary()) :: binary()
|
|
def set_dir(filename, directory) do
|
|
case Path.dirname(filename) do
|
|
"." -> Path.join(directory, Path.basename(filename))
|
|
_ -> filename
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generic lookup function with multiple criteria.
|
|
Port of lookup/2 from snmpc_lib.erl
|
|
"""
|
|
@spec lookup(term(), [term()]) :: {:ok, term()} | {:error, :not_found}
|
|
def lookup(key, list) when is_list(list) do
|
|
case Enum.find(list, &match_lookup_criteria(key, &1)) do
|
|
nil -> {:error, :not_found}
|
|
value -> {:ok, value}
|
|
end
|
|
end
|
|
|
|
# Private helper functions
|
|
|
|
defp get_parent_oid([]), do: nil
|
|
defp get_parent_oid(oid) when length(oid) == 1, do: nil
|
|
|
|
defp get_parent_oid(oid) do
|
|
Enum.drop(oid, -1)
|
|
end
|
|
|
|
defp find_root_oids(oid_table) do
|
|
oid_table
|
|
|> Enum.filter(fn {_name, entry} -> entry.parent == nil end)
|
|
|> Enum.map(fn {name, _entry} -> name end)
|
|
end
|
|
|
|
defp resolve_oid_tree([], oid_table, _visited), do: {:ok, oid_table}
|
|
|
|
defp resolve_oid_tree([name | rest], oid_table, visited) do
|
|
if MapSet.member?(visited, name) do
|
|
resolve_oid_tree(rest, oid_table, visited)
|
|
else
|
|
case resolve_single_oid(name, oid_table) do
|
|
{:ok, updated_table} ->
|
|
new_visited = MapSet.put(visited, name)
|
|
resolve_oid_tree(rest, updated_table, new_visited)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp resolve_single_oid(name, oid_table) do
|
|
case Map.get(oid_table, name) do
|
|
%{status: :resolved} = _entry ->
|
|
{:ok, oid_table}
|
|
|
|
%{status: :unresolved, oid: symbolic_oid} = entry ->
|
|
case resolve_symbolic_oid(symbolic_oid, oid_table) do
|
|
{:ok, numeric_oid} ->
|
|
updated_entry = %{entry | oid: numeric_oid, status: :resolved}
|
|
updated_table = Map.put(oid_table, name, updated_entry)
|
|
{:ok, updated_table}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
|
|
nil ->
|
|
{:error, "OID not found: #{name}"}
|
|
end
|
|
end
|
|
|
|
defp resolve_symbolic_oid(oid, oid_table) when is_list(oid) do
|
|
# Convert symbolic OID elements to numeric
|
|
case resolve_oid_elements(oid, oid_table, []) do
|
|
{:ok, numeric_oid} -> {:ok, numeric_oid}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp resolve_oid_elements([], _oid_table, acc), do: {:ok, Enum.reverse(acc)}
|
|
|
|
defp resolve_oid_elements([element | rest], oid_table, acc) do
|
|
case resolve_oid_element(element, oid_table) do
|
|
{:ok, numeric_value} ->
|
|
resolve_oid_elements(rest, oid_table, [numeric_value | acc])
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp resolve_oid_element(%{name: _name, value: value}, _oid_table) when is_integer(value) do
|
|
{:ok, value}
|
|
end
|
|
|
|
defp resolve_oid_element(%{name: name}, oid_table) do
|
|
case tr_oid(name, oid_table) do
|
|
{:ok, [numeric_value]} -> {:ok, numeric_value}
|
|
{:ok, oid} when length(oid) > 1 -> {:error, "Invalid OID reference: #{name}"}
|
|
{:error, reason} -> {:error, "Cannot resolve OID reference: #{name} (#{reason})"}
|
|
end
|
|
end
|
|
|
|
defp resolve_oid_element(%{value: value}, _oid_table) when is_integer(value) do
|
|
{:ok, value}
|
|
end
|
|
|
|
defp update_single_entry_oid(entry, oid_table, verbosity) do
|
|
case Map.get(entry, :oid) do
|
|
nil ->
|
|
entry
|
|
|
|
symbolic_oid ->
|
|
case tr_oid(symbolic_oid, oid_table) do
|
|
{:ok, numeric_oid} ->
|
|
vprint(verbosity, :debug, "Utilities", "update_oid", "Resolved ~s -> ~w", [
|
|
symbolic_oid,
|
|
numeric_oid
|
|
])
|
|
|
|
%{entry | oid: numeric_oid}
|
|
|
|
{:error, reason} ->
|
|
print_error("Cannot resolve OID for #{entry.name}: #{reason}", verbosity)
|
|
entry
|
|
end
|
|
end
|
|
end
|
|
|
|
defp normalize_type({:integer, constraints}), do: {:ok, %{type: :integer, constraints: constraints}}
|
|
|
|
defp normalize_type({:octet_string, constraints}), do: {:ok, %{type: :octet_string, constraints: constraints}}
|
|
|
|
defp normalize_type({:object_identifier}), do: {:ok, %{type: :object_identifier, constraints: []}}
|
|
|
|
defp normalize_type({:named_type, name}), do: {:ok, %{type: :named_type, name: name, constraints: []}}
|
|
|
|
defp normalize_type(type) when is_atom(type), do: {:ok, %{type: type, constraints: []}}
|
|
defp normalize_type(invalid), do: {:error, "Invalid type definition: #{inspect(invalid)}"}
|
|
|
|
defp validate_type_constraints(%{type: :integer, constraints: constraints} = type_def) do
|
|
case validate_integer_constraints(constraints) do
|
|
:ok -> {:ok, type_def}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp validate_type_constraints(%{type: :octet_string, constraints: constraints} = type_def) do
|
|
case validate_size_constraints(constraints) do
|
|
:ok -> {:ok, type_def}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp validate_type_constraints(type_def), do: {:ok, type_def}
|
|
|
|
defp validate_integer_constraints([]), do: :ok
|
|
|
|
defp validate_integer_constraints([{:range, min, max} | rest])
|
|
when is_integer(min) and is_integer(max) and min <= max do
|
|
validate_integer_constraints(rest)
|
|
end
|
|
|
|
defp validate_integer_constraints([{:enum, _values} | rest]) do
|
|
# Note: Enum value validation not yet implemented
|
|
validate_integer_constraints(rest)
|
|
end
|
|
|
|
defp validate_integer_constraints([invalid | _]) do
|
|
{:error, "Invalid integer constraint: #{inspect(invalid)}"}
|
|
end
|
|
|
|
defp validate_size_constraints([]), do: :ok
|
|
|
|
defp validate_size_constraints([size | rest]) when is_integer(size) and size >= 0 do
|
|
validate_size_constraints(rest)
|
|
end
|
|
|
|
defp validate_size_constraints([{:range, min, max} | rest])
|
|
when is_integer(min) and is_integer(max) and min <= max and min >= 0 do
|
|
validate_size_constraints(rest)
|
|
end
|
|
|
|
defp validate_size_constraints([invalid | _]) do
|
|
{:error, "Invalid size constraint: #{inspect(invalid)}"}
|
|
end
|
|
|
|
defp validate_bit_names(bit_definitions) do
|
|
names = Enum.map(bit_definitions, & &1.name)
|
|
|
|
if length(names) == length(Enum.uniq(names)) do
|
|
:ok
|
|
else
|
|
{:error, "Duplicate bit names found"}
|
|
end
|
|
end
|
|
|
|
defp validate_bit_values(bit_definitions, verbosity) do
|
|
values = Enum.map(bit_definitions, & &1.value)
|
|
|
|
if length(values) == length(Enum.uniq(values)) do
|
|
vprint(verbosity, :debug, "Utilities", "validate_bits", "Bit validation successful", [])
|
|
:ok
|
|
else
|
|
{:error, "Duplicate bit values found"}
|
|
end
|
|
end
|
|
|
|
defp verbosity_level(:silent), do: 0
|
|
defp verbosity_level(:warning), do: 1
|
|
defp verbosity_level(:info), do: 2
|
|
defp verbosity_level(:debug), do: 3
|
|
|
|
defp match_lookup_criteria(key, {key, _value}), do: true
|
|
defp match_lookup_criteria(key, %{name: key}), do: true
|
|
defp match_lookup_criteria(key, %{id: key}), do: true
|
|
defp match_lookup_criteria(_key, _item), do: false
|
|
end
|