Колхозим мониторинг ИБП с протоколом Megatec в Zabbix

Возникла необходимость в мониторинге зоопарка ИБП, в наличии были Ippon, Powercom и Krauler. В качестве средства мониторинга использован Zabbix.
Естественно, задачу необходимо было решить 1) дешево 2) еще дешевле, поэтому вариант с SNMP модулями был отвергнут сразу. Было решено использовать соединение по serial портам, благо имелся опыт разработки к Ippon и APC. К слову сказать к APC на вторичном рынке есть SNMP модули по вменяемой цене, а вот к недорогим ИБП я смог найти только новые модули по цене 11–20 тыс. рублей.
В ходе работы были поставлены следующие дополнительные задачи:
1. найти и проверить кабели для подключения каждого ИБП, ибо таковые в комплекте отсутствовали
2. Реализовать некий модуль, который бы с одной стороны имел интерфейс RS-232 и понимал протокол обмена данными с каждым ИБП, а с другой — имел сетевой интерфейс и мог отправлять данные в виде zabbix_trapper.
3. Протестировать модель сбора и передачи данных, части кода, а также формат данных.

В ходе реализации второго и третьего пункта хотелось собрать в одном месте все данные, которые бы в будущем позволили реализовать отдельное устройство на м/к.
Итак:

  • 1. Кабели

Кабелей не было ни для одного ИБП. В принципе, первая задача не представляет проблемы для людей, умеющих считать контакты на разъеме DB9. Как выяснилось это умеют не только лишь все, включая меня. Контакты на разъемах F и M расположены зеркально, зато подписаны, в общем ошибиться невозможно, если быть внимательным.
— Для Ippon Smart Winner 2000 подошел кабель от модели back power pro, обычный прямой кабель 2–2, 3–3, 5–5. В принципе по этому бесперебойнику все было в документации, включая протокол обмена.
— Для Krauler Memo RT 2000 разъем также описан в документации на сайте krauler.ru, используется аналогичный кабель 2–2,3–3, 5–5.
— Для Powercom SXL-2000A распайка кабеля также есть в документации www.pcm.ru/data/docs/cables.zip, ссылку нашел на forum.pcm.ru. Кабель 2–9, 3–6, 5–7. (ПК-ИБП).

Небольшой оффтоп. Основу серверной составляют два хоста Vmware ESXi, которые размещены в стойке с двумя же ИБП. Часть серверов имеет 2 блока питания. А часть, к сожалению, только один, отчего периодически страдают. Zabbix в настоящее время размещен на одном из хостов в виде виртуальной машины. В принципе развернуть небольшой виртуальный сервер на Ubuntu (я использую эту платформу для сервисов) для реализации какой-либо задачи проблемы не представляет.

Таким образом сложилась схема: COM-порт на ESXi → Com-порт на виртаульной машине → Программа на С, которая возвращает данные с ИБП → Скрипт или программа которая посылает zabbix trap. Последние два пункта, в дальнейшем следует объединить. Хотя работает и так.

Задачу решал последовательно, распайка кабеля для одного ИБП, распайка дополнительного разъема для сервера (их надо два, плюс переходник rj-45 в DB9), проверка соединения, проверка программы, настройка элементов в Заббикс.

  • 2. Опрос СОМ-порта программой

Первое, что понадобилось, это выбрать сервер для размещения исполняемого кода. Я не стал ломать голову и разместил все это на Zabbix сервере.
Второе, подумал, как обрабатывать данные. Ввиду наличия в Баше конструкции

array=( $(/home/appliance/uniups) )

которая разбивает строку на массив данных, я решил просто возвращать отрезок строки MMM.M NNN.N PPP.P QQQ RR.R S.SS TT.T

Код программы
#include    /* Стандартные объявления ввода/вывода */
#include 
#include 
#include 

using namespace std;
#include   /* Объявления стандартных функций UNIX */
#include    /* Объявления управления файлами */
#include    /* Объявления кодов ошибок */
#include  /* Объявления управления POSIX-терминалом */
#include 
#include 

