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-файлами становится намного приятнее, и они перестают быть такой уж болью.

Комментарии (9)


  1. acmnu
    22.10.2018 11:14

    LDAP тоже Light DAP. И там тоже есть OID. Ох эти рамантические 70е: безумные и идеальные стандарты придумывали каждый день, их никто не мог реализовать полностью, поэтому и появлялись все эти Light и Simple


  1. Hokum
    22.10.2018 11:46

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

    Кажется заголовок излишне громкий. В чем "невозможность", в том что нет вот сразу готовой библиотеки, которая все делает?


    "Парсер"? Серьезно? Просто обернули в методы стороннюю библиотеку и это уже парсер.


    В целом статья полезная, но заголовок уж слишком "кричащий".


    1. irony_man Автор
      22.10.2018 13:59
      +1

      Спасибо за замечание. Подумаю, что делать с названием =)
      А насчёт парсера: библиотека, которая легла в основу парсера, содержит некоторое количество подводных камней и неочевидных вещей, с которыми мне пришлось столкнуться, поэтому хотелось рассказать людям о варианте парсера, который будет понятен и будет работать.


    1. irony_man Автор
      22.10.2018 14:27
      +1

      Название исправлено.


  1. xcore78
    23.10.2018 02:22

    В целом, лучше SNMP хоронить, а не поддерживать :)


    1. really4g
      23.10.2018 07:32

      Не соглашусь. Точнее не соглашусь что есть достойная замена такому "замечательному простому протоколу". Хоть в названии simple относится больше к понятию простой в части получения информации об устройстве, но никак не о реализации самого взаимодействия.
      Самое главное, что поддержка этого протокола есть практически в каждом сетевом утюге, чайнике. А как иначе читать состояние удаленной сетевой железки? Конечно есть ssh в некоторых случаях, но мне неизвестны адекватные реализации в системах мониторинга получения данных используя протокол ssh. К тому же snmp по сути точнее mib устройства есть абстрактная модель, которая позволяет получить состояние и при наличии такой возможности передать новые значение, параметром. Расскажу примером: есть каналы поднятые на Cisco, есть каналы поднятые на Linux серверах. Монитрим все zabbix. В случае серверов ставим агента читаем состояние сетевых соединений, делаем периодические пинги. В случае с Cisco помимо чтения состояниея интерфейсов, периодически проверяем доступность канала посылом пинга на граничные устройства. В случае с Cisco я вижу выход использовать только snmp. Если есть другие способы с радостью буду их использовать, т.к. в текущей реализации есть ряд оговорок в связке Zabbix-snmp-Cisco-ping.


      1. xcore78
        23.10.2018 20:23

        А как иначе читать состояние удаленной сетевой железки?


        Поясните, пожалуйста, риторический это вопрос или нет. Т.е. вы не знаете, или апеллируете к своему, видимо, не сетевому опыту?


        1. really4g
          23.10.2018 21:02

          Мой опыт сетевой, хоть и небольшой. Но правда использование для мониторинга snmp стандарт де факто и я правда не знаю другого способа. Не рассматриваем проприетарные решения от Cisco, у меня их не настолько много чтобы брать их ПО и платить за него, хотя оно вполне может решить все наши вопросы.


          1. xcore78
            23.10.2018 21:30

            Почитайте, пожалуйста, про Streamed Telemetry.

            Также, если у вас есть лаба, прикиньте, сколько snmp траффика достаточно, чтобы положить CPU на железке, сломав сеть.

            Если вы собираете counters через snmp, то sflow для вас существует уже много лет.

            Также почитайте про Ganglia, может помочь понять, как надо строить действительно большие системы. Есть отличная книга: www.amazon.com/Monitoring-Ganglia-Tracking-Dynamic-Application/dp/1449329705