Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
367 lines
9.6 KiB
Bash
367 lines
9.6 KiB
Bash
#!/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 <<EOF
|
|
Использование:
|
|
|
|
$SCRIPT_NAME -u USER -c CONFIG
|
|
|
|
Параметры:
|
|
|
|
-u USER SSH пользователь
|
|
-c CONFIG файл конфигурации (host;service1,service2,...)
|
|
|
|
Пример:
|
|
|
|
$SCRIPT_NAME -u admin -c ./services.csv
|
|
|
|
EOF
|
|
}
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Arguments
|
|
# ----------------------------------------------------------------------
|
|
|
|
while getopts ":u:c:h" opt; do
|
|
case "$opt" in
|
|
u)
|
|
SSH_USER="$OPTARG"
|
|
;;
|
|
c)
|
|
CONFIG_FILE="$OPTARG"
|
|
;;
|
|
h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
\?)
|
|
echo "Неизвестный параметр: -$OPTARG" >&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 </dev/null; then
|
|
echo "ERROR: systemd unit not found: $SERVICE"
|
|
FAILED=1
|
|
continue
|
|
fi
|
|
|
|
echo "Restarting $SERVICE..."
|
|
|
|
if ! systemctl restart "$SERVICE" </dev/null; then
|
|
echo "ERROR: failed to restart $SERVICE"
|
|
FAILED=1
|
|
continue
|
|
fi
|
|
|
|
# Небольшая пауза перед проверкой
|
|
sleep 2
|
|
|
|
if systemctl is-active --quiet "$SERVICE"; then
|
|
echo "SUCCESS: $SERVICE is active"
|
|
else
|
|
echo "ERROR: $SERVICE is not active after restart"
|
|
|
|
systemctl status "$SERVICE" \
|
|
--no-pager \
|
|
--full </dev/null 2>&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
|