From af6a43945276e878df1949892b6cbd7dc5b72f07 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Thu, 25 Jun 2026 17:16:03 +0700 Subject: [PATCH] docs: fix 12 documentation inaccuracies in README.md and AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README.md fixes: - Architecture diagram now shows pulse-lets-go-agent layer - Features list: added agent, auto-trunk, WS gateway, user-01 protection - route.lua example: replaced fake curl.fetch() with real api:execute() - systemd unit: Wants=nats.service removed (NATS on separate host) - Gateway mapping: removed grep 'pulse-' filter (misses existing gateways) - Production deployment: added agent deployment step (between NATS and route.lua) - Makefile commands: added build-agent, build-siptest, siptest-*, emulate-* AGENTS.md fixes: - Structure: added missing cmd/emulator, cmd/siptest, cmd/pulse-lets-go-agent, esl/, ami/, contrib/ - NATS model: added sip_gateway field - config.json: added nats_user, nats_password, tls, rate_limit, log, esl blocks - Internal packages: added esl/ and ami/ - route.lua name: route_call → pulse_route (matches production) --- AGENTS.md | 38 ++++++++++++- README.md | 164 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 134 insertions(+), 68 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 252790c..a59146b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,8 @@ "max_calls": 250, "idle_cpu": 35, "load_avg": 0.85, - "call_failure_rate": 0.5 + "call_failure_rate": 0.5, + "sip_gateway": "sip:pbx-01.lan:5060" } ``` @@ -174,7 +175,7 @@ Response (нет нод вообще): FS использует Lua скрипт в dialplan → `mod_curl` дёргает `/api/route` → получает JSON → парсит `sip_gateway` → bridge. ``` - + @@ -188,6 +189,8 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall ```json { "nats_url": "nats://localhost:4222", + "nats_user": "", + "nats_password": "", "listen_addr": ":8080", "jwt_secret": "auto-generated-on-first-run", "monitoring_api_key": "auto-generated-on-first-run", @@ -201,6 +204,28 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall "idle_score": 0.20, "fail_score": 0.10 } + }, + "tls": { + "enabled": false, + "cert_file": "", + "key_file": "" + }, + "rate_limit": { + "enabled": true, + "route_per_sec": 5000, + "api_per_sec": 100 + }, + "log": { + "level": "info", + "format": "text" + }, + "esl": { + "host": "", + "port": 8021, + "password": "", + "password_env": "", + "sofia_profile": "external", + "gateway_prefix": "pulse" } } ``` @@ -210,13 +235,19 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall ``` pulse-lets-go/ ├── cmd/pulse-lets-go/main.go +├── cmd/pulse-lets-go-agent/ +├── cmd/emulator/ +├── cmd/siptest/ ├── internal/ │ ├── api/ — handlers: auth, nodes, trunks, users, monitoring, ws │ ├── engine/ — scorer + router (sync.RWMutex, best_node кэш) │ ├── nats/ — subscriber, in-memory node store +│ ├── esl/ — FreeSWITCH ESL client (raw TCP) +│ ├── ami/ — Asterisk AMI client (raw TCP) │ ├── config/ — manager: чтение/атомарная запись JSON │ ├── models/ — все общие типы │ └── log/ — ASCII-metrics логгер + ротация +├── contrib/ — route.lua, agent конфиги ├── data/ — JSON конфиги (gitignored) ├── web/ — SvelteKit + shadcn-svelte + Tailwind │ ├── src/routes/ @@ -224,7 +255,8 @@ pulse-lets-go/ │ │ ├── +page.svelte # дашборд │ │ ├── login/+page.svelte │ │ ├── trunks/+page.svelte -│ │ └── nodes/+page.svelte +│ │ ├── nodes/+page.svelte +│ │ └── users/+page.svelte │ ├── src/lib/ │ │ ├── components/ │ │ └── api.ts diff --git a/README.md b/README.md index 35cf892..47738fe 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,17 @@ ## Features +- **pulse-lets-go-agent** — агент сбора метрик для FreeSWITCH (ESL) и Asterisk (AMI). Работает на каждой PBX-ноде. - **Scoring engine** — lethal checks (disabled, stale, overload, CPU, failure rate) + weighted score (call, load, idle, fail) с конфигурируемыми весами. - **NATS шина** — асинхронный сбор метрик через `pulse.metrics.`. -- **REST API** — JWT (access + refresh), роли admin/viewer, rate limiting, CORS. +- **Auto-создание транков** — при получении первой метрики от новой ноды с `sip_gateway` создаётся balance-транк автоматически. +- **Gateway health WS** — статус FS-gateway (registered/unregistered) транслируется на фронт через WebSocket. +- **REST API** — JWT (access + refresh), роли admin/viewer, rate limiting, CORS, health endpoints. - **WebSocket** — real-time трансляция метрик на SvelteKit дашборд. - **FreeSWITCH ESL** — управление Sofia-шлюзами через raw TCP ESL. - **Monitoring** — Prometheus и Zabbix эндпоинты с отдельным API-key. - **SvelteKit UI** — дашборд, таблицы нод, транков, пользователей. +- **Защита пользователей** — первичный администратор (user-01) не может быть удалён или понижен до viewer. - **Graceful shutdown** — корректное завершение с таймаутом 10 с. - **systemd ready** — unit с изоляцией (NoNewPrivileges, ProtectSystem, PrivateTmp). @@ -28,46 +32,54 @@ ## Архитектура ``` -┌────────────────┐ pulse.metrics. ┌──────────────────┐ -│ PBX-01 │────── every 5s ────────────▶ │ │ -│ (FS node) │ │ NATS Server │ -├────────────────┤ │ nats://:4222 │ -│ PBX-02 │────── every 5s ────────────▶ │ │ -│ (FS node) │ └────────┬─────────┘ -├────────────────┤ │ -│ PBX-03 │────── every 5s ────────────▶ │ -│ (FS node) │ │ -└────────────────┘ ▼ - ┌──────────────────────┐ - │ pulse-lets-go │ - │ │ - │ ┌────────────────┐ │ - ┌───────▶│ NATS Sub │ │ - │ │ (pulse.metrics)│ │ - │ └────────┬───────┘ │ - │ ▼ │ - │ ┌────────────────┐ │ - │ │ Engine │ │ - │ │ (scoring) │──│──▶ /api/route - │ └────────┬───────┘ │ ↓ - │ ▼ │ FreeSWITCH bridge - │ ┌────────────────┐ │ sip:pbx-03:5060 - │ │ REST API │──│──▶ /api/nodes - │ │ (JWT auth) │ │ /api/trunks - │ └────────┬───────┘ │ /api/users - │ ▼ │ - │ ┌────────────────┐ │ - │ │ WebSocket │──│──▶ SvelteKit UI - │ └────────────────┘ │ (ws://:8080/ws/metrics) - │ ┌────────────────┐ │ - │ │ ESL Client │──│──▶ FreeSWITCH ESL - │ │ (gateways) │ │ :8021 - │ └────────────────┘ │ - │ ┌────────────────┐ │ - │ │ Monitoring │──│──▶ /api/monitoring/prometheus - │ │ (API-key) │ │ /api/monitoring/zabbix - │ └────────────────┘ │ - └──────────────────────┘ +┌─────────────────────┐ ESL/AMI ┌──────────────────────┐ +│ PBX-01 │──────────▶│ pulse-lets-go-agent │ +│ (FS or Asterisk) │ collect │ (on each PBX node) │ +├─────────────────────┤ ├──────────────────────┤ +│ PBX-02 │──────────▶│ pulse-lets-go-agent │ +│ (FS or Asterisk) │ ├──────────────────────┤ +├─────────────────────┤ ├──────────────────────┤ +│ PBX-N │──────────▶│ pulse-lets-go-agent │ +│ (FS or Asterisk) │ └─────────┬────────────┘ +└─────────────────────┘ │ pulse.metrics. + │ every 5s + ▼ + ┌──────────────┐ + │ NATS Server │ + │ nats://:4222 │ + └──────┬───────┘ + │ + ▼ + ┌──────────────────────┐ + │ pulse-lets-go │ + │ │ + │ ┌────────────────┐ │ + │ │ NATS Sub │ │ + │ │ (pulse.metrics)│ │ + │ └────────┬───────┘ │ + │ ▼ │ + │ ┌────────────────┐ │ + │ │ Engine │ │ + │ │ (scoring) │──│──▶ /api/route + │ └────────┬───────┘ │ ↓ + │ ▼ │ FreeSWITCH bridge + │ ┌────────────────┐ │ sip:pbx-03:5060 + │ │ REST API │──│──▶ /api/nodes + │ │ (JWT auth) │ │ /api/trunks + │ └────────┬───────┘ │ /api/users + │ ▼ │ + │ ┌────────────────┐ │ + │ │ WebSocket │──│──▶ SvelteKit UI + │ └────────────────┘ │ (ws://:8080/ws/metrics) + │ ┌────────────────┐ │ + │ │ ESL Client │──│──▶ FreeSWITCH ESL + │ │ (gateways) │ │ :8021 + │ └────────────────┘ │ + │ ┌────────────────┐ │ + │ │ Monitoring │──│──▶ /api/monitoring/prometheus + │ │ (API-key) │ │ /api/monitoring/zabbix + │ └────────────────┘ │ + └──────────────────────┘ ``` --- @@ -336,20 +348,24 @@ curl -H "X-API-Key: $(jq -r '.monitoring_api_key' data/config.json)" \ FreeSWITCH использует Lua-скрипт в dialplan. Скрипт дёргает `/api/route` через `mod_curl` и бриджится на полученный gateway. ```lua --- contrib/route.lua (упрощённо) -local url = string.format("%s/api/route?caller_id=%s&dest=%s&ingress_trunk=%s", - session:getVariable("balancer_url"), - session:getVariable("caller_id_number"), - session:getVariable("destination_number"), - session:getVariable("ingress_trunk")) +-- contrib/route.lua (сокращённо) +api = freeswitch.API() +caller_id = session:getVariable("caller_id_number") or "" +dest = session:getVariable("destination_number") or "" +url = "http://localhost:8080/api/route?caller_id=" .. caller_id .. "&dest=" .. dest -local response = curl.fetch({url = url, timeout = tonumber(route_timeout)}) -local ok, result = cjson.decode(response.body) +raw = api:execute("curl", url) +body = raw:match("\r?\n\r?\n(.+)") or raw -if result.fallback then - session:bridge(result.sip_gateway) +local ok, route = pcall(cjson.decode, body) +if not ok then session:hangup("NORMAL_TEMPORARY_FAILURE") return end + +if route.fallback then + session:bridge(route.sip_gateway) -- оператор +elseif route.sip_gateway then + session:bridge(route.sip_gateway) -- лучшая нода else - session:bridge(result.sip_gateway) + session:hangup("NORMAL_TEMPORARY_FAILURE") end ``` @@ -415,7 +431,7 @@ make install-systemd # устанавливает systemd unit # deploy/pulse-lets-go.service [Unit] Description=Pulse Lets Go — Telephone Load Balancer -Wants=nats.service +After=network.target [Service] User=pulse @@ -498,13 +514,28 @@ sudo mkdir -p /opt/nats # Запуск: nats-server -p 4222 -D & ``` -**Шаг 3 — route.lua в FS scripts** +**Шаг 3 — Деплой agent на PBX-ноды** (повторить для каждой UC/Asterisk ноды) +```bash +# Скопировать бинарник и конфиг на каждую PBX: +scp bin/pulse-lets-go-agent admin@PBX_IP:/usr/local/bin/ +scp contrib/agent-uc.json admin@PBX_IP:/etc/pulse-lets-go-agent/agent.json + +# Запустить: +ssh admin@PBX_IP "systemctl start pulse-lets-go-agent" + +# Проверить лог: +# journalctl -u pulse-lets-go-agent -f +# → [ami] подключён к 127.0.0.1:6154 +# → [agent] опубликована метрика: calls=0/150 load=0.28 cpu=99% status=ok +``` + +**Шаг 4 — route.lua в FS scripts** ```bash sudo mkdir -p /etc/freeswitch/scripts sudo cp /tmp/route.lua /etc/freeswitch/scripts/ ``` -**Шаг 4 — Dialplan** (добавить `pulse_route` extension) +**Шаг 5 — Dialplan** (добавить `pulse_route` extension) ```xml @@ -518,17 +549,18 @@ sudo cp /tmp/route.lua /etc/freeswitch/scripts/ sudo fs_cli -x "reloadxml" ``` -**Шаг 5 — Маппинг существующих FS gateway в pulse-lets-go транки** +**Шаг 6 — Создание balance-транков для каждой PBX-ноды** ```bash -# Получить список gateway из FS: -echo "api sofia profile external gwlist" | nc 127.0.0.1 8021 | grep "pulse-" -# → список имён gateway +# Получить список gateway из FS (или список нод из конфига): +echo "api sofia profile external gwlist" | nc 127.0.0.1 8021 -# Создать balance-транки для каждого gateway через API: -# POST /api/trunks {"name":"uc-69 баланс","type":"balance","node_id":"uc-69","gateway":"sip:10.x.x.x:5060"} +# Создать balance-транк для каждой UC-ноды: +curl -X POST .../api/trunks + -d '{"name":"uc06","type":"balance","node_id":"uc06","gateway":"sip:uc-pbx.lan:5060","enabled":true}' ``` +Транки также создаются автоматически при получении первой метрики от агента (если в метрике передан `sip_gateway`). -**Шаг 6 — Запуск pulse-lets-go** +**Шаг 7 — Запуск pulse-lets-go** ```bash sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go # config.json создастся автоматически при первом запуске @@ -537,7 +569,7 @@ sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go PULSE_DATA_DIR=/etc/pulse-lets-go ./pulse-lets-go ``` -**Шаг 7 — Проверка** +**Шаг 8 — Проверка** ```bash curl http://127.0.0.1:8080/api/health | jq '.connections' # → {"nats":"connected","esl":"connected"} @@ -751,8 +783,10 @@ curl http://localhost:8080/api/nodes | jq '.[] | {node_id, score, active_calls}' | `make test-race` | Тесты с race detector | | `make lint` | `go vet ./...` | | `make fmt` | `go fmt ./...` | -| `make emulate-normal` | Эмуляция метрик (3 здоровые ноды) | -| `make emulate-chaos` | Эмуляция случайных сбоев | +| `make build-agent` | Сборка pulse-lets-go-agent | +| `make build-siptest` | Сборка SIP-тестера | +| `make emulate-*` | Эмуляция метрик (normal, overload, chaos, stale, ...) | +| `make siptest-*` | SIP-тестирование (uas, uac, full) | ### Стек