
Bash: Пишем универсальный скрипт проверки доступности хостов
Приветствую!
Сегодня напишем полезный Bash скрипт🧑💻, который будет выполнять различные проверки доступности хостов в сети🌐. В качестве примера покажу, как выполнять проверку связи с помощью утилиты ping
🏓 и запускать трассировку при её потери⚡. Разумеется, с сохранением вывода в журнал📑.
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐. |
Немного предыстории📜. Данный скрипт я написал, когда мы с коллегой решали одну проблему: на одном из серверов Linux, в рандомные моменты, кратковременно терялась сетевая связь с перечнем хостов. Одним из способов диагностики, который мы настроили: оперативное выполнение трассировки к проблемным узлам в момент их недоступности с помощью скрипта.
Скрипт check_hosts.sh
Этот скрипт представляет собой универсальный инструмент мониторинга доступности, который работает перманентно и в асинхронном режиме выполняет ряд действий, а именно:
- 1️⃣запускает необходимые проверки;
- 2️⃣при обнаружении проблем (после заданного количества неудачных попыток) запускает диагностическую (или любую другую) команду;
- 3️⃣при восстановлении доступности также запускает отдельную команду.
Сущность хост тут условная. Скрипт позволяет удобно настроить любые проверки, в случае не успеха которых будет запускать нужное действие. Таким образом можно не только проверять сетевую доступность, но отслеживать процессы ОС, парсить логи и т.д.
В моём примере скрипт:
- выполняет
ping
списка хостов; - в случае недоступности для проблемного хоста выполняет команду трассировки:
mtr
в режиме отчета-wb
; - в случае восстановления просто выводится текст «Пример команды восстановления для <хост>».
Скрипт также поддерживает логирование в 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 скрипты🐚.