Получаем списки mac-адресов на портах управляемых свичей в Zabbix

    В организации, где я работаю, используется довольно большое количество управляемых свичей, разнесенных по нескольким зданиям. Захотелось видеть MAC-адреса на их портах не только с помощью telnet или ssh, а прямо в веб-интерфейсе системы мониторинга Zabbix.

    Сбор информации и теория

    В поисках информации о том, как это сделать, был спрошен Гугл, и после непродолжительных поисков был найден чудесный документ за авторством Cisco, описывающий алгоритм получения искомых адресов и определения портов, на которых они фигурируют.
    Беглое изучение выводов snmpwalk со свича 3Com 4200G показало, что в моем случае даже не требуется определять vlan, и достаточно всего трех шагов для определения MAC-адресов по портам.

    Как вы можете понят из документации, нам всего-то надо:
    1) получить список адресов командой
    $snmpwalk -c public -v 2c hostname .1.3.6.1.2.1.17.4.3.1.1

    17.4.3.1.1.0.0.12.7.172.8 = Hex: 00 00 0C 07 AC 08
    17.4.3.1.1.0.1.2.27.80.145 = Hex: 00 01 02 1B 50 91
    17.4.3.1.1.0.1.3.72.77.90 = Hex: 00 01 03 48 4D 5A
    17.4.3.1.1.0.1.3.72.221.191 = Hex: 00 01 03 48 DD BF
    ...

    2) получить спискок портов командой
    $snmpwalk -c public -v 2c hostname .1.3.6.1.2.1.17.4.3.1.2
    17.4.3.1.2.0.0.12.7.172.8 = 13
    17.4.3.1.2.0.1.2.27.80.128 = 13
    17.4.3.1.2.0.1.2.27.80.145 = 13
    17.4.3.1.2.0.1.2.163.145.225 = 13
    ...

    Если мы опрашиваем простой свич, то вот этот чудесный номер (13 в примере) — это и есть наш порт на свиче.

    3) Осталось только найти соответствие между номерами OIDов, и мы увидим, что OIDы, оканчивающиеся на 12.7.172.8 дадут нам mac и номер порта, на котором этот mac висит.

    Переходим от теории к практике

    Чтобы это все дело автоматизировать, я набросал простенький скрипт на перле (UPDATE0: код обновлен по совету gescheit; UPDATE1: код еще раз обновлен по совету FreeLSD)

    #! /usr/bin/perl
    #====================================================================
    #
    #                   QUERY MAC ADDRESSES FROM 3COM SWITCH FROM SELECTED PORT
    #====================================================================
     
     
    use strict;
    use Net::SNMP qw(snmp_dispatcher oid_lex_sort);
     
    my $show_vendor = 1;
    my $script = "/usr/share/zabbix/scripts/mac.sh -s";
    my $debug = 0;
    my $file_name = "/tmp/$ARGV[0]-getmac.tmp";
    my $interval = 90#refresh rate
    my $new = 0;
    my @list;
    my $write_secs = (stat($file_name))[9];
    if ($debug == 1){
            print "file $file_name updated at ", scalar(localtime($write_secs))"\n";
    };
    if ($write_secs + $interval < time) {    #file updated less then $interval seconds ago
            if ($debug == 1){print "generating new mac table \n";} 
            $new = 1;        
            open FH, ">$file_name" or die "can't open '$file_name': $!";
    }else {
            if ($debug == 1){print "using old mac table \n";} 
            open FR, "<$file_name" or die "can't open '$file_name': $!";
            while(my $line = <FR>) {
                    chomp($line);
                    my ($p$m) = split/;/$line;
                    @list[$p] = "@list[$p]$m, ";
            }
    }
    #=====================================
    my $session;
    my $error;
    my $port = $ARGV[1];
    if($new == 1) {
            #=== Setup session to remote host ===
            ($session$error) = Net::SNMP->session(
            -hostname  => $ARGV[0] || 'localhost',
            -community => 'public',
            -version => '2c',
            -translate   => [-octetstring => 0],
            -port      => 161
            );
            #=== Was the session created? ===
            if (!defined($session)) {
                    printf("ERROR: %s\n"$error);
                    exit 1;
            }
    };
    #==================================
     
    #=== OIDs queried to retrieve information ====
    my $TpFdbAddress = '1.3.6.1.2.1.17.4.3.1.1';
    my $TpFdbPort    = '1.3.6.1.2.1.17.4.3.1.2';
    #=============================================
    my $result;
    my @tmp;
    my $x;
    if($new == 1) {
            if (defined($result = $session->get_table($TpFdbAddress))) {
                    foreach (oid_lex_sort(keys(%{$result}))) {
                            $x = unpack('H*',$result->{$_});
                            $x =~ s/(..(?!\Z))/\1:/g;
                            push( @tmp, $x);
                    }
            }else {
                    if($debug == 1) {
                            printf("ERROR: %s\n\n"$session->error());
                    }
            }
    #==========================================
    #=== Print the returned MAC ports ===
            $result;
            if (defined($result = $session->get_table(-baseoid => $TpFdbPort))) {
                    my $i = 0;
                    my $out = "";
                    my $res = 0;
                    my $tmp_port;
                    my $tmp_mac_list = "";
                    foreach (oid_lex_sort(keys(%{$result}))) { 
                            if($result->{$_} == $port) {
                                    $res = 1;
                                    if( $show_vendor == 1) {        
                                            $out = `$script $tmp[$i]`;
                                            printf("%s(%s)"$tmp[$i]$out);
                                    }else {
                                            printf("%s"$tmp[$i]);
                                    };
                                    print ", ";
                            };
                            if( $show_vendor == 1) {        
                                    $out = `$script $tmp[$i]`;
                                    printf FH ("%s;%s(%s)\n"$result->{$_}$tmp[$i]$out);
                            }else {
                                    printf FH ("%s;%s\n"$result->{$_}$tmp[$i]);
                            };
                    $i++;
                    }
                    if ($res == 0) {
                            print "null";
                    };
            }else {
                    if($debug == 1) {
                            printf("ERROR: %s\n\n"$session->error());
                    }else {
                            print "null";
                    };
            }
    }else {
            if(@list[$port]) {        
                    print "@list[$port]";
            }else {
                    print "null";
            };
    }
    print "\n";
    #=============================================
    #=== Close the session and exit the program ===
    if($new == 1) {
            $session->close;
            close FH;
    }else {
            close FR;
    }
    exit 0;
     
     


    Код очень бесхитростный, если нет файлика с уже полученными OIDами, то мы получаем OIDы с mac-адресами, и пишем в массив. А потом в такой же последовательности получаем OIDы с портами, что дает нам соответствие n-го значения в массиве n-му значению полученного OIDа с номером порта. Затем скидываем полученные данные в файлик.
    Если же файлик при старте скрипта уже есть, и он не старше $interval, который в моем случае составляет 90 секунд — то берем данные из него. Это позволило использовать всего 2 snmp запроса на свич.
    Скрипт принимает на вход два параметра, первый из которых это адрес устройства (такова специфика zabbix, для внешних скриптов всегда первым параметом будет адрес узла сети), а вторым параметром — интересующий нас номер порта. Если на этом порту адресов нет, скрипт вернет строку с текстом «null». В новой версии мы еще можем узнавать производителя устройства по mac-адресу. Для включения этой возможности существует переменная $show_vendor, при значении которой, равном единице, скрипт пытается получить данные о производителе от другого скрипта, указанного в переменной script. Этому скрипту передается mac-адрес утройства. Для себя, я реализовал довольной простой скрипт на sh, вся суть которого сводится к запуску одной строки:

    awk --assign IGNORECASE=1 '/hex/ && /'$mac'/ {for (x=3; x<=NF; x++) {printf("%s ",$x)}}' $filename


    Таинственный файлик, скрывающийся под переменной $filename, получается по ссылке, и содержит все нужные нам данные, заботливо вобранные ieee.

    Я эту одну строчку слегка доработал, добавив даже режим «обновления», выкачивающий самую свежу версию списка производителей. Вот что получилось в итоге:

    #!/bin/sh
    #get vendor from mac-address
    if [ -z "$1" ]then
            echo "no args specified, exiting! Use $0 [-option] mac"
            echo "where [-option] can be -u (update databse and exit) or -s (silent mode, just show vendor)"
            exit 1 
    fi
    #don't forget that Zabbix don't set $PATH when running scripts
    filename=/usr/share/zabbix/scripts/oui.txt
    tmpfile=/tmp/oui.txt
    link=http://standards.ieee.org/develop/regauth/oui/oui.txt
    sed=/bin/sed
    awk=/bin/awk
     
    case $1 in
            -s)
                    silent=1
                    mac=$2
                    if [ -z "$2" ]then
                            echo "no mac specified, exiting!"
                            exit 1
                    fi
                    ;;
            -u)
                    wget $link -O $tmpfile
                    if [ $? -gt 0 ]then 
                            echo "download error, exiting"
                            exit 1
                    else 
                            echo "Download ok!"
                            echo "Moving $tmpfile to $filename..."
                            mv -f $tmpfile $filename
                            if [ $? -gt 0 ]then
                                    echo "Error!"
                            else
                                    echo "Success!"
                            fi
                            exit 0
                    fi
                    ;;
            *)        
                    mac=$1
                    ;;
            esac
    if [ ! -f $filename ]then 
            if [ -z $silent ]then
                    echo "no mac list file, dowload it? [y/n]"
            else
                    exit 1
            fi
            while :
            do 
                    read INPUT_STRING
                    case $INPUT_STRING in
                    y)
                            echo "Trying to download from $link"
                            wget $link -O $filename
                            if [ $? -gt 0 ]then 
                                    echo "download error, exiting"
                                    exit 1
                            else 
                                    echo "Download ok!"
                            fi
                            break
                            ;;
                    n)
                            echo "exiting!"
                            exit 0
                            ;;
                    *)
                            echo "wrong input, use [y/n]"
                            ;;
                    esac
            done
    fi
    if [ ${#mac} -lt 8 ]then
            mac=`echo "$mac" | $sed 's/^\(..\)\(..\)\(..\)/\1-\2-\3/' `
    else
            mac=`echo "$mac" | $sed -e 's/:/-/g'`
    fi
    mac=${mac:0:8} 
    if [ -z $silent ]then
            echo "Searching for $mac..."
    fi
    result=`$awk --assign IGNORECASE=1 '/hex/ && /'$mac'/ {for (x=3; x<=NF; x++) {printf("%s ",$x)}}' $filename`
    if [ -z "$result" ]then
            result="no info"
    fi
    echo -n $result
     


    Запустим скрипт, выдающий нам производителя, для проверки на работоспособность:

    $./mac.sh "000000"
    no mac list file, dowload it? [y/n]
    y
    Trying to download from standards.ieee.org/develop/regauth/oui/oui.txt
    --2011-10-06 14:20:16-- standards.ieee.org/develop/regauth/oui/oui.txt
    Распознаётся proxy.organization.ltd... 192.168.0.1
    Устанавливается соединение с proxy.organization.ltd|192.168.0.1|:3128... соединение установлено.
    Запрос Proxy послан, ожидается ответ... 200 OK
    Длина: 2493060 (2,4M) [text/plain]
    Saving to: «oui.txt»

    100%[==========================================================================================================================================================================>] 2 493 060 784K/s в 3,1s

    2011-10-06 14:20:26 (784 KB/s) - «oui.txt» saved [2493060/2493060]

    Download ok!
    Searching for 00-00-00...
    XEROX CORPORATION

    Если все работает, двигаемся дальше.

    Теперь надо в конфиге zabbixa настроить время выполнения скрипта, увеличив его до нужных значений. Если выбранного вами значения таймаута будет недостаточно, вы сразу это поймете, поскольку элементы данных, отражающие mac-адреса, будут один за одним переходить в категорию «неподдердживаемые» с описанием ошибки вроде «script execution timeout».
    Правим конфиг:
    #vim /etc/zabbix/zabbix-server.conf

    Ищем там такие строки
    ### Option: Timeout
    # Specifies how long we wait for agent, SNMP device or external check (in seconds).
    # Range: 1-30


    и раскомментируем и меняем на нужное значение
    Timeout=5

    Там же ищем
    ### Option: ExternalScripts
    # Location of external scripts
    ExternalScripts=/usr/share/zabbix/scripts/


    и при необходимости меняем на правильный путь к скриптам.
    Вот по этому пути мы наш скрипт на перле и располагаем. Я его назвал бесхитростно — get_mac.pl

    Теперь мы в Zabbix настроим в шаблоне для свича нужный тип данных:



    И так для всех портов.

    После этого, вы можете получать данные об адресах на портах свича прямо в zabbix.
    У меня это выглядит так (обновленный скриншот с именами производителей выложить не могу, поскольку хабрасторадж не работает без флеша, а с гуглохромовским флешем не работает тоже):


    Не пугайтесь, глядя на 16 порт — это аплинк.

    Данная методика проверена и успешно работает уже месяц на девяти свичах марки 3Com: 4200G, 4210, 2916, 2924. Версия Zabbix 1.8.5, недавно обновил на 1.8.7. Учитывая, что я писал все это по документации Cisco, для свичей Cisco проблем также быть не должно.

    P.S. Я считаю, что скрипт можно (и нужно) доработать и переработать, но я не силен в perl, и потому прошу у хабрасообщества помощи в этом благородном деле, чтобы выложить в wiki такого чудесного проекта, как Zabbix, достойное и качественное решение. Так же планирую вместе с улучшенным скриптом выложить туда свои шаблоны для свичей 3Com 4200G, 4210, 2924.
    UPD0 Код скрипта обновлен по совету gescheit
    UPD1 Cтатья и код обновлены по совету FreeLSD
    • +6
    • 40,2k
    • 9
    Поделиться публикацией

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

      0
      Немного не понятно вот это место: «у меня для гарантированного опроса 9 свичей, из которых 3 48-портовых, и 5 24-портовых и 1 16-портовый, потребовалось установить время выполнения скрипта в 15 секунд». Скрипт же выполняется для каждого порта, причем тут количество свичей?
      Для увеличения производительности я бы рекомендовал сделать такой финт: при первом запросе таблиц с портами с маками сохранить их и дату во временном хранилище, при последующих запросах брать данные уже из этого файла, а если дата в файле просрочена на 5 минут например, запустить процесс обновления табличек. Тогда скрипт не будет каждый раз получать одну и ту же информацию.
        0
        Скрипт выполняется для каждого порта, но как я понял, для 48-портового устройства Zabbixом будет запущено сразу 48 скриптов, и устройства не всегда вовремя отвечают. Поэтому я привел статистику, что при 9 таких устройствах максимальное время работы скрипта ни разу за месяц не превышало 15 секунд.
          0
          Вот поэтому я и рекомендую использовать временное хранилище, тогда будет один скрипт на один свитч
            0
            Да, мысль хорошая, надо выкачиваться сразу все OIDы, показывающие mac-адреса и порты, а потом просто искать по потрам информацию. Спасибо за идею!
        0
        Да, это невероятно удобно для поиска на каком порту висит та или иная железка.
        Учитывая, что диапазоны mac адресов заняты своим производителем, можно доработать и понять какая железка висит на том или ином порту.
          0
          Вы подали интересную мысль, спасибо. Я поискал базы mac-адресов, и нашел эту. Вроде вполне полная, и обновляется ежедневно. Попробую прикрутить ее к этому скрипту как-нибудь на досуге. Если получится — выложу сюда, и наконец добавлю в wiki заббикса… А вообще — очень рад, что статья показалась вам полезной.
            0
            Обновил статью и код, теперь, как вы и предлагали, выводятся еще и производители.
              0
              вот маньяк. Спасибо )
            0
            У цисок все сложнее, при большом количестве vlanов приходится для каждого влана создавать запрос через snmpwalk.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое