Bash: Пишем универсальный скрипт проверки доступности хостов

Bash: Пишем универсальный скрипт проверки доступности хостов

Приветствую!

Сегодня напишем полезный Bash скрипт🧑‍💻, который будет выполнять различные проверки доступности хостов в сети🌐. В качестве примера покажу, как выполнять проверку связи с помощью утилиты ping🏓 и запускать трассировку при её потери⚡. Разумеется, с сохранением вывода в журнал📑.

Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.

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

Скрипт check_hosts.sh

Этот скрипт представляет собой универсальный инструмент мониторинга доступности, который работает перманентно и в асинхронном режиме выполняет ряд действий, а именно:

  • 1️⃣запускает необходимые проверки;
  • 2️⃣при обнаружении проблем (после заданного количества неудачных попыток) запускает диагностическую (или любую другую) команду;
  • 3️⃣при восстановлении доступности также запускает отдельную команду.

Сущность хост тут условная. Скрипт позволяет удобно настроить любые проверки, в случае не успеха которых будет запускать нужное действие. Таким образом можно не только проверять сетевую доступность, но отслеживать процессы ОС, парсить логи и т.д.

В моём примере скрипт:

  1. выполняет ping списка хостов;
  2. в случае недоступности для проблемного хоста выполняет команду трассировки: mtr в режиме отчета -wb;
  3. в случае восстановления просто выводится текст «Пример команды восстановления для <хост>».

Скрипт также поддерживает логирование в stdout, в файл или в syslog, может работать как через Systemd, так и самостоятельно, а также предотвращает повторный запуск с помощью файла блокировки (flock).

Ниже собственно, сам скрипт📑:

#!/usr/bin/env bash

# Параметры безопасности работы скрипта
set -Eeuo pipefail

# =============================================================
# ========== НАЧАЛО СЕКЦИИ ПОЛЬЗОВАТЕЛЬСКИХ НАСТРОЕК ==========

# Явное определение PATH
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"

# Запускать скрипт с помощью Systemd
SYSTEMD_USAGE=false

# Параметры логирования
LOG_TO_STDOUT=true    # просто в stdout
LOG_TO_FILE=false     # лог в файл (<имя_скрипта>.log)
LOG_TO_SYSLOG=false   # лог в syslog (тег=<имя_скрипта>)

# Параметры проверки
CHECK_INTERVAL=5     # задержка между проверками
CHECK_THRESHOLD=3    # количество не успешных попыток
CHECK_HOSTS=(        # список проверяемых хостов
    "r4ven.me"
    "arena.r4ven.me"
    "192.168.122.1"
    "1.1.1.1"
    "8.8.8.8"
)
CHECK_UTILS=("ping" "mtr") # используемые утилиты (проверяется их наличие)

# Команда проверки
check_cmd() { timeout 6 ping -c 1 -W 5 "${1-}" &> /dev/null; }
# Команда, запускаемая после не успешных $CHECK_THRESHOLD попыток
fail_cmd() { 
    fail_cmd_result=$(mtr --report-wide --show-ips "${1-}")

    echo "[${1-}]: Fail command output:"
    echo "----------------------------------"
    echo "$fail_cmd_result"
    echo "----------------------------------"
}
# Команда, запускаемая после восстановления доступности
restore_cmd() { echo "Пример команды восстановления для ${1-}"; }

# ========== КОНЕЦ СЕКЦИИ ПОЛЬЗОВАТЕЛЬСКИХ НАСТРОЕК ==========
# ============================================================

# Служебные переменные
SCRIPT_PID=$$
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd -P)
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
SCRIPT_LOG="${SCRIPT_DIR}/${SCRIPT_NAME%.*}.log"
SCRIPT_LOG_PREFIX='[%Y-%m-%d %H:%M:%S.%3N]'
SCRIPT_LOCK="${SCRIPT_DIR}/${SCRIPT_NAME%.*}.lock"
SYSTEMD_SERVICE="${SCRIPT_NAME%.*}.service"


# Очистки при срабатывании обработчиков
cleanup() {
    trap - SIGINT SIGTERM SIGHUP SIGQUIT ERR EXIT

    [[ -n "${fd_lock-}" ]] && exec {fd_lock}>&-

    if [[ -f "$SCRIPT_LOCK" && $(< "$SCRIPT_LOCK") == "$$" ]]; then
        rm -f "$SCRIPT_LOCK"
    fi

    if [[ -n "${monitor_pids-}" ]]; then
        kill -9 "${monitor_pids[@]}" 2> /dev/null || true
    fi
}

trap cleanup SIGINT SIGTERM SIGHUP SIGQUIT ERR EXIT


# Предотвращение повторного запуска экземпляра скрипта
exec {fd_lock}>> "${SCRIPT_LOCK}"

if ! flock -n "$fd_lock"; then
    echo "Экземпляр скрипта уже запущен, выход..."
    exit 1
fi

echo "$SCRIPT_PID" > "$SCRIPT_LOCK"


