So, you want to contribute a device mapper to the Sonar poller. Great! Here’s a walk through on how to get started.
First, you’ll need a copy of the source code. Head over to our repository and fork the poller.
Now let’s step through how to build a new mapper, and test it easily. All the code you’ll want to look at is in the src directory.
We’re going to mainly focus on DeviceIdentifiers
and DeviceMappers
, but for reference, here’s whats in each folder if you want to explore or work on something more complex.
SysInfo
class in here that figures out the number of processors on a poller to determine how many workers there should be in a pool. Realistically, this could probably live in Services.Device
, which represents a specific device to be monitored, or MonitoringTemplate
which represents a monitoring template defined in Sonar.DeviceFactory
takes the JSON data from Sonar and formats it into Device
models, as well as figuring out which devices need to be polled via ICMP or SNMP.Formatter
in here that formats data, and an SnmpClient
that performs SNMP queries.Let’s circle back to building a new device mapper. I mentioned at the start that we’d focus on mappers and identifiers. To get identifiers out of the way, the only time you need a device identifier is if a vendor has multiple types of devices that all respond with the same response to a system.sysObjectID query (OID 1.3.6.1.2.1.1.2.0.) If they do, check out the existing device identifiers to see how they work.
Essentially, you’ll need to build a device identifier to use some other method of figuring out what the actual device is and then return the appropriate mapper based on your investigation. For example, the Ubiquiti device identifier looks at the interface names on a device to try to determine what it is.
Generally speaking, if you can not use a device identifier, you should not. It adds more queries to the mix, and delays monitoring more, but if there’s no other choice, build one.
Getting back to device mappers, building a new one is fairly simple. Building a new device mapper means extending the BaseDeviceMapper
class, so let’s start there. Device mappers always live inside a directory named after the manufacturer, so let’s imagine we’re making a mapper for an imaginary manufacturer like MySuperCoolRouters. First, I’m going to make a folder named MySuperCoolRouters in the DeviceMappers directory.
Next, I’m going to make a class for each type of device I want to support. Let’s imagine MySuperCoolRouters makes a super cool router called TheCoolestRouterEver. To support this device with a custom mapper, I’m going to make a new class called TheCoolestRouterEver, and extend the BaseDeviceMapper
class.
If you take a look at the BaseDeviceMapper
class, you’ll see that data is always passed into this class during an SNMP polling cycle, and that the base mapper will perform various queries to determine things like the interfaces on the device. The important things to notice are that you always have the Device model available to you inside your class (as $this->model
) and an array of NetworkInterface
objects available as $this->interfaces
. Check out the NetworkInterface
model to see all the data available in it — this is where we’ll likely do most of our data injection.
Your mapper will be instantiated if a device returns a response to the system.sysObjectID query that is run inside SnmpGet
that matches a mapper definition in devices.json
.
The devices.json
file lives in the config directory, and looks something like this:
To add your poller to the mix, add a new object to this file, with the appropriate response and the namespace of your new device mapper. Note the double forward slashes in here — they are needed for escaping.
Note that the response here doesn’t need to be exact. If you’re building a mapper that is valid for 1.2.3.4, 1.2.3.5, and 1.2.3.6, you can enter 1.2.3 as the response here, and as long as there isn’t a more specific response listed, the poller will use your mapper for anything starting 1.2.3.
Now it’s time to add logic to your mapper!
When your mapper is instantiated, it will be passed a Device
object into the constructor, which is handled by the BaseDeviceMapper
. Next, the map
function will be called, passing in an SnmpResult
object. We want to pass this offer to the underlying base mapper to run before we do any work on it, as the base mapper will do all the heavy lifting of fetching interfaces and other data for us. To do this, let’s call the parent map function inside our new mapper.
This gets us an SnmpResult
object that will have the interfaces
array populated with valid NetworkInterface
objects. From this point, you can look at other device mappers in the repository to see typical things that are done to add more data here, but I’ll continue to work through this fictional example. Let’s say, for example, that TheCoolestRouterEver
is some kind of aggregation device that customers are connected to, and via the SNMP OID 1.2.3.4.5.6, it returns a list of the MAC addresses that are connected to interface eth0, which is where customers are always connected. What we want to do then, is query that OID, fetch the MAC addresses, and attach them to the eth0 network interface, so that Sonar can use that data for the parent/child system, and Sonar Pulse.
Let’s get started by adding a new function to our class called getAttachedCustomers
.
Now, inside this function, we’re going to run an SNMP query to get this list. We can do this by accessing the SnmpClient
class that’s available through the Device
object, which is exposed through the base mapper.
Assuming no exception is thrown here, the $result
variable will be an SnmpResponse
object. We can call getAll()
on this object to get an associative array, where the key is the OID, and the value is the response.
As mentioned earlier in this article, in this hypothetical situation, we know we want to attach the results of this query to the eth0 interface. We need to iterate the interfaces in the $this->interfaces
array to find the eth0 interface, and then attach the MAC addresses given by this SNMP query to it.
To do this, you can just iterate the $this->interfaces
array, calling getName()
on each object until you find eth0. Remember, the contents of this array are NetworkInterface
objects, so you can check that class for other methods.
Next, we are going to call getConnectedLayer1Macs()
on the network interface. When you want to attach customers to a network interface, we set them as Layer1 connected, regardless of the layer they are connected to. Layer1 always takes precedence when Sonar calculates relationships for Pulse and the parent/child system, so if we know these customers are connected here, it’s best and safest to attach them this way.
Next, we simply add the results of our SNMP query to the $existingMacs
array, and set it back on the interface. Finally, we update the NetworkInterface
object with the new MACs, and finally update the $this->interfaces
array.
Your custom mapper must return the SnmpResult
object for this data to make it back to Sonar. So, back to our map
function — we need to call this new private method, update the SnmpResult
object, and then return it.
As you can see, I added the call to getAttachedCustomers
inside the main map
function, and I also added another line to the getAttachedCustomers
function to push my updated interface array into the SnmpResult
object.
You are, of course, free to do anything here — it’s just imperative that you allow the base mapper to run its map function, and that you return an updated SnmpResult
object. Take a look at the Ws6Mini
class for an example of collecting data here using SSH, or the MikroTik
class for an example of collecting data using an API.
Now it’s time to test this! You could, of course, upload all this data to your poller and see if it works, but there’s a quicker test. The poller has a PHP shell built in, powered by PsySH. You can access it by typing vendor/bin/psysh from the root poller directory.
From in here, you can instantiate your new mapper and test it in real time. Let’s step through it! First, we need to instantiate a Device
object. We’re going to fake out some of the data in it that we don’t care about to test this. One of the items required is a MonitoringTemplate
object. Below, I’ve added a fake array of data you can paste in to help create a default one. Make sure to replace the snmp_community
and snmp_version
values with appropriate information if you are using SNMP inside your customer mapper — this is where that information will come from.
Now we can create a monitoring template.
Now we can instantiate our Device object. Make sure you enter a valid IP address for the device so we can test the mapper on it.
When instantiating the Device
class, the first input is an inventory item ID (which doesn’t matter for testing), the second is configuration data (of which, you only need to worry about updating ip
), and the third is the monitoring template.
Now we can test out the device mapper! All the device mapper requires is the Device
object in its constructor, so let’s do that. For my test, I’m using the CanopyPMPAccessPoint
mapper, but you would instantiate your new class here.
Almost there! Now we just need an SnmpResult
object to pass into the map
function. To instantiate that, we need an SnmpResponse
object, but we can just create an empty one.
Finally, we can call map
on our mapper, get the updated SnmpResult
back and see what it looks like.
Check out the SnmpResult
class for examples of what you can see here. A quick and dirty way to see everything is to just call ->toArray()
on it.
While there’s some effort to run through here to do the initial setup, it’s easy and copy and paste in, and it gives you an immediate view at your mapper response, and to see if there are any errors or exceptions thrown.
Once you have a working mapper you’re happy with, push your changes back up into your fork, then navigate back to the poller repository on GitHub. Click the Pull Requests tab at the top, and open a new pull request with your changes. We’ll review them and either offer feedback or accept your new mapper to be made available in the public repository to everyone. Thanks!