Мониторинг триггеров в GUI под Windows

Примеры из жизни как развернуть и настроить систему мониторинга zabbix
Ответить
Kitsum
Сообщения: 15
Зарегистрирован: 01 окт 2014, 13:03
Откуда: далеко из-за мкада

Мониторинг триггеров в GUI под Windows

Сообщение Kitsum »

Доброе время суток.

Прежде чем вдаваться в описание программы, укажу на ситуацию которая привела к созданию очередного велосипеда.
Есть группа в которую входит Ваш покорный слуга + два сотрудника. В наше присутствие на рабочем месте, события в Zabbix (2.2) отслеживаются постоянно, а если быть точным то 8 часов в день, 5 дней в неделю. Исключения составляют праздничные дни и отпуска совпавшие с изменениями графика работы у коллег.
В наше отсутствие мониторингом инфраструктуры занимается (по крайней мере должен заниматься) дежурный персонал. В этом и кроется проблема!
Как выясняется, люди работающие в режиме день - ночь - 48 часов дома, не проявляют особого интереса к мониторингу состояния триггеров со всеми вытекающими из этого последствиями. Ну да Бог с ними, воспитательные работы - задача административного персонала.

Наша задача более явно оповещать об изменениях в активных триггерах на машинах дежурного персонала под управлением ОС Windows.
Под этим я подразумеваю:
1. Оповещать о переходе активного триггера в состояние TRUE
  • - Звуковым сигналом
    - Всплывающей подсказкой в трее
2. Возможность видеть проблемные триггеры в GUI Windows

Велосипед заключается в том, что мы будем использовать PHP для реализации GUI под Windows. Да, да, да ... можете плевать в мою сторону и я полностью согласен, что C++ намного эффективнее, но мои познаний в C++ еще не достаточно, для перехода на новую ступень эволюции.
И так, принято решение разделить программу на две части. Первая будет выполнена в виде PHP скрипта находящегося на сервере и по переходу по ссылке вида "http://server/trigger/index.php" мы будем получать список проблемных триггеров в формате JSON. Вторая часть программы будет выполнена в виде клиентского приложения разработанного в IDE DevelStudio (если этот продукт можно считать таковым) и например раз в минуту забирать JSON строку с сервера, проводить необходимые преобразования с информацией и оповещать конечного пользователя если это необходимо.

