Skip to content

Repository files navigation

SnmpKit

Hex.pm Documentation License Elixir CI

A pure Elixir SNMP toolkit: manager operations for SNMPv1, v2c and v3, a trap and inform receiver, a native MIB compiler, and simulated devices for tests. It does not depend on Erlang's :snmp application.

Installation

def deps do
  [
    {:snmpkit, "~> 2.0"}
  ]
end

Breaking changes in 2.0

2.0 is a consolidation release. Most facade request and response shapes are the same as in 1.4, but the following changes can require application updates:

  • Renamed modules: SnmpMgr.EngineV2 is now SnmpMgr.Engine, SnmpMgr.MultiV2 is now SnmpMgr.Multi, SnmpLib.MIB.* is now SnmpKit.MIB.*, SnmpSim.SafeFile is now SnmpKit.SafeFile (a delegate is retained), and SnmpSim.Device.ErrorInjector is now SnmpSim.Device.ErrorConditions.
  • Removed manager and library APIs: the old opt-in request-batching engine and its Router, CircuitBreaker, Metrics, SnmpMgr.Application and SnmpMgr.Supervisor; SnmpMgr.SocketManager; the Task-per-target Multi, its strategy: option and Multi.monitor/3; SnmpLib.Config, Pool, Cache, Monitor, Dashboard and the SnmpLib.MIB facade; and SnmpKit.TestSupport. The related SnmpMgr/SnmpKit.SNMP engine delegates, ErrorHandler circuit-breaker functions, Core spawn-based async/value-only GET helpers, Bulk.get_bulk_multi/2, and unused public Security, Auth, Priv, USM and Keys helpers were also removed.
  • Removed simulator APIs: SnmpSim.Application, MultiDeviceStartup, TestScenarios, TestHelpers.* and Performance.*. A simulated device no longer invents hard-coded objects when it has no profile or objects: map. Several public Device.OidHandler calculation/fallback helpers were removed; its uptime helpers moved to Device.Metrics. The counter and jitter helpers formerly on ValueSimulator moved to ValueSimulator.Counters and .Variance.
  • Return values: get_async/3 and get_bulk_async/3 return a Task; set/4 returns :ok instead of {:ok, :success}; Multi.execute_mixed/2 returns enriched varbind maps instead of {oid, type, value} tuples; Sim.start_device_population/2 pre-warms devices and returns [%{type, port, pid, target}]; and benchmark_device/3 changes the meaning of avg_response_time and adds optimal_response_time.
  • Validation, errors and defaults: invalid, empty or mistyped OIDs now return {:error, {:invalid_oid, input, reason}} instead of querying a MIB root; retries default to one everywhere (some lower layers used three and some shared-socket paths used zero); malformed unsigned SNMP values are rejected rather than decoded as zero; SNMPv1 end-of-MIB is {:error, :no_such_name}; and privacy without authentication is {:error, :priv_requires_auth}. Multi-OID SnmpMgr.Bulk.get_bulk/3 requests are rejected instead of silently sending only the first OID.
  • Formatting and MIB parsing: formatted values now follow loaded or built-in MIB metadata instead of guessing meanings from bare integer and counter values. Raw parser/tokenizer output uses binary identifiers, changes DEFVAL and literal handling, includes a warnings list, and changes illegal-character errors to include the line number.
  • Configuration and dependencies: manager defaults are read from config :snmpkit (the :snmp_mgr key still works); input_roots: now also confines MIB compilation, linting and loading; and :telemetry is a required dependency rather than an optional one.

The 2.0 migration guide has the full rename and removal tables with replacements for each entry.

Quick start

Everything below runs against a simulated device, so it works offline.

