diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..8a998c0 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,117 @@ +# Массовый деплой InfoUser Agent + +Скрипт `deploy-agent.sh` устанавливает или обновляет агент `info-user-agent` на списке Linux-хостов по SSH. + +## Принцип работы + +1. Пытается подключиться по ключу `id_rsa`. +2. Если ключ не подходит — подключается по паролю и копирует публичный ключ в `~/.ssh/authorized_keys` (последующие запуски будут по ключу). +3. Устанавливает/обновляет агент: + - Сначала `apt-get install info-user-agent` из подключённого apt-репозитория. + - Если apt не сработал — fallback через `dpkg -i` из `.deb` в корне проекта. +4. Пишет `config.json` с адресом сервера и токеном. +5. Запускает systemd-таймер и триггерит первый heartbeat. +6. Раскладывает результат в `deploy-ok.txt` / `deploy-deferred.txt` и логи в `logs/`. + +## Требования + +На машине, с которой запускается скрипт: + +- `bash` +- `ssh`, `scp` +- `sshpass` +- `ping` + +На целевых хостах: + +- Доступен SSH-сервер. +- Пользователь входит в `sudo` (без пароля на сам sudo запросы идут с паролем). +- Подключён apt-репозиторий с пакетом `info-user-agent` (для основного пути установки). + +## Подготовка + +### 1. SSH-ключ + +Положите приватный ключ рядом со скриптом: + +```bash +cp /path/to/id_rsa ./id_rsa +chmod 600 ./id_rsa +``` + +Публичный ключ `./id_rsa.pub` будет сгенерирован автоматически при первом запуске. + +### 2. Список хостов + +Создайте `ip.txt` (один хост на строку): + +```text +user@192.168.255.50 +user@192.168.255.16 +192.168.255.71 +# это комментарий +``` + +Если пользователь не указан, используется `user`. + +### 3. Параметры подключения + +По умолчанию: + +- SSH-порт: `22` +- SSH/Sudo-пароль: `1q2w3e$R` +- Пользователь по умолчанию: `user` +- Адрес сервера: `http://192.168.255.180:3000` +- Токен агента: `a916c8cd656b3f40acc2920820abf88e` + +Переопределить можно через переменные окружения: + +```bash +export SSH_PASS='MyS3cr3t$' +export SERVER_URL='http://monitor.example.com:3000' +export AGENT_TOKEN='my-token' +export SSH_PORT=2222 +export DEFAULT_USER='admin' +./deploy-agent.sh +``` + +## Запуск + +```bash +cd /home/user/moder/info-user/deploy +./deploy-agent.sh +``` + +## Результат + +После завершения рядом со скриптом появятся: + +- `deploy-ok.txt` — хосты, на которых всё прошло успешно. +- `deploy-deferred.txt` — хосты, которые надо доразворачивать (нет пинга, нет SSH, ошибка установки). +- `logs/` — подробные логи по каждому хосту. +- `ip.txt.bak.YYYYMMDD_HHMMSS` — бэкап списка хостов. + +Если в `deploy-deferred.txt` что-то есть, исправьте проблему и запустите скрипт снова — он переберёт весь список заново. + +## Установка отдельного хоста вручную + +Если хост не попадает под массовый деплой: + +```bash +# На хосте +sudo apt-get update +sudo apt-get install -y info-user-agent +sudo info-user-agent-setup +# или отредактируйте /opt/info-user-agent/config.json вручную +sudo systemctl start info-user-agent.timer +sudo systemctl start info-user-agent +``` + +## Troubleshooting + +- **"SSH недоступен (ключ и пароль)"** — хост пингуется, но не принимает ни ключ, ни пароль. Проверьте пользователя/пароль, настройки SSH (`PermitRootLogin` / `PasswordAuthentication`). +- **"Ошибка установки"** — apt-репозиторий недоступен и fallback `.deb` не найден. Положите актуальный `.deb` в корень проекта (`info-user-agent_*.deb`). +- **sshpass не найден** — установите: + ```bash + sudo apt-get install -y sshpass + ``` diff --git a/deploy/deploy-agent.sh b/deploy/deploy-agent.sh new file mode 100755 index 0000000..f15dd83 --- /dev/null +++ b/deploy/deploy-agent.sh @@ -0,0 +1,389 @@ +#!/usr/bin/env bash +# +# deploy-agent.sh — массовая установка/обновление InfoUser Agent. +# +# Использование: +# cd /home/user/moder/info-user/deploy +# ./deploy-agent.sh +# +# Перед запуском: +# 1. Положить id_rsa в ./id_rsa +# 2. Создать ./ip.txt со списком хостов (user@IP или просто IP) +# 3. При необходимости изменить SERVER_URL/AGENT_TOKEN ниже или через env +# +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +HOSTS_FILE="${SCRIPT_DIR}/ip.txt" +KEY_FILE="${SCRIPT_DIR}/id_rsa" +PUB_FILE="${SCRIPT_DIR}/id_rsa.pub" + +OK_FILE="${SCRIPT_DIR}/deploy-ok.txt" +DEFER_FILE="${SCRIPT_DIR}/deploy-deferred.txt" +LOG_DIR="${SCRIPT_DIR}/logs" + +PKG_NAME="info-user-agent" +AGENT_CONFIG_DIR="/opt/info-user-agent" +AGENT_CONFIG_FILE="${AGENT_CONFIG_DIR}/config.json" + +SERVER_URL="${SERVER_URL:-http://192.168.255.180:3000}" +AGENT_TOKEN="${AGENT_TOKEN:-a916c8cd656b3f40acc2920820abf88e}" +SSH_PASS="${SSH_PASS:-1q2w3e\$R}" +SSH_PORT="${SSH_PORT:-22}" +DEFAULT_USER="${DEFAULT_USER:-user}" +MAX_RETRIES="${MAX_RETRIES:-3}" +RETRY_SLEEP="${RETRY_SLEEP:-3}" + +# Цвета +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SSH_KEY_OPTS=( + -o BatchMode=yes + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=6 + -o ConnectionAttempts=1 + -o ServerAliveInterval=5 + -o ServerAliveCountMax=1 + -o PasswordAuthentication=no + -p "$SSH_PORT" +) + +SSH_PASS_OPTS=( + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=6 + -o ConnectionAttempts=1 + -o ServerAliveInterval=5 + -o ServerAliveCountMax=1 + -p "$SSH_PORT" +) + +SCP_KEY_OPTS=( + -i "$KEY_FILE" + -o BatchMode=yes + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=6 + -o ConnectionAttempts=1 + -p "$SSH_PORT" +) + +SCP_PASS_OPTS=( + -o StrictHostKeyChecking=accept-new + -o ConnectTimeout=6 + -o ConnectionAttempts=1 + -p "$SSH_PORT" +) + +print_header() { + echo -e "${BLUE}=== InfoUser Agent deploy ===${NC}" + echo "SERVER_URL: $SERVER_URL" + echo "AGENT_TOKEN: ${AGENT_TOKEN:0:8}..." + echo "HOSTS_FILE: $HOSTS_FILE" + echo "KEY_FILE: $KEY_FILE" + echo "LOG_DIR: $LOG_DIR" + echo "" +} + +check_prerequisites() { + local missing=() + command -v ssh >/dev/null 2>&1 || missing+=("ssh") + command -v scp >/dev/null 2>&1 || missing+=("scp") + command -v sshpass >/dev/null 2>&1 || missing+=("sshpass") + command -v ping >/dev/null 2>&1 || missing+=("ping") + + if [[ ${#missing[@]} -gt 0 ]]; then + echo -e "${RED}Ошибка: не найдены утилиты: ${missing[*]}${NC}" >&2 + exit 1 + fi + + if [[ ! -f "$HOSTS_FILE" ]]; then + echo -e "${RED}Ошибка: файл со списком хостов не найден: $HOSTS_FILE${NC}" >&2 + echo "Создайте его по примеру ip.txt.example" >&2 + exit 1 + fi + + if [[ ! -f "$KEY_FILE" ]]; then + echo -e "${RED}Ошибка: SSH-ключ не найден: $KEY_FILE${NC}" >&2 + exit 1 + fi + + # Генерируем .pub если нет + if [[ ! -f "$PUB_FILE" ]]; then + echo "Генерирую публичный ключ: $PUB_FILE" + ssh-keygen -y -f "$KEY_FILE" > "$PUB_FILE" + fi + + # Ищем fallback .deb в корне проекта + DEB_FILE="" + for f in "${PROJECT_DIR}"/${PKG_NAME}_*_all.deb; do + [[ -f "$f" ]] && DEB_FILE="$f" + done + + if [[ -z "$DEB_FILE" ]]; then + echo -e "${YELLOW}Предупреждение: fallback .deb не найден в $PROJECT_DIR. Будет использован только apt.${NC}" + else + echo "Fallback .deb: $DEB_FILE" + fi +} + +normalize_target() { + local line="$1" + if [[ "$line" == *"@"* ]]; then + echo "$line" + else + echo "${DEFAULT_USER}@${line}" + fi +} + +extract_host() { + local target="$1" + echo "${target#*@}" +} + +safe_name() { + local target="$1" + echo "${target//[@.:\/]/_}" +} + +with_retries() { + local desc="$1" + shift + local attempt=1 + while true; do + if "$@"; then + return 0 + fi + if (( attempt >= MAX_RETRIES )); then + return 1 + fi + echo " ${desc}: повтор ${attempt}/${MAX_RETRIES}..." + attempt=$((attempt + 1)) + sleep "$RETRY_SLEEP" + done +} + +ping_check() { + local host="$1" + ping -c 1 -W 1 "$host" >/dev/null 2>&1 +} + +ssh_key_check() { + local target="$1" + ssh -i "$KEY_FILE" -n "${SSH_KEY_OPTS[@]}" "$target" "echo ok" >/dev/null 2>&1 +} + +ssh_pass_check() { + local target="$1" + sshpass -p "$SSH_PASS" ssh -n "${SSH_PASS_OPTS[@]}" "$target" "echo ok" >/dev/null 2>&1 +} + +detect_auth_method() { + local target="$1" + if ssh_key_check "$target"; then + echo "key" + return 0 + fi + if ssh_pass_check "$target"; then + echo "pass" + return 0 + fi + echo "fail" +} + +install_key_on_host() { + local target="$1" + local log_file="$2" + { + echo "==> SSH по паролю успешен, добавляю ключ в authorized_keys" + sshpass -p "$SSH_PASS" ssh -n "${SSH_PASS_OPTS[@]}" "$target" "mkdir -p ~/.ssh && chmod 700 ~/.ssh" + sshpass -p "$SSH_PASS" scp "${SCP_PASS_OPTS[@]}" "$PUB_FILE" "$target:/tmp/info_user_key.pub" + sshpass -p "$SSH_PASS" ssh -n "${SSH_PASS_OPTS[@]}" "$target" "cat /tmp/info_user_key.pub >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && rm -f /tmp/info_user_key.pub" + } >>"$log_file" 2>&1 || true +} + +ssh_exec() { + local method="$1" + local target="$2" + local cmd="$3" + if [[ "$method" == "key" ]]; then + ssh -i "$KEY_FILE" -n "${SSH_KEY_OPTS[@]}" "$target" "$cmd" + else + sshpass -p "$SSH_PASS" ssh -n "${SSH_PASS_OPTS[@]}" "$target" "$cmd" + fi +} + +scp_to_host() { + local method="$1" + local target="$2" + local local_path="$3" + local remote_path="$4" + if [[ "$method" == "key" ]]; then + scp "${SCP_KEY_OPTS[@]}" "$local_path" "$target:$remote_path" + else + sshpass -p "$SSH_PASS" scp "${SCP_PASS_OPTS[@]}" "$local_path" "$target:$remote_path" + fi +} + +configure_agent() { + local method="$1" + local target="$2" + local log_file="$3" + + local config_json + config_json=$(printf '{"server_url":"%s","agent_token":"%s","interval_minutes":60,"send_public_ip":false}' "$SERVER_URL" "$AGENT_TOKEN") + + local remote_cmd + remote_cmd=$(printf 'echo "%s" | sudo -S tee %s >/dev/null' "$config_json" "$AGENT_CONFIG_FILE") + + { + echo "==> Настраиваю $AGENT_CONFIG_FILE" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S mkdir -p '$AGENT_CONFIG_DIR'" + ssh_exec "$method" "$target" "$remote_cmd" + + echo "==> Запускаю таймер и первый heartbeat" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S systemctl daemon-reload" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S systemctl enable info-user-agent.timer" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S systemctl start info-user-agent.timer" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S systemctl start info-user-agent" + } >>"$log_file" 2>&1 +} + +install_agent() { + local method="$1" + local target="$2" + local log_file="$3" + + # Сначала пробуем apt + { + echo "==> Пробую установить через apt" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S apt-get update -qq" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S apt-get install -y '$PKG_NAME'" + } >>"$log_file" 2>&1 && return 0 + + # Fallback на dpkg если apt не сработал и есть .deb + if [[ -z "$DEB_FILE" ]]; then + echo " apt не сработал, fallback .deb отсутствует" >>"$log_file" 2>&1 + return 1 + fi + + local deb_name + deb_name="$(basename "$DEB_FILE")" + local remote_deb="/tmp/${deb_name}" + + { + echo "==> apt не сработал, пробую dpkg -i $deb_name" + scp_to_host "$method" "$target" "$DEB_FILE" "$remote_deb" + ssh_exec "$method" "$target" "echo '$SSH_PASS' | sudo -S dpkg -i '$remote_deb'" + ssh_exec "$method" "$target" "rm -f '$remote_deb'" + } >>"$log_file" 2>&1 +} + +process_host() { + local raw_line="$1" + local target + local host_only + local safe_host + local log_file + local auth_method + + target="$(normalize_target "$raw_line")" + host_only="$(extract_host "$target")" + safe_host="$(safe_name "$target")" + log_file="${LOG_DIR}/${safe_host}.log" + + echo "" + echo -e "${YELLOW}===== $target =====${NC}" + + # Пинг + if ! with_retries "ping" ping_check "$host_only"; then + echo -e "${RED}❌ Пинг не проходит: $target -> отложен${NC}" + echo "$target" >> "$DEFER_FILE" + return 0 + fi + + # Определяем метод авторизации + auth_method="$(detect_auth_method "$target")" + if [[ "$auth_method" == "fail" ]]; then + echo -e "${RED}❌ SSH недоступен (ключ и пароль): $target -> отложен${NC}" + echo "$target" >> "$DEFER_FILE" + return 0 + fi + + if [[ "$auth_method" == "key" ]]; then + echo -e "${GREEN}✅ SSH по ключу: $target${NC}" + else + echo -e "${GREEN}✅ SSH по паролю: $target (добавляю ключ)${NC}" + install_key_on_host "$target" "$log_file" + fi + + # Установка/обновление + if install_agent "$auth_method" "$target" "$log_file"; then + echo -e "${GREEN}✅ Агент установлен/обновлён: $target${NC}" + else + echo -e "${RED}❌ Ошибка установки: $target -> отложен (см. $log_file)${NC}" + echo "$target" >> "$DEFER_FILE" + return 0 + fi + + # Настройка и запуск + if configure_agent "$auth_method" "$target" "$log_file"; then + echo -e "${GREEN}✅ Настроен и запущен: $target${NC}" + echo "$target" >> "$OK_FILE" + else + echo -e "${RED}❌ Установлен, но не удалось настроить/запустить: $target -> отложен (см. $log_file)${NC}" + echo "$target" >> "$DEFER_FILE" + fi +} + +print_summary() { + local ok_count + local defer_count + ok_count="$(wc -l < "$OK_FILE" 2>/dev/null || echo 0)" + defer_count="$(wc -l < "$DEFER_FILE" 2>/dev/null || echo 0)" + + echo "" + echo -e "${BLUE}=== Итог ===${NC}" + echo -e "${GREEN}Успешно: $ok_count${NC}" + echo -e "${RED}Отложено: $defer_count${NC}" + echo "Логи: $LOG_DIR/" + if [[ -s "$DEFER_FILE" ]]; then + echo "Повторить для отложенных: ./deploy-agent.sh" + fi +} + +main() { + print_header + check_prerequisites + + mkdir -p "$LOG_DIR" + : > "$OK_FILE" + : > "$DEFER_FILE" + + # Бэкап ip.txt + cp -f "$HOSTS_FILE" "${HOSTS_FILE}.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true + + local total=0 + total="$(grep -vE '^\s*$|^\s*#' "$HOSTS_FILE" | wc -l)" + echo -e "${BLUE}Хостов в списке: $total${NC}" + + while IFS= read -r line || [[ -n "$line" ]]; do + # Убираем CR (Windows-концы строк) + line="${line//$'\r'/}" + + # trim + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + + [[ -z "$line" || "$line" =~ ^# ]] && continue + process_host "$line" + done < "$HOSTS_FILE" + + print_summary +} + +main "$@" diff --git a/deploy/ip.txt.example b/deploy/ip.txt.example new file mode 100644 index 0000000..ce359c4 --- /dev/null +++ b/deploy/ip.txt.example @@ -0,0 +1,7 @@ +# Список хостов для массового деплоя InfoUser Agent. +# Формат: user@IP или просто IP (будет использован DEFAULT_USER). +# Пустые строки и строки, начинающиеся с #, игнорируются. + +user@192.168.255.50 +user@192.168.255.16 +192.168.255.71