int fd; /* Файловый дескриптор для порта */
char buf[512];/*размер зависит от размера строки принимаемых данных*/
int main (int argc, char* argv[])
 {
    int iIn,iOut;
    string UPSAnswer;
    if (argc>=2)
{
        fd = open(argv[1], O_RDWR | O_NOCTTY | O_NDELAY); /*'open_port()' - Открывает последовательный порт */
        if (fd == -1)   {
           printf("error port\n");
           perror("open_port: Unable to open port - ");   }
     else
        {
         struct termios options; /*структура для установки порта*/
         tcgetattr(fd, &options); /*читает пораметры порта*/

         cfsetispeed(&options, B2400); /*установка скорости порта*/
         cfsetospeed(&options, B2400); /*установка скорости порта*/

         options.c_cflag &= ~PARENB; /*выкл проверка четности*/
         options.c_cflag &= ~CSTOPB; /*выкл 2-х стобит, вкл 1 стопбит*/
         options.c_cflag &= ~CSIZE; /*выкл битовой маски*/
         options.c_cflag |= CS8; /*вкл 8бит*/
         tcsetattr(fd, TCSANOW, &options); /*сохронения параметров порта*/
        }

        char Params[64];

        if(argc>=3)
        {
            if (!strcmp(argv[2],"status"))
            {
                iOut = write(fd, "Q1\r", 3);
                usleep(800000);
                if (iOut < 0)  fputs("write() of 4 bytes failed!\n", stderr);
                iIn=read(fd,buf,250); /*чтения приходящих данных из порта*/

                 strncpy(Params,&buf[38],11);
                 Params[46]=0;
            }
            else if (!strcmp(argv[2],"help"))
            {
                printf ("Денег нет. Но вы держитесь!\r\n");
            }
            else if (!strcmp(argv[2],"name"))
            {
                iOut = write(fd, "I\r", 3);
                usleep(800000);
                if (iOut < 0)  fputs("write() of 4 bytes failed!\n", stderr);
                iIn=read(fd,buf,250); /*чтения приходящих данных из порта*/
                strncpy(Params,&buf[0],60);

            }
            else if (!strcmp(argv[2],"stat2"))
            {
                iOut = write(fd, "F\r", 3);
                usleep(800000);
                if (iOut < 0)  fputs("write() of 4 bytes failed!\n", stderr);
                iIn=read(fd,buf,250); /*чтения приходящих данных из порта*/
                strncpy(Params,&buf[1],60);
         Params[20]=0;
            }
            else
            {
                printf ("Щта? \r\n");
            }
        }
        else
        {
           /* читаем данный по Q1 */
                usleep(1800);
                iIn=read(fd,buf,250);
                usleep(1800);
           iOut = write(fd, "Q1\r", 3);
           usleep(800000);
           if (iOut < 0)  fputs("write() of 4 bytes failed!\n", stderr);
           iIn=read(fd,buf,250); /*чтения приходящих данных из порта*/

           strncpy(Params,&buf[1],36);
           Params[36]=' ';
        }
    close(fd);
    printf("%s\r\n",Params);
}
else
printf("Usage %s /dev/ttySx", argv[0]);

}



Работу с портом нашел в гугле, можно сделать чтение из Баш, но из Баша не получилось добиться стабильной работы. В принципе код на С в linux — Ъ. Первым был протестирован Ippon, затем в ходе изучения ИБП Powercom был запущен монитор порта, который показал, что PowerCom также работает по протоколу Megatec, а родная программа опрашивает ИБП при запуске командами «I» и «F», а затем циклически «Q1». Я повесил имя порта в виде »/dev/ttyS0» или »/dev/ttyS1» на первый аргумент, второй аргумент позволяет запросить дополнительные параметры, по коду видно.
В каталоге /home/appliance/ разместил программу, назвал ее uniups.cpp. Скомпилировал

g++ -o uniups uniups.cpp 

.
В принципе результат примерно такой (да, работаю под рутом, даже не комментируйте)

root@zabbix:~# ./uniups /dev/ttyS0
204.4 204.4 204.4 035 49.9 54.8 54.5
root@zabbix:~# ./uniups /dev/ttyS0 name
I
root@zabbix:~# ./uniups /dev/ttyS1 name
#POWERCOM        SXL-2000A  LCD  V4.3
root@zabbix:~# ./uniups /dev/ttyS1
216.3 216.3 216.3 000 50.0 54.2 30.0
root@zabbix:~# ./uniups /dev/ttyS1 stat2
220.0 009 048.0 50.0
root@zabbix:~#


Что немаловажно, в конце концов оказалось, что на всех ИБП используется одинаковый протокол, что позволило использовать одну программу. Используется подключение на 2400, 8N1, все остальное off. Не обошлось без ложки дегтя, имя ИБП нормально возвращает только Powercom, Ippon команду «I» не понимает, а Krauler возвращает »# R1.1.1». Кроме того, Krauler возвращает напряжение на элементе, а остальные ИБП — на батареи, в документации это описано как 

SS.S or S.SS For on-line units battery voltage/cell is provided in the form S.SS. For standby units actual battery voltage is provided in the form SS.S


Ввиду этого пришлось поставить патч на скрипт. Об этом ниже.

  • 3. Отправка данных на Заббикс

С отправкой данный получился зоопарк на все случаи жизни. На сервере Заббикс воспользовался утилитой zabbix_sender для ускорения процесса.

zabbix_sender -z адрес_сервера -p 10051 -s Имя_ИБП_в_Заббикс -k Ключ -o Значение


