157 lines
7.7 KiB
Lua
157 lines
7.7 KiB
Lua
--[[
|
||
route.lua — запрашивает pulse-lets-go для маршрутизации вызова.
|
||
Телефонный балансировщик нагрузки на базе FreeSWITCH + NATS.
|
||
|
||
┌─ Размещение ─────────────────────────────────────────────────────┐
|
||
│ /etc/freeswitch/scripts/route.lua │
|
||
│ │
|
||
│ ┌─ Использование в dialplan ────────────────────────────────────┐│
|
||
│ │ <extension name="route_call"> ││
|
||
│ │ <condition field="destination_number" expression="^.*$"> ││
|
||
│ │ <action application="lua" data="route.lua"/> ││
|
||
│ │ </condition> ││
|
||
│ │ </extension> ││
|
||
│ └───────────────────────────────────────────────────────────────┘│
|
||
│ │
|
||
│ ┌─ Переменные канала (опционально) ─────────────────────────────┐│
|
||
│ │ balancer_url — URL pulse-lets-go (default: localhost:8080) ││
|
||
│ │ ingress_trunk — ID входящего транка ││
|
||
│ └───────────────────────────────────────────────────────────────┘│
|
||
│ │
|
||
│ ┌─ Требования FreeSWITCH ───────────────────────────────────────┐│
|
||
│ │ mod_curl — <load module="mod_curl"/> в modules.conf.xml ││
|
||
│ │ cjson — входит в стандартную поставку FS Lua ││
|
||
│ └───────────────────────────────────────────────────────────────┘│
|
||
└───────────────────────────────────────────────────────────────────┘
|
||
|
||
Логика:
|
||
1. Извлечь caller_id + destination из переменных сессии
|
||
2. HTTP GET /api/route?caller_id=X&dest=Y&ingress_trunk=Z
|
||
3. Разобрать JSON-ответ
|
||
4. При fallback=true → bridge на fallback gateway (оператор)
|
||
5. При ошибке → кладём трубку с NORMAL_TEMPORARY_FAILURE
|
||
6. Нормальный маршрут → bridge на sip_gateway выбранной ноды
|
||
]]--
|
||
|
||
-- ===================================================================
|
||
-- Конфигурация
|
||
-- ===================================================================
|
||
|
||
-- URL балансировщика (переопределяется через переменную канала)
|
||
local BALANCER_URL = session:getVariable("balancer_url") or "http://localhost:8080"
|
||
|
||
-- Таймаут HTTP-запроса в секундах (через параметр curl)
|
||
local HTTP_TIMEOUT = tonumber(session:getVariable("route_timeout") or "3")
|
||
|
||
-- JSON-парсер
|
||
local cjson = require("cjson")
|
||
|
||
-- API FreeSWITCH для вызова mod_curl
|
||
local api = freeswitch.API()
|
||
|
||
-- ===================================================================
|
||
-- Вспомогательные функции
|
||
-- ===================================================================
|
||
|
||
-- Логирование с префиксом [route]
|
||
local function log(level, msg)
|
||
freeswitch.consoleLog(level, "[route] " .. msg .. "\n")
|
||
end
|
||
|
||
-- Выполнить HTTP GET и вернуть тело ответа (без заголовков).
|
||
-- mod_curl API возвращает сырой HTTP-ответ: заголовки + \r?\n\r?\n + тело.
|
||
local function http_get(url)
|
||
local cmd = "curl " .. url
|
||
local raw = api:execute("curl", cmd)
|
||
|
||
if not raw or raw == "" then
|
||
return nil, "пустой ответ от curl"
|
||
end
|
||
|
||
-- Отделяем заголовки от тела
|
||
-- Ищем двойной перенос строки (CRLF или LF)
|
||
local body = raw:match("\r?\n\r?\n(.+)")
|
||
if not body then
|
||
-- Если нет заголовков — весь ответ это тело
|
||
return raw, nil
|
||
end
|
||
return body, nil
|
||
end
|
||
|
||
-- Безопасная отправка трубки с кодом причины
|
||
local function safe_hangup(cause)
|
||
if session:ready() then
|
||
session:hangup(cause or "NORMAL_TEMPORARY_FAILURE")
|
||
end
|
||
end
|
||
|
||
-- ===================================================================
|
||
-- Основная логика
|
||
-- ===================================================================
|
||
|
||
-- Извлекаем параметры вызова
|
||
local caller_id = session:getVariable("caller_id_number") or ""
|
||
local dest = session:getVariable("destination_number") or ""
|
||
local ing_trunk = session:getVariable("ingress_trunk") or ""
|
||
|
||
-- Собираем URL
|
||
local query = string.format("caller_id=%s&dest=%s", caller_id, dest)
|
||
if ing_trunk ~= "" then
|
||
query = query .. "&ingress_trunk=" .. ing_trunk
|
||
end
|
||
local url = BALANCER_URL .. "/api/route?" .. query
|
||
|
||
log("info", "запрос маршрута: " .. url)
|
||
|
||
-- Запрашиваем маршрут у pulse-lets-go
|
||
local body, curl_err = http_get(url)
|
||
if not body then
|
||
log("err", "ошибка HTTP: " .. (curl_err or "unknown"))
|
||
safe_hangup()
|
||
return
|
||
end
|
||
|
||
-- Парсим JSON
|
||
local ok, route = pcall(cjson.decode, body)
|
||
if not ok or type(route) ~= "table" then
|
||
log("err", "ошибка парсинга JSON: " .. tostring(route))
|
||
safe_hangup()
|
||
return
|
||
end
|
||
|
||
-- ── Фолбэк (все ноды unhealthy) ──────────────────────────────────
|
||
if route.fallback and route.sip_gateway then
|
||
log("info", string.format("FALLBACK → %s (причина: %s)",
|
||
route.sip_gateway, route.reason or "all_nodes_unhealthy"))
|
||
|
||
if route.nodes then
|
||
for _, n in ipairs(route.nodes) do
|
||
log("info", string.format(" %s: score=%d reason=%s",
|
||
n.id or "?", n.score or 0, n.reason or "-"))
|
||
end
|
||
end
|
||
|
||
session:execute("bridge", route.sip_gateway)
|
||
return
|
||
end
|
||
|
||
-- ── Ошибка API (нет зарегистрированных нод) ──────────────────────
|
||
if route.error then
|
||
log("err", "ошибка балансировщика: " .. route.error)
|
||
safe_hangup()
|
||
return
|
||
end
|
||
|
||
-- ── Успешный маршрут ─────────────────────────────────────────────
|
||
if route.sip_gateway then
|
||
log("info", string.format("ROUTE: %s score=%d → %s",
|
||
route.node_id or "?", route.score or 0, route.sip_gateway))
|
||
|
||
session:execute("bridge", route.sip_gateway)
|
||
return
|
||
end
|
||
|
||
-- ── Непредвиденная ситуация ──────────────────────────────────────
|
||
log("err", "нет sip_gateway в ответе балансировщика")
|
||
safe_hangup()
|