[Из песочницы] SNMP + Java – невозможное возможно: пишем парсер MIB-файлов

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

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

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

tvlqaihbpnaqtlie8wr7hulxyhg.jpeg

Вступление


Всё началось с того, что поступила задача написать автотесты для проверки работы аудио-видео регистратора по протоколу 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-файлами:


    net.percederberg.mibble
    mibble
    2.9.3



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


    
        opennms
        OpenNMS
        http://repo.opennms.org/maven2/
    



Если проект собирается с помощью мавена без ошибок, всё готово для работы. Осталось только создать класс парсера (назовём его 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 getTableColumnNames(String tableName) {
    ArrayList 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-файлами становится намного приятнее, и они перестают быть такой уж болью.

© Habrahabr.ru