#!/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