SNMP + Java – невозможное возможно: пишем парсер MIB-файлов

от автора

SNMP – не самый юзер-френдли протокол: MIB-файлы слишком длинные и запутанные, а OID’ы просто невозможно запомнить. А что если возникла необходимость работать с SNMP на Java? Например, написать автотесты для проверки API SNMP-сервера.

Путём проб и ошибок при наличии довольно скудного количества информации по теме мы все же придумали, как подружить Java и SNMP.

В этой серии статей я постараюсь поделиться полученным опытом работы с протоколом. Первая статья в серии будет посвящена реализации парсера MIB-файлов на Java. Во второй части я расскажу о написании SNMP-клиента. В третьей части речь пойдёт о реальном примере использования написанной библиотеки: автотестах для проверки взаимодействия с устройством по протоколу SNMP.


Вступление

Всё началось с того, что поступила задача написать автотесты для проверки работы аудио-видео регистратора по протоколу SNMP. Осложняло ситуацию то, что информации по взаимодействию с SNMP на Java не так уж много, особенно если говорить о русскоязычном сегменте интернета. Конечно, можно было посмотреть в сторону C# или Python. Но в C# ситуация с протоколом примерно такая же сложная, как и в Java. В питоне же есть пара неплохих библиотек, но у нас уже была готовая инфраструктура для автотестов REST API данного устройства именно на Java.

Как и для любого другого протокола взаимодействия по сети, нам нужен был SNMP-клиент для работы с различными видами запросов. Автотесты должны были уметь проверять успешность GET- и SET-запросов для скалярных и табличных параметров. Для таблиц требовалось также иметь возможность проверить добавление и удаление записей, если сама таблица допускает эти операции.

Но помимо клиента библиотека должна была содержать класс для работы с MIB-файлами. Этот класс должен был уметь распарсить MIB-файл для получения типов данных, границ допустимых значений и т.д., чтобы не хардкодить то, что всегда может измениться.

В ходе поиска подходящих библиотек для Java мы не нашли ни одной библиотеки, которая позволяла бы работать и с запросами, и с MIB-файлами. Поэтому мы остановились на двух разных библиотеках. Для клиента вполне логичным показался выбор широко используемой org.snmp4j.snmp4j (https://www.snmp4j.org), а для парсера MIB-файлов выбор пал на не особо известную библиотеку net.percederberg.mibble (https://www.mibble.org). Если с snmp4j выбор был очевиден, то mibble была выбрана за наличие достаточно подробной (хоть и англоязычной) документации с примерами. Итак, начнём.

Пишем парсер MIB-файлов

Все, кто когда-либо видел MIB-файлы, знают, что это боль. Попробуем это исправить с помощью несложного парсера, который значительно облегчит поиск информации из файла, сведя его к вызову того или иного метода.

Такой парсер можно использовать как отдельную утилиту для работы с MIB-файлами или включить в любой другой проект под SNMP, например, при написании SNMP-клиента или автоматизации тестирования.

Подготовка проекта

Для удобства сборки мы используем Maven. В зависимости добавляем библиотеку net.percederberg.mibble (https://www.mibble.org), которая облегчит нам работу с MIB-файлами:

<dependency>     <groupId>net.percederberg.mibble</groupId>     <artifactId>mibble</artifactId>     <version>2.9.3</version> </dependency> 

Так как она лежит не в центральном репозитории Maven, добавляем в pom.xml следующий код:

<repositories>     <repository>         <id>opennms</id>         <name>OpenNMS</name>         <url>http://repo.opennms.org/maven2/</url>     </repository> </repositories> 

Если проект собирается с помощью мавена без ошибок, всё готово для работы. Осталось только создать класс парсера (назовём его MIBParser) и импортировать всё, что нам нужно, а именно:

import net.percederberg.mibble.*;

Загрузка и валидация MIB-файла

Внутри класса будет только одно поле – объект типа net.percederberg.mibble.Mib для хранения загруженного MIB-файла:

 private Mib mib; 

Для загрузки файла пишем вот такой метод:

 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; } 

Класс net.percederberg.mibble.MIBLoader валидирует файл, который мы пытаемся загрузить и бросит исключение net.percederberg.mibble.MibLoaderException в случае, если найдёт в нём какие-то ошибки, в том числе ошибки импорта из других MIB-файлов, если они не лежат в той же директории или не содержат импортируемые MIB-символы.

В методе loadMib ловим все исключения, пишем о них в лог и прокидываем дальше, т.к. на этом этапе продолжение работы невозможно – файл не валидный.

Написанный метод мы вызываем в конструкторе парсера:

 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."); } 

Если файл успешно загрузился и распарсился, продолжаем работу.

Методы для получения информации из MIB-файла

С помощью методов класса net.percederberg.mibble.Mib можно искать отдельные символы MIB-файла по имени или OID с помощью вызова метода getSymbol(String name) или getSymbolByOid(String oid) соответственно. Эти методы возвращают нам объект net.percederberg.mibble.MibSymbol, методами которого мы будем пользоваться для получения необходимой информации по конкретному MIB-символу.

Начнём с самого простого и напишем методы для получения имени символа по его OID и, наоборот, OID по имени:

 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; } 