# Логирование вывода
log_pipe() {
    while IFS= read -r line; do
        log_line="$(date +"${SCRIPT_LOG_PREFIX}") - $line"
        if [[ "${LOG_TO_STDOUT}" == "true" ]]; then echo "$log_line"; fi
        if [[ "${LOG_TO_FILE}" == "true" ]]; then echo "$log_line" >> "$SCRIPT_LOG"; fi
        if [[ "${LOG_TO_SYSLOG}" == "true" ]]; then logger -t "${SCRIPT_NAME}" -- "$line"; fi
    done
}

exec > >(log_pipe) 2>&1


# Проверка наличия используемых утилит
for util in "${CHECK_UTILS[@]}"; do
    if ! which "$util" &> /dev/null; then
        echo "Ошибка: утилита $util не установлена"
        exit 1
    fi
done


# Настройка запуска скрипта с помощью Systemd
if [[ "$SYSTEMD_USAGE" == "true" ]]; then
    # проверка root полномочий
    if [[ $EUID -ne 0 ]]; then
      echo "Пожалуйста выполните запуск от имени root"
      exit 1
    fi
    
    # проверка, был ли скрипт запущен через Systemd
    if [[ $PPID -ne 1 ]]; then
      if [[ ! -f /etc/systemd/system/"$SYSTEMD_SERVICE" ]]; then
        cat << EOF > /etc/systemd/system/"${SYSTEMD_SERVICE}"
[Unit]
Description=$SCRIPT_NAME
After=network-online.target
Wants=network-online.target

[Service]
Restart=on-failure
RestartSec=5
ExecStart=$SCRIPT_DIR/$SCRIPT_NAME

[Install]
WantedBy=multi-user.target
EOF
        systemctl daemon-reload
        systemctl enable "$SYSTEMD_SERVICE"
        systemctl start "$SYSTEMD_SERVICE"
        exit 0
      else
        systemctl start "$SYSTEMD_SERVICE"
        exit 0
      fi
    fi
fi


# Функция мониторинга доступности
monitor_host() {
    local host="${1-}"
    local check_count=0
    local is_failed=0  # 0 - хост доступен, 1 - хост недоступен
    
    echo "Запуск проверки доступности $host"
    
    while true; do  # бесконечный цикл
        if check_cmd "$host"; then  # запуск команды проверки доступности
            if [[ "$is_failed" -eq 1 ]]; then  # действия при восстановления после недоступности
                echo "[$host]: Доступность восстановлена"
                echo "[$host]: Запуск команды в случае восстановления..."

                restore_cmd "$host" || true

                is_failed=0     # сброс флага недоступности
                check_count=0   # сброс счетчика 
                
            else
                check_count=0   # хост доступен, сбрасываем счетчик
            fi
        else  # действия в случае недоступности
            ((++check_count))  # увеличение счетчика

            echo "[$host]: Неудачная проверка доступности ($check_count/$CHECK_THRESHOLD)"
            
            if [[ "$check_count" -ge "$CHECK_THRESHOLD" && "$is_failed" -eq 0 ]]; then  # пороговые действия
                echo "[$host]: Запуск команды в случае недоступности..."

                fail_cmd "$host" || true

                is_failed=1     # устанавливаем флаг недоступности

                sleep $CHECK_INTERVAL  # задержка перед следующей проверкой
            fi
        fi
        
        sleep $CHECK_INTERVAL   # ожидание перед следующей итерацией цикла
    done
}


# Вывод списка проверяемых хостов
echo "Мониторинг доступности запущен для следующих хостов:"
echo "${CHECK_HOSTS[@]}"

# Запуск мониторинга для каждого хоста в отдельном процессе
declare -a monitor_pids=()

for host in "${CHECK_HOSTS[@]}"; do
    monitor_host "$host" &
    monitor_pids+=("$!")
done

# Ожидание завершения всех фоновых процессов (фактически бесконечно)
wait

💡Скрипт также доступен в моём репозитории на GitHub.

Хватит многабукав, перейдем лучше к практике 🖥️.

Демонстрация работы

Скачиваем скрипт, например, в ~/.local/bin и делаем его исполняемым:

curl --create-dirs -fsSL https://raw.githubusercontent.com/r4ven-me/bash/main/check_hosts.sh \
    --output ~/.local/bin/check_hosts

chmod +x ~/.local/bin/check_hosts

💡Про права на файлы в Linux мы подробнее говорили тут.

Перед использованием необходимо задать свои значения. Открываем скрипт на редактирование любым удобным редактором, например, Neovim:

nvim ~/.local/bin/check_hosts