# A simulated router on localhost:1161, built from a walk file that ships
# with the library (also :cable_modem and :switch). v3_users: is only
# needed for the SNMPv3 call below.
{:ok, profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(:router)
{:ok, _device} =
  SnmpKit.Sim.start_device(profile,
    port: 1161,
    v3_users: [%{name: "admin", auth: :sha256, auth_password: "auth-secret",
                 priv: :aes128, priv_password: "priv-secret"}]
  )
target = "127.0.0.1:1161"

# GET returns one enriched varbind map
{:ok, %{value: descr, type: :octet_string, oid: "1.3.6.1.2.1.1.1.0"}} =
  SnmpKit.SNMP.get(target, "sysDescr.0")

# WALK returns a list of them, in OID order
{:ok, system} = SnmpKit.SNMP.walk(target, "system")
require Logger
Enum.each(system, fn %{name: name, formatted: value} -> Logger.info("#{name} = #{value}") end)

# SNMPv3: discovery, key localization and time sync are automatic
{:ok, _} = SnmpKit.SNMP.get(target, "sysDescr.0",
  version: :v3, security_name: "admin",
  auth_protocol: :sha256, auth_password: "auth-secret",
  priv_protocol: :aes128, priv_password: "priv-secret")

# Multi-target calls return one result per request, in request order
[{:ok, [%{value: ^descr}]}, {:ok, [%{name: "sysName.0"}]}] =
  SnmpKit.SNMP.get_multi([{target, "sysDescr.0"}, {target, "sysName.0"}])

# MIB lookups work without any loading; the common IETF MIBs are built in
{:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} = SnmpKit.MIB.resolve("sysDescr.0")
{:ok, "sysDescr.0"} = SnmpKit.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1, 0])

# Your own MIBs
{:ok, compiled} = SnmpKit.MIB.compile("priv/mibs/MY-ENTERPRISE-MIB.mib")
:ok = SnmpKit.MIB.load(compiled)

The API in one screen

Module What it is for
SnmpKit.SNMP Manager operations: get, get_next, set, walk, get_bulk, bulk walks, tables, streams, async, multi-target, pretty formatting
SnmpKit.MIB Name/OID resolution, tree navigation, MIB compilation and loading
SnmpKit.Trap Receive SNMPv1/v2c traps and informs; SnmpKit.SNMP.send_trap/4 and send_inform/4 send them
SnmpKit.Telemetry The :telemetry spans and events every request, walk, multi-target call, trap and simulated device emits
SnmpKit.Agent Serve your own data over SNMP: scalars, tables and custom handlers, v1/v2c/v3, traps out
SnmpKit.Sim Start one simulated device, or a population of them
SnmpKit.SnmpSim Configuration-driven simulation of whole device groups
SnmpKit Shortcuts for the most common calls (get, walk, resolve, ...)

Lower layers are public too when you need them: SnmpKit.SnmpLib (PDU encoding, ASN.1, transport, SNMPv3 security), SnmpKit.SnmpMgr (engine, multi-target coordinator, walk strategies) and SnmpKit.MIB.Parser / SnmpKit.MIB.Compiler (the native MIB toolchain).

Your own SNMP agent

SnmpKit.Agent exposes an application's data to any NMS over SNMPv1, v2c and v3. Scalars go in with put/4 (a function value is read live), tables come from a row-producing function, and anything else is a small module implementing SnmpKit.Agent.Handler:

{:ok, agent} =
  SnmpKit.Agent.start_link(
    port: 1161,
    communities: %{"public" => :read, "private" => :write},
    v3_users: [%{name: "ops", auth: :sha256, auth_password: "auth-secret", access: :write}],
    system: [descr: "orders-api 3.2", name: "orders-01", location: "rack 4"]
  )

:ok = SnmpKit.Agent.put(agent, "hrSystemProcesses.0", :gauge32, fn -> length(Process.list()) end)

:ok =
  SnmpKit.Agent.register(agent, "ifEntry", SnmpKit.Agent.Table,
    columns: [{1, :integer}, {2, :octet_string}, {8, :integer}],
    rows: fn -> [{1, %{1 => 1, 2 => "lo", 8 => 1}}, {2, %{1 => 2, 2 => "eth0", 8 => 1}}] end
  )