Скрипт на сервере
Изначально была попытка использовать Zabbix API (на базе http://zabbixapi.confirm.ch/), но информацию приходилось вытаскивать за несколько шагов, что было не очень удобно, хотя более корректно.
Принято решение забирать данные напрямую из MySQL одним запросом, преобразовывать их в общий массив, конвертировать в JSON и отдавать во внешнюю среду. Скрипт легко модифицировать и в случае необходимости добавить ограничение по количеству запросов в минуту с одного хоста и естественно организовать доступ к данным всем IP адресам кроме избранных.

Для более корректной и правильной работы необходимо завести нового пользователя в MySQL и дать ему права только на выполнение SELECT в базе zabbix
Вам необходимо изменить соответствующую информацию сразу после комментария "# Укажите нужные данные для авторизации"
Сам скрипт:

Код: Выделить всё

<?PHP
# Обработка ошибок
function error($message)
{
    return json_encode(array('error' => $message));
}
# Работа с БД
class db extends PDO
{
    # Начальные данные для подключения к серверу
    private $settings = array(
        'host'  => 'localhost',
        'db'    => '',
        'login' => 'root',
        'pass'  => '',
    );
    #
    function __construct($settings = array())
    {
        # Изменяем конфигурацию
        if(is_array($settings) and 0 != count($settings)) {
            foreach($settings as $key => $val) {
                if(isset($this->settings[$key])) $this->settings[$key] = $val;
            }
        }
        # Подключаемся к базе данных
        try {
            parent::__construct('mysql:host='.$this->settings['host'].';dbname='.$this->settings['db'].';charset=utf8', $this->settings['login'], $this->settings['pass']);
        }
        catch(PDOException $error) {
            exit(error($error->getMessage()));
        }
    }
}
# Укажите нужные данные для авторизации
$settings = array(
    'db'    => 'zabbix', 
    'login' => 'login', 
    'pass'  => 'password'
);
$db = new db($settings);
$query = $db->query("
SELECT
    t.triggerid,
    h.host,
    t.priority,
    t.description,
    t.lastchange
FROM
    triggers t,
    hosts h,
    items i,
    functions f
WHERE
    t.value=1 AND
    t.status=0 AND
    f.triggerid=t.triggerid AND
    i.itemid=f.itemid AND
    i.status=0 AND
    h.hostid=i.hostid AND
    h.status=0
ORDER BY
    t.lastchange DESC
");
$message = array();
if($query->rowCount()) {
    foreach($query->fetchAll(PDO::FETCH_ASSOC) as $var => $val) {
        $message[$val['triggerid']] = $val;
    }
}
echo json_encode($message);
?>
Скрипт выполняет SQL запрос указанные ниже. Вы можете проверить его корректность в консоли:

Код: Выделить всё

SELECT
    t.triggerid,
    h.host,
    t.priority,
    t.description,
    t.lastchange
FROM
    triggers t,
    hosts h,
    items i,
    functions f
WHERE
    t.value=1 AND
    t.status=0 AND
    f.triggerid=t.triggerid AND
    i.itemid=f.itemid AND
    i.status=0 AND
    h.hostid=i.hostid AND
    h.status=0
ORDER BY
    t.lastchange DESC
На данный момент выводятся все проблемные триггеры независимо для какой группы пользователей они предназначены, администраторы или гости или Васе Пупкину.

Теперь о второй части велосипеда.
Большая часть программы (до компиляции) лежит в каталоге с проектом, в файле /scripts/thread.php и представляет из себя набор функций:

Код: Выделить всё

<?php
function _constructor()
{
    if(($settings = read_config('config.ini')) == false) {
        $settings['server']['trigger'] = 'http://localhost/trigger/index.php';
        ini::open("config.ini");
        ini::write("server", "trigger", $settings['server']['trigger']);
        messageDlg("Not performed the initial configuration.\r\nCheck config.ini and restart the program!", mtWarning, MB_OK);
    }
    # Устанавливаем doubleBuffered для всех объектов на форме
    foreach(c("Main")->componentList as $obj) $obj->doubleBuffered = true;
    # Загружаем изображения статусов триггеров
    for($i=1; $i<=6; $i++) c("ImageList1")->addFromFile('icons\\'.$i.'.png');
    # Действия с TrayIcon
    _eventTrayIcon(c("Main->trayIcon"));
    # Запускаем фоновый поток занимающийся обработкой информации полученной от сервера
    thread($settings['server']['trigger']);
}
function _gui($name, $key, $val = false, $func = false)
{
    if($val) {
        if($func) return c($name)->{$key}->$val(); else c($name)->{$key} = $val;
    } 
    else return c($name)->{$key};
}
function _alert($title, $text)
{
    # Всплывающее окно
    $trayIcon = c("trayIcon");
    $trayIcon->title = $title;
    $trayIcon->text = $text;
    $trayIcon->hideBalloonTip(); # Если висит предыдущее сообщение, убираем его
    $trayIcon->showBalloonTip();
    # Звуковой сигнал
    $sqPlayer = c('sqPlayer');   
    $sqPlayer->fileName = '/audio/alarm.ogg';
    if(!$sqPlayer->isPlay()) $sqPlayer->play();
}
function zabbix($param)
{
    $param = TThread::get($param);
    while(true) {
        if(($json = file_get_contents($param->serverUrl)) == false) {
            syncEx('_gui', array('statusBar', 'simpleText', 'No connection to the server'));
        }
        else {
            if(($trigger = json_decode($json)) == null) {
                syncEx('_gui', array('statusBar', 'simpleText', 'json can not be converted'));
            }
            else {
                # Очищаем список триггеров
                syncEx('_gui', array('listViewLastStatus', 'items', 'clear', true));
                foreach($trigger as $key => $val) {
                    # Различные преобразования
                    $val->lastchange  = date("d.m.Y H:i", $val->lastchange);
                    $val->description = iconv('UTF-8', 'CP1251//IGNORE', $val->description);
                    $val->description = str_replace('{HOST.NAME}', $val->host, $val->description);
                    # Попытка найти новый триггер
                    if(is_array($oldTrigger) and $oldTrigger[$key] == null) {
                        syncEx('_alert', array($val->host, $val->description));
                    }
                    # Добавление нового триггера в GUI
                    $item = syncEx('_gui', array('listViewLastStatus', 'items', 'add', true));
                    $item->caption = $val->host;
                    $item->subItems = array($val->lastchange, $val->description);
                    $item->imageIndex = $val->priority;
                    # Временный массив
                    $tmp[$key] = $val->host;
                }
                $oldTrigger = $tmp; unset($tmp);
                # Чистим статус бар
                syncEx('_gui', array('statusBar', 'simpleText', null));
            }
        }
        # Интервал обновления
        delay(60000);
    }
}
function thread($url)
{
    $daemon = new TTHread('zabbix');
    $daemon->serverUrl = $url;
    $daemon->priority = tpLower;
    $daemon->resume();
}
function read_config($file) {
    if(!file_exists($file)) return false;
    $settings = array();
    ini::open($file);
    ini::readSections($section);
    foreach($section as $id => $sec) {
        ini::readKeys($sec, $keys);
        foreach($keys as $id => $key) {
            ini::read($sec, $key, $settings[$sec][$key]);
        }
    }
    return $settings;
}
function _eventTrayIcon($trayIcon)
{
    global $trayPopup;

    $trayPopup = new TPopupMenu;
    $trayPopup->name = 'trayPopup';
        $itemPopup = new TMenuItem;
        $itemPopup->caption = 'Выход';
        $itemPopup->onClick = function() {
            exit();
    };
    $trayPopup->addItem($itemPopup);
    $trayIcon->onClick = function() {       
        global $APPLICATION;
        // Показываем окно и выводим его поверх всех окон
        $APPLICATION->restore();
        $APPLICATION->toFront();
    };
    $trayIcon->onDblClick = function() {
        global $APPLICATION;
        // Показываем окно и выводим его поверх всех окон
        $APPLICATION->restore();
        $APPLICATION->toFront();
    };
    $trayIcon->onMouseDown = function() use($trayPopup) {
        if(get_key_state(2) < 0) $trayPopup->popup(cursor_pos_x(), cursor_pos_y());
    };
}
?>
Суть проста. В проекте при инициализации основного окна программы вызывается конструктор "_constructor()" который пытается найти в корне config.ini с содержимым такого вида:

Код: Выделить всё

[server]
trigger=http://server/trigger/index.php
Если файл не найден, вылетит предупреждение с описание, что необходимо сделать. После идет небольшая магия и создаётся второй поток задача которого взять на себя выполнение долгих операций (вся эта кухня нужна, чтобы не было "подвисания" основного окна программы). В потоке живет бесконечный цикл выполняющийся с паузами в 60 секунд. В теле цикла идет обращение к указанному файлу на сервере, сбор информации в формате JSON и перевод её в объектный вид с последующей модификацией (конвертирование UNIX времени в читаемый формат, перевод кодировки из UTF-8 в CP1251 и подмена макроса {HOST.NAME} на соответствующее имя). В случае обнаружения нового триггера запускается воспроизведение звукового файла находящегося по адресу /audio/alarm.ogg и происходит появление всплывающей подсказки в трее.

В общем то, вот и вся суть. Теперь немного картинок.
P1.png
P2.png
P2.png (9.51 КБ) 6316 просмотров
P3.png
P4.png
P4.png (12.22 КБ) 6316 просмотров
Я полностью открыт для Ваших плевков и негодования, но кому интересно, исходники и скомпилированная программа прикреплены к посту.
В архиве dvs.zip только исходник для DevelStudio, недостающие каталоги audio и icons можно взять из скомпилированного проекта: https://www.dropbox.com/s/v5af9qn3ofqof ... d.zip?dl=0
В архиве index.zip серверная часть.

Всем спасибо, все свободны.
Вложения
index.zip
(936 байт) 494 скачивания
dvs.zip
(77.56 КБ) 566 скачиваний
Kitsum
Сообщения: 15
Зарегистрирован: 01 окт 2014, 13:03
Откуда: далеко из-за мкада

Re: Мониторинг триггеров в GUI под Windows

Сообщение Kitsum »

Доброе время суток.

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

Задача поставлена, идем исполнять, но с начало забегу немного вперед и продемонстрирую текущий функционал.

Общее окно программы переработано и теперь отображает список всех групп из Zabbix с их хостами. Список активных триггеров переехал в нижнюю часть окна и отображает дополнительную информацию об узлах с которыми не все гладко. Сам список можно увеличивать, если все совсем плохо, или уменьшать если выражение "Хьюстон у нас проблемы" Вы слышите только в фильмах. Также появились цветные маркеры важности триггера. По двойному клику по активному триггеру мы перемещаемся в соответствующую группу где находится проблемный узел.
zti3.jpg
zti3.jpg (12.45 КБ) 6169 просмотров
zti4.jpg
Не забыли и про то, ради чего вся начальная песня и писалась - трей с всплывающими оповещениями и звуковым сигналом. Основное окно сворачивается именно туда, чтобы не залеживаться на панели где ценится каждый свободный пиксель.
zti1.jpg
Хосты в группах стали кликабельны, правда пока только ПКМ, и имеют всплывающее меню позволяющее выполнять банальные действия:
  • Подключение к узлу:
    • Telnet
      SSH
      RDP
      VNC
    Проверка соединения:
    • Ping
      Traceroute
Естественно его можно расширить, добавив всяких вкусняшек по мере возрастания аппетита IT отдела.
zti5.jpg
zti6.jpg
Программа по прежнему имеет серверную и клиентскую часть, что позволило организовать кэширование запросов и осуществлять оповещения в момент определения проблемы сервером, а не по таймеру обновления, как это организовано в dashboard zabbix. Раньше узнали о проблеме, раньше приступили к устранению. Естественно и устаревшие триггеры убираются также оперативно.
Также это позволило избавить программу от информации о доступе к серверу, что мне кажется не приемлемым для оповещалки. Чем меньше дырок, тем крепче сон.

Приступим к реализации.

Ну и естественно, Ваш покорный слуга еще не научился писать на C++ ничего более сложного чем "Hello World", поэтому клиент и сервер будет написан на PHP. За исключением того момента, что клиентская часть (при помощи магии Гарри Поттера) станет бинарным файлом. Бог с ним, серверная часть не доступна обывателю, а это самое важное!!!

Для начала, нужно придумать безопасный и понятный алгоритм не нагружающий сервер т.к клиентов может быть 100и или 1000и, всякое бывает. Все не безопасные команды обязаны выполняться на сервере без участия клиента. Дальнейшая работа клиента: обработка данных, выполнение действий с триггерами и т.п должно выполняться только у клиента, не затрагивая сервер.

1. При обращении клиента к серверу по http происходит проверка его ip на наличие в списках разрешенных. В противном случае показываем фигу.
2. Проверяем, наличие актуального кеша, и отдаём его.
3. Если кеша нет, генерируем новый.
4. Если запрос прилетел от zabbix: принудительно считаем кеш устаревшим, генерируем новый и после этого рассылаем всем клиентам оповещения по UDP протоколу. Это позволит свести расход ресурсов базы данных к нулю т.к все клиенты ринутся забирать новые данные, а все уже в кеше.

Супер, все дешево и сердито.

Серверная часть представляет из себя файл index.php где-то на Вашем web сервере. Актуальна для Zabbix 2.2.5

Код: Выделить всё

<?PHP
$cacheFile = '/tmp/zabbixTrigger_json';
$cacheInterval = 60;
$client = array(
    # Администраторы
    '10.10.10.10',
    # Дежурные
    '10.10.10.11',
    # Начальство
    '10.10.10.12',
);
$settings = array(
    'db'    => 'zabbix', 
    'login' => 'zabbix',
    'pass'  => 'zabbix'
);
# Определяем кто запустил скрипт, zabbix или хост из вне
$alarm = (substr(PHP_SAPI, 0, 3) == 'cli') ? true : false;
/*
if($alarm) {
    # Помечаем файл как актуальный
    if(!file_exists(__DIR__.$cacheFile)) file_put_contents(__DIR__.$cacheFile, '');
    else touch(__DIR__.$cacheFile, time() + $cacheInterval);
}
*/
# Не работаем с не зарегистрированными клиентами
if(!$alarm) {
    if(!in_array($_SERVER['REMOTE_ADDR'], $client)) exit();
    #file_put_contents(__DIR__.'/testLog', date("d.m.Y H:i").': '.print_r($argv, true)."\r\n", LOCK_EX | FILE_APPEND);
}
# Работа с БД
class db extends PDO
{
    # Начальные данные для подключения к серверу
    private $settings = array(
        'host'  => 'localhost',
        'db'    => '',
        'login' => 'root',
        'pass'  => '',
    );
    #
    function __construct($settings = array())
    {
        # Изменяем конфигурацию
        if(is_array($settings) and 0 != count($settings)) {
            foreach($settings as $key => $val) {
                if(isset($this->settings[$key])) $this->settings[$key] = $val;
            }
        }
        # Подключаемся к базе данных
        try {
            parent::__construct('mysql:host='.$this->settings['host'].';dbname='.$this->settings['db'].';charset=utf8', $this->settings['login'], $this->settings['pass']);
        }
        catch(PDOException $error) {
            exit(error($error->getMessage()));
        }
    }
}

if(!file_exists($cacheFile) or $cacheInterval < (time()-filemtime($cacheFile)) or $alarm) {
    $message = array();
    
    $db = new db($settings);
    $query = $db->query("
    SELECT
        t.triggerid,
        h.hostid,
        h.host,
        g.groupid,
        t.priority,
        t.description,
        t.lastchange,
        t.error,
        it.ip
    FROM
        triggers t,
        hosts h,
        items i,
        functions f,
        interface it,
        groups g,
        hosts_groups hg
    WHERE
        t.value=1 AND
        t.status=0 AND
        f.triggerid=t.triggerid AND
        i.itemid=f.itemid AND
        i.status=0 AND
        h.hostid=i.hostid AND
        h.status=0 AND
        h.hostid = it.hostid AND
        h.hostid = hg.hostid AND hg.groupid = g.groupid
    ORDER BY
        t.lastchange DESC
    ");
    if($query->rowCount()) {
        foreach($query->fetchAll(PDO::FETCH_ASSOC) as $var => $val) $message['trigger'][$val['triggerid']] = $val;
    }
    $query = $db->query("
    SELECT 
        h.hostid,
        h.host,
        h.name showname,
        g.name,
        g.groupid,
        i.ip
    FROM 
        hosts h,
        groups g,
        hosts_groups hg,
        interface i
    WHERE 
        h.status=0 and h.flags=0
        and
        h.hostid = hg.hostid and hg.groupid = g.groupid and h.hostid = i.hostid
    ORDER BY g.name ASC, h.name ASC
    ");
    if($query->rowCount()) {
        foreach($query->fetchAll(PDO::FETCH_ASSOC) as $var => $val) {
            $message['host'][$val['groupid']][$val['hostid']] = $val;
            if(!isset($message['group'][$val['groupid']])) $message['group'][$val['groupid']] = $val['name'];
        }
    }
    $query = $db->query("SELECT `sysmapid`, `name` FROM `sysmaps` ORDER BY sysmapid ASC");
    if($query->rowCount()) {
        foreach($query->fetchAll(PDO::FETCH_ASSOC) as $var => $val) {
            $message['map'][$val['sysmapid']] = $val['name'];
        }
    }
    file_put_contents($cacheFile, json_encode($message), LOCK_EX);
    if(!$alarm) chmod($cacheFile, 0666);
}
if(!$alarm) echo file_get_contents($cacheFile);
else {
    # Отсылаем администраторам предупреждение о событии
    $socket = socket_create(AF_INET, SOCK_DGRAM, GetProtoByName('udp'));
    foreach($client as $ip) {
        socket_connect($socket, $ip, 5055);
        socket_write($socket, 'trigger');
    }
    socket_close($socket);
}
?>
index.7z
(1.65 КБ) 507 скачиваний
Нас Интересуют настройки, я прокомментирую их отдельно. Внимательно читаем комментарии!

Код: Выделить всё

# Храним файл кеша вне web сервера!!!, желательно во временной директории вашей UNIX системы.
$cacheFile = '/tmp/zabbixTrigger_json';
# Время жизни кеша в секундах, по его истечении и только по запросу клиента кеш файл перезаписывается новыми данными (если не было сигнала от zabbix).
$cacheInterval = 60;
# Список разрешенных IP адресов, они же являются и списком для оповещения.
$client = array(
    # Администраторы
    '10.10.10.10',
    # Дежурные
    '10.10.10.11',
    # Начальство
    '10.10.10.12',
);
# Данные доступа к базе данных zabbix
$settings = array(
    'db'    => 'zabbix', 
    'login' => 'zabbix',
    'pass'  => 'zabbix'
);
Для дополнительной безопасности рекомендую создать отдельную учетную запись в базе данных с правами только на чтение и только к базе zabbix. Это актуально в наши дни. Сноуден проверит!

Переходи в zabbix

Первым делом необходимо создать новый скрипт: Администрирование -> Скрипты -> Создать скрипт
Суть скрипта в том, что при его вызове идет запуск php через cli и ему передаётся параметр в виде файла который необходимо обработать. Разумеется этот файл и есть наш index.php только логика его работы кардинально меняется т.к отсутствует apach в цепочке запуска и скрипт об этом узнает.
zti10.jpg
Теперь переходим в: Настройка -> Действия выбираем Источник событий -> Триггеры и жмем Создать действие
И, как Вы угадали, создаем действие, которое будет выполнено если узел не обслуживается и состояние триггера равно TRUE или FALSE
zti7.jpg
zti8.jpg
В разделе операции прописываем запуск нашего скрипта
zti9.jpg
Вуаля! Если Вы все сделали правильно (без кардинальных изменений), то я рад приветствовать нового ездока моего велосипеда. Поедем тандемом!

Но нужен клиент.
Вес 4.2mb
https://www.dropbox.com/s/huy2k16958kh0 ... .1.7z?dl=0

Для тех, кто читает титры в кинотеатре

При запуске будет создан файл config.ini в корне программы. Измените его настройки.

Код: Выделить всё

[server]
url="http://zabbix/zabbix-gui"
update="60"

[client]
showalarm="20"
Параметр url отвечает за http путь до скрипта на сервере, update за интервал (в секундах) обновления данных с сервера, а showalarm вроде как должен влиять на время отображения всплывающей подсказки в трее.

Теперь немного структуры.

1. В каталоге sounds находится звук оповещения о новом триггере, формат ogg
2. Каталог icons содержит маркеры триггеров. trigger00 - для нормального состояния, а все последующие соответствуют начальным настройкам zabbix во вкладке Администрирование -> Общие раздел Важность триггеров. Можете изменить на свои цвета, программа все подхватит при перезагрузки.
3. Ну и на закуску каталог ext, это как говорится для меломанов, содержит фри программы для обработки внешних действий клиента. Вы можете заменить их на свои при условии, что стандартные имена параметров запуска и их последовательность совпадают.

PS: На этом все, спасибо тем кого это заинтересовало. Предлагайте свои идеи или способы доработки велосипеда. А если найдется программист на C++ так милости просим.

PSS: Есть и другая версия клиента, позволяющая смотреть карты сети и графики узлов, но она требует немного больше ресурсов и велосипед перевода php в exe не позволяет показывать их очень плавно (постоянные промаргивания при изменении размера окна). Если есть желающие помочь перевести это хозяйство на C++ то я с радостью прыгну на хвост.
Ответить