В принципе для опроса двух ИБП этого достаточно, к этому моменту меня однако на тестировании у меня стоит третий, а в перспективе будет еще и четвертый, поэтому я выбрал первый попавшийся и, по странному стечению обстоятельств единственный линуксовый сервер на втором хосте Vmware и прокинул на него СОМ порты.

Использовать агента заббикса или копировать zabbix_sender не стал, нашел описание последнего на
Вся соль заключается в кодировании данных в Base64. Для проверки я использовал команду

echo "\nS3JhdWxlck1lbW9SVDIwMDA=\nRnJlcQ==\nNDkuNA==\n\n" | nc -q 0 192.168.53.23 10051

где 192.168.53.23 — адрес сервера Zabbix. Она работает.
Вышеописанный скрипт я назвал zabbix_sender.pl и поместил на сервере, скрипт у меня выглядит так:

#!/usr/bin/perl

use IO::Socket;
use IO::Select;
use MIME::Base64;

my ($zabbixserver,$hostname,$item,$data) = @_;

$zabbixserver= @ARGV[0];
$hostname= @ARGV[1];
$item= @ARGV[2];
$data= @ARGV[3];

 my $timeout=10;
 my $request=sprintf("\n%s\n%s\n%s\n\n",
 encode_base64($hostname),encode_base64($item),encode_base64($data));

 my $sock = new IO::Socket::INET ( PeerAddr => $zabbixserver, PeerPort => '10051', Proto => 'tcp', Timeout => $timeout);
 die "Could not create socket: $!\n" unless $sock;
 $sock->send($request);
 my @handles=IO::Select->new($sock)->can_read($timeout);
 if (scalar(@handles) > 0)
 {
  $sock->recv($result,1024);
  print "answer from zabbix server $zabbixserver: $result\n";
 }
 else
 {
  print "no answer from zabbix server\n";
 }
 $sock->close();


Perl на сервере уже был.

Далее понадобился скрипт, который бы увязывал все компоненты воедино и который бы можно было поместить в Cron. Здесь пример для одного порта, для другого порта можно просто скопировать этот же код и подставить другое имя. Цикл делать не стал, слабо себе представляю машину с более чем 2 СОМ портами.

#!/bin/bash
array=( $(/путь/uniups /dev/ttyS0) )
Names=( InVolt FaultVolt OutVolt Current Freq UBatt UTemp NA )
correct[5]="24.0"
j=0;
echo "Checking UPS on serial A - ${#array[@]}"
if  [ ${#array[@]} -gt "7" ]
then
param=""
for i in "${array[@]}"
do
   if [[ ${correct[$j]} ]]
      then
         param=$( echo "scale = 0; $i * ${correct[$j]}" | bc)
       else.
      param=$i
    fi
   /путь/zabbix_sender.pl 192.168.53.23 KraulerMemoRT2000 ${Names[$j]} $param
   j=$j+1;
done
else
echo "No data on A"
fi

Небольшие комментарии:
— array — массив, который возвращает программа опроса ИБП, Names — массив с именами Item, на Zabbix сервере должны быть заведены элементы с такими же именами. KraulerMemoRT2000 — имя бесперебойника, должно совпадать с именем хоста на сервере.
— возник баг, последнее возвращаемое скриптом значение, воспринимается сервером Zabbix как некорректное, независимо, причину не нашел, просто добавил элемент NA, который в принципе отсылается в никуда, на сервере его заводить не надо, после чего все стало нормально.
— производится проверка на количество элементов массива ${#array[@]}, возвращаемых при опросе ИБП. Эта проверка отсекает ошибки при чтении с СОМ порта, в том числе когда ИБП выключен. В последнем случае до патча возникала интересная ошибка: Первый в массиве элемент данных отсылался с пустым значением, соответственно на стороне сервера возникала ошибка данных, а триггер по этому элементу не срабатывал. И что самое печальное, что данные числились полученными, т.е мониторниг выводил полученные данные под видом актуальных, в то время, как ИБП был отключен.
— correct[5]=»24.0» это коррекция напряжения для батарей (5ый элемент в массиве), в том случае если ИБП возвращает напряжение на элементе (battery voltage/cell). У меня 6 элементов в батарее, 4 шт последовательно, итого 24. В принципе это описано в протоколе. Я посчитал лишним создавать отдельный элемент, так как все ИБП у меня мониторятся по шаблону и у всех 48 вольт. При мониторинге разновольтовых ИБП, конечно, надо будет несколько поменять структуру, возможно оптимальным будет забить параметры батарей на сервере.

Вышеописанный скрипт добавлен в cron на ежеминутное выполнение. Настройка заббикса в данной статье не рассматривается.

В принципе, это все. Буду рад, если собранная информация кому то пригодится.

© Geektimes