# Any manager, including this one, can read it now
{:ok, %{1 => %{2 => "lo"}, 2 => %{2 => "eth0"}}} =
  SnmpKit.SNMP.get_table("127.0.0.1:1161", "ifTable")

# and traps go out with the agent's sysUpTime
:ok = SnmpKit.Agent.notify(agent, "linkDown", [{"ifIndex.2", :integer, 2}], targets: ["nms.example.com"])

Put {SnmpKit.Agent, port: 161, name: MyApp.Agent, subtrees: [...]} in a supervision tree for production. The API guide covers access control, SET handling and writing handlers.

Results

Every operation returns enriched varbind maps:

%{
  name: "sysUpTime.0",            # nil when no MIB name is known
  oid: "1.3.6.1.2.1.1.3.0",
  oid_list: [1, 3, 6, 1, 2, 1, 1, 3, 0],
  type: :timeticks,
  value: 12345678,
  formatted: "1 day 10 hours 17 minutes 36 seconds 78 centiseconds"
}

formatted follows the MIB: ifOperStatus reads "up", ifType reads "ethernetCsmacd", ifPhysAddress reads "00:1a:2b:3c:4d:5e", and a loaded vendor MIB's enumerations and DISPLAY-HINTs apply the same way. Name resolution and formatting can be switched off per call (include_names: false, include_formatted: false) or globally through configuration, which matters on hot paths that walk large tables.

Errors are tagged tuples: {:error, :timeout}, {:error, :no_such_object} (SNMPv2c), {:error, :no_such_name} (SNMPv1), {:error, :not_writable}, and so on.

Multi-target operations

get_multi, get_bulk_multi, walk_multi and walk_table_multi run every request concurrently over one shared UDP socket with centralized response correlation. Nothing needs to be started by hand; the engine comes up on the first call.

requests = [
  {"switch-1", "ifTable"},
  {"switch-2", "ifTable", timeout: 30_000},   # per-request options
  {"router-1", "ipRouteTable"}
]

results = SnmpKit.SNMP.walk_multi(requests, max_concurrent: 20, walk_timeout: 120_000)
# [{:ok, [...]}, {:ok, [...]}, {:error, :timeout}]   (request order)

SnmpKit.SNMP.get_multi(requests, return_format: :map)
# %{{"switch-1", "ifTable"} => {:ok, [...]}, ...}

See Concurrent Multi and the timeout guide.

Configuration

Defaults are read from the application environment at startup and can be changed at runtime through SnmpKit.SnmpMgr.Config:

# config/config.exs
config :snmpkit,
  community: "public",
  timeout: 5_000,          # per-PDU timeout, ms; walks are capped by walk_timeout:
  retries: 1,
  port: 161,
  version: :v2c,
  include_names: true,
  include_formatted: true,
  auto_start_services: true

# Limits applied when reading walk files and MIBs
config :snmpkit,
  max_input_file_bytes: 50_000_000,
  max_compiled_mib_bytes: 50_000_000,
  input_roots: ["priv"]   # optional jail for user-supplied file paths

Documentation

Command line

Five mix tasks give you a shell without writing a script:

mix snmpkit.get 192.168.1.1 sysDescr.0 sysUpTime.0 -c public
mix snmpkit.walk 192.168.1.1 ifTable --table          # named columns
mix snmpkit.mib.compile priv/mibs                     # prints parser warnings
mix snmpkit.mib.lint VENDOR-MIB.mib --context priv/mibs # semantic checks, smilint-style
mix snmpkit.sim --device router --port 1161           # a simulated device until Ctrl-C
mix snmpkit.sim devices.yaml                          # a whole population from a config

Development

mix test                       # unit + integration suite, SNMPv3 included
mix test --include performance # timing-sensitive tests
mix test --include mib_oracle  # cross-check the MIB parser (needs smilint / snmptranslate)
mix lint                       # format check, dialyzer

Contributions are welcome; see CONTRIBUTING.md.

License

SnmpKit is released under the MIT License.

About

SNMP Kit for Elixir with SNMP Manager and Simulator

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages