Files
cpca-tools/restart_services.sh
T
2026-09-20 11:25:21 +03:00

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