#!/usr/bin/env bash set -u set -o pipefail SCRIPT_NAME="$(basename "$0")" LOG_FILE="./restart_services.log" SSH_USER="" CONFIG_FILE="" # ---------------------------------------------------------------------- # Logging # ---------------------------------------------------------------------- log() { local level="$1" shift printf '[%s] [%s] %s\n' \ "$(date '+%Y-%m-%d %H:%M:%S')" \ "$level" \ "$*" | tee -a "$LOG_FILE" } # ---------------------------------------------------------------------- # Usage # ---------------------------------------------------------------------- usage() { cat <&2 usage exit 1 ;; :) echo "Для параметра -$OPTARG требуется значение." >&2 usage exit 1 ;; esac done if [[ -z "$SSH_USER" || -z "$CONFIG_FILE" ]]; then usage exit 1 fi if [[ ! -f "$CONFIG_FILE" ]]; then echo "Ошибка: файл конфигурации не найден: $CONFIG_FILE" >&2 exit 1 fi # ---------------------------------------------------------------------- # Dependencies # ---------------------------------------------------------------------- for cmd in ssh base64 mktemp date tee grep sed; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "Ошибка: не найдена команда '$cmd'" >&2 exit 1 fi done # ---------------------------------------------------------------------- # Temp files / cleanup # ---------------------------------------------------------------------- TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT # ---------------------------------------------------------------------- # SSH # # -tt обязательно: sudo на удалённом хосте запрашивает пароль # через настоящий TTY. Пароль SSH спрашивает сам ssh. # # Локальный stdin остаётся терминалом и используется только # для ввода паролей. Удалённый скрипт передаётся в командной # строке (base64) и не занимает stdin. # ---------------------------------------------------------------------- SSH_OPTS=( -tt -o ConnectTimeout=15 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 ) # run_remote_script HOST LOCAL_SCRIPT # Выполняет локальный файл скрипта на хосте под "sudo bash". run_remote_script() { local host="$1" local script="$2" local b64 b64=$(base64 -w0 "$script") ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \ "t=\$(mktemp) && printf '%s' '$b64' | base64 -d > \"\$t\" && sudo bash \"\$t\"; rc=\$?; rm -f \"\$t\"; exit \$rc" } # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } # ---------------------------------------------------------------------- # Restart services # ---------------------------------------------------------------------- restart_services() { local host="$1" local services="$2" log INFO "[$host] Подготовка к перезапуску служб" local remote_script="$TMP_DIR/remote_$$.sh" local host_out="$TMP_DIR/out_$$.log" # Список служб встраивается в скрипт (heredoc), stdin не используется { cat <<'REMOTE_SCRIPT' #!/bin/bash set -u set -o pipefail if [[ "$EUID" -ne 0 ]]; then echo "ERROR: скрипт должен выполняться от root (sudo)" exit 10 fi SERVICES=$(cat <<'SERVICES_LIST' REMOTE_SCRIPT printf '%s\n' "$services" cat <<'REMOTE_SCRIPT' SERVICES_LIST ) FAILED=0 IFS="," read -ra SERVICE_LIST <<< "$SERVICES" for SERVICE in "${SERVICE_LIST[@]}"; do # Убираем пробелы SERVICE="${SERVICE#"${SERVICE%%[![:space:]]*}"}" SERVICE="${SERVICE%"${SERVICE##*[![:space:]]}"}" if [[ -z "$SERVICE" ]]; then continue fi echo "--------------------------------------------------" echo "Service: $SERVICE" # Проверяем наличие unit if ! systemctl cat "$SERVICE" >/dev/null 2>&1 &1 || true FAILED=1 fi done echo "--------------------------------------------------" if [[ "$FAILED" -ne 0 ]]; then echo "RESULT=FAILED" exit 1 fi echo "RESULT=SUCCESS" exit 0 REMOTE_SCRIPT } > "$remote_script" # ------------------------------------------------------------------ # Выполнение. # # Вывод удалённого скрипта идёт через tee одновременно в лог # и во временный файл (для проверки результата). tee не буферизует # вывод, поэтому приглашение sudo (без перевода строки) видно сразу. # ------------------------------------------------------------------ : > "$host_out" local rc=0 run_remote_script "$host" "$remote_script" 2>&1 | tee -a "$LOG_FILE" "$host_out" || rc=$? # pty на удалённой стороне добавляет CR sed -i 's/\r$//' "$host_out" rm -f "$remote_script" if [[ "$rc" -eq 255 ]]; then log ERROR "[$host] Ошибка SSH-соединения" return 1 fi if [[ "$rc" -eq 0 ]] && grep -q '^RESULT=SUCCESS$' "$host_out"; then log INFO "[$host] Все службы успешно перезапущены" return 0 fi log ERROR "[$host] Не все службы были успешно перезапущены (rc=$rc)" return 1 } # ---------------------------------------------------------------------- # Main # ---------------------------------------------------------------------- log INFO "==============================================" log INFO "Запуск $SCRIPT_NAME" log INFO "SSH user: $SSH_USER" log INFO "Config: $CONFIG_FILE" log INFO "==============================================" TOTAL=0 SUCCESS=0 FAILED=0 # ---------------------------------------------------------------------- # Читаем конфигурацию в массивы заранее: во время ssh stdin должен # оставаться терминалом (ввод паролей), а не файлом конфигурации. # ---------------------------------------------------------------------- HOSTS=() HOST_SERVICES=() while IFS=';' read -r host services || [[ -n "$host" ]]; do # Убираем BOM из первой строки host="${host#$'\xef\xbb\xbf'}" host="$(trim "$host")" services="$(trim "$services")" # Пустая строка [[ -z "$host" ]] && continue # Комментарий [[ "$host" == \#* ]] && continue # Проверяем конфигурацию if [[ -z "$services" ]]; then log ERROR "[$host] Не указан список служб" ((FAILED++)) ((TOTAL++)) continue fi HOSTS+=("$host") HOST_SERVICES+=("$services") ((TOTAL++)) done < "$CONFIG_FILE" for i in ${HOSTS[@]+"${!HOSTS[@]}"}; do host="${HOSTS[$i]}" services="${HOST_SERVICES[$i]}" log INFO "----------------------------------------------" log INFO "Host: $host" log INFO "Services: $services" if restart_services "$host" "$services"; then ((SUCCESS++)) else ((FAILED++)) fi done # ---------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------- log INFO "==============================================" log INFO "Результат выполнения" log INFO "Всего хостов : $TOTAL" log INFO "Успешно : $SUCCESS" log INFO "Ошибок : $FAILED" log INFO "Log : $LOG_FILE" log INFO "==============================================" # Убираем CR, попавшие в лог из удалённого TTY sed -i 's/\r$//' "$LOG_FILE" if [[ $FAILED -gt 0 ]]; then exit 1 fi exit 0