SNMP is not the user-friendly protocol: MIB files are too long and complicated, and OIDs are simply impossible to remember. What if it became necessary to work with SNMP on Java? For example, write autotests to test the SNMP server API.
By trial and error in the presence of a fairly meager amount of information on the topic, we still figured out how to make friends with Java and SNMP.
In this series of articles I will try to share my experience with the protocol. The first article in the series will be devoted to the implementation of the parser for MIB files in Java. In the second part I will talk about writing an SNMP client. In the third part, we will discuss a real example of using the written library: autotests for checking the interaction with a device using the SNMP protocol.
')

Introduction
It all started with the fact that the task came to write autotests to test the work of the audio-video recorder via the SNMP protocol. The situation was complicated by the fact that there is not so much information on interaction with SNMP in Java, especially if we speak about the Russian-speaking segment of the Internet. Of course, it was possible to look towards C # or Python. But in C #, the protocol situation is about as complex as in Java. In Python, there are a couple of good libraries, but we already had a ready infrastructure for REST API autotests for this device in Java.
Like for any other protocol of interaction over the network, we needed an SNMP client to work with various kinds of requests. Self-tests should be able to check the success of GET and SET queries for scalar and tabular parameters. For tables, it was also required to be able to check the addition and deletion of records if the table itself permits these operations.
But besides the client, the library had to contain a class for working with MIB files. This class should have been able to parse a MIB file to get data types, permissible value boundaries, etc., so as not to hardcore something that can always change.
During the search for suitable libraries for Java, we did not find a single library that would allow working with queries and with MIB files. Therefore, we stopped at two different libraries. For the client, the choice of the widely used org.snmp4j.snmp4j (https://www.snmp4j.org) seemed quite logical, and for the parser of MIB files, the choice was not on the not very famous net.percederberg.mibble library (https: // www. mibble.org). If the choice was obvious with snmp4j, then mibble was chosen for having sufficiently detailed (albeit English) documentation with examples. So, let's begin.
Writing MIB files parser
Anyone who has ever seen MIB files knows that this is a pain. Let's try to fix it with the help of a simple parser, which will greatly facilitate the search for information from a file, reducing it to a call to a particular method.
Such a parser can be used as a separate utility for working with MIB files or included in any other project under SNMP, for example, when writing an SNMP client or automating testing.
Project preparation
For ease of assembly, we use Maven. Depending on, we add the net.percederberg.mibble library (https://www.mibble.org), which will make it easier for us to work with MIB files:
<dependency> <groupId>net.percederberg.mibble</groupId> <artifactId>mibble</artifactId> <version>2.9.3</version> </dependency>
Since it is not located in the central Maven repository, we add the following code to pom.xml:
<repositories> <repository> <id>opennms</id> <name>OpenNMS</name> <url>http://repo.opennms.org/maven2/</url> </repository> </repositories>
If the project is going with the help of mavena without errors, everything is ready for work. It remains only to create a parser class (let's call it MIBParser) and import everything we need, namely:
import net.percederberg.mibble.*;
Download and validate MIB file
Inside the class there will be only one field - an object of type net.percederberg.mibble.Mib for storing the loaded MIB file:
private Mib mib;
To download the file, write this method:
private Mib loadMib(File file) throws MibLoaderException, IOException { MibLoader loader = new MibLoader(); Mib mib; file = file.getAbsoluteFile(); try { loader.addDir(file.getParentFile()); mib = loader.load(file); } catch (MibLoaderException e) { e.getLog().printTo(System.err); throw e; } catch (IOException e) { e.printStackTrace(); throw e; } return mib; }
The net.percederberg.mibble.MIBLoader class validates the file we are trying to load and throws the net.percederberg.mibble.MibLoaderException exception if it finds any errors, including import errors from other MIB files, if do not lie in the same directory or do not contain imported MIB-symbols.
In the loadMib method, we catch all the exceptions, write about them to the log and push further, because At this stage, the continuation of work is impossible - the file is not valid.
We call the written method in the parser constructor:
public MIBParser(File file) throws MibLoaderException, IOException { if (!file.exists()) throw new FileNotFoundException("File not found in location: " + file.getAbsolutePath()); mib = loadMib(file.getAbsoluteFile()); if (!mib.isLoaded()) throw new MibLoaderException(file, "Not loaded."); }
If the file is successfully loaded and parsed, continue to work.
Methods for obtaining information from a MIB file
Using the methods of the net.percederberg.mibble.Mib class, you can search for individual characters of a MIB file by name or OID by calling the getSymbol (String name) or getSymbolByOid (String oid) methods, respectively. These methods return the object net.percederberg.mibble.MibSymbol to us, the methods of which we will use to obtain the necessary information on a specific MIB symbol.
Let's start with the simplest and write methods to get the name of a symbol by its OID and, conversely, OID by name:
public String getName(String oid) { return mib.getSymbolByOid(oid).getName(); } public String getOid(String name) { String oid = null; MibSymbol s = mib.getSymbol(name); if (s instanceof MibValueSymbol) { oid = ((MibValueSymbol) s).getValue().toString(); if (((MibValueSymbol) s).isScalar()) oid = new OID(oid).append(0).toDottedString(); } return oid; }
Perhaps these are the features of a particular MIB file that I needed to work with, but for some reason, for scalar parameters, an OID was returned without a zero at the end, so code was added to the method for obtaining an OID, in case MIB- the scalar character simply adds to the resulting OID ".0" using the append (int index) method from the net.percederberg.mibble.OID class. If you work without a crutch, congratulations :)
To get the rest of the data on a symbol, we write one auxiliary method, where we get the net.percederberg.mibble.snmp.SnmpObjectType object, which contains all the necessary information about the MIB symbol from which it was obtained.
private SnmpObjectType getSnmpObjectType(MibSymbol symbol) { if (symbol instanceof MibValueSymbol) { MibType type = ((MibValueSymbol) symbol).getType(); if (type instanceof SnmpObjectType) { return (SnmpObjectType) type; } } return null; }
For example, we can get the type of MIB symbol:
public String getType(String name) { MibSymbol s = mib.getSymbol(name); if (getSnmpObjectType(s).getSyntax().getReferenceSymbol() == null) return getSnmpObjectType(s).getSyntax().getName(); else return getSnmpObjectType(s).getSyntax().getReferenceSymbol().getName(); }
There are 2 ways to get the type, because for primitive types, the first option works:
getSnmpObjectType(s).getSyntax().getName();
and for imported ones, the second:
getSnmpObjectType(s).getSyntax().getReferenceSymbol().getName();
You can get the level of access to the symbol:
public String getAccess(String name) { MibSymbol s = mib.getSymbol(name); return getSnmpObjectType(s).getAccess().toString(); }
The minimum acceptable value of a numeric parameter:
public Integer getDigitMinValue(String name) { MibSymbol s = mib.getSymbol(name); String syntax = getSnmpObjectType(s).getSyntax().toString(); if (syntax.contains("STRING")) return null; Pattern p = Pattern.compile("(-?\\d+)..(-?\\d+)"); Matcher m = p.matcher(syntax); if (m.find()) { return Integer.parseInt(m.group(1)); } return null; }
Maximum allowable value of a numeric parameter:
public Integer getDigitMaxValue(String name) { MibSymbol s = mib.getSymbol(name); String syntax = getSnmpObjectType(s).getSyntax().toString(); if (syntax.contains("STRING")) return null; Pattern p = Pattern.compile("(-?\\d+)..(-?\\d+)"); Matcher m = p.matcher(syntax); if (m.find()) { return Integer.parseInt(m.group(2)); } return null; }
The minimum allowed string length (depends on the type of string):
public Integer getStringMinLength(String name) { MibSymbol s = this.mib.getSymbol(name); String syntax = this.getSnmpObjectType(s).getSyntax().toString(); Pattern p = Pattern.compile("(-?\\d+)..(-?\\d+)"); Matcher m = p.matcher(syntax); return syntax.contains("STRING") && m.find()?Integer.valueOf(Integer.parseInt(m.group(1))):null; }
The maximum allowable length of the string (depends on the type of string):
public Integer getStringMaxLength(String name) { MibSymbol s = mib.getSymbol(name); String syntax = getSnmpObjectType(s).getSyntax().toString(); Pattern p = Pattern.compile("(-?\\d+)..(-?\\d+)"); Matcher m = p.matcher(syntax); if (syntax.contains("STRING") && m.find()) { return Integer.parseInt(m.group(2)); } return null; }
You can also get the names of all columns in the table by its name:
public ArrayList<String> getTableColumnNames(String tableName) { ArrayList<String> mibSymbolNamesList = new ArrayList<>(); MibValueSymbol table = (MibValueSymbol) mib.findSymbol(tableName, true); if (table.isTable() && table.getChild(0).isTableRow()) { MibValueSymbol[] symbols = table.getChild(0).getChildren(); for (MibValueSymbol mvs : symbols) { mibSymbolNamesList.add(mvs.getName()); } } return mibSymbolNamesList; }
First, in the standard way, we obtain the MIB symbol by name:
MibValueSymbol table = (MibValueSymbol) mib.findSymbol(tableName, true);
Then we check whether it is a table and that the child MIB symbol is a row of this table and, if the condition returns true, in the loop, go over the children of the table row and add the element name to the resulting array:
if (table.isTable() && table.getChild(0).isTableRow()) { MibValueSymbol[] symbols = table.getChild(0).getChildren(); for (MibValueSymbol mvs : symbols) { mibSymbolNamesList.add(mvs.getName()); } }
Results
These methods are sufficient for obtaining any information from the MIB file for each specific symbol, knowing only its name. For example, when writing an SNMP client, you can include such a parser in it, so that the client methods accept, instead of OIDs, the names of MIB symbols. This will increase the reliability of the code, since A typo in the OID can lead to the wrong character to which we want to refer. And there are no OIDs - no problems.
A plus is the readability of the code, and therefore its support. It is easier to grasp the essence of the project, if the code operates with human names.
Another application is test automation. In the test data, you can get the boundary values of numeric parameters dynamically from a MIB file. So, if the boundary values of some MIB symbols in the new version of the component being tested change, there is no need to change the autotest code.
In general, using the parser to work with MIB files becomes much more pleasant, and they cease to be such a pain.