Files
ssh-monitor/update_ssh_monitor.sh
T

1824 lines
70 KiB
Bash
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="${REPO_URL:-}"
GIT_BRANCH="${GIT_BRANCH:-main}"
GIT_REF="${GIT_REF:-}"
GIT_VERIFY_MODE="${GIT_VERIFY_MODE:-off}"
GIT_ALLOW_RESET="${GIT_ALLOW_RESET:-0}"
MANIFEST_VERIFIED=0
MANIFEST_STRICT=0
LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor"
LOG_FILE="/var/log/update_script.log"
# Режим отладки: 1 = включён, 0 = выключен
DEBUG=0
DEPLOY_MODE=0
QUIET=0
# Итог последнего прогона (для лога и консоли)
SUMMARY_SCRIPT="—"
SUMMARY_SCRIPT_REASON="—"
SUMMARY_SAC_CLIENT="—"
SUMMARY_SAC_REASON="—"
SUMMARY_CONFIG="—"
SUMMARY_GIT="—"
SUMMARY_SERVICE_RESTART="—"
SUMMARY_UPDATER="—"
SUMMARY_WATCHDOG="—"
SUMMARY_HARDENING="—"
SUMMARY_MANIFEST="—"
CONFIG_FILE="/etc/ssh-monitor.conf"
DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh"
EXPECTED_HEARTBEAT_INTERVAL_MINUTES=240
EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE=$((EXPECTED_HEARTBEAT_INTERVAL_MINUTES * 60 + 3600))
SAC_SPOOL_DIR_DEFAULT="/var/lib/ssh-monitor/sac-spool"
SYSTEMD_UNIT_PATH="/etc/systemd/system/ssh-monitor.service"
LOCAL_WATCHDOG_PATH="/usr/local/bin/ssh-monitor-watchdog"
WATCHDOG_SERVICE_PATH="/etc/systemd/system/ssh-monitor-watchdog.service"
WATCHDOG_TIMER_PATH="/etc/systemd/system/ssh-monitor-watchdog.timer"
# === Список переменных, объявленных в скрипте (для логирования при DEBUG=1) ===
SCRIPT_VARS=("UPDATE_DIR" "SCRIPT_NAME" "REPO_URL" "GIT_BRANCH" "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
--quiet не печатать шаги в stdout (только в $LOG_FILE)
-h, --help эта справка
Лог действий: /var/log/update_script.log (в конце каждого прогона — блок «Итог обновления»).
При первой установке создаётся /etc/ssh-monitor.conf из example.
При обновлении в существующий конфиг дописываются отсутствующие SAC-переменные
и обязательные ключи безопасности (CONFIG_STRICT_PERMS, SAC_TLS_INSECURE и т.д.);
выставляется UseSAC=fallback (Telegram/SMTP не перезаписываются).
На каждом прогоне автоматически выставляются chmod/chown на конфиг, spool и state-файлы
(основной сценарий — кнопка «Обновить» в SAC; cron/timer — опционально).
Переменная REPO_URL обязательна (export, SAC или Environment= в systemd/cron).
EOF
}
require_repo_url() {
if [ -n "${REPO_URL:-}" ]; then
return 0
fi
local msg="ERROR: REPO_URL не задан. Задайте export REPO_URL=https://git.papatramp.ru/PapaTramp/ssh-monitor.git или Environment=REPO_URL=... в systemd unit / cron."
echo "$msg" >&2
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: REPO_URL не задан" >>"$LOG_FILE" 2>/dev/null || true
exit 1
}
default_required_config_value() {
case "$1" in
CONFIG_STRICT_PERMS) printf '0' ;;
SAC_TLS_INSECURE) printf '0' ;;
*) printf '' ;;
esac
}
# Ключи SAC: дописываются из ssh-monitor.conf.example, если отсутствуют в /etc.
SAC_CONFIG_KEYS=(
UseSAC
SAC_URL
SAC_API_KEY
SAC_SPOOL_DIR
SAC_FAIL_COUNT_FILE
SAC_FALLBACK_FAILURES
SAC_TIMEOUT_SEC
SAC_SPOOL_FLUSH_MAX_FILES
SAC_SPOOL_MAX_AGE_HOURS
)
SAC_USE_MODE_DEFAULT="fallback"
# Обязательные ключи конфига (добавляются updater'ом, если отсутствуют).
REQUIRED_CONFIG_KEYS=(
CONFIG_STRICT_PERMS
SAC_TLS_INSECURE
)
find_ssh_monitor_conf_example() {
local example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor.conf.example"
if [ -f "$example" ]; then
printf '%s\n' "$example"
return 0
fi
return 1
}
config_conf_value() {
local key="$1" file="$2" line val
[ -f "$file" ] || return 1
line=$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | tail -1) || return 1
val=${line#*=}
val=${val#"${val%%[![:space:]]*}"}
val=${val%"${val##*[![:space:]]}"}
val=${val#\"}
val=${val%\"}
val=${val#\'}
val=${val%\'}
printf '%s\n' "$val"
}
config_example_value() {
local key="$1" example="$2"
config_conf_value "$key" "$example" 2>/dev/null || true
}
backup_config_if_needed() {
local file="$1" flag_var="$2"
if [ -z "${!flag_var:-}" ] || [ "${!flag_var}" -eq 0 ]; then
cp "$file" "${file}.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
printf -v "$flag_var" '%s' 1
fi
}
ensure_ssh_monitor_config_file() {
local example
CONFIG_CHANGED=0
example=$(find_ssh_monitor_conf_example) || {
log_message "INFO" "WARNING: ssh-monitor.conf.example not found in clone"
return 0
}
if [ -f "$CONFIG_FILE" ]; then
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_info "Уже существует: $CONFIG_FILE (дополним SAC-переменные при необходимости)"
fi
return 0
fi
log_message "INFO" "Creating $CONFIG_FILE from ssh-monitor.conf.example"
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_ok "Создан $CONFIG_FILE из ssh-monitor.conf.example"
fi
if ! cp "$example" "$CONFIG_FILE"; then
log_message "INFO" "ERROR: Failed to copy ssh-monitor.conf.example to $CONFIG_FILE"
return 1
fi
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
chown root:root "$CONFIG_FILE" 2>/dev/null || true
CONFIG_CHANGED=1
return 0
}
sync_sac_config_keys() {
local file="$CONFIG_FILE"
local example backed_up=0
local key val current changed=0 added=0
[ -f "$file" ] || return 1
example=$(find_ssh_monitor_conf_example) || {
log_message "INFO" "WARNING: ssh-monitor.conf.example not found; SAC merge skipped"
return 0
}
if ! config_key_exists "UseSAC" "$file"; then
backup_config_if_needed "$file" backed_up
{
printf '\n# --- Security Alert Center (SAC) ---\n'
printf '# off | exclusive | dual | fallback — см. security-alert-center docs/agent-integration.md\n'
} >>"$file"
log_message "INFO" "Config $file: добавлен блок SAC (ключи отсутствовали)."
changed=1
fi
for key in "${SAC_CONFIG_KEYS[@]}"; do
if config_key_exists "$key" "$file"; then
continue
fi
val=$(config_example_value "$key" "$example")
if [ -z "$val" ] && [ "$key" != "SAC_API_KEY" ]; then
case "$key" in
UseSAC) val="$SAC_USE_MODE_DEFAULT" ;;
SAC_SPOOL_DIR) val="$SAC_SPOOL_DIR_DEFAULT" ;;
SAC_FAIL_COUNT_FILE) val="/var/lib/ssh-monitor/sac-fail.count" ;;
SAC_FALLBACK_FAILURES) val="5" ;;
SAC_TIMEOUT_SEC) val="45" ;;
SAC_SPOOL_FLUSH_MAX_FILES) val="50" ;;
SAC_SPOOL_MAX_AGE_HOURS) val="72" ;;
SAC_URL) val="https://sac.kalinamall.ru" ;;
esac
fi
backup_config_if_needed "$file" backed_up
set_config_kv "$key" "$val" "$file"
log_message "INFO" "Config $file: добавлен ${key}=${val:-<пусто>}"
changed=1
added=$((added + 1))
done
current=$(config_conf_value "UseSAC" "$file" 2>/dev/null || true)
if [ "$current" != "$SAC_USE_MODE_DEFAULT" ]; then
backup_config_if_needed "$file" backed_up
set_config_kv "UseSAC" "$SAC_USE_MODE_DEFAULT" "$file"
log_message "INFO" "Config $file: UseSAC ${current:-<нет>}$SAC_USE_MODE_DEFAULT (deploy policy)."
changed=1
fi
current=$(config_conf_value "SAC_TIMEOUT_SEC" "$file" 2>/dev/null || true)
if [ "$current" = "12" ]; then
backup_config_if_needed "$file" backed_up
set_config_kv "SAC_TIMEOUT_SEC" "45" "$file"
log_message "INFO" "Config $file: SAC_TIMEOUT_SEC 12 → 45 (ingest timeout)."
changed=1
fi
if [ "$changed" -eq 0 ]; then
return 1
fi
if [ "$DEPLOY_MODE" -eq 1 ]; then
if [ "$added" -gt 0 ]; then
say_info "SAC: добавлено ключей — $added; UseSAC=$SAC_USE_MODE_DEFAULT."
else
say_info "SAC: UseSAC=$SAC_USE_MODE_DEFAULT."
fi
fi
CONFIG_CHANGED=1
return 0
}
sync_required_config_keys() {
local file="$CONFIG_FILE"
local example backed_up=0
local key val changed=0
[ -f "$file" ] || return 1
example=$(find_ssh_monitor_conf_example) || example=""
for key in "${REQUIRED_CONFIG_KEYS[@]}"; do
if config_key_exists "$key" "$file"; then
continue
fi
val="$(config_example_value "$key" "$example")"
if [ -z "$val" ]; then
val="$(default_required_config_value "$key")"
fi
backup_config_if_needed "$file" backed_up
if ! grep -qE '^[[:space:]]*# --- Security / updater ---' "$file" 2>/dev/null; then
printf '\n# --- Security / updater (добавлено update_ssh_monitor.sh) ---\n' >>"$file"
fi
set_config_kv "$key" "$val" "$file"
log_message "INFO" "Config $file: добавлен обязательный ${key}=${val}"
changed=1
done
if [ "$changed" -eq 0 ]; then
return 1
fi
CONFIG_CHANGED=1
return 0
}
harden_ssh_monitor_config_perms() {
[ -f "$CONFIG_FILE" ] || return 0
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
chown root:root "$CONFIG_FILE" 2>/dev/null || true
}
load_ssh_monitor_perms_lib() {
local p candidates=()
candidates+=("${LOCAL_SCRIPT_PATH%/*}/ssh-monitor-perms.sh")
candidates+=("$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-perms.sh")
for p in "${candidates[@]}"; do
if [ -f "$p" ]; then
# shellcheck source=/dev/null
source "$p"
return 0
fi
done
return 1
}
# SAC-first: при каждом обновлении (SAC, cron или вручную) выставляет chmod/chown на конфиг,
# spool, счётчики SAC и state-файлы из /etc/ssh-monitor.conf — без участия админа.
apply_runtime_security_hardening() {
local spool fail_count agent_id path key
local -a state_keys defaults
local i=0
log_action "security: применение прав (конфиг, spool, state) — основной путь через SAC/update"
harden_ssh_monitor_config_perms
chmod 750 "$DEPLOY_INSTALL_PATH" 2>/dev/null || true
chown root:root "$DEPLOY_INSTALL_PATH" 2>/dev/null || true
if ! load_ssh_monitor_perms_lib; then
log_action "WARNING: ssh-monitor-perms.sh недоступен — только chmod конфига и базовых каталогов"
mkdir -p /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
chmod 700 /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
chown root:root /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
SUMMARY_HARDENING="частично (нет perms lib)"
return 0
fi
ssh_monitor_secure_dir "/var/lib/ssh-monitor"
spool="$(config_conf_value SAC_SPOOL_DIR "$CONFIG_FILE" 2>/dev/null || echo "$SAC_SPOOL_DIR_DEFAULT")"
fail_count="$(config_conf_value SAC_FAIL_COUNT_FILE "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/ssh-monitor/sac-fail.count")"
agent_id="$(config_conf_value SAC_AGENT_ID_FILE "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/ssh-monitor/agent_instance_id")"
ssh_monitor_harden_path "$spool"
ssh_monitor_harden_path "$fail_count"
ssh_monitor_harden_path "$agent_id"
state_keys=(
LAST_HEARTBEAT_FILE
LAST_REPORT_FILE
LAST_SSH_CHECK_FILE
LAST_SUDO_CHECK_FILE
LAST_SECURITY_EVENTS_FILE
LAST_LOGIND_CHECK_FILE
BAN_LIST_FILE
HEALTHCHECK_STATUS_FILE
)
defaults=(
/var/log/last_heartbeat.txt
/var/log/last_daily_report.txt
/var/log/last_ssh_check
/var/log/last_sudo_check
/var/log/last_security_events_check
/var/log/last_logind_check
/var/log/ssh_bans.txt
""
)
for key in "${state_keys[@]}"; do
path="$(config_conf_value "$key" "$CONFIG_FILE" 2>/dev/null || true)"
if [ -z "$path" ]; then
path="${defaults[$i]}"
fi
if [ -n "$path" ]; then
ssh_monitor_harden_path "$path"
fi
i=$((i + 1))
done
if [ -f /etc/ssh_monitor_whitelist.txt ]; then
ssh_monitor_harden_path /etc/ssh_monitor_whitelist.txt
fi
SUMMARY_HARDENING="права применены"
log_action "security: runtime paths hardened (spool, state, config)"
}
maybe_enable_deploy_for_missing_install() {
if [ "$DEPLOY_MODE" -eq 1 ]; then
return 0
fi
if [ -f "$LOCAL_SCRIPT_PATH" ]; then
return 0
fi
log_action "ssh-monitor не найден в $LOCAL_SCRIPT_PATH — автоматический режим --deploy"
DEPLOY_MODE=1
}
set_config_kv() {
local key="$1" val="$2" file="$3" esc
esc=$(printf '%s' "$val" | sed -e 's/[\\&/]/\\&/g' -e 's/"/\\"/g')
if grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null; then
sed -i -E "s/^[[:space:]]*${key}=.*/${key}=\"${esc}\"/" "$file"
else
printf '%s="%s"\n' "$key" "$esc" >>"$file"
fi
}
config_key_exists() {
local key="$1" file="$2"
grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null
}
config_key_mentioned() {
local key="$1" file="$2"
grep -qE "^[[:space:]]*(#.*)?${key}=" "$file" 2>/dev/null
}
sync_server_ipv4_hint_config() {
local file="$CONFIG_FILE" backed_up=0
[ -f "$file" ] || return 1
if config_key_mentioned "SERVER_IPV4" "$file"; then
return 1
fi
backup_config_if_needed "$file" backed_up
{
printf '\n# --- Явный IPv4 для SAC host.ipv4 (раскомментируйте при необходимости) ---\n'
printf '# SERVER_IPV4="10.0.0.10"\n'
} >>"$file"
log_message "INFO" "Config $file: добавлена подсказка # SERVER_IPV4."
CONFIG_CHANGED=1
return 0
}
sync_daily_report_enabled_config() {
local file="$CONFIG_FILE" backed_up=0 current
[ -f "$file" ] || return 1
if config_key_exists "DAILY_REPORT_ENABLED" "$file"; then
current=$(config_conf_value "DAILY_REPORT_ENABLED" "$file" 2>/dev/null || true)
if [ "$current" = "1" ]; then
return 1
fi
backup_config_if_needed "$file" backed_up
set_config_kv "DAILY_REPORT_ENABLED" "1" "$file"
log_message "INFO" "Config $file: DAILY_REPORT_ENABLED ${current:-<пусто>} → 1 (отчёт с агента)."
else
backup_config_if_needed "$file" backed_up
{
printf '\n# --- Суточный отчёт report.daily.ssh (1 = с агента; 0 = только SAC) ---\n'
printf 'DAILY_REPORT_ENABLED="1"\n'
} >>"$file"
log_message "INFO" "Config $file: добавлен DAILY_REPORT_ENABLED=1."
fi
CONFIG_CHANGED=1
return 0
}
append_heartbeat_interval_block() {
local file="$1"
{
printf '\n# --- SAC agent.heartbeat (интервал в минутах; %s = 4 ч) ---\n' "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES"
printf 'HEARTBEAT_INTERVAL_MINUTES="%s"\n' "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES"
printf '# Порог watchdog: интервал + 1 ч запас (%s с)\n' "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE"
printf 'WATCHDOG_MAX_HEARTBEAT_AGE="%s"\n' "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE"
} >>"$file"
}
sync_heartbeat_interval_config() {
local file="$CONFIG_FILE"
local current_interval current_watchdog
local changed=0
local backed_up=0
[ -f "$file" ] || return 1
backup_config_once() {
if [ "$backed_up" -eq 0 ]; then
cp "$file" "${file}.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
backed_up=1
fi
}
if ! config_key_exists "HEARTBEAT_INTERVAL_MINUTES" "$file"; then
backup_config_once
if ! config_key_exists "WATCHDOG_MAX_HEARTBEAT_AGE" "$file"; then
append_heartbeat_interval_block "$file"
log_message "INFO" "Config $file: добавлены HEARTBEAT_INTERVAL_MINUTES=$EXPECTED_HEARTBEAT_INTERVAL_MINUTES и WATCHDOG_MAX_HEARTBEAT_AGE=$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (отсутствовали в конфиге)."
else
set_config_kv "HEARTBEAT_INTERVAL_MINUTES" "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" "$file"
log_message "INFO" "Config $file: добавлен HEARTBEAT_INTERVAL_MINUTES=$EXPECTED_HEARTBEAT_INTERVAL_MINUTES (отсутствовал в конфиге)."
fi
changed=1
else
current_interval=$(config_conf_value "HEARTBEAT_INTERVAL_MINUTES" "$file" 2>/dev/null || true)
if [ "$current_interval" != "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" ]; then
backup_config_once
set_config_kv "HEARTBEAT_INTERVAL_MINUTES" "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" "$file"
log_message "INFO" "Config $file: HEARTBEAT_INTERVAL_MINUTES ${current_interval:-?}$EXPECTED_HEARTBEAT_INTERVAL_MINUTES (deploy policy)."
changed=1
fi
fi
if ! config_key_exists "WATCHDOG_MAX_HEARTBEAT_AGE" "$file"; then
backup_config_once
set_config_kv "WATCHDOG_MAX_HEARTBEAT_AGE" "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" "$file"
log_message "INFO" "Config $file: добавлен WATCHDOG_MAX_HEARTBEAT_AGE=$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (отсутствовал в конфиге)."
changed=1
else
current_watchdog=$(config_conf_value "WATCHDOG_MAX_HEARTBEAT_AGE" "$file" 2>/dev/null || true)
if [ "$current_watchdog" != "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" ]; then
backup_config_once
set_config_kv "WATCHDOG_MAX_HEARTBEAT_AGE" "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" "$file"
log_message "INFO" "Config $file: WATCHDOG_MAX_HEARTBEAT_AGE ${current_watchdog:-?}$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (deploy policy)."
changed=1
fi
fi
if [ "$changed" -eq 0 ]; then
return 1
fi
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_info "Heartbeat: ${EXPECTED_HEARTBEAT_INTERVAL_MINUTES} мин, watchdog ${EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE} с."
fi
CONFIG_CHANGED=1
return 0
}
parse_command_line() {
DEPLOY_MODE=0
while [ $# -gt 0 ]; do
case "$1" in
--deploy)
DEPLOY_MODE=1
shift
;;
--debug)
DEBUG=1
shift
;;
--quiet)
QUIET=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
}
# Всегда в лог; в stderr — если не --quiet (stdout не трогаем: его используют $(find_*))
log_action() {
log_message "INFO" "$1"
if [ "$QUIET" -eq 0 ]; then
printf '[update_ssh_monitor] %s\n' "$1" >&2
fi
}
get_git_short_rev() {
local repo_dir="$1"
git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || printf 'unknown'
}
# Remote для fetch/pull: всегда kalinamall с URL из REPO_URL (SAC передаёт REPO_URL при SSH-обновлении).
resolve_git_remote_name() {
local repo="$1"
local name=kalinamall
if git -C "$repo" remote get-url "$name" &>/dev/null; then
git -C "$repo" remote set-url "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1
else
git -C "$repo" remote add "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1 \
|| git -C "$repo" remote set-url "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1
fi
printf '%s' "$name"
}
load_git_verify_settings() {
local v
if [ -f "$CONFIG_FILE" ]; then
if [ -z "${GIT_REF:-}" ]; then
v=$(config_conf_value GIT_REF "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_REF="$v"
fi
if [ -z "${GIT_VERIFY_MODE:-}" ] || [ "$GIT_VERIFY_MODE" = "off" ]; then
v=$(config_conf_value GIT_VERIFY_MODE "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_VERIFY_MODE="$v"
fi
if [ "${GIT_ALLOW_RESET:-0}" = "0" ]; then
v=$(config_conf_value GIT_ALLOW_RESET "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_ALLOW_RESET="$v"
fi
fi
GIT_VERIFY_MODE="$(printf '%s' "${GIT_VERIFY_MODE:-off}" | tr '[:upper:]' '[:lower:]')"
case "$GIT_VERIFY_MODE" in
off | tag | commit) ;;
*)
log_action "WARN: GIT_VERIFY_MODE=$GIT_VERIFY_MODE неизвестен — off"
GIT_VERIFY_MODE=off
;;
esac
case "${GIT_ALLOW_RESET:-0}" in
1 | yes | true | on) GIT_ALLOW_RESET=1 ;;
*) GIT_ALLOW_RESET=0 ;;
esac
case "${RELEASE_MANIFEST_STRICT:-0}" in
1 | yes | true | on) MANIFEST_STRICT=1 ;;
*) MANIFEST_STRICT=0 ;;
esac
}
verify_git_ref() {
local repo="$1" ref="$2" mode="$3"
local obj_type full
[ "$mode" = "off" ] && return 0
[ -n "$ref" ] || {
log_action "ERROR: GIT_VERIFY_MODE=$mode требует GIT_REF"
return 1
}
case "$mode" in
tag)
if ! git -C "$repo" rev-parse "refs/tags/$ref" >/dev/null 2>&1; then
log_action "ERROR: GIT_REF=$ref не найден как тег в клоне"
return 1
fi
obj_type=$(git -C "$repo" cat-file -t "refs/tags/$ref" 2>/dev/null || true)
if [ "$obj_type" != "tag" ] && [ "$obj_type" != "commit" ]; then
log_action "ERROR: GIT_REF=$ref: неожиданный тип объекта ($obj_type)"
return 1
fi
log_action "git verify: тег $ref OK (type=$obj_type)"
;;
commit)
full=$(git -C "$repo" rev-parse "$ref^{commit}" 2>/dev/null || true)
if [ -z "$full" ] || [ "${#full}" -ne 40 ]; then
log_action "ERROR: GIT_REF=$ref не является полным commit SHA"
return 1
fi
log_action "git verify: commit ${full:0:12}… OK"
;;
esac
return 0
}
find_release_manifest_path() {
local repo="$1" version="$2"
local p="$repo/release/manifest-${version}.json"
if [ -f "$p" ]; then
printf '%s\n' "$p"
return 0
fi
return 1
}
read_repo_release_version() {
local repo="$1" ver
ver=$(read_ssh_monitor_version "$repo/ssh-monitor")
if [ -z "$ver" ] && [ -f "$repo/version.txt" ]; then
ver=$(tr -d '\r\n' <"$repo/version.txt")
fi
printf '%s' "$ver"
}
verify_release_manifest() {
local repo="$1" version manifest_path rc
MANIFEST_VERIFIED=0
version=$(read_repo_release_version "$repo")
if [ -z "$version" ]; then
SUMMARY_MANIFEST="версия не определена"
log_action "WARN: не удалось определить версию релиза для manifest"
[ "$MANIFEST_STRICT" -eq 1 ] && return 1
return 0
fi
manifest_path=$(find_release_manifest_path "$repo" "$version" 2>/dev/null || true)
if [ -z "$manifest_path" ]; then
SUMMARY_MANIFEST="manifest-${version}.json не найден (skip)"
log_action "WARN: release/manifest-${version}.json не найден — проверка SHA пропущена"
if [ "$MANIFEST_STRICT" -eq 1 ]; then
log_action "ERROR: RELEASE_MANIFEST_STRICT=1 — manifest обязателен"
return 1
fi
return 0
fi
log_action "manifest: проверка $manifest_path"
if ! MANIFEST_PATH="$manifest_path" REPO_DIR="$repo" python3 <<'PY' >>"$LOG_FILE" 2>&1
import hashlib
import json
import os
import subprocess
import sys
manifest_path = os.environ["MANIFEST_PATH"]
repo = os.environ["REPO_DIR"]
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
expected_commit = (manifest.get("git_commit") or "").strip()
if expected_commit:
actual = subprocess.check_output(["git", "-C", repo, "rev-parse", "HEAD"], text=True).strip()
if actual != expected_commit:
print(f"commit mismatch: HEAD={actual[:12]} manifest={expected_commit[:12]}", file=sys.stderr)
sys.exit(2)
files = manifest.get("files") or {}
for rel, spec in files.items():
path = os.path.join(repo, rel)
if not os.path.isfile(path):
print(f"missing file: {rel}", file=sys.stderr)
sys.exit(3)
want = str(spec).removeprefix("sha256:")
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
got = h.hexdigest()
if got != want:
print(f"sha256 mismatch: {rel}", file=sys.stderr)
sys.exit(4)
print("OK")
PY
then
SUMMARY_MANIFEST="ОШИБКА verify"
log_action "ERROR: manifest verify failed (см. $LOG_FILE)"
return 1
fi
MANIFEST_VERIFIED=1
SUMMARY_MANIFEST="OK ($version)"
log_action "manifest: verify OK ($version)"
return 0
}
# fetch + checkout GIT_REF или fast-forward по GIT_BRANCH; без reset --hard (кроме GIT_ALLOW_RESET=1).
sync_git_repository() {
local repo="$1" branch="${2:-main}"
local remote upstream git_before git_after ref="${GIT_REF:-}"
remote="$(resolve_git_remote_name "$repo")"
upstream="${remote}/${branch}"
if [ -f "$repo/.git/MERGE_HEAD" ]; then
log_action "git: незавершённый merge — abort"
git -C "$repo" merge --abort >>"$LOG_FILE" 2>&1 || true
fi
git_before="$(get_git_short_rev "$repo")"
if [ -n "$ref" ]; then
log_action "git: fetch $remote (ref=$ref, verify=$GIT_VERIFY_MODE) → $REPO_URL"
if ! git -C "$repo" fetch --prune "$remote" \
"+refs/heads/*:refs/remotes/${remote}/*" \
"+refs/tags/*:refs/tags/*" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="fetch FAILED ($git_before)"
log_action "ERROR: git fetch failed (см. $LOG_FILE)"
return 1
fi
if ! verify_git_ref "$repo" "$ref" "$GIT_VERIFY_MODE"; then
SUMMARY_GIT="verify FAILED ($ref)"
return 1
fi
if ! git -C "$repo" checkout -f "$ref" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="checkout FAILED ($ref)"
log_action "ERROR: git checkout $ref failed (см. $LOG_FILE)"
return 1
fi
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="checkout $ref ok, $git_before$git_after"
log_action "git: checkout $ref OK ($git_before$git_after)"
return 0
fi
log_action "git: fetch --prune $remote $branch (было $git_before) → $REPO_URL"
if ! git -C "$repo" fetch --prune "$remote" "$branch" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="fetch FAILED ($git_before)"
log_action "ERROR: git fetch failed (см. $LOG_FILE)"
return 1
fi
if git -C "$repo" pull --ff-only "$remote" "$branch" >>"$LOG_FILE" 2>&1; then
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="pull ok, $git_before$git_after"
log_action "git: fast-forward OK ($git_before$git_after)"
return 0
fi
if [ "$GIT_ALLOW_RESET" -eq 1 ]; then
log_action "WARN: fast-forward невозможен — GIT_ALLOW_RESET=1, reset --hard $upstream"
if ! git -C "$repo" reset --hard "$upstream" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="reset FAILED ($git_before)"
log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)"
return 1
fi
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="reset ok, $git_before$git_after"
log_action "git reset --hard: $git_before$git_after"
return 0
fi
SUMMARY_GIT="diverged ($git_before)"
log_action "ERROR: git: история разошлась (ff-only невозможен). Ручной clone или GIT_ALLOW_RESET=1"
echo "ERROR: git pull --ff-only failed; история разошлась. Исправьте клон в $repo или задайте GIT_ALLOW_RESET=1 (риск)." >&2
return 1
}
read_ssh_monitor_version() {
local f="$1"
local line
[ -f "$f" ] || return 0
line=$(grep -E '^[[:space:]]*SSH_MONITOR_VERSION=' "$f" 2>/dev/null | head -1) || return 0
line=${line#SSH_MONITOR_VERSION=}
line=${line#\"}
line=${line%\"}
line=${line#\'}
line=${line%\'}
printf '%s' "$line"
}
# Версия SAC-модуля совпадает с SSH_MONITOR_VERSION основного скрипта в том же каталоге.
read_sac_client_version() {
local sac_file="$1"
local dir="${sac_file%/*}"
read_ssh_monitor_version "$dir/ssh-monitor"
}
warn_version_mismatch() {
local repo_ssh="$1"
local repo_ver vt
repo_ver="$(read_ssh_monitor_version "$repo_ssh")"
[ -f "$repo_ssh" ] || return 0
if [ -f "${repo_ssh%/*}/version.txt" ]; then
vt="$(tr -d '\r\n' < "${repo_ssh%/*}/version.txt")"
if [ -n "$repo_ver" ] && [ -n "$vt" ] && [ "$repo_ver" != "$vt" ]; then
log_action "WARN: SSH_MONITOR_VERSION=$repo_ver != version.txt=$vt"
fi
fi
}
file_sha256() {
sha256sum "$1" 2>/dev/null | awk '{print $1}' || true
}
log_update_summary() {
log_action "===== Итог обновления ====="
log_action "ssh-monitor: $SUMMARY_SCRIPT ($SUMMARY_SCRIPT_REASON)"
log_action "sac-client.sh: $SUMMARY_SAC_CLIENT ($SUMMARY_SAC_REASON)"
log_action "конфиг: $SUMMARY_CONFIG"
log_action "git: $SUMMARY_GIT"
log_action "updater ($DEPLOY_INSTALL_PATH): $SUMMARY_UPDATER"
log_action "watchdog: $SUMMARY_WATCHDOG"
log_action "security hardening: $SUMMARY_HARDENING"
log_action "manifest: $SUMMARY_MANIFEST"
log_action "перезапуск ssh-monitor.service: $SUMMARY_SERVICE_RESTART"
log_action "полный лог: $LOG_FILE"
log_action "==========================="
}
# Функция для логирования шагов с комментариями (работает только при 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) || {
SUMMARY_SAC_CLIENT="не найден в клоне"
SUMMARY_SAC_REASON="путь $UPDATE_DIR/$SCRIPT_NAME/sac-client.sh"
log_action "WARNING: sac-client.sh не найден в клоне git"
debug_step "sac-client.sh не найден в клоне репозитория"
return 0
}
local_dst="${LOCAL_SCRIPT_PATH%/*}/sac-client.sh"
local remote_ver local_ver
remote_ver="$(read_sac_client_version "$remote")"
local_ver="$(read_sac_client_version "$local_dst")"
log_action "sac-client: remote=$remote ver=${remote_ver:-?} local=$local_dst ver=${local_ver:-?}"
remote_sum=$(file_sha256 "$remote")
if [ -z "$remote_sum" ]; then
SUMMARY_SAC_CLIENT="ошибка"
SUMMARY_SAC_REASON="не удалось sha256 remote"
log_action "ERROR: не удалось посчитать sha256 для $remote"
return 1
fi
if [ "$DEPLOY_MODE" -ne 1 ] && [ -f "$local_dst" ]; then
local_sum=$(file_sha256 "$local_dst")
if [ "$local_sum" = "$remote_sum" ]; then
SUMMARY_SAC_CLIENT="без изменений"
SUMMARY_SAC_REASON="sha256 совпадает; ver=${remote_ver:-?} (${local_sum:0:16}…)"
log_action "sac-client.sh: checksum совпадает, копирование не требуется"
debug_step "sac-client.sh без изменений"
return 0
fi
log_action "sac-client.sh: checksum отличается (local ${local_sum:0:16}… != remote ${remote_sum:0:16}…)"
elif [ ! -f "$local_dst" ]; then
log_action "sac-client.sh: локальный файл отсутствует, будет установлен"
debug_step "Локальный sac-client.sh отсутствует, будет установлен"
fi
debug_step "Копируем sac-client.sh в $local_dst"
if ! cp "$remote" "$local_dst"; then
SUMMARY_SAC_CLIENT="ошибка копирования"
SUMMARY_SAC_REASON="cp $remote -> $local_dst"
log_action "ERROR: не удалось скопировать sac-client.sh"
return 1
fi
chmod 644 "$local_dst"
SAC_CLIENT_CHANGED=1
SUMMARY_SAC_CLIENT="обновлён"
SUMMARY_SAC_REASON="ver ${local_ver:-?}${remote_ver:-?}, sha256 ${remote_sum:0:16}"
log_action "sac-client.sh обновлён → $local_dst"
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_ok "sac-client.sh → $local_dst"
fi
return 0
}
find_ssh_monitor_perms_remote() {
local p="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-perms.sh"
if [ -f "$p" ]; then
printf '%s\n' "$p"
return 0
fi
return 1
}
sync_ssh_monitor_perms_lib() {
local remote local_dst remote_sum local_sum=""
remote=$(find_ssh_monitor_perms_remote) || {
log_action "WARNING: ssh-monitor-perms.sh не найден в клоне git"
return 0
}
local_dst="${LOCAL_SCRIPT_PATH%/*}/ssh-monitor-perms.sh"
remote_sum=$(file_sha256 "$remote")
if [ -f "$local_dst" ]; then
local_sum=$(file_sha256 "$local_dst")
if [ "$local_sum" = "$remote_sum" ]; then
return 0
fi
fi
if ! cp "$remote" "$local_dst"; then
log_action "ERROR: не удалось скопировать ssh-monitor-perms.sh"
return 1
fi
chmod 644 "$local_dst"
log_action "ssh-monitor-perms.sh обновлён → $local_dst"
return 0
}
find_remote_ssh_monitor_path() {
local repo="$UPDATE_DIR/$SCRIPT_NAME"
local candidates=() git_path="" found="" p
REMOTE_SCRIPT_PATH="$repo/ssh-monitor"
ALT_REMOTE_PATH="$repo/src/branch/main/ssh-monitor"
candidates=(
"$REMOTE_SCRIPT_PATH"
"$ALT_REMOTE_PATH"
)
for p in "${candidates[@]}"; do
if [ -f "$p" ]; then
log_action "ssh-monitor в клоне: $p"
printf '%s\n' "$p"
return 0
fi
done
if [ -d "$repo/.git" ]; then
if git -C "$repo" cat-file -e "HEAD:ssh-monitor" 2>/dev/null; then
if [ ! -f "$repo/ssh-monitor" ]; then
log_action "ssh-monitor есть в git HEAD, но отсутствует на диске — git checkout HEAD -- ssh-monitor"
git -C "$repo" checkout HEAD -- ssh-monitor >> "$LOG_FILE" 2>&1 || true
fi
if [ -f "$repo/ssh-monitor" ]; then
log_action "ssh-monitor восстановлен: $repo/ssh-monitor"
printf '%s\n' "$repo/ssh-monitor"
return 0
fi
fi
git_path=$(git -C "$repo" ls-files 'ssh-monitor' 2>/dev/null | grep -v '^contrib/' | head -1)
if [ -z "$git_path" ]; then
git_path=$(git -C "$repo" ls-files 2>/dev/null | grep -E '/ssh-monitor$' | grep -v contrib | grep -v watchdog | head -1)
fi
if [ -n "$git_path" ] && [ -f "$repo/$git_path" ]; then
log_action "ssh-monitor в клоне (git ls-files): $repo/$git_path"
printf '%s\n' "$repo/$git_path"
return 0
fi
fi
found=$(find "$repo" -maxdepth 4 -type f -name 'ssh-monitor' ! -path '*/contrib/*' 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found" ]; then
log_action "ssh-monitor найден поиском: $found"
printf '%s\n' "$found"
return 0
fi
log_action "WARNING: ssh-monitor не найден в $repo"
if [ -d "$repo" ]; then
log_action "корень клона: $(ls -1 "$repo" 2>/dev/null | tr '\n' ' ')"
fi
return 1
}
install_ssh_monitor_from() {
local actual_script_path="$1"
local backup_path=""
if [ -f "$LOCAL_SCRIPT_PATH" ]; then
backup_path="${LOCAL_SCRIPT_PATH}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$LOCAL_SCRIPT_PATH" "$backup_path" 2>/dev/null || true
log_action "резервная копия: $backup_path"
fi
if ! cp "$actual_script_path" "$LOCAL_SCRIPT_PATH"; then
log_action "ERROR: не удалось скопировать $actual_script_path$LOCAL_SCRIPT_PATH"
return 1
fi
chmod +x "$LOCAL_SCRIPT_PATH"
log_action "ssh-monitor установлен: $LOCAL_SCRIPT_PATH (версия $(read_ssh_monitor_version "$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 /var/lib/ssh-monitor 2>/dev/null || true
mkdir -p "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
chmod 700 "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
chown root:root "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true
say_ok "UPDATE_DIR=$UPDATE_DIR, spool=$SAC_SPOOL_DIR_DEFAULT"
}
deploy_install_config() {
say_step "Конфигурация"
ensure_ssh_monitor_config_file || return 1
sync_sac_config_keys || true
sync_required_config_keys || true
sync_heartbeat_interval_config || true
sync_server_ipv4_hint_config || true
sync_daily_report_enabled_config || true
apply_runtime_security_hardening
}
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"
elif [ ! -f "$example" ]; then
say_warn "Unit example не найден: $example"
else
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
fi
ensure_watchdog_installed || true
}
find_watchdog_remote() {
local p="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog"
if [ -f "$p" ]; then
printf '%s\n' "$p"
return 0
fi
return 1
}
sync_watchdog_script() {
WATCHDOG_CHANGED=0
local remote local_sum remote_sum
remote=$(find_watchdog_remote) || {
SUMMARY_WATCHDOG="скрипт не найден в клоне"
log_action "WARNING: ssh-monitor-watchdog не найден в клоне git"
return 0
}
remote_sum=$(file_sha256 "$remote")
if [ -z "$remote_sum" ]; then
SUMMARY_WATCHDOG="ошибка sha256 remote"
return 1
fi
if [ -f "$LOCAL_WATCHDOG_PATH" ]; then
local_sum=$(file_sha256 "$LOCAL_WATCHDOG_PATH")
if [ "$local_sum" = "$remote_sum" ]; then
log_action "ssh-monitor-watchdog: checksum совпадает, копирование не требуется"
return 0
fi
log_action "ssh-monitor-watchdog: обновление (sha ${local_sum:0:16}… → ${remote_sum:0:16}…)"
else
log_action "ssh-monitor-watchdog: локальный файл отсутствует, будет установлен"
fi
if ! cp "$remote" "$LOCAL_WATCHDOG_PATH"; then
SUMMARY_WATCHDOG="ошибка копирования"
log_action "ERROR: не удалось скопировать ssh-monitor-watchdog → $LOCAL_WATCHDOG_PATH"
return 1
fi
chmod 750 "$LOCAL_WATCHDOG_PATH"
WATCHDOG_CHANGED=1
SUMMARY_WATCHDOG="скрипт обновлён"
if [ "$DEPLOY_MODE" -eq 1 ]; then
say_ok "ssh-monitor-watchdog → $LOCAL_WATCHDOG_PATH"
fi
return 0
}
install_watchdog_systemd_units() {
local svc_example timer_example units_changed=0
svc_example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog.service.example"
timer_example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog.timer.example"
if [ ! -f "$svc_example" ] || [ ! -f "$timer_example" ]; then
log_action "WARNING: watchdog unit examples не найдены в клоне"
return 0
fi
if [ ! -f "$WATCHDOG_SERVICE_PATH" ] \
|| ! cmp -s "$svc_example" "$WATCHDOG_SERVICE_PATH" 2>/dev/null; then
cp "$svc_example" "$WATCHDOG_SERVICE_PATH"
units_changed=1
log_action "watchdog: установлен/обновлён $WATCHDOG_SERVICE_PATH"
fi
if [ ! -f "$WATCHDOG_TIMER_PATH" ] \
|| ! cmp -s "$timer_example" "$WATCHDOG_TIMER_PATH" 2>/dev/null; then
cp "$timer_example" "$WATCHDOG_TIMER_PATH"
units_changed=1
log_action "watchdog: установлен/обновлён $WATCHDOG_TIMER_PATH"
fi
if [ "$units_changed" -eq 1 ]; then
WATCHDOG_CHANGED=1
fi
if ! command -v systemctl &>/dev/null; then
log_action "watchdog: systemctl не найден, units только скопированы"
return 0
fi
systemctl daemon-reload 2>/dev/null || true
if systemctl enable ssh-monitor-watchdog.timer 2>/dev/null; then
log_action "watchdog: enable ssh-monitor-watchdog.timer"
fi
if systemctl start ssh-monitor-watchdog.timer 2>/dev/null; then
log_action "watchdog: start ssh-monitor-watchdog.timer"
if [ -z "$SUMMARY_WATCHDOG" ] || [ "$SUMMARY_WATCHDOG" = "—" ]; then
SUMMARY_WATCHDOG="timer активен"
elif [ "${WATCHDOG_CHANGED:-0}" -eq 1 ]; then
SUMMARY_WATCHDOG="${SUMMARY_WATCHDOG}; timer активен"
fi
else
log_action "WARNING: systemctl start ssh-monitor-watchdog.timer не выполнен"
fi
if [ "$DEPLOY_MODE" -eq 1 ] && [ "${WATCHDOG_CHANGED:-0}" -eq 1 ]; then
say_ok "watchdog: units + timer (ssh-monitor-watchdog.timer)"
fi
return 0
}
ensure_watchdog_installed() {
sync_watchdog_script || return 1
install_watchdog_systemd_units || return 1
if [ -z "$SUMMARY_WATCHDOG" ] || [ "$SUMMARY_WATCHDOG" = "—" ]; then
if [ -f "$LOCAL_WATCHDOG_PATH" ] && systemctl is-enabled ssh-monitor-watchdog.timer &>/dev/null; then
SUMMARY_WATCHDOG="без изменений (timer enabled)"
elif [ -f "$LOCAL_WATCHDOG_PATH" ]; then
SUMMARY_WATCHDOG="скрипт на месте, timer настроен"
else
SUMMARY_WATCHDOG="не установлен"
fi
fi
return 0
}
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
systemctl status ssh-monitor-watchdog.timer
systemctl list-timers | grep ssh-monitor-watchdog
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
}
# Копирует update_ssh_monitor.sh из git-клона в /opt/scripts/ и при необходимости re-exec.
sync_installed_updater_from_clone() {
local remote="$UPDATE_DIR/$SCRIPT_NAME/update_ssh_monitor.sh"
local self installed_path remote_sum installed_sum=""
local repo="$UPDATE_DIR/$SCRIPT_NAME" ver manifest_path=""
if [ ! -f "$remote" ]; then
SUMMARY_UPDATER="не найден в клоне"
log_action "updater: в клоне нет $remote"
return 0
fi
ver=$(read_repo_release_version "$repo")
if [ -n "$ver" ]; then
manifest_path=$(find_release_manifest_path "$repo" "$ver" 2>/dev/null || true)
if [ -n "$manifest_path" ] && [ "${MANIFEST_VERIFIED:-0}" != "1" ]; then
SUMMARY_UPDATER="manifest не verified"
log_action "ERROR: updater: $manifest_path не verified — копирование пропущено"
return 1
fi
fi
self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")"
installed_path="$(readlink -f "$DEPLOY_INSTALL_PATH" 2>/dev/null || realpath "$DEPLOY_INSTALL_PATH" 2>/dev/null || printf '%s' "$DEPLOY_INSTALL_PATH")"
remote_sum=$(file_sha256 "$remote")
if [ -f "$DEPLOY_INSTALL_PATH" ]; then
installed_sum=$(file_sha256 "$DEPLOY_INSTALL_PATH")
fi
if [ "$remote_sum" = "$installed_sum" ]; then
SUMMARY_UPDATER="актуален (sha ${remote_sum:0:16}…)"
log_action "updater: $DEPLOY_INSTALL_PATH совпадает с git (commit $(get_git_short_rev "$UPDATE_DIR/$SCRIPT_NAME"))"
return 0
fi
mkdir -p "$(dirname "$DEPLOY_INSTALL_PATH")"
if ! cp "$remote" "$DEPLOY_INSTALL_PATH"; then
SUMMARY_UPDATER="ошибка копирования"
log_action "ERROR: не удалось обновить $DEPLOY_INSTALL_PATH из git"
return 1
fi
chmod 750 "$DEPLOY_INSTALL_PATH"
SUMMARY_UPDATER="обновлён из git (${installed_sum:0:8}…→${remote_sum:0:8}…)"
log_action "updater: записан $DEPLOY_INSTALL_PATH из клона (новый sha ${remote_sum:0:16}…)"
if [ "$self" = "$installed_path" ] && [ "${UPDATER_REEXEC_GUARD:-0}" != "1" ]; then
log_action "updater: перезапуск с новой версией (расширенный лог)…"
UPDATER_REEXEC_GUARD=1 exec /bin/bash "$DEPLOY_INSTALL_PATH" "$@"
fi
if [ "$self" != "$installed_path" ]; then
log_action "updater: сейчас запущен $self — в cron используйте $DEPLOY_INSTALL_PATH"
fi
return 0
}
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
SUMMARY_SERVICE_RESTART="выполнен"
log_action "ssh-monitor.service перезапущен"
debug_step "Сервис успешно перезапущен"
local i
for i in $(seq 1 30); do
if systemctl is-active --quiet ssh-monitor.service 2>/dev/null; then
sleep 2
break
fi
sleep 1
done
else
SUMMARY_SERVICE_RESTART="не удался"
log_action "WARNING: systemctl restart ssh-monitor.service не выполнен"
debug_step "ПРЕДУПРЕЖДЕНИЕ: Не удалось перезапустить сервис"
fi
else
SUMMARY_SERVICE_RESTART="пропущен (нет systemctl)"
log_action "systemctl не найден, перезапуск сервиса пропущен"
debug_step "systemctl не найден, сервис не перезапущен"
fi
}
# Функция для обновления скрипта (принимает "$@" для re-exec updater)
update_script() {
local git_repo="$UPDATE_DIR/$SCRIPT_NAME"
local git_before="" git_after="" pull_ok=1
local actual_script_path="" script_changed=0
local local_ver="" remote_ver="" local_sum="" remote_sum=""
SUMMARY_SCRIPT="—"
SUMMARY_SCRIPT_REASON="—"
SUMMARY_SAC_CLIENT="—"
SUMMARY_SAC_REASON="—"
SUMMARY_CONFIG="—"
SUMMARY_GIT="—"
SUMMARY_HARDENING="—"
SUMMARY_MANIFEST="—"
SUMMARY_SERVICE_RESTART="не требовался"
load_git_verify_settings
load_ssh_monitor_perms_lib || true
log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG, git_ref=${GIT_REF:-$GIT_BRANCH})"
debug_step "Запуск функции обновления скрипта"
debug_step "Гарантируем наличие директории для работы"
if ! ensure_directories; then
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="нет каталога $UPDATE_DIR или нет прав на запись"
log_update_summary
return 1
fi
debug_step "Переходим в директорию для клона репозитория: $UPDATE_DIR"
if ! cd "$UPDATE_DIR" 2>/dev/null; then
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="cannot cd $UPDATE_DIR"
log_action "ERROR: cannot cd $UPDATE_DIR"
log_update_summary
return 1
fi
debug_step "Проверяем наличие репозитория и обновляем/клонируем"
if [ -d "$SCRIPT_NAME" ]; then
if sync_git_repository "$git_repo" "$GIT_BRANCH"; then
pull_ok=1
else
pull_ok=0
log_action "WARNING: git sync не удался — remote в клоне может быть устаревшим"
fi
else
local clone_ref="${GIT_REF:-$GIT_BRANCH}"
log_action "git: clone -b $clone_ref $REPO_URL$UPDATE_DIR/$SCRIPT_NAME"
debug_step "Выполняем git clone -b $clone_ref $REPO_URL $SCRIPT_NAME"
if ! git clone -b "$clone_ref" "$REPO_URL" "$SCRIPT_NAME" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="clone FAILED"
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="git clone failed"
log_action "ERROR: git clone failed (см. $LOG_FILE)"
log_update_summary
return 1
fi
if [ -n "${GIT_REF:-}" ] && ! verify_git_ref "$git_repo" "$GIT_REF" "$GIT_VERIFY_MODE"; then
SUMMARY_GIT="verify FAILED ($GIT_REF)"
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="GIT_REF verify failed"
log_update_summary
return 1
fi
git_after="$(get_git_short_rev "$git_repo")"
SUMMARY_GIT="clone ok, commit $git_after"
log_action "git clone: успех (commit $git_after)"
fi
if ! verify_release_manifest "$git_repo"; then
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="manifest verify failed"
log_update_summary
return 1
fi
sync_installed_updater_from_clone "$@"
actual_script_path="$(find_remote_ssh_monitor_path 2>/dev/null || true)"
actual_script_path="${actual_script_path//$'\n'/}"
actual_script_path="${actual_script_path//$'\r'/}"
debug_step "Путь к ssh-monitor в клоне: ${actual_script_path:-не найден}"
if [ -n "$actual_script_path" ] && [ -f "$actual_script_path" ]; then
warn_version_mismatch "$actual_script_path"
fi
SAC_CLIENT_CHANGED=0
if ! sync_sac_client; then
log_update_summary
return 1
fi
sync_ssh_monitor_perms_lib || true
WATCHDOG_CHANGED=0
if ! ensure_watchdog_installed; then
log_action "WARNING: установка watchdog завершилась с ошибкой"
fi
CONFIG_CHANGED=0
if ! ensure_ssh_monitor_config_file; then
log_update_summary
return 1
fi
if sync_sac_config_keys; then
SUMMARY_CONFIG="SAC-ключи дополнены, UseSAC=fallback"
else
SUMMARY_CONFIG="SAC без изменений"
fi
sync_required_config_keys && SUMMARY_CONFIG="${SUMMARY_CONFIG}; security keys"
sync_heartbeat_interval_config || true
sync_server_ipv4_hint_config || true
sync_daily_report_enabled_config || true
harden_ssh_monitor_config_perms
if [ "${CONFIG_CHANGED:-0}" -eq 1 ] && [ "$SUMMARY_CONFIG" = "SAC без изменений" ]; then
SUMMARY_CONFIG="heartbeat/watchdog выровнены"
elif [ "${CONFIG_CHANGED:-0}" -eq 1 ] && [ "$SUMMARY_CONFIG" != "SAC без изменений" ]; then
SUMMARY_CONFIG="${SUMMARY_CONFIG}; heartbeat при необходимости"
fi
if [ -n "$actual_script_path" ] && [ -f "$actual_script_path" ]; then
if [ "$DEPLOY_MODE" -eq 1 ]; then
log_action "режим --deploy: установка ssh-monitor → $LOCAL_SCRIPT_PATH"
if install_ssh_monitor_from "$actual_script_path"; then
script_changed=1
SUMMARY_SCRIPT="установлен (--deploy)"
SUMMARY_SCRIPT_REASON="$(read_ssh_monitor_version "$LOCAL_SCRIPT_PATH")"
else
SUMMARY_SCRIPT="ошибка установки"
log_update_summary
return 1
fi
else
if [ ! -f "$LOCAL_SCRIPT_PATH" ]; then
LOCAL_CHECKSUM=""
local_ver="(нет файла)"
else
LOCAL_CHECKSUM=$(file_sha256 "$LOCAL_SCRIPT_PATH")
local_ver="$(read_ssh_monitor_version "$LOCAL_SCRIPT_PATH")"
fi
REMOTE_CHECKSUM=$(file_sha256 "$actual_script_path")
remote_ver="$(read_ssh_monitor_version "$actual_script_path")"
log_action "сравнение: local=$LOCAL_SCRIPT_PATH ver=$local_ver sha=${LOCAL_CHECKSUM:0:16}"
log_action "сравнение: remote=$actual_script_path ver=$remote_ver sha=${REMOTE_CHECKSUM:0:16}"
if [ "$pull_ok" -eq 0 ]; then
log_action "примечание: git pull не удался — remote может быть устаревшим"
fi
if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then
log_action "checksum различается — обновляем $LOCAL_SCRIPT_PATH"
if install_ssh_monitor_from "$actual_script_path"; then
script_changed=1
SUMMARY_SCRIPT="обновлён"
SUMMARY_SCRIPT_REASON="${local_ver}${remote_ver}, sha изменился"
else
SUMMARY_SCRIPT="ошибка копирования"
log_update_summary
return 1
fi
else
SUMMARY_SCRIPT="без изменений"
SUMMARY_SCRIPT_REASON="sha256 совпадает; версия local=$local_ver remote=$remote_ver"
log_action "ssh-monitor: файлы идентичны, копирование не требуется"
fi
fi
else
SUMMARY_SCRIPT="не найден в клоне"
SUMMARY_SCRIPT_REASON="проверьте структуру репозитория после git pull"
log_action "ERROR: ssh-monitor не найден в клоне $UPDATE_DIR/$SCRIPT_NAME"
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
fi
apply_runtime_security_hardening
if [ "$script_changed" -eq 1 ] || [ "${SAC_CLIENT_CHANGED:-0}" -eq 1 ] || [ "${CONFIG_CHANGED:-0}" -eq 1 ]; then
restart_ssh_monitor_service
else
log_action "перезапуск сервиса не нужен (ни один файл не изменился)"
fi
log_script_vars
log_update_summary
return 0
}
# === Основной блок выполнения ===
parse_command_line "$@"
deploy_require_root
require_repo_url
load_ssh_monitor_perms_lib || true
if declare -F ssh_monitor_update_mark_begin >/dev/null 2>&1; then
ssh_monitor_update_mark_begin
fi
if [ "$DEPLOY_MODE" -eq 1 ]; then
printf '\n=== ssh-monitor: первичная установка (--deploy) ===\n'
say_step "Репозиторий и файлы"
fi
_updater_self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")"
_updater_sha="$(file_sha256 "$_updater_self")"
log_message "INFO" "=== Script update started (deploy=$DEPLOY_MODE) ==="
log_action "запуск updater: $_updater_self (sha ${_updater_sha:0:16}…)"
log_action "хост=$(hostname 2>/dev/null || echo '?') user=$(id -un 2>/dev/null) uid=$(id -u 2>/dev/null)"
log_action "пути: LOCAL=$LOCAL_SCRIPT_PATH UPDATE_DIR=$UPDATE_DIR REPO=$REPO_URL"
debug_step "Инициализация скрипта обновления"
if ! ensure_directories; then
log_message "INFO" "ERROR: Failed to ensure directories. Exiting."
exit 1
fi
maybe_enable_deploy_for_missing_install
cleanup_update_state() {
if declare -F ssh_monitor_update_mark_end >/dev/null 2>&1; then
ssh_monitor_update_mark_end
fi
}
trap cleanup_update_state EXIT
debug_step "Начинаем проверку и установку ipset (только --deploy / first_deploy)"
if [ "$DEPLOY_MODE" -eq 1 ]; then
if ! ensure_ipset_installed; then
log_message "ERROR" "CRITICAL: ipset installation or verification failed"
echo "ERROR: Работа deploy невозможна без установленного ipset" >&2
exit 1
fi
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 ==="
log_action "завершено успешно (код 0). Итог см. выше или: tail -30 $LOG_FILE"
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) ==="
log_action "завершено с ошибкой (код $exit_code). См. $LOG_FILE"
fi
exit $exit_code