# SnmpKit

[![Hex.pm](https://img.shields.io/hexpm/v/snmpkit.svg)](https://hex.pm/packages/snmpkit)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/snmpkit)
[![License](https://img.shields.io/github/license/awksedgreep/snmpkit.svg)](LICENSE)
[![Elixir CI](https://github.com/awksedgreep/snmpkit/actions/workflows/elixir.yml/badge.svg)](https://github.com/awksedgreep/snmpkit/actions/workflows/elixir.yml)

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

```elixir
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](docs/v2-migration.md) 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.

```elixir
# 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`:

```elixir
{: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](docs/unified-api-guide.md#snmp-agent)
covers access control, SET handling and writing handlers.

## Results

Every operation returns enriched varbind maps:

```elixir
%{
  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.

```elixir
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](docs/concurrent-multi.md) and the
[timeout guide](docs/timeouts.md).

## Configuration

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

```elixir
# 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

- [2.0 migration guide](docs/v2-migration.md)
- [Unified API guide](docs/unified-api-guide.md)
- [Concurrent multi-target operations](docs/concurrent-multi.md)
- [Timeouts and retries](docs/timeouts.md)
- [MIB guide](docs/mib-guide.md) and [checking the parser against libsmi and net-snmp](docs/mib-parser-oracle.md)
- [Testing guide](docs/testing-guide.md)
- Livebooks: [quickstart](livebooks/01_quickstart.livemd), [SNMP operations](livebooks/02_snmp_operations.livemd), [MIB management](livebooks/03_mib_management.livemd), [device simulation](livebooks/04_device_simulation.livemd), [high performance](livebooks/05_high_performance.livemd), [your own SNMP agent](livebooks/06_snmp_agent.livemd), [SNMPv3](livebooks/07_snmpv3.livemd), [traps and informs](livebooks/08_traps_and_informs.livemd), [telemetry and rates](livebooks/09_telemetry_and_rates.livemd)
- [Examples](examples/README.md)
- [Full API reference](https://hexdocs.pm/snmpkit)

## Command line

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

```sh
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

```sh
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](CONTRIBUTING.md).

## License

SnmpKit is released under the [MIT License](LICENSE).
