diff --git a/README.md b/README.md index 16bbda4..59df1b8 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,50 @@ curl -s https://panel.example.com/api/v1/servers ``` +## Массовое развёртывание агентов (в том числе ИИ-агентом) + +`monitor/deploy.sh` делает весь цикл сам: создаёт сервер на панели → +заходит по SSH → ставит агента → ждёт первый пакет. Одна команда на +любое число хостов: + +```bash +cd monitor +./deploy.sh --panel https://panel.example.com user@web-01 user@db-01 user@cache-01 +``` + +Опции: + +| Опция | Что делает | +| --- | --- | +| `--panel URL` | адрес панели (обязательно) | +| `--token TOKEN` | GHARD_ADMIN_TOKEN — нужен, только если токен на панели задан | +| `--interval N` | интервал агента в секундах (по умолчанию 30) | +| `--recreate` | удалить существующую запись сервера на панели и создать заново (новый ключ); без него ключ переиспользуется из `/opt/hard-monitor/.env` на хосте | +| `--git-install` | ставить curl-ом из репозитория; по умолчанию файлы агента заливаются scp с локальной машины | + +Требования к хостам: SSH-доступ по ключу, `python3`, `sudo` без пароля. +Повторный запуск на уже развёрнутом хосте — обновление агента +(запись на панели и ключ сохраняются). + +### Развёртывание ИИ-агентом (Claude Code и т.п.) + +Скрипт специально сделан неинтерактивным и идемпотентным, чтобы его мог +запустить ИИ-агент с SSH-доступом к машинам. Промт для агента: + +```text +Разверни hard-monitor на моих серверах. Панель: https://panel.example.com +1. Склонируй https://git.gnexus.space/git/root/hard-panel.git (нужна папка monitor/) +2. Запусти: cd monitor && ./deploy.sh --panel https://panel.example.com user@host1 user@host2 ... +3. Если какой-то хост упал с ошибкой — разберись (ssh, journalctl -u hard-monitor), + почини и перезапусти deploy.sh только для этого хоста +4. Проверь, что все серверы online: curl -s https://panel.example.com/api/v1/servers + (у каждого status: "online") +``` + +Агенту не нужен доступ к UI панели: серверы создаются через API, ключи +никогда не показываются человеку и не сохраняются — install.sh сразу +кладёт их в `/opt/hard-monitor/.env` (права 600). + ## Обновление **Panel** — на сервере с панелью: diff --git a/monitor/deploy.sh b/monitor/deploy.sh new file mode 100755 index 0000000..20af68d --- /dev/null +++ b/monitor/deploy.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Массовое развёртывание hard-monitor по SSH — в одну команду на список хостов. +# Для каждой машины: создать сервер на панели (если нет) → залить агента → +# установить (install.sh) → дождаться первого пакета. +# +# ./deploy.sh --panel https://panel.example.com user@host1 user@host2 +# +# Опции: +# --panel URL адрес панели (обязательно) +# --token TOKEN GHARD_ADMIN_TOKEN панели (нужен, если токен на панели задан) +# --interval N интервал агента в секундах (по умолчанию 30) +# --recreate если сервер с таким именем уже есть на панели — удалить +# его и создать заново (новый ключ); иначе ключ берётся +# из /opt/hard-monitor/.env на хосте, а запись переиспользуется +# --git-install ставить curl-ом прямо из репозитория (по умолчанию scp +# локальных файлов — работает даже без доступа хоста к гиту) +# +# Требования к хостам: ssh-доступ по ключу, python3, sudo без пароля +# (или запусти с agent'ом, который умеет отвечать на пароль sudo). + +set -euo pipefail + +PANEL="" +TOKEN="" +INTERVAL=30 +RECREATE=0 +GIT_INSTALL=0 +TARGETS=() +RAW_BASE="https://git.gnexus.space/git/root/hard-panel/raw/branch/master" + +say() { printf '\e[1;32m==>\e[0m %s\n' "$*"; } +warn() { printf '\e[1;33m!!\e[0m %s\n' "$*"; } +die() { printf '\e[1;31mошибка:\e[0m %s\n' "$*" >&2; exit 1; } +usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --panel) PANEL="$2"; shift 2 ;; + --token) TOKEN="$2"; shift 2 ;; + --interval) INTERVAL="$2"; shift 2 ;; + --recreate) RECREATE=1; shift ;; + --git-install) GIT_INSTALL=1; shift ;; + -h|--help) usage 0 ;; + -*) die "неизвестная опция: $1" ;; + *) TARGETS+=("$1"); shift ;; + esac +done + +[[ "$PANEL" =~ ^https?:// ]] || { usage >&2; die "укажи --panel https://panel.example.com"; } +[[ ${#TARGETS[@]} -gt 0 ]] || { usage >&2; die "укажи хотя бы один хост: user@host"; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +api() { # api METHOD PATH [JSON] + local method="$1" path="$2" body="${3:-}" + curl -sS -X "$method" -H "Content-Type: application/json" \ + ${TOKEN:+-H "Authorization: Bearer $TOKEN"} \ + ${body:+-d "$body"} "$PANEL/api/v1$path" +} + +# server_id по имени (пусто, если нет); python3 есть всюду, где есть ssh+curl +find_server() { + api GET /servers | python3 -c " +import json, sys +name = sys.argv[1] +for s in json.load(sys.stdin): + if s['name'] == name: + print(s['id']); break +" "$1" +} + +# первый пакет от сервера id (0/1) +is_online() { + api GET /servers | python3 -c " +import json, sys +sid = int(sys.argv[1]) +for s in json.load(sys.stdin): + if s['id'] == sid: + print(1 if s.get('status') == 'online' else 0); break +else: + print(0) +" "$1" +} + +for target in "${TARGETS[@]}"; do + say "$target" + ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" true 2>/dev/null \ + || die "$target: нет ssh-доступа (проверь ключ/алиас)" + + name="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" 'hostname -s')" + + server_id="$(find_server "$name" || true)" + key="" + + if [[ -n "$server_id" ]]; then + if [[ $RECREATE -eq 1 ]]; then + warn "сервер #$server_id «$name» уже есть — удаляю (--recreate)" + api DELETE "/servers/$server_id" >/dev/null + server_id="" + else + say "сервер #$server_id «$name» уже есть — переиспользую запись" + key="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" \ + "grep -oP '^SERVER_KEY=\K.*' /opt/hard-monitor/.env 2>/dev/null || true")" + if [[ -z "$key" ]]; then + die "на $target нет /opt/hard-monitor/.env с ключом. Удали сервер «$name» на панели или запусти с --recreate" + fi + say "ключ взят из /opt/hard-monitor/.env на хосте" + fi + fi + + if [[ -z "$server_id" ]]; then + say "создаю сервер «$name» на панели" + resp="$(api POST /servers "{\"name\": \"$name\"}")" + server_id="$(printf '%s' "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" + key="$(printf '%s' "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["key"])')" + fi + + if [[ $GIT_INSTALL -eq 1 ]]; then + say "устанавливаю агента (curl из $RAW_BASE)" + ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" \ + "curl -fsSL $RAW_BASE/monitor/install.sh | sudo bash -s -- '$PANEL' '$key' '$INTERVAL'" + else + say "заливаю файлы агента и устанавливаю" + tmpd="/tmp/hard-monitor-install.$$" + ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" "mkdir -p '$tmpd'" + scp -q "$SCRIPT_DIR/install.sh" "$SCRIPT_DIR/hard_monitor.py" \ + "$SCRIPT_DIR/hard-monitor.service" "$target:$tmpd/" + ssh -o BatchMode=yes -o ConnectTimeout=10 -t "$target" \ + "sudo bash '$tmpd/install.sh' '$PANEL' '$key' '$INTERVAL'; rc=\$?; rm -rf '$tmpd'; exit \$rc" \ + || die "$target: установка не удалась" + fi + + say "жду первый пакет (до $((INTERVAL * 4)) с)" + ok=0 + for _ in $(seq 1 $((INTERVAL * 4 / 5))); do + sleep 5 + if [[ "$(is_online "$server_id")" == "1" ]]; then ok=1; break; fi + done + if [[ $ok -eq 1 ]]; then + say "✓ $target: агент онлайн (сервер #$server_id «$name»)" + else + die "$target: пакет не пришёл — смотри journalctl -u hard-monitor на хосте" + fi +done + +say "все хосты развёрнуты. Дашборд: $PANEL" \ No newline at end of file