Files
ssh-monitor/update_ssh_monitor.sh
T
PTah 3a1c9655ca feat: exclusive pilot — spool flush and verify script
Replay spool on each monitor loop; sac_post_payload refactor;
pilot-verify-exclusive.sh and --check-sac hints for exclusive mode.
2026-05-27 13:25:47 +10:00

626 lines
24 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# === Конфигурация скрипта ===
UPDATE_DIR="/opt/scripts/update"
SCRIPT_NAME="ssh-monitor"
REPO_URL="https://git.kalinamall.ru/PapaTramp/ssh-monitor.git"
LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor"
LOG_FILE="/var/log/update_script.log"
# Режим отладки: 1 = включён, 0 = выключен
DEBUG=0
DEPLOY_MODE=0
CONFIG_FILE="/etc/ssh-monitor.conf"
DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh"
SAC_SPOOL_DIR_DEFAULT="/var/lib/ssh-monitor/sac-spool"
SYSTEMD_UNIT_PATH="/etc/systemd/system/ssh-monitor.service"
# === Список переменных, объявленных в скрипте (для логирования при DEBUG=1) ===
SCRIPT_VARS=("UPDATE_DIR" "SCRIPT_NAME" "REPO_URL" "LOCAL_SCRIPT_PATH" "LOG_FILE" "DEBUG" "REMOTE_SCRIPT_PATH" "LOCAL_CHECKSUM" "REMOTE_CHECKSUM")
# === Функции ===
say_step() {
printf '\n==> %s\n' "$1"
}
say_ok() {
printf '[OK] %s\n' "$1"
log_message "INFO" "$1"
}
say_warn() {
printf '[WARN] %s\n' "$1"
log_message "INFO" "WARNING: $1"
}
say_info() {
printf ' %s\n' "$1"
}
show_help() {
cat <<'EOF'
update_ssh_monitor.sh — обновление или первичная установка ssh-monitor
Использование:
sudo ./update_ssh_monitor.sh обновление (git pull + checksum)
sudo ./update_ssh_monitor.sh --deploy первичная установка / полный deploy
sudo ./first_deploy.sh то же, что --deploy
Опции:
--deploy установить ssh-monitor, sac-client, пример конфига, systemd unit
--debug подробный лог в /var/log/update_script.log
-h, --help эта справка
После --deploy отредактируйте /etc/ssh-monitor.conf (Telegram и/или SAC).
EOF
}
parse_command_line() {
DEPLOY_MODE=0
while [ $# -gt 0 ]; do
case "$1" in
--deploy)
DEPLOY_MODE=1
shift
;;
--debug)
DEBUG=1
shift
;;
-h | --help)
show_help
exit 0
;;
*)
printf 'Неизвестная опция: %s\n' "$1" >&2
show_help >&2
exit 2
;;
esac
done
}
deploy_require_root() {
if [ "$DEPLOY_MODE" -ne 1 ]; then
return 0
fi
if [ "$(id -u)" -ne 0 ]; then
printf 'ERROR: режим --deploy нужно запускать от root (sudo)\n' >&2
exit 1
fi
}
# Основная функция логирования
log_message() {
local level="$1"
local message="$2"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
# Создаём лог-файл и директорию при необходимости
if [ ! -f "$LOG_FILE" ]; then
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
touch "$LOG_FILE" 2>/dev/null
fi
if [ "$level" = "INFO" ]; then
echo "$timestamp INFO: $message" >> "$LOG_FILE"
elif [ "$level" = "DEBUG" ] && [ "$DEBUG" -eq 1 ]; then
echo "$timestamp DEBUG: $message" >> "$LOG_FILE"
elif [ "$level" = "ERROR" ]; then
echo "$timestamp ERROR: $message" >> "$LOG_FILE"
fi
}
# Функция для логирования шагов с комментариями (работает только при DEBUG=1)
debug_step() {
if [ "$DEBUG" -eq 1 ]; then
log_message "DEBUG" "# $1"
fi
}
# Функция для логирования только объявленных в скрипте переменных
log_script_vars() {
if [ "$DEBUG" -eq 1 ]; then
log_message "DEBUG" "=== Значения переменных скрипта ==="
for var_name in "${SCRIPT_VARS[@]}"; do
local var_value="${!var_name}"
log_message "DEBUG" "$var_name='$var_value'"
done
log_message "DEBUG" "=== Конец списка переменных ==="
fi
}
# === КРИТИЧЕСКАЯ ФУНКЦИЯ: Проверка и установка ipset ===
ensure_ipset_installed() {
debug_step "Проверяем наличие ipset в системе"
# Проверяем, установлен ли ipset
if ! command -v ipset &>/dev/null; then
log_message "INFO" "ipset not found. Attempting silent installation..."
debug_step "ipset не обнаружен, начинаем установку в silent режиме"
# Определяем пакетный менеджер и устанавливаем ipset
if command -v apt-get &>/dev/null || command -v apt &>/dev/null; then
debug_step "Обнаружен Debian/Ubuntu, используем apt"
log_message "INFO" "Detected apt package manager, installing ipset..."
# Тихая установка через apt
if apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq ipset >/dev/null 2>&1; then
log_message "INFO" "ipset installed successfully via apt"
debug_step "ipset успешно установлен через apt"
else
log_message "ERROR" "Failed to install ipset via apt"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через apt"
return 1
fi
elif command -v yum &>/dev/null; then
debug_step "Обнаружен RHEL/CentOS, используем yum"
log_message "INFO" "Detected yum package manager, installing ipset..."
if yum install -y -q ipset >/dev/null 2>&1; then
log_message "INFO" "ipset installed successfully via yum"
debug_step "ipset успешно установлен через yum"
else
log_message "ERROR" "Failed to install ipset via yum"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через yum"
return 1
fi
elif command -v dnf &>/dev/null; then
debug_step "Обнаружен Fedora/RHEL 8+, используем dnf"
log_message "INFO" "Detected dnf package manager, installing ipset..."
if dnf install -y -q ipset >/dev/null 2>&1; then
log_message "INFO" "ipset installed successfully via dnf"
debug_step "ipset успешно установлен через dnf"
else
log_message "ERROR" "Failed to install ipset via dnf"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через dnf"
return 1
fi
elif command -v pacman &>/dev/null; then
debug_step "Обнаружен Arch Linux, используем pacman"
log_message "INFO" "Detected pacman package manager, installing ipset..."
if pacman -S --noconfirm --quiet ipset >/dev/null 2>&1; then
log_message "INFO" "ipset installed successfully via pacman"
debug_step "ipset успешно установлен через pacman"
else
log_message "ERROR" "Failed to install ipset via pacman"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через pacman"
return 1
fi
elif command -v zypper &>/dev/null; then
debug_step "Обнаружен openSUSE, используем zypper"
log_message "INFO" "Detected zypper package manager, installing ipset..."
if zypper --quiet --non-interactive install ipset >/dev/null 2>&1; then
log_message "INFO" "ipset installed successfully via zypper"
debug_step "ipset успешно установлен через zypper"
else
log_message "ERROR" "Failed to install ipset via zypper"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через zypper"
return 1
fi
else
log_message "ERROR" "Unsupported package manager. Cannot install ipset"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: Пакетный менеджер не распознан"
return 1
fi
# Проверяем, что ipset действительно установился
if ! command -v ipset &>/dev/null; then
log_message "ERROR" "ipset installation verification failed"
log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset"
debug_step "КРИТИЧЕСКАЯ ОШИБКА: ipset не обнаружен после установки"
return 1
fi
else
debug_step "ipset уже установлен в системе"
log_message "INFO" "ipset is already installed"
# Проверяем версию ipset (информационно)
if [ "$DEBUG" -eq 1 ]; then
local ipset_version=$(ipset --version 2>&1)
log_message "DEBUG" "ipset version: $ipset_version"
fi
fi
log_message "INFO" "ipset check/installation completed successfully"
return 0
}
# === Гарантированное создание директорий ===
ensure_directories() {
debug_step "Проверяем и создаём необходимые директории"
# Создаём UPDATE_DIR если не существует
if [ ! -d "$UPDATE_DIR" ]; then
log_message "INFO" "Creating directory: $UPDATE_DIR"
debug_step "Директория $UPDATE_DIR не существует, создаём"
if mkdir -p "$UPDATE_DIR" 2>/dev/null; then
log_message "INFO" "Directory created successfully"
else
log_message "INFO" "ERROR: Cannot create directory $UPDATE_DIR"
return 1
fi
else
debug_step "Директория $UPDATE_DIR уже существует"
fi
# Проверяем права на запись
if [ ! -w "$UPDATE_DIR" ]; then
log_message "INFO" "WARNING: Directory $UPDATE_DIR is not writable"
debug_step "ПРЕДУПРЕЖДЕНИЕ: Нет прав на запись в $UPDATE_DIR"
return 1
fi
return 0
}
# Путь к sac-client.sh в клоне репозитория (корень git)
find_sac_client_remote() {
local candidates=(
"$UPDATE_DIR/$SCRIPT_NAME/sac-client.sh"
)
local p
for p in "${candidates[@]}"; do
if [ -f "$p" ]; then
printf '%s\n' "$p"
return 0
fi
done
return 1
}
# Копирует sac-client.sh рядом с LOCAL_SCRIPT_PATH (всегда после git pull, по checksum).
# Устанавливает SAC_CLIENT_CHANGED=1 при фактическом обновлении файла.
sync_sac_client() {
SAC_CLIENT_CHANGED=0
local remote local_dst local_sum remote_sum
remote=$(find_sac_client_remote) || {
log_message "INFO" "WARNING: sac-client.sh not found in repository clone"
debug_step "sac-client.sh не найден в клоне репозитория"
return 0
}
local_dst="${LOCAL_SCRIPT_PATH%/*}/sac-client.sh"
remote_sum=$(sha256sum "$remote" 2>/dev/null | awk '{print $1}')
if [ -z "$remote_sum" ]; then
log_message "INFO" "ERROR: Failed to checksum remote sac-client.sh"
return 1
fi
if [ "$DEPLOY_MODE" -ne 1 ] && [ -f "$local_dst" ]; then
local_sum=$(sha256sum "$local_dst" 2>/dev/null | awk '{print $1}')
if [ "$local_sum" = "$remote_sum" ]; then
log_message "INFO" "sac-client.sh checksums match. No update needed."
debug_step "sac-client.sh без изменений"
return 0
fi
elif [ ! -f "$local_dst" ]; then
log_message "INFO" "Local sac-client.sh not found. Will install."
debug_step "Локальный sac-client.sh отсутствует, будет установлен"
fi
debug_step "Копируем sac-client.sh в $local_dst"
if ! cp "$remote" "$local_dst"; then
log_message "INFO" "ERROR: Failed to copy sac-client.sh"
return 1
fi
chmod 644 "$local_dst"
SAC_CLIENT_CHANGED=1
log_message "INFO" "sac-client.sh updated."
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_ok "sac-client.sh → $local_dst"
fi
return 0
}
find_remote_ssh_monitor_path() {
REMOTE_SCRIPT_PATH="$UPDATE_DIR/$SCRIPT_NAME/src/branch/main/$SCRIPT_NAME"
ALT_REMOTE_PATH="$UPDATE_DIR/$SCRIPT_NAME/$SCRIPT_NAME"
if [ -f "$REMOTE_SCRIPT_PATH" ]; then
printf '%s\n' "$REMOTE_SCRIPT_PATH"
return 0
fi
if [ -f "$ALT_REMOTE_PATH" ]; then
log_message "INFO" "Script found at alternative path"
printf '%s\n' "$ALT_REMOTE_PATH"
return 0
fi
return 1
}
install_ssh_monitor_from() {
local actual_script_path="$1"
if [ -f "$LOCAL_SCRIPT_PATH" ]; then
cp "$LOCAL_SCRIPT_PATH" "${LOCAL_SCRIPT_PATH}.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
fi
if ! cp "$actual_script_path" "$LOCAL_SCRIPT_PATH"; then
log_message "INFO" "ERROR: Failed to copy script"
return 1
fi
chmod +x "$LOCAL_SCRIPT_PATH"
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_ok "ssh-monitor → $LOCAL_SCRIPT_PATH"
fi
return 0
}
deploy_prepare_dirs() {
say_step "Каталоги"
mkdir -p "$UPDATE_DIR" /opt/scripts /var/log "$SAC_SPOOL_DIR_DEFAULT" /var/lib/ssh-monitor 2>/dev/null || true
say_ok "UPDATE_DIR=$UPDATE_DIR, spool=$SAC_SPOOL_DIR_DEFAULT"
}
deploy_install_config() {
local example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor.conf.example"
say_step "Конфигурация"
if [ -f "$CONFIG_FILE" ]; then
say_info "Уже существует: $CONFIG_FILE (не перезаписываем)"
return 0
fi
if [ ! -f "$example" ]; then
say_warn "Пример конфига не найден в клоне: $example"
return 0
fi
cp "$example" "$CONFIG_FILE"
chmod 600 "$CONFIG_FILE"
say_ok "Создан $CONFIG_FILE из ssh-monitor.conf.example"
}
deploy_install_systemd() {
local example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor.service.example"
say_step "systemd"
if [ -f "$SYSTEMD_UNIT_PATH" ]; then
say_info "Уже существует: $SYSTEMD_UNIT_PATH"
return 0
fi
if [ ! -f "$example" ]; then
say_warn "Unit example не найден: $example"
return 0
fi
cp "$example" "$SYSTEMD_UNIT_PATH"
if command -v systemctl &>/dev/null; then
systemctl daemon-reload 2>/dev/null || true
systemctl enable ssh-monitor.service 2>/dev/null || true
say_ok "Установлен $SYSTEMD_UNIT_PATH (enable ssh-monitor.service)"
else
say_ok "Скопирован $SYSTEMD_UNIT_PATH (systemctl не найден)"
fi
}
deploy_install_self() {
local self_path
self_path="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")"
say_step "Скрипт автообновления"
if [ "$self_path" = "$DEPLOY_INSTALL_PATH" ]; then
say_info "Уже запущен из $DEPLOY_INSTALL_PATH"
return 0
fi
mkdir -p "$(dirname "$DEPLOY_INSTALL_PATH")"
cp "$self_path" "$DEPLOY_INSTALL_PATH"
chmod 750 "$DEPLOY_INSTALL_PATH"
say_ok "update_ssh_monitor.sh → $DEPLOY_INSTALL_PATH"
}
print_config_variables_help() {
say_step "Заполните в $CONFIG_FILE"
cat <<'EOF'
Обязательно хотя бы один канал уведомлений ИЛИ SAC:
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
— или MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO
Security Alert Center:
UseSAC=off | dual | exclusive | fallback
Пилот 2.1 (только SAC): UseSAC=exclusive
SAC_URL=https://sac.kalinamall.ru
SAC_API_KEY=<ключ из SAC sac-api.env / bootstrap>
Проверка после правок:
/usr/local/bin/ssh-monitor --check-config
/usr/local/bin/ssh-monitor --check-sac
systemctl restart ssh-monitor.service
systemctl status ssh-monitor.service
EOF
}
print_deploy_summary() {
say_step "Итог установки"
say_info "Монитор: $LOCAL_SCRIPT_PATH"
say_info "SAC client: ${LOCAL_SCRIPT_PATH%/*}/sac-client.sh"
say_info "Конфиг: $CONFIG_FILE"
say_info "Git-клон: $UPDATE_DIR/$SCRIPT_NAME"
say_info "Updater: $DEPLOY_INSTALL_PATH"
say_info "Лог updater: $LOG_FILE"
}
deploy_post_install() {
deploy_prepare_dirs
deploy_install_config
deploy_install_systemd
deploy_install_self
print_deploy_summary
print_config_variables_help
}
restart_ssh_monitor_service() {
debug_step "Перезапускаем сервис ssh-monitor.service"
if command -v systemctl &>/dev/null; then
if systemctl restart ssh-monitor.service 2>/dev/null; then
log_message "INFO" "Service restarted successfully."
debug_step "Сервис успешно перезапущен"
else
log_message "INFO" "WARNING: Failed to restart service (may not be running)"
debug_step "ПРЕДУПРЕЖДЕНИЕ: Не удалось перезапустить сервис"
fi
else
log_message "INFO" "systemctl not found, service not restarted"
debug_step "systemctl не найден, сервис не перезапущен"
fi
}
# Функция для обновления скрипта
update_script() {
log_message "INFO" "Starting script update..."
debug_step "Запуск функции обновления скрипта"
# Гарантируем наличие директории
debug_step "Гарантируем наличие директории для работы"
if ! ensure_directories; then
return 1
fi
# Переходим в директорию для клона репозитория
debug_step "Переходим в директорию для клона репозитория: $UPDATE_DIR"
if ! cd "$UPDATE_DIR" 2>/dev/null; then
log_message "INFO" "ERROR: Cannot cd to $UPDATE_DIR"
return 1
fi
# Клонируем или обновляем репозиторий
debug_step "Проверяем наличие репозитория и обновляем/клонируем"
if [ -d "$SCRIPT_NAME" ]; then
log_message "INFO" "Repository exists, pulling updates..."
debug_step "Выполняем git pull origin main"
if ! git -C "$SCRIPT_NAME" pull origin main >> "$LOG_FILE" 2>&1; then
log_message "INFO" "WARNING: git pull failed, continuing with existing local copy..."
fi
else
log_message "INFO" "Cloning repository from $REPO_URL"
debug_step "Выполняем git clone $REPO_URL $SCRIPT_NAME"
if ! git clone "$REPO_URL" "$SCRIPT_NAME" >> "$LOG_FILE" 2>&1; then
log_message "INFO" "ERROR: git clone failed"
return 1
fi
fi
local actual_script_path=""
actual_script_path="$(find_remote_ssh_monitor_path 2>/dev/null || true)"
debug_step "Путь к ssh-monitor в клоне: ${actual_script_path:-не найден}"
SAC_CLIENT_CHANGED=0
if ! sync_sac_client; then
return 1
fi
local script_changed=0
if [ -n "$actual_script_path" ] && [ -f "$actual_script_path" ]; then
if [ "$DEPLOY_MODE" -eq 1 ]; then
log_message "INFO" "Deploy: installing ssh-monitor to $LOCAL_SCRIPT_PATH"
if install_ssh_monitor_from "$actual_script_path"; then
script_changed=1
else
return 1
fi
else
log_message "INFO" "Remote script found. Comparing checksums..."
if [ ! -f "$LOCAL_SCRIPT_PATH" ]; then
LOCAL_CHECKSUM=""
else
LOCAL_CHECKSUM=$(sha256sum "$LOCAL_SCRIPT_PATH" 2>/dev/null | awk '{print $1}')
fi
REMOTE_CHECKSUM=$(sha256sum "$actual_script_path" 2>/dev/null | awk '{print $1}')
if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then
log_message "INFO" "Checksums differ. Updating $LOCAL_SCRIPT_PATH..."
if install_ssh_monitor_from "$actual_script_path"; then
script_changed=1
log_message "INFO" "Script updated successfully."
else
return 1
fi
else
log_message "INFO" "Checksums match. No update needed."
fi
fi
else
log_message "INFO" "No remote script found. No update needed."
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_warn "ssh-monitor не найден в клоне $UPDATE_DIR/$SCRIPT_NAME"
fi
if [ "$DEBUG" -eq 1 ] && [ -d "$UPDATE_DIR/$SCRIPT_NAME" ]; then
find "$UPDATE_DIR/$SCRIPT_NAME" -type f -name "$SCRIPT_NAME" 2>/dev/null | while read -r found_file; do
log_message "DEBUG" "Найден файл: $found_file"
done
fi
fi
if [ "$DEPLOY_MODE" -eq 1 ]; then
script_changed=1
elif [ "$script_changed" -eq 1 ] || [ "${SAC_CLIENT_CHANGED:-0}" -eq 1 ]; then
restart_ssh_monitor_service
fi
# Логируем значения переменных скрипта при DEBUG=1
log_script_vars
return 0
}
# === Основной блок выполнения ===
parse_command_line "$@"
deploy_require_root
if [ "$DEPLOY_MODE" -eq 1 ]; then
printf '\n=== ssh-monitor: первичная установка (--deploy) ===\n'
say_step "Репозиторий и файлы"
fi
log_message "INFO" "=== Script update started (deploy=$DEPLOY_MODE) ==="
debug_step "Инициализация скрипта обновления"
if ! ensure_directories; then
log_message "INFO" "ERROR: Failed to ensure directories. Exiting."
exit 1
fi
debug_step "Начинаем проверку и установку ipset (КРИТИЧЕСКИЙ ШАГ)"
if ! ensure_ipset_installed; then
log_message "ERROR" "CRITICAL: ipset installation or verification failed"
echo "ERROR: Работа обновленного скрипта невозможна без установленного ipset" >&2
exit 1
fi
update_script
exit_code=$?
if [ "$exit_code" -eq 0 ] && [ "$DEPLOY_MODE" -eq 1 ]; then
deploy_post_install
restart_ssh_monitor_service
if command -v systemctl &>/dev/null; then
if systemctl start ssh-monitor.service 2>/dev/null; then
say_ok "systemctl start ssh-monitor.service"
else
say_warn "systemctl start не выполнен (проверьте --check-config и конфиг)"
fi
fi
fi
if [ $exit_code -eq 0 ]; then
log_message "INFO" "=== Script update completed successfully ==="
if [ "$DEPLOY_MODE" -eq 1 ]; then
printf '\n=== Установка завершена. Отредактируйте %s и перезапустите сервис. ===\n' "$CONFIG_FILE"
fi
else
log_message "INFO" "=== Script update completed with errors (exit code: $exit_code) ==="
fi
exit $exit_code