SNMP in Elixir
CommentsNot too long ago, I finally got around to trying out Elixir. It’s amazing. Seriously, you should try it out. It has this peculiar and compelling quality of making me feel like I’m a better programmer than I am. It’s that good.
Anyway, this post is not about how awesome Elixir is (very). Over the past few days, I’ve been spiking out a small app to demonstrate some Elixir concepts to my team. As it happens, the app I chose to build needed to do SNMP polling. I knew that Erlang had really robust support for SNMP (it is, after all, a language designed by a telecom company!), so I expected this to be simple.
Turns out that it wasn’t as simple as I expected, as the information that was out there was mostly geared towards experienced Erlangers (which I am not), and seems to assume you want to run your own SNMP agent (which I do not). As such, I thought I’d contribute what I’ve learned.
Erlang’s Opinionated About SNMP
We might expect that using the snmp
app would be as simple as adding the
following to our mix.exs
:
def application do
[applications: [:snmp],
mod: {MyNiftyApp, []}]
end
Unfortunately, while that step is certainly necessary, it’s not all we’re going to need to do to get started. Let’s look at why.
To issue the simplest SNMP get request possible in Erlang, we’ll be calling
the sync_get/3
function in the :snmpm
module. Here’s its signature, from
its doc page:
sync_get(UserId, TargetName, Oids) -> {ok, SnmpReply, Remaining} | {error, Reason}
So, Oids
refers to a concept that is probably already familiar to you if
you’re working with SNMP. But what’s this UserId
and TargetName
stuff?
TargetName
corresponds to an SNMP agent (connection details for the agent on
a device you’d like to poll) and UserId
is probably nothing like you’d expect
it to be. It’s actually an SNMP manager “user”, which is a bundle of data that
includes (among other things) a callback module – that is, a module that
implements the snmpm_user
behaviour. We can write our own callback module, or use the one provided with
Erlang, snmpm_user_default
, which logs handler calls to info. We can also get
away with supplying nil
for UserId
from Elixir, though I’m not entirely
sure what will happen if a callback needs to be invoked, in that case.
Because we’re dealing with a functional language, we don’t have the OO convenience of instantiating an SNMP session object to provide connection details once. Given the alternative of providing an Erlang API that required us to provide every connection detail on every request, it was wisely decided that a registry should be maintained, so we could refer to these values by name, instead.
Consequently, we have functions like register_user/3
and register_agent/3
in
the :snmpm
module, to handle assigning these names. If we know the details in
advance, though, we can also take advantage of Erlang’s conventions for loading
users and agents when the :snmp
app is started.
Creating Configuration Files
To do so, we’ll add the following to config.exs
:
config :snmp, :manager,
config: [dir: './config/snmp', db_dir: './config/snmp_db']
:snmp
will look for configuration files in the directories we specify via
dir
(db_dir
is used by :snmp
to store state). These files are written in
Erlang, and the directories used for agents and managers are normally
different. In our case, we’re only configuring a manager, so we will just set
the directory to an snmp
subdirectory under config
. We’ll need 3 files:
manager.conf
, users.conf
, and agents.conf
.
Let’s look at sample versions of each:
manager.conf:
{address, [127,0,0,1]}.
{port, 5000}.
{engine_id, "default"}.
{max_message_size, 484}.
Here, we’re specifying:
- our manager is accessible on localhost (127.0.0.1, expressed as a list of octets)
- we’re using port 5000 for communicating with agents
- our name is “default” (note that this is an Erlang character list, and not an Elixir string, or binary)
- we’ll accept messages in blocks of 484 bytes, which is the smallest value allowed while still while still complying with the standard
users.conf:
{"default_user", snmpm_user_default, undefined, []}.
Here, we’re defining:
- a user named “default_user” (we’re very creative)
- the
snmpm_user_default
module as our callback module (which will log as info any callbacks received) undefined
as our “user data” (which is passed along to our module callbacks)- an empy list for default agent configuration options
agents.conf:
{"default_user", "default_agent", "public", [127,0,0,1], 161, "default",
infinity, 484, v2, v2c, "initial", noAuthNoPriv}.
This is where the really interesting stuff is. We’re defining:
- an agent associated with the default user
- the agent name is “default_agent”
- its community string is “public”
- its IP address is 127.0.0.1 (you’d replace this with your device’s IP)
- its port is 161 (the default SNMP port)
- the engine ID of “engine” (these are used with USM, beyond this post’s scope)
- no retransmission timeout
- a maximum message size of 484 bytes for messages to this agent
- the agent uses SNMP v2
- its security model is v2c (community string)
- a security name of “initial” (again, beyond the scope of this post)
- a security level of
noAuthNoPriv
, which is what we want for simple community string based security
Whew! That’s a whole lot of configuration. For more details, you can read the Erlang SNMP documentation, but it’s a pretty dense read if you just want to do a bit of SNMP polling, which – get ready for it – we’re finally able to do!
If you fire up an iex -S mix
session in your app, and you’ve configured
everything correctly, you should now be able to run the following…
:snmpm.sync_get('default_user', 'default_agent', [[1,3,6,1,2,1,1,1,0]])
…which will return…
{:ok,
{:noError, 0,
[{:varbind, [1, 3, 6, 1, 2, 1, 1, 1, 0], :"OCTET STRING",
'Linux localhost [...]',
1}]}, 4982}
Note that we’re using character lists and not binaries in our function call, and the numeric OID is expressed as a list instead of a string, as you may be used to. We could also have specified multiple OIDs in our request – notice that it’s a list of lists.
The result is a tuple consisting of {:ok, reply, remaining}
, as we saw
earlier, where reply
is a tuple consisting of:
- an error status, in this case
:noError
- an error index, in this case 0, since no error occurred. If an error had occurred, this would be the index of the OID responsible for the error
- a list of varbind records of the form
{:varbind, oid, type, value, org_id}
. You may wish to extract this record from its corresponding hrl file, but I found it more convenient to define my own struct and map the varbinds to it in a wrapper module.
remaining
is the number of milliseconds remaining in our request’s timeout.
Since the default request timeout is 5 seconds, this means our request took
18ms.
Conclusion
I hope this helps you get started with SNMP in Elixir. We haven’t yet talked about compiling MIBs in order to use short names for objects, which might be the topic of a separate post, if there’s interest.
comments powered by Disqus