Возможно, это особенности конкретного MIB-файла, с которым мне было необходимо работать, но по каким-то причинам для скалярных параметров возвращался OID без нуля на конце, поэтому в метод получения OID’а был добавлен код, который в случае, если MIB-символ скалярный, просто добавляет к полученному OID «.0» с помощью метода append(int index) из класса net.percederberg.mibble.OID. Если у вас работает и без костыля, поздравляю 🙂

Для получения остальных данных по символу пишем один вспомогательный метод, где получаем объект net.percederberg.mibble.snmp.SnmpObjectType, содержащий в себе всю необходимую информацию о том MIB-символе, из которого он получен.

 private SnmpObjectType getSnmpObjectType(MibSymbol symbol) {     if (symbol instanceof MibValueSymbol) {         MibType type = ((MibValueSymbol) symbol).getType();         if (type instanceof SnmpObjectType) {             return (SnmpObjectType) type;         }     }     return null; } 

Например, мы можем получить тип MIB-символа:

 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(); } 

Здесь есть предусмотрено 2 способа получения типа, т.к. для примитивных типов работает первый вариант:

 getSnmpObjectType(s).getSyntax().getName(); 

а для импортированных – второй:

 getSnmpObjectType(s).getSyntax().getReferenceSymbol().getName(); 

Можно получить уровень доступа к символу:

 public String getAccess(String name) {     MibSymbol s = mib.getSymbol(name);     return getSnmpObjectType(s).getAccess().toString(); } 

Минимальное допустимое значение числового параметра:

 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; } 

Максимальное допустимое значение числового параметра:

 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; } 

Минимальную допустимую длину строки (зависит от типа строки):

 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; } 

Максимальную допустимую длину строки (зависит от типа строки):

 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; } 

Также можно получить имена всех колонок в таблице по её имени:

 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; } 

Сначала стандартным образом получаем MIB-символ по имени:

 MibValueSymbol table = (MibValueSymbol) mib.findSymbol(tableName, true); 

Затем проверяем, является ли он таблицей и что дочерний MIB-символ – строка этой таблицы и, если условие возвращает true, в цикле идём по дочерним элементам табличной строки и добавляем имя элемента в результирующий массив:

 if (table.isTable() && table.getChild(0).isTableRow()) {     MibValueSymbol[] symbols = table.getChild(0).getChildren();     for (MibValueSymbol mvs : symbols) {         mibSymbolNamesList.add(mvs.getName());     } } 

Итоги

Этих методов хватает для получения любой информации из MIB-файла по каждому конкретному символу, зная только его имя. Например, при написании SNMP-клиента можно включить в него такой парсер, чтобы методы клиента на вход принимали не OID’ы, а имена MIB-символов. Это повысит надёжность кода, т.к. опечатка в OID’е может привести не к тому символу, к которому мы хотим обратиться. А нет OID’ов – нет и проблем.

В плюс идёт читаемость кода, а значит и его поддерживаемость. Проще вникнуть в суть проекта, если код оперирует человеческими названиями.

Ещё одно применение – это автоматизации тестирования. В тестовых данных можно получать граничные значения числовых параметров динамически из MIB-файла. Так, если изменятся граничные значения у каких-то MIB-символов в новой версии тестируемого компонента, не придётся менять код автотестов.

В целом, при помощи парсера работать с MIB-файлами становится намного приятнее, и они перестают быть такой уж болью.


ссылка на оригинал статьи https://habr.com/post/427267/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *