Skip to main content

Диагностика кратковременных потерь ICMP между центральным филиалом и филиалом

Zabbix, лёгкий blackbox, action-диагностика и контроль ICMP pinger

1. Назначение инструкции

Инструкция описывает схему диагностики кратковременных срабатываний Zabbix вида:

Коммутатор филиала недоступен по ICMP

Задача - не просто получить факт недоступности, а разделить возможные причины:

  1. проблема маршрута, WAN или провайдера между центральным филиалом и филиалом;
  2. проблема самого центрального Zabbix: ICMP pinger, очередь проверок, перегрузка;
  3. проблема конкретного коммутатора, адреса, ACL или management VLAN;
  4. кратковременная ICMP-проблема, которую обычный action-скрипт может не успеть поймать.

В рамках данной схемы локальная проблема на стороне филиала считается маловероятной, если в момент отвалов в syslog коммутаторов нет событий link down/up, STP, loop, errdisable, reload, storm-control и других тревожных сообщений.

2. Термины

Термин Значение
Центральный филиал Площадка, где расположен центральный Zabbix / Zabbix-кластер
Филиал Удалённая площадка с коммутаторами
Шлюз филиала L3-шлюз, маршрутизатор или firewall, через который доступна сеть управления филиала
Коммутаторы филиала Коммутаторы, до которых центральный Zabbix выполняет ICMP-проверки
Blackbox Лёгкий независимый файловый логгер, который постоянно проверяет несколько контрольных адресов
Action-диагностика Диагностический скрипт, запускаемый Zabbix при срабатывании триггера
ICMP pinger Внутренний процесс Zabbix, выполняющий ICMP-проверки icmpping, icmppingloss, icmppingsec

3. Итоговая архитектура

Центральный филиал
|
+-- Zabbix server / cluster
|   +-- ICMP-проверки до коммутаторов филиала
|   +-- ICMP-проверка до шлюза филиала
|   +-- Zabbix internal items: icmp pinger / queue / poller
|   +-- информационные триггеры по состоянию Zabbix
|   +-- action-скрипт расширенной диагностики
|
+-- Blackbox-логгер
    +-- fping до шлюза филиала
    +-- fping до 1-2 ключевых коммутаторов филиала
    +-- fping до gateway центрального филиала
    +-- fping до контрольного адреса другого направления

Важное решение: blackbox не отправляет значения в Zabbix. Он работает как независимый файловый журнал. Это снижает сложность, не создаёт дополнительных trapper items и не смешивает диагностический журнал с основной системой мониторинга.

4. Логика работы

4.1. Что делает Zabbix

Zabbix штатно проверяет:

  1. шлюз филиала;
  2. коммутаторы филиала;
  3. контрольный адрес другого направления;
  4. состояние собственных ICMP pinger-процессов;
  5. очередь проверок;
  6. общую занятость poller и unreachable poller.

ICMP-проверки icmpping, icmppingloss, icmppingsec являются simple checks. Для них не нужен Zabbix agent на проверяемом устройстве, потому что проверку выполняет сам Zabbix server или proxy.

4.2. Что делает blackbox

Blackbox постоянно, например раз в 5 секунд, проверяет несколько контрольных IP и пишет результат в файл.

Blackbox нужен, чтобы после события можно было посмотреть, был ли реальный кратковременный провал связи в тот момент, когда Zabbix сообщил о недоступности коммутатора.

Blackbox не заменяет Zabbix. Он используется как независимое доказательство.

4.3. Что делает action-скрипт

Action-скрипт запускается только при аварии, например при недоступности шлюза филиала или ключевого коммутатора. Он собирает расширенную диагностику:

  1. ping до проблемного узла;
  2. ping до шлюза филиала;
  3. fping по контрольному списку;
  4. traceroute;
  5. mtr;
  6. состояние CPU/RAM/load;
  7. состояние сетевых интерфейсов;
  8. состояние процессов Zabbix/fping;
  9. последние значения Zabbix internal items через Zabbix API;
  10. фрагмент blackbox-лога за последние строки.

Важно: action-скрипт может опоздать, если реальная потеря длилась меньше времени срабатывания триггера. Поэтому используется гибрид: лёгкий blackbox фиксирует кратковременные провалы постоянно, а action-скрипт собирает расширенную диагностику только при событии.

5. Этап 1. Контроль шлюза филиала

5.1. Создать host в Zabbix

Пример имени:

FILIAL-GW

Интерфейс:

IP address: <IP_ШЛЮЗА_ФИЛИАЛА>

Шаблон:

Template Module ICMP Ping

или собственный шаблон ICMP.

5.2. Основные ICMP items

Минимально нужны:

icmpping
icmppingloss
icmppingsec

Пример ключа:

icmpping[,5,200,56,1000]

Расшифровка параметров:

Параметр Значение
5 Количество ICMP-запросов в одной проверке
200 Интервал между запросами, мс
56 Размер ICMP-пакета
1000 Timeout, мс

5.3. Триггер недоступности шлюза филиала

Пример выражения:

max(/FILIAL-GW/icmpping[,5,200,56,1000],#3)=0

Рекомендуемое имя:

[HIGH] Шлюз филиала недоступен по ICMP из центрального филиала

Смысл: последние 3 проверки ICMP до шлюза филиала были неуспешны.

6. Этап 2. Зависимости триггеров

Для каждого коммутатора филиала триггер:

Коммутатор филиала недоступен по ICMP

должен зависеть от триггера:

Шлюз филиала недоступен по ICMP

Логика:

Если недоступен шлюз филиала, не нужно создавать пачку вторичных аварий по каждому коммутатору филиала.

Рекомендуемая цепочка:

Коммутатор филиала недоступен
    зависит от
Шлюз филиала недоступен

7. Этап 3. Контроль состояния Zabbix ICMP pinger

7.1. Зачем это нужно

Если Zabbix сообщает, что коммутатор филиала недоступен, нужно понять:

Zabbix действительно увидел потерю связи
или
сам Zabbix был перегружен и не успевал выполнять ICMP-проверки.

Для этого создаются internal items на host:

Zabbix server

Тип item:

Zabbix internal

7.2. Обязательные internal items

Название Ключ Тип Ед. Интервал
Zabbix ICMP pinger avg busy zabbix[process,"icmp pinger",avg,busy] Numeric float % 1m
Zabbix ICMP pinger max busy zabbix[process,"icmp pinger",max,busy] Numeric float % 1m
Zabbix ICMP pinger count zabbix[process,"icmp pinger",count] Numeric unsigned   5m
Zabbix queue over 1m zabbix[queue,1m,] Numeric unsigned   1m
Zabbix queue over 5m zabbix[queue,5m,] Numeric unsigned   1m
Zabbix required performance zabbix[requiredperformance] Numeric float vps 1m

7.3. Что означает каждый item

zabbix[process,"icmp pinger",avg,busy]

Показывает среднюю занятость всех ICMP pinger-процессов.

Если значение низкое, например 10-20%, в среднем pinger-процессы не перегружены. Но этот item может скрыть ситуацию, когда один процесс занят почти полностью, а остальные свободны.

zabbix[process,"icmp pinger",max,busy]

Самый важный показатель для данной задачи.

Он показывает занятость самого загруженного ICMP pinger-процесса. Если значение 90-100%, значит хотя бы один ICMP pinger близок к насыщению.

zabbix[process,"icmp pinger",count]

Показывает количество запущенных ICMP pinger-процессов. Это косвенно связано с параметром StartPingers в конфигурации Zabbix server.

Если значение равно 1, а ICMP-проверок много, один pinger может стать узким местом.

zabbix[queue,1m,]

Показывает количество элементов данных, которые задержаны в очереди более чем на 1 минуту.

Если одновременно с этим появляются ICMP-аварии, возможна проблема не в филиале, а в производительности Zabbix.

zabbix[queue,5m,]

Показывает элементы, задержанные более чем на 5 минут. Это уже серьёзный признак деградации мониторинга.

zabbix[requiredperformance]

Показывает требуемую производительность Zabbix в values per second. Сам по себе этот item не является аварией, но полезен для анализа роста нагрузки.

7.4. Желательные internal items

Название Ключ Интервал
Zabbix poller avg busy zabbix[process,poller,avg,busy] 1m
Zabbix poller max busy zabbix[process,poller,max,busy] 1m
Zabbix unreachable poller avg busy zabbix[process,"unreachable poller",avg,busy] 1m
Zabbix unreachable poller max busy zabbix[process,"unreachable poller",max,busy] 1m

Они нужны, чтобы не смотреть только на ICMP pinger, а видеть общую деградацию процессов Zabbix.

8. Информационные триггеры по состоянию Zabbix

Эти триггеры не заменяют основные аварии по ICMP. Их задача - предупреждать, что в момент ICMP-аварии сам Zabbix мог быть перегружен.

8.1. ICMP pinger загружен более 75%

Поле Значение
Имя [INFO] Zabbix ICMP pinger загружен более 75%
Severity Information
Выражение avg(/Zabbix server/zabbix[process,"icmp pinger",max,busy],5m)>75
Смысл Самый загруженный ICMP pinger в течение 5 минут был занят более чем на 75%. При одновременных ICMP-авариях нужно учитывать возможную задержку проверок.

8.2. ICMP pinger близок к перегрузке

Поле Значение
Имя [WARNING] Zabbix ICMP pinger близок к перегрузке
Severity Warning
Выражение avg(/Zabbix server/zabbix[process,"icmp pinger",max,busy],5m)>90
Смысл ICMP pinger стабильно загружен. Результаты ICMP-проверок в этот период нельзя считать полностью достоверным доказательством проблемы канала.

8.3. Есть очередь проверок более 1 минуты

Поле Значение
Имя [INFO] В Zabbix есть очередь проверок более 1 минуты
Severity Information
Выражение avg(/Zabbix server/zabbix[queue,1m,],5m)>10
Смысл Zabbix начал отставать по выполнению проверок. При одновременных ICMP-алертах нужно проверить состояние Zabbix server.

8.4. Есть очередь проверок более 5 минут

Поле Значение
Имя [WARNING] В Zabbix есть очередь проверок более 5 минут
Severity Warning
Выражение last(/Zabbix server/zabbix[queue,5m,])>0
Смысл Признак деградации мониторинга. Если в этот момент появляются ICMP-проблемы, они могут быть следствием задержек на стороне Zabbix.

8.5. Количество ICMP pinger-процессов меньше ожидаемого

Пример, если ожидаем минимум 3 процесса:

last(/Zabbix server/zabbix[process,"icmp pinger",count])<3

Рекомендуемое имя:

[INFO] Количество ICMP pinger-процессов меньше ожидаемого

Смысл: проверить параметр StartPingers в конфигурации Zabbix server. Возможно, количество ICMP pinger-процессов недостаточно для текущего количества ICMP-проверок.

9. Этап 4. Лёгкий blackbox без отправки данных в Zabbix

9.1. Назначение blackbox

Blackbox нужен для независимой фиксации кратковременных ICMP-провалов.

Он не должен быть тяжёлым:

не запускать постоянно mtr;
не запускать постоянно traceroute;
не проверять все коммутаторы филиала;
не делать snmpwalk;
не отправлять значения в Zabbix.

Постоянно выполняется только лёгкая ICMP-проверка 3-5 адресов.

9.2. Установка пакетов

sudo apt update
sudo apt install -y fping mtr-tiny traceroute jq curl

Пакеты:

Пакет Зачем нужен
fping Лёгкая пакетная ICMP-проверка нескольких адресов
mtr-tiny Диагностика маршрута по событию
traceroute Разовый трассировщик маршрута по событию
jq Формирование и разбор JSON при обращении к Zabbix API
curl HTTP-запросы к Zabbix API

9.3. Файл целей blackbox

Файл:

/etc/zabbix/blackbox_targets.conf

Содержимое с подробными комментариями:

# Файл целей для лёгкого blackbox-логгера.
# Формат строки:
# <короткое_имя> <IP-адрес>
#
# Короткое имя используется в логах.
# IP-адрес используется для ICMP-проверки.
#
# Пустые строки и строки, начинающиеся с #, игнорируются.

# Шлюз филиала.
# Главная контрольная точка: если недоступен шлюз филиала,
# то проблема, скорее всего, не в одном конкретном коммутаторе.
branch_gw        10.44.100.1

# Ключевой коммутатор филиала.
# Нужен для сравнения с событиями Zabbix по коммутаторам.
branch_core_sw   10.44.100.10

# Один обычный access-коммутатор филиала.
# Нужен как дополнительная точка, чтобы понять:
# падает один адрес или направление до филиала целиком.
branch_access_sw 10.44.100.20

# Gateway центрального филиала.
# Нужен для проверки, не просела ли локальная сеть центрального филиала.
central_gw       10.x.x.1

# Контрольный адрес другого направления.
# Нужен для проверки, проблема только с этим филиалом или шире.
other_direction  10.y.y.1

9.4. Скрипт blackbox

Файл:

/usr/local/sbin/ping-blackbox.sh

Скрипт с подробными комментариями:

#!/bin/bash

# =====================================================================
# ping-blackbox.sh
# =====================================================================
# Назначение:
#   Лёгкий независимый ICMP blackbox-логгер.
#   Скрипт постоянно проверяет небольшой список контрольных адресов
#   и пишет результат в файловый лог.
#
# Важно:
#   - Скрипт НЕ отправляет данные в Zabbix.
#   - Скрипт НЕ запускает mtr/traceroute постоянно.
#   - Скрипт НЕ проверяет все коммутаторы филиала.
#   - Основная задача - зафиксировать кратковременный провал связи.
#
# Рекомендуемый запуск:
#   Через systemd service от пользователя zabbix.
# =====================================================================

# Файл со списком целей.
# В каждой рабочей строке ожидается два поля:
#   1) короткое имя цели;
#   2) IP-адрес цели.
TARGETS_FILE="/etc/zabbix/blackbox_targets.conf"

# Каталог для основного лога blackbox.
LOG_DIR="/var/log/zabbix/blackbox"

# Каталог для хранения последнего состояния каждой цели.
# Здесь будут небольшие служебные файлы вида branch_gw.state.
STATE_DIR="/var/lib/zabbix/blackbox-state"

# Основной лог blackbox.
LOG_FILE="${LOG_DIR}/blackbox.log"

# Интервал между циклами проверки всех целей.
# Для минутных отвалов достаточно 5 секунд.
# Если нужно ловить очень короткие провалы, можно уменьшить до 1 секунды.
INTERVAL_SEC=5

# Даже если состояние не меняется, пишем контрольную строку раз в минуту.
# Это помогает понять, что сам blackbox в это время работал.
CONTROL_LOG_EVERY_SEC=60

# Создаём каталоги, если они ещё не существуют.
mkdir -p "$LOG_DIR" "$STATE_DIR"

# Если файла целей нет, пишем ошибку и завершаемся.
# Без списка целей дальнейшая работа бессмысленна.
if [ ! -f "$TARGETS_FILE" ]; then
    echo "$(date -Is) ERROR targets file not found: $TARGETS_FILE" >> "$LOG_FILE"
    exit 1
fi

# Основной бесконечный цикл.
# Скрипт работает постоянно, пока его не остановит systemd или администратор.
while true; do
    # Текущее время в двух форматах:
    # NOW_EPOCH нужен для расчёта интервалов;
    # NOW_ISO нужен для читаемого лога.
    NOW_EPOCH="$(date +%s)"
    NOW_ISO="$(date -Is)"

    # Читаем файл целей построчно.
    while read -r NAME IP; do
        # Пропускаем пустые строки.
        [[ -z "$NAME" ]] && continue

        # Пропускаем комментарии.
        [[ "$NAME" =~ ^# ]] && continue

        # Если IP не указан, строка считается некорректной и пропускается.
        [[ -z "$IP" ]] && continue

        # Файл с последним состоянием конкретной цели: OK / FAIL / UNKNOWN.
        STATE_FILE="${STATE_DIR}/${NAME}.state"

        # Файл с временем последней записи в лог по данной цели.
        LAST_LOG_FILE="${STATE_DIR}/${NAME}.lastlog"

        # По умолчанию считаем, что предыдущее состояние неизвестно.
        PREV_STATE="UNKNOWN"

        # Если файл состояния уже есть, читаем предыдущее состояние из него.
        [ -f "$STATE_FILE" ] && PREV_STATE="$(cat "$STATE_FILE")"

        # По умолчанию считаем, что контрольная строка ещё ни разу не писалась.
        LAST_LOG_EPOCH=0

        # Если файл времени последней записи есть, читаем значение.
        [ -f "$LAST_LOG_FILE" ] && LAST_LOG_EPOCH="$(cat "$LAST_LOG_FILE")"

        # Выполняем один ICMP-запрос через fping.
        # -C 1      - один запрос к цели;
        # -q        - тихий режим, итог пишется компактно;
        # -t 1000   - timeout 1000 мс.
        #
        # fping часто пишет результат в stderr, поэтому 2>&1 обязателен.
        RESULT="$(fping -C 1 -q -t 1000 "$IP" 2>&1)"
        RC=$?

        # Если код возврата 0, цель ответила.
        if [ "$RC" -eq 0 ]; then
            CURRENT_STATE="OK"

            # Пример строки fping:
            # 10.44.100.1 : 18.40
            # Извлекаем значение после двоеточия и убираем пробелы.
            RTT_MS="$(echo "$RESULT" | awk -F':' '{gsub(/ /,"",$2); print $2}')"

            # Если по какой-то причине RTT не распознан, пишем unknown.
            [ -z "$RTT_MS" ] && RTT_MS="unknown"
        else
            # Любой ненулевой код возврата считаем неуспешной ICMP-проверкой.
            CURRENT_STATE="FAIL"
            RTT_MS="timeout"
        fi

        # Флаг необходимости записи в лог.
        NEED_LOG=0

        # Записываем в лог при изменении состояния.
        # Это главное событие: OK -> FAIL или FAIL -> OK.
        if [ "$CURRENT_STATE" != "$PREV_STATE" ]; then
            NEED_LOG=1
        fi

        # Рассчитываем, сколько секунд прошло с последней контрольной записи.
        AGE=$((NOW_EPOCH - LAST_LOG_EPOCH))

        # Даже если состояние не менялось, раз в минуту пишем контрольную строку.
        # Это подтверждает, что blackbox в этот период был запущен.
        if [ "$AGE" -ge "$CONTROL_LOG_EVERY_SEC" ]; then
            NEED_LOG=1
        fi

        # Если запись нужна, пишем строку в лог и обновляем служебные файлы.
        if [ "$NEED_LOG" -eq 1 ]; then
            echo "${NOW_ISO} ${NAME} ${IP} ${CURRENT_STATE} rtt_ms=${RTT_MS} prev=${PREV_STATE}" >> "$LOG_FILE"
            echo "$CURRENT_STATE" > "$STATE_FILE"
            echo "$NOW_EPOCH" > "$LAST_LOG_FILE"
        fi

    done < "$TARGETS_FILE"

    # Пауза до следующего цикла проверки всех целей.
    sleep "$INTERVAL_SEC"
done

9.5. Права на blackbox

sudo chmod +x /usr/local/sbin/ping-blackbox.sh
sudo mkdir -p /var/log/zabbix/blackbox /var/lib/zabbix/blackbox-state
sudo chown -R zabbix:zabbix /var/log/zabbix/blackbox /var/lib/zabbix/blackbox-state

10. systemd service для blackbox

Файл:

/etc/systemd/system/ping-blackbox.service

Содержимое с комментариями:

# =====================================================================
# ping-blackbox.service
# =====================================================================
# Назначение:
#   Сервис systemd для постоянного запуска лёгкого ICMP blackbox-логгера.
# =====================================================================

[Unit]
# Человекочитаемое описание сервиса.
Description=Lightweight ICMP blackbox logger

# Запускать сервис после появления сети.
After=network-online.target

# Явно просим systemd дождаться network-online.target.
Wants=network-online.target

[Service]
# Тип simple подходит для обычного foreground-скрипта.
Type=simple

# Запускаем от пользователя zabbix, чтобы не использовать root без необходимости.
User=zabbix

# Группа также zabbix, чтобы были права на каталоги /var/log/zabbix и /var/lib/zabbix.
Group=zabbix

# Основной исполняемый файл сервиса.
ExecStart=/usr/local/sbin/ping-blackbox.sh

# Автоматически перезапускать сервис при аварийном завершении.
Restart=always

# Пауза перед перезапуском, чтобы не получить бесконечный быстрый restart-loop.
RestartSec=5

[Install]
# Запускать сервис в обычном multi-user режиме.
WantedBy=multi-user.target

Команды запуска:

sudo systemctl daemon-reload
sudo systemctl enable --now ping-blackbox.service
sudo systemctl status ping-blackbox.service

Проверка лога:

tail -f /var/log/zabbix/blackbox/blackbox.log

11. logrotate для blackbox

Файл:

/etc/logrotate.d/ping-blackbox

Содержимое с комментариями:

# Ротация логов лёгкого blackbox-логгера.
/var/log/zabbix/blackbox/*.log {
    # Ротировать каждый день.
    daily

    # Хранить 30 архивных ротаций.
    rotate 30

    # Сжимать старые логи.
    compress

    # Не выдавать ошибку, если лог-файла нет.
    missingok

    # Не ротировать пустой файл.
    notifempty

    # Копировать текущий лог в архив и обнулять исходный файл.
    # Это удобно для простого bash-скрипта, который постоянно пишет в один файл.
    copytruncate
}

Проверка конфигурации logrotate:

sudo logrotate -d /etc/logrotate.d/ping-blackbox

12. Конфигурация доступа к Zabbix API для action-скрипта

Для добавления в action-лог последних значений internal items используется Zabbix API.

Файл:

/etc/zabbix/netdiag_api.conf

Содержимое с комментариями:

# =====================================================================
# netdiag_api.conf
# =====================================================================
# Назначение:
#   Конфигурация доступа action-скрипта к Zabbix API.
#   Используется только для чтения последних значений internal items.
#
# Важно:
#   Файл содержит API token. Нужно ограничить права доступа.
# =====================================================================

# URL API вашего Zabbix frontend.
# Обычно это адрес вида:
# https://zabbix.example.local/api_jsonrpc.php
ZBX_API_URL="https://zabbix.example.local/api_jsonrpc.php"

# API token пользователя Zabbix с минимально необходимыми правами на чтение.
# Рекомендуется отдельный технический пользователь или отдельный API token.
ZBX_API_TOKEN="PASTE_API_TOKEN_HERE"

# Техническое имя host в Zabbix, на котором созданы internal items.
# Важно: использовать именно техническое имя host, а не только visible name.
ZBX_INTERNAL_HOST="Zabbix server"

Права:

sudo chown root:zabbix /etc/zabbix/netdiag_api.conf
sudo chmod 640 /etc/zabbix/netdiag_api.conf

Если API-вставка пока не нужна, этот файл можно не создавать. Action-скрипт должен корректно пропустить блок Zabbix internal state.

13. Action-диагностика по событию

13.1. Когда запускать action-скрипт

Рекомендуемый основной запуск:

Шлюз филиала недоступен по ICMP

Почему лучше запускать по шлюзу:

Если падает весь филиал, не нужно запускать отдельную диагностику по каждому коммутатору.

Дополнительно можно запускать по ключевому коммутатору, если шлюз филиала доступен, но конкретный коммутатор недоступен.

13.2. Action-скрипт

Файл:

/usr/local/sbin/zabbix-netdiag.sh

Скрипт с подробными комментариями:

#!/bin/bash

# =====================================================================
# zabbix-netdiag.sh
# =====================================================================
# Назначение:
#   Диагностический скрипт для запуска из Zabbix action.
#   Собирает сетевой и системный снимок в момент ICMP-проблемы.
#
# Что собирает:
#   - ping до проблемного узла;
#   - ping до шлюза филиала;
#   - fping по контрольному списку blackbox;
#   - traceroute до проблемного узла;
#   - mtr до проблемного узла;
#   - нагрузку ОС;
#   - статистику сетевых интерфейсов;
#   - процессы zabbix/fping/mtr/traceroute;
#   - последние строки blackbox-лога;
#   - последние значения Zabbix internal items через API, если настроен API.
#
# Ожидаемые параметры:
#   $1 - Zabbix EVENT.ID
#   $2 - Zabbix HOST.NAME
#   $3 - IP или DNS проверяемого узла, обычно {HOST.CONN}
#   $4 - IP шлюза филиала, обычно {$BRANCH_GW}
#
# Пример запуска из Zabbix:
#   /usr/local/sbin/zabbix-netdiag.sh "{EVENT.ID}" "{HOST.NAME}" "{HOST.CONN}" "{$BRANCH_GW}"
# =====================================================================

# Строгий режим не включаем через set -e,
# потому что часть диагностических команд может завершаться с ошибкой.
# Например, ping может вернуть timeout - это ожидаемая диагностическая ситуация.

EVENT_ID="$1"
HOST_NAME="$2"
TARGET_IP="$3"
BRANCH_GW="$4"

# Каталог для диагностических логов.
OUT_DIR="/var/log/zabbix/netdiag"

# Файл со списком целей blackbox.
TARGETS_FILE="/etc/zabbix/blackbox_targets.conf"

# Основной blackbox-лог.
BLACKBOX_LOG="/var/log/zabbix/blackbox/blackbox.log"

# Конфигурация доступа к Zabbix API.
API_CONF="/etc/zabbix/netdiag_api.conf"

# Формируем timestamp для имени файла.
TS="$(date +%Y%m%d_%H%M%S)"

# Безопасно чистим имя host для использования в имени файла.
# Заменяем потенциально проблемные символы на подчёркивание.
SAFE_HOST_NAME="$(echo "$HOST_NAME" | sed -E 's/[^A-Za-z0-9_.-]+/_/g')"

# Итоговый файл диагностики.
OUT_FILE="${OUT_DIR}/${TS}_event_${EVENT_ID}_${SAFE_HOST_NAME}.log"

# Создаём каталог для логов, если он отсутствует.
mkdir -p "$OUT_DIR"

# ---------------------------------------------------------------------
# Функция: section
# Назначение:
#   Печатать красивый заголовок раздела в диагностический лог.
# ---------------------------------------------------------------------
section() {
    echo
    echo "===== $1 ====="
}

# ---------------------------------------------------------------------
# Функция: run_cmd
# Назначение:
#   Выполнить диагностическую команду и записать:
#   - саму команду;
#   - stdout/stderr;
#   - код возврата.
#
# Почему это важно:
#   Если команда завершилась ошибкой, это тоже диагностическая информация.
# ---------------------------------------------------------------------
run_cmd() {
    echo "+ $*"
    "$@"
    local RC=$?
    echo "Command exit code: ${RC}"
    echo
}

# ---------------------------------------------------------------------
# Функция: load_api_config
# Назначение:
#   Загрузить параметры Zabbix API из /etc/zabbix/netdiag_api.conf.
#
# Если файла нет, API-блок будет пропущен, а остальная диагностика выполнится.
# ---------------------------------------------------------------------
load_api_config() {
    if [ -f "$API_CONF" ]; then
        # shellcheck disable=SC1090
        . "$API_CONF"
        return 0
    fi

    return 1
}

# ---------------------------------------------------------------------
# Функция: zbx_api_call
# Назначение:
#   Выполнить JSON-RPC запрос к Zabbix API.
#
# Требования:
#   - установлен curl;
#   - установлен jq;
#   - задан ZBX_API_URL;
#   - задан ZBX_API_TOKEN.
# ---------------------------------------------------------------------
zbx_api_call() {
    local PAYLOAD="$1"

    curl -sk \
        -H 'Content-Type: application/json-rpc' \
        -d "$PAYLOAD" \
        "$ZBX_API_URL"
}

# ---------------------------------------------------------------------
# Функция: zbx_get_hostid
# Назначение:
#   Получить hostid по техническому имени host Zabbix.
#
# Важно:
#   ZBX_INTERNAL_HOST должен совпадать с техническим именем host,
#   на котором созданы internal items.
# ---------------------------------------------------------------------
zbx_get_hostid() {
    local PAYLOAD

    PAYLOAD="$(jq -n \
        --arg token "$ZBX_API_TOKEN" \
        --arg host "$ZBX_INTERNAL_HOST" \
        '{
            jsonrpc: "2.0",
            method: "host.get",
            params: {
                output: ["hostid", "host", "name"],
                filter: { host: [$host] }
            },
            auth: $token,
            id: 1
        }')"

    zbx_api_call "$PAYLOAD" | jq -r '.result[0].hostid // empty'
}

# ---------------------------------------------------------------------
# Функция: zbx_get_item_last_by_key
# Назначение:
#   Получить последнее значение item по hostid и точному key_.
#
# На выходе печатается строка:
#   <key> = <lastvalue>, lastclock=<timestamp>
# ---------------------------------------------------------------------
zbx_get_item_last_by_key() {
    local HOSTID="$1"
    local ITEM_KEY="$2"
    local PAYLOAD RESPONSE NAME VALUE CLOCK CLOCK_HUMAN

    PAYLOAD="$(jq -n \
        --arg token "$ZBX_API_TOKEN" \
        --arg hostid "$HOSTID" \
        --arg key "$ITEM_KEY" \
        '{
            jsonrpc: "2.0",
            method: "item.get",
            params: {
                output: ["itemid", "name", "key_", "lastvalue", "lastclock"],
                hostids: [$hostid],
                filter: { key_: [$key] },
                limit: 1
            },
            auth: $token,
            id: 1
        }')"

    RESPONSE="$(zbx_api_call "$PAYLOAD")"

    NAME="$(echo "$RESPONSE" | jq -r '.result[0].name // empty')"
    VALUE="$(echo "$RESPONSE" | jq -r '.result[0].lastvalue // empty')"
    CLOCK="$(echo "$RESPONSE" | jq -r '.result[0].lastclock // empty')"

    if [ -n "$CLOCK" ] && [ "$CLOCK" != "0" ]; then
        CLOCK_HUMAN="$(date -d "@$CLOCK" -Is 2>/dev/null || echo "$CLOCK")"
    else
        CLOCK_HUMAN="unknown"
    fi

    if [ -z "$VALUE" ]; then
        echo "${ITEM_KEY} = NOT_FOUND_OR_EMPTY"
    else
        echo "${ITEM_KEY} = ${VALUE}, lastclock=${CLOCK_HUMAN}, name=${NAME}"
    fi
}

# ---------------------------------------------------------------------
# Функция: write_zabbix_internal_state
# Назначение:
#   Записать в лог последние значения Zabbix internal items.
#
# Если API не настроен, блок будет аккуратно пропущен.
# ---------------------------------------------------------------------
write_zabbix_internal_state() {
    section "ZABBIX INTERNAL STATE"

    if ! command -v jq >/dev/null 2>&1; then
        echo "jq not installed; skip Zabbix API block"
        return
    fi

    if ! command -v curl >/dev/null 2>&1; then
        echo "curl not installed; skip Zabbix API block"
        return
    fi

    if ! load_api_config; then
        echo "API config not found: $API_CONF"
        echo "Skip Zabbix internal state block"
        return
    fi

    if [ -z "$ZBX_API_URL" ] || [ -z "$ZBX_API_TOKEN" ] || [ -z "$ZBX_INTERNAL_HOST" ]; then
        echo "API config is incomplete; skip Zabbix internal state block"
        return
    fi

    local HOSTID
    HOSTID="$(zbx_get_hostid)"

    if [ -z "$HOSTID" ]; then
        echo "Cannot find Zabbix host by technical name: $ZBX_INTERNAL_HOST"
        echo "Check ZBX_INTERNAL_HOST in $API_CONF"
        return
    fi

    echo "Zabbix API URL: $ZBX_API_URL"
    echo "Zabbix internal host: $ZBX_INTERNAL_HOST"
    echo "Zabbix internal hostid: $HOSTID"
    echo

    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,"icmp pinger",avg,busy]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,"icmp pinger",max,busy]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,"icmp pinger",count]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[queue,1m,]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[queue,5m,]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[requiredperformance]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,poller,avg,busy]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,poller,max,busy]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,"unreachable poller",avg,busy]'
    zbx_get_item_last_by_key "$HOSTID" 'zabbix[process,"unreachable poller",max,busy]'
}

# ---------------------------------------------------------------------
# Основная запись диагностического файла.
# Весь вывод внутри блока перенаправляется в OUT_FILE.
# ---------------------------------------------------------------------
{
    echo "===== NETDIAG START ====="
    echo "Date: $(date -Is)"
    echo "Event ID: ${EVENT_ID}"
    echo "Host name: ${HOST_NAME}"
    echo "Target IP: ${TARGET_IP}"
    echo "Branch gateway: ${BRANCH_GW}"
    echo "Output file: ${OUT_FILE}"

    section "PING TARGET"
    if [ -n "$TARGET_IP" ]; then
        run_cmd ping -c 20 -i 0.2 -W 1 "$TARGET_IP"
    else
        echo "TARGET_IP is empty; skip ping target"
    fi

    section "PING BRANCH GATEWAY"
    if [ -n "$BRANCH_GW" ]; then
        run_cmd ping -c 20 -i 0.2 -W 1 "$BRANCH_GW"
    else
        echo "BRANCH_GW is empty; skip ping branch gateway"
    fi

    section "FPING CONTROL TARGETS"
    if [ -f "$TARGETS_FILE" ]; then
        echo "+ fping control targets from $TARGETS_FILE"
        awk 'NF >= 2 && $1 !~ /^#/ {print $2}' "$TARGETS_FILE" | fping -C 5 -q -t 1000 2>&1
        echo "Command exit code: $?"
    else
        echo "Targets file not found: $TARGETS_FILE"
    fi

    section "TRACEROUTE TARGET"
    if [ -n "$TARGET_IP" ]; then
        run_cmd traceroute -n -w 1 -q 3 "$TARGET_IP"
    else
        echo "TARGET_IP is empty; skip traceroute"
    fi

    section "MTR TARGET"
    if [ -n "$TARGET_IP" ]; then
        run_cmd mtr -r -c 30 -n "$TARGET_IP"
    else
        echo "TARGET_IP is empty; skip mtr"
    fi

    section "OS LOAD"
    run_cmd uptime

    section "MEMORY"
    run_cmd free -m

    section "TOP SNAPSHOT"
    top -b -n 1 | head -n 30

    section "NETWORK INTERFACES STATS"
    run_cmd ip -s link

    section "ZABBIX / FPING PROCESSES"
    ps -eo pid,comm,%cpu,%mem,etime,args | grep -E 'zabbix|fping|mtr|traceroute' | grep -v grep

    section "BLACKBOX LAST 200 LINES"
    if [ -f "$BLACKBOX_LOG" ]; then
        tail -n 200 "$BLACKBOX_LOG"
    else
        echo "Blackbox log not found: $BLACKBOX_LOG"
    fi

    write_zabbix_internal_state

    section "NETDIAG END"
    echo "Date: $(date -Is)"
} >> "$OUT_FILE" 2>&1

# Печатаем путь к файлу в stdout.
# Это удобно, если Zabbix action или ручной запуск сохраняет вывод команды.
echo "$OUT_FILE"

13.3. Права на action-скрипт

sudo chmod +x /usr/local/sbin/zabbix-netdiag.sh
sudo mkdir -p /var/log/zabbix/netdiag
sudo chown -R zabbix:zabbix /var/log/zabbix/netdiag

14. Настройка Zabbix action

14.1. Макрос шлюза филиала

На уровне host group филиала или конкретного host создать макрос:

{$BRANCH_GW}=10.44.100.1

14.2. Команда запуска

Пример команды:

/usr/local/sbin/zabbix-netdiag.sh "{EVENT.ID}" "{HOST.NAME}" "{HOST.CONN}" "{$BRANCH_GW}"

14.3. Рекомендуемое условие запуска

Основной вариант:

Trigger name contains: Шлюз филиала недоступен

или через tag:

scope=netdiag
site=<имя_филиала>

14.4. Почему не запускать по каждому коммутатору

Если недоступен весь филиал, Zabbix может создать много событий по коммутаторам. Если на каждое событие запускать mtr, traceroute, ping, это создаст лишнюю нагрузку и шум.

Поэтому рекомендуемая логика:

1. Основной action запускать по недоступности шлюза филиала.
2. По отдельному коммутатору запускать только если шлюз филиала доступен.
3. Для массовых событий использовать зависимости триггеров.

15. Как анализировать результаты

15.1. Вероятная проблема WAN / провайдер / маршрут

Признаки:

Zabbix: шлюз филиала недоступен.
Zabbix: несколько коммутаторов филиала недоступны.
Blackbox: branch_gw FAIL.
Blackbox: central_gw OK.
Blackbox: other_direction OK.
Zabbix icmp pinger max busy низкий.
Zabbix queue 1m = 0.
Zabbix queue 5m = 0.
Syslog коммутаторов филиала чистый.

Вывод:

Наиболее вероятна проблема между центральным филиалом и филиалом:
WAN / провайдер / маршрутизация / firewall.

15.2. Вероятная проблема самого Zabbix

Признаки:

Zabbix: коммутаторы филиала недоступны.
Zabbix: шлюз филиала недоступен.
Blackbox не подтверждает потерю направления.
Zabbix icmp pinger max busy = 90-100%.
Zabbix queue 1m растёт.
Zabbix queue 5m больше 0.

Вывод:

В этот период сам Zabbix находился в деградации.
ICMP-аварию нельзя считать чистым доказательством проблемы канала.
Сначала проверить производительность Zabbix server / кластера.

15.3. Вероятная проблема центрального филиала

Признаки:

Blackbox: branch_gw FAIL.
Blackbox: other_direction FAIL.
Blackbox: central_gw FAIL или нестабилен.

Вывод:

Проблема шире одного филиала.
Проверить сеть центрального филиала, gateway, uplink, маршрутизацию, firewall.

15.4. Вероятная проблема конкретного коммутатора

Признаки:

Zabbix: один коммутатор филиала недоступен.
Zabbix: шлюз филиала доступен.
Другие коммутаторы филиала доступны.
Blackbox: branch_gw OK.
Zabbix pinger не перегружен.
Queue нет.

Вывод:

Проблема ближе к конкретному коммутатору, его адресу, ACL, management VLAN или обработке ICMP на самом устройстве.

16. Рекомендуемая последовательность внедрения

16.1. Минимальный этап

  1. Создать host FILIAL-GW.
  2. Добавить ICMP-проверки до шлюза филиала.
  3. Настроить зависимости: коммутаторы филиала зависят от шлюза филиала.
  4. Создать internal items:
    • zabbix[process,"icmp pinger",avg,busy];
    • zabbix[process,"icmp pinger",max,busy];
    • zabbix[process,"icmp pinger",count];
    • zabbix[queue,1m,];
    • zabbix[queue,5m,];
    • zabbix[requiredperformance].
  5. Создать информационные триггеры по pinger и queue.

16.2. Blackbox

  1. Установить fping, mtr-tiny, traceroute, jq, curl.
  2. Создать /etc/zabbix/blackbox_targets.conf.
  3. Создать /usr/local/sbin/ping-blackbox.sh.
  4. Создать systemd service.
  5. Настроить logrotate.
  6. Проверить, что лог пишется и фиксирует смену состояния.

16.3. Action-диагностика

  1. Создать /usr/local/sbin/zabbix-netdiag.sh.
  2. Настроить запуск по триггеру недоступности шлюза филиала.
  3. Передать в скрипт EVENT.ID, HOST.NAME, HOST.CONN и BRANCH_GW.
  4. Сохранять диагностические логи в /var/log/zabbix/netdiag/.
  5. При необходимости настроить /etc/zabbix/netdiag_api.conf для чтения последних значений internal items через API.
  6. Добавить в уведомление путь к диагностическому файлу или команду для просмотра последних логов.

17. Проверка работоспособности

17.1. Проверить blackbox

sudo systemctl status ping-blackbox.service
sudo tail -f /var/log/zabbix/blackbox/blackbox.log

Ожидаемый пример строк:

2026-05-25T12:10:00+03:00 branch_gw 10.44.100.1 OK rtt_ms=18.4 prev=UNKNOWN
2026-05-25T12:15:25+03:00 branch_gw 10.44.100.1 FAIL rtt_ms=timeout prev=OK
2026-05-25T12:16:03+03:00 branch_gw 10.44.100.1 OK rtt_ms=20.1 prev=FAIL

17.2. Проверить action-скрипт вручную

sudo -u zabbix /usr/local/sbin/zabbix-netdiag.sh "manual-test" "TEST-HOST" "10.44.100.10" "10.44.100.1"

Проверить созданный лог:

ls -lh /var/log/zabbix/netdiag/
tail -n 100 /var/log/zabbix/netdiag/*.log

17.3. Проверить Zabbix API-блок

Проверить, что файл доступен пользователю zabbix:

sudo -u zabbix test -r /etc/zabbix/netdiag_api.conf && echo OK

Проверить ручной запуск action-скрипта и убедиться, что в логе появился раздел:

===== ZABBIX INTERNAL STATE =====

18. Важные замечания по Zabbix HA / кластеру

Если Zabbix работает в HA-режиме, нужно разделять:

1. Zabbix internal items - состояние активного Zabbix server как приложения.
2. OS-level monitoring каждой ноды - CPU/RAM/Disk/Network конкретной VM или сервера.

Рекомендуемая структура:

Zabbix server application
    internal items:
    - icmp pinger busy
    - queue
    - requiredperformance

Zabbix node 01
    agent items:
    - CPU
    - RAM
    - disk I/O
    - network interface

Zabbix node 02
    agent items:
    - CPU
    - RAM
    - disk I/O
    - network interface

Иначе можно увидеть, что Zabbix-приложение вроде бы работает нормально, но не заметить проблему конкретной ноды, сетевого интерфейса или дисковой подсистемы.

19. Итоговая логика системы

Zabbix ICMP
    обнаруживает проблему.

Шлюз филиала
    показывает, проблема в одном коммутаторе или во всём направлении.

Dependencies
    подавляют вторичные аварии по коммутаторам.

Zabbix internal items
    показывают, был ли сам Zabbix способен нормально выполнять ICMP-проверки.

Information triggers
    предупреждают о возможной деградации Zabbix.

Blackbox
    независимо фиксирует кратковременные провалы в файловом логе.

Action-скрипт
    собирает расширенную диагностику при аварии.

Главный результат: после очередного события будет не просто сообщение:

Коммутатор филиала недоступен

а диагностическая картина:

  1. шлюз филиала был доступен или недоступен;
  2. несколько коммутаторов филиала падали одновременно или нет;
  3. центральный gateway был доступен или нет;
  4. другое направление было доступно или нет;
  5. ICMP pinger Zabbix был перегружен или нет;
  6. очередь Zabbix была или нет;
  7. blackbox подтвердил кратковременный провал или нет;
  8. mtr / traceroute показали проблему на маршруте или нет.

По этой картине можно обоснованно разделить:

коммутатор / филиал / WAN-провайдер / центральный филиал / сам Zabbix.