Тут необходимо задать следующие переменные под свои нужды:

  • SYSTEMD_USAGE — флаг (true|false), указывает, запускать ли скрипт как systemd-сервис;
  • LOG_TO_STDOUT — флаг (true|false), определяет, выводить ли логи в стандартный вывод;
  • LOG_TO_FILE — флаг (true|false), определяет, нужно ли сохранять логи в файл, расположенный в той же директории, что и скрипт;
  • LOG_TO_SYSLOG — флаг (true|false), включает отправку логов в системный журнал с помощью logger;
  • CHECK_INTERVAL — интервал между проверками доступности хоста (в секундах);
  • CHECK_THRESHOLD — количество неудачных проверок подряд, после которых хост считается недоступным и запускается команда fail_cmd();
  • CHECK_HOSTS — массив IP-адресов/доменов/прочих элементов, которые нужно проверять;
  • CHECK_UTILS — массив утилит (например, ping, ssh, curl, nc), используемых для проверки доступности (скрипт проверяет их наличие в системе);

И соответственно команды:

  • check_cmd() — функция, бесконечно выполняющая проверку доступности хоста;
  • fail_cmd() — функция, вызываемая один раз (до сброса счетчика) при переходе хоста в состояние недоступности (например, отправка уведомления, перезапуск сервиса);
  • restore_cmd() — функция, вызываемая один раз (до сброса счетчика) при восстановлении доступности хоста (также, например, уведомление, запуск восстановительных действий т.д.).

Демонстрация работы скрипта:

check_hosts

Тут видно, что хост arena.r4ven.me (из $CHECK_HOSTS) был недоступен, выполнилась трассировка, а после его восстановления запустилась соответствующая команда (вывод сообщения💬).

Всё работает🙃.

Запуск с помощью Systemd

Для работы скрипта в качестве демона Linux, предусмотрена возможность запуска с помощью системы инициализации Systemd.

Все необходимые настройки для такого запуска уже есть в скрипте. Для активации необходимо открыть скрипт:

nvim ~/.local/bin/check_hosts

И установить переменную SYSTEMD_USAGE в значение true, сохранить, закрыть и запустить скрипт от имени суперпользователя root, например, с помощью sudo:

💡При необходимости скорректируйте содержимое файла-юнита: блок here-doc (cat << EOF).

sudo ~/.local/bin/check_hosts

sudo systemctl status check_hosts

💡По умолчанию включён автозапуск Systemd сервиса при старте ОС.

Смотреть вывод скрипта можно в системном журнале:

sudo journalctl -fu check_hosts

Другие примеры использования

Ниже приведены некоторые примеры использования скрипта check_hosts.sh. Как уже ранее говорилось, необходимо лишь задать свои параметры/команды.

Вариант №1 — проверка доступности URL и выполнение перезапуска веб сервера:

CHECK_HOSTS=("r4ven.me" "arena.r4ven.me" "192.168.122.150")
CHECK_UTILS=("curl" "ssh")
check_cmd() {
    [[ $(curl -w "%{http_code}" -o /dev/null -fsSL https://"${1-}"/status) -eq 200 ]]
}
fail_cmd() { 
    ssh \
    -o UserKnownHostsFile=/dev/null \
    -o StrictHostKeyChecking=no \
    -i "${HOME}"/.ssh/id_ed25519_web \
    -l ivan \
    -p 2222 \
    "${1-}" \
    sudo systemctl restart nginx
}
restore_cmd() {
    ssh \
    -o UserKnownHostsFile=/dev/null \
    -o StrictHostKeyChecking=no \
    -i "${HOME}"/.ssh/id_ed25519_web \
    -l ivan \
    -p 2222 \
    "${1-}" \
    systemctl status nginx
}

Вариант №2 — проверка доступности TCP порта и отправка данных в Zabbix:

CHECK_HOSTS=("r4ven.me" "arena.r4ven.me" "192.168.122.150")
CHECK_UTILS=("nc" "zabbix_sender")
check_cmd() { nc -w 5 -z "${1-}" 443; }
fail_cmd() {
    zabbix_sender \
    -c /etc/zabbix/zabbix_agent2.conf \
    -k 'site.status' \
    -o 0
}
restore_cmd() {
    zabbix_sender \
    -c /etc/zabbix/zabbix_agent2.conf \
    -k 'site.status' \
    -o 1
}

Вариант №3 — проверка состояния Docker контейнера и отправка уведомлений по Email с помощью консольного SMTP клиента msmtp:

CHECK_HOSTS=("unbound" "pi-hole" "openconnect")
CHECK_UTILS=("docker" "msmtp")
check_cmd() {
    [[ $(docker inspect --format='{{.State.Health.Status}}' unbound 2> /dev/null "${1-}") != "healthy" ]]
}
fail_cmd() {
    echo "Subject: Docker status\n\nContainer ${1-} is unhealthy" | msmtp kar-kar@r4ven.me
}
restore_cmd() {
    echo "Subject: Docker status\n\nContainer ${1-} is healthy again" | msmtp kar-kar@r4ven.me
}

Надеюсь материал оказался вам полезным😇. Другие посты по теме программирования на языке оболочки можно найти в разделе: Shell скрипты🐚.

Не забывайте про нашу телегу📱и чат💬
Всех благ✌️

That should be it. If not, check the logs 🙂

Подписаться
Уведомить о
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии