Как и все АSTERISK'еры я не раз сталкивался с проблемой того, что на PBX существует несколько транков, которые используются для исходящей связи. И как у многих, у моих заказчиков тоже часть этих транков является основными, а остальные играют роль резервных, на случай падения/занятости/чего-либо еще первых.
Стандартным механизмом решения такой проблемы считается следующий пример:
exten => _<Че то там>,1,Dial(SIP/trunk/<Че то там>)
exten => _<Че то там>,n,GotoIf($["${DIALSTATUS}" != «ANSWER»]?Dial_Another_Prov:Hangup)
exten => _<Че то там>,n(Dial_Another_Prov),Dial(SIP/trunk2/<Че то там>)
exten => _<Че то там>,n(hangup),Hangup()
Ну или вот такой пример, который впрочем лежит на просторах сети
[macro-safedial]
exten => s,1,Set(DIALSTART=${EPOCH})
exten => s,n,Dial(${ARG1},${ARG2},${ARG3},${ARG4})
exten => s,n,Goto(s-${DIALSTATUS},1)
exten => s-NOANSWER,1,GotoIf($["${DTIME}" = «0»]?here)
exten => s-NOANSWER,n,Hangup
exten => s-NOANSWER,n(here),Verbose(1,Need failover for "${ARG1}")
exten => s-BUSY,1,Busy
exten => s-CHANUNAVAIL,1,Verbose(1,Need failover for "${ARG1}")
exten => s-CONGESTION,1,Congestion
exten => _s-.,1,Congestion
exten => s-,1,Congestion
Через какое-то время мне начали претить такие решения, исходя из соображений их громозкости и увеличения количества резервных каналов у одного из заказчиков, у которого стоял вопрос во что бы то ни стало дозвониться до клиента. Оно в общем то и понятно: телефония должна всегда оставаться телефонией, и работать. На то оно и PBX — чтобы автоматизировать работу и избавить от головных болей.
Между делом переводя всех своих подопечных с обычного диалплана на lua было принято решение — воять.
Что ж. У нас под руками отличный рабочий инструмент — целый ЯЗЫК программирования. Который, как и многие его собратья, умеет работать с сетевыми интерфейсами. А это значит что мы можем использовать это свойство на свои блага. Чего бы не посмотреть на состояния транков и уже затем вызвать доступный? Нужно всего то:
1. Подключиться к AMI
2. Получить имена транков
3. Получить их статусы
И так. Первым делом цепляем библиотеку сокетов:
Для анализа транков я буду использовать AMI (как уже наверное все догадались исходя из названия). Так как AMI работает по tcp стеку, то его я и опишу:
Далее я описываю контекст из которого будут вызываться транки и навешиваю на него нужную нам функцию. Скажем… outgoing_calls_external_dst По сути эта функция- есть сущность контекста. То есть аналог контекста в extensions.conf (Это я расписывать кодом не буду. Все есть на wiki.asterisk.org)
Здесь я, при получении звонка подключусь к AMI интерфейсу своего asterisk:
Дальше в общем-то начинается самое интересное. Запрашиваем у ASTERISK все пиры. «Зачем все?» — спросит читатель. «Ведь есть же SIPshowregistry!». Да. Есть. Но во-первых он покажет нам только транки с регистрацией, а во-вторых, если провайдер стал недоступен, а время регистрации еще не истекло, то информация о состоянии транка все равно будет невалидной. «Но SIPpeers покажет и клиентов тоже!» — и это будет правильным замечанием. поэтому нужно подготовить транки.
В sip/users/<Куда вы там еще кто складывает свои транки> для каждого транка я:
1. Включил qualify
2. Прописал параметр description = line
То есть иными словами — все что описано как line и есть транк. Почему это важно? потому что SIPpeers вернет нам вот такое описание для каждого пира. Более того — он вернет вам это в том порядке, в котором они прописаны у вас в файле/таблице mysql
Channeltype: SIP
ObjectName: mysupertrunk
ChanObjectType: peer
IPaddress: -none-
IPport: 0
Dynamic: yes
AutoForcerport: no
Forcerport: yes
AutoComedia: no
Comedia: yes
VideoSupport: no
TextSupport: no
ACL: no
Status: UNKNOWN
RealtimeDevice: no
Description: line
В общем то распарсив все что есть из пиров на сервере мы таким образом отлично отделим зерна от плевел и сложим зерна в одну корзину под названием trunks:
В общем то теперь у нас есть массив/табличка всех транков на нашем ASTERISK.
осталось только выяснить какой из них доступен и позвонить через него. Сделать это можно через SIPpeerstatus:
Ну и не забываем закрыть за собой дверь))
Это в общем-то самый простой пример того, как можно использовать AMI непосредственно в самом диалплане. Так же ничего не мешает узнавать и занятость каналов. Необходимо будет только распарcить вывод команды sip show inuse. Прикручивается сюда и mysql коннекторы и redis, и все что угодно при необходимости. Без костылей.
P.S. Для ленивых есть целая библиотека ami-lua. Только вот с документацией там… никак.
Стандартным механизмом решения такой проблемы считается следующий пример:
exten => _<Че то там>,1,Dial(SIP/trunk/<Че то там>)
exten => _<Че то там>,n,GotoIf($["${DIALSTATUS}" != «ANSWER»]?Dial_Another_Prov:Hangup)
exten => _<Че то там>,n(Dial_Another_Prov),Dial(SIP/trunk2/<Че то там>)
exten => _<Че то там>,n(hangup),Hangup()
Ну или вот такой пример, который впрочем лежит на просторах сети
[macro-safedial]
exten => s,1,Set(DIALSTART=${EPOCH})
exten => s,n,Dial(${ARG1},${ARG2},${ARG3},${ARG4})
exten => s,n,Goto(s-${DIALSTATUS},1)
exten => s-NOANSWER,1,GotoIf($["${DTIME}" = «0»]?here)
exten => s-NOANSWER,n,Hangup
exten => s-NOANSWER,n(here),Verbose(1,Need failover for "${ARG1}")
exten => s-BUSY,1,Busy
exten => s-CHANUNAVAIL,1,Verbose(1,Need failover for "${ARG1}")
exten => s-CONGESTION,1,Congestion
exten => _s-.,1,Congestion
exten => s-,1,Congestion
Через какое-то время мне начали претить такие решения, исходя из соображений их громозкости и увеличения количества резервных каналов у одного из заказчиков, у которого стоял вопрос во что бы то ни стало дозвониться до клиента. Оно в общем то и понятно: телефония должна всегда оставаться телефонией, и работать. На то оно и PBX — чтобы автоматизировать работу и избавить от головных болей.
Между делом переводя всех своих подопечных с обычного диалплана на lua было принято решение — воять.
Что ж. У нас под руками отличный рабочий инструмент — целый ЯЗЫК программирования. Который, как и многие его собратья, умеет работать с сетевыми интерфейсами. А это значит что мы можем использовать это свойство на свои блага. Чего бы не посмотреть на состояния транков и уже затем вызвать доступный? Нужно всего то:
1. Подключиться к AMI
2. Получить имена транков
3. Получить их статусы
И так. Первым делом цепляем библиотеку сокетов:
local socket = require("socket")
Для анализа транков я буду использовать AMI (как уже наверное все догадались исходя из названия). Так как AMI работает по tcp стеку, то его я и опишу:
tcp = socket.tcp()
tcp:settimeout(100)
Далее я описываю контекст из которого будут вызываться транки и навешиваю на него нужную нам функцию. Скажем… outgoing_calls_external_dst По сути эта функция- есть сущность контекста. То есть аналог контекста в extensions.conf (Это я расписывать кодом не буду. Все есть на wiki.asterisk.org)
Здесь я, при получении звонка подключусь к AMI интерфейсу своего asterisk:
tcp:connect("127.0.0.1", 5038)
result = tcp:receive()
tcp:send("Action: Login\r\n")
tcp:send("Username: pr\r\n")
tcp:send("Secret: 1\r\n\r\n")
LoginIsOk = 0
while LoginIsOk == 0 do
result=tcp:receive() -- перебираем входящие сообщения пока не встретим сообщение о удачном соединении.
if string.find(result,"Authentication accepted")~=nil then
LoginIsOk = 1
end
if string.find(result,"Response: Error")~=nil then
LoginIsOk = 2
end
end
Дальше в общем-то начинается самое интересное. Запрашиваем у ASTERISK все пиры. «Зачем все?» — спросит читатель. «Ведь есть же SIPshowregistry!». Да. Есть. Но во-первых он покажет нам только транки с регистрацией, а во-вторых, если провайдер стал недоступен, а время регистрации еще не истекло, то информация о состоянии транка все равно будет невалидной. «Но SIPpeers покажет и клиентов тоже!» — и это будет правильным замечанием. поэтому нужно подготовить транки.
В sip/users/<Куда вы там еще кто складывает свои транки> для каждого транка я:
1. Включил qualify
2. Прописал параметр description = line
То есть иными словами — все что описано как line и есть транк. Почему это важно? потому что SIPpeers вернет нам вот такое описание для каждого пира. Более того — он вернет вам это в том порядке, в котором они прописаны у вас в файле/таблице mysql
Channeltype: SIP
ObjectName: mysupertrunk
ChanObjectType: peer
IPaddress: -none-
IPport: 0
Dynamic: yes
AutoForcerport: no
Forcerport: yes
AutoComedia: no
Comedia: yes
VideoSupport: no
TextSupport: no
ACL: no
Status: UNKNOWN
RealtimeDevice: no
Description: line
В общем то распарсив все что есть из пиров на сервере мы таким образом отлично отделим зерна от плевел и сложим зерна в одну корзину под названием trunks:
tcp:send("Action: SIPpeers\r\n\r\n")
while result ~= "EventList: start" do
result = tcp:receive()
end
trunks = {}
i = 1
while result ~= "Event: PeerlistComplete" do
result = tcp:receive()
if string.find(result,"ObjectName")~=nil then
ObjectName = splitted_value(result,": ") --splitted_value - это самописная функция, которая разделяет строку на подстроки и возращает результат
end
if string.find(result,"Description")~=nil then
Description = splitted_value(result,": ")
end
if Description == "line" then
trunks[i] = ObjectName
i = i + 1
Description=nil -- обязательно обнуляем переменную. Иначе попадем в бесконечный цикл.
end
end
В общем то теперь у нас есть массив/табличка всех транков на нашем ASTERISK.
осталось только выяснить какой из них доступен и позвонить через него. Сделать это можно через SIPpeerstatus:
for key,val in pairs(trunks) do
tcp:send("Action: SIPpeerstatus\r\n")
tcp:send("Peer: "..val.."\r\n\r\n")
while result~="Event: SIPpeerstatusComplete" do
result=tcp:receive()
if string.find(result,"PeerStatus:")~=nil then
status=split(result,": ") --split еще одна самописная функция, которая делит подстроку и возвращает таблицу. Предыдущая функция включает в себя эту
if status[2]=="Reachable" then
app.Dial("SIP/"..val.."/"..extension)
end
end
end
end
Ну и не забываем закрыть за собой дверь))
tcp:send("Action: Logoff\r\n\r\n")
while result~="Response: Goodbye" do
result=tcp:receive()
end
tcp:close()
Это в общем-то самый простой пример того, как можно использовать AMI непосредственно в самом диалплане. Так же ничего не мешает узнавать и занятость каналов. Необходимо будет только распарcить вывод команды sip show inuse. Прикручивается сюда и mysql коннекторы и redis, и все что угодно при необходимости. Без костылей.
P.S. Для ленивых есть целая библиотека ami-lua. Только вот с документацией там… никак.
miga
Достаточно было добавить все транки во внутреннюю БД под ключами, например Trunk/{1,2,...n} = {peer_name} и до посинения перебирать их в цикле штатными средствами.
Ovoshlook
1. А это не штатные средства?
2. И как вашим методом можно вызвать сразу рабочий транк? Без попыток вызова не рабочих?
miga
1. Это ненужное усложнение для решения простой задачи. Представьте себя на месте человека, который пришел после вас. А теперь представьте, что он импульсивный и склонный к насилию маньяк, который знает, где вы живете и которому не очень понравилось, что ему пришлось потратить полчаса, чтобы разобраться, как PBX находит пиру и еще полчаса, чтобы отладить, почему эта конструкция разломалась.
2. При включенном qualify попытки набора мертвых пир будут сразу отбиваться, так что живой найдется быстро.
Ovoshlook
1. так я за 2 запроса к интерфейсу получаю необходимые мне данные. Вы же советуете сделать n запросов в базу. Мой метод как минимум производительнее))
А по поводу того что кто то там не разберется или будет этт делать слишком долго: есть такой инструмент — документация. Очень помогает. А если и документация не поможет — то это уже вопрос компетенции. А само по себе ничего не ломается.
2. Ну как бы уже ответил по поводу производительности.
miga
Никакая документация не может служить оправданием для того, чтобы делать простые вещи сложно. Особенно для склонного к насилию маньяка :)
Насчет производительности — очень спорное утверждение, внутренняя БД астериска — это крохотная беркли (или sqlite в новых версиях), ходить в которую практически бесплатно.
Городить такую штуку имеет смысл, если нужна еще какая-то сложная бизнес-логика поверх простого вызова (нетривиальная IVR, например) — тогда да, можно сесть, покурить AMI, написать хороший скрипт с обработкой ошибок, логированием и проч.
Ovoshlook
По поводу маньяка оставлю без комментариев).
А по поводу производительности — как бы то бесплатно ни было — это лишние шаги в обращении данным которые сыплятся сами. Надо только слушать. Ну тут можно долго спорить. Можно РОСТО взять как нибудь и замерить скорость и нагрузку. Тогда будет понятно.
Я не говорю что ваш метод не имеет права на существование. И вполне жизнепригоден, и не лишен изящества. Моей задачей было в общем то показать как можно напрямую из диалплана работать с AMI. Не используя при этом AGI во всех его интерпритациях. С данной задачей я справился вполне.
Calc
2. *
dial через макрос в цикле в диалплане
10 каналов отбивается меньше чем за секунду. Нагрузка на сервер отсутствует.
у вас быстрее произойдут проблемы с tcp:connect(«127.0.0.1», 5038) чем с самим диалпланом.
Ovoshlook
Я про диалплан в conf файлах уже давно и думать забыл. Как и о более половины встроенных конструкций типа GotoIfTime, GotoIf, realtime диалплана, а так же макросов и подобной ерунды предложенной разработчиками, ибо скорость выполнения и удобство реализации оставляют желать лучшего.
tcp:connect(«127.0.0.1», 5038) не отработает только в том случае, если только навернется вся сеть на сервере. Но тогда и актуальность данной проблемы потеряет всякий смысл.
Calc
как показала 6ти летняя практика, рано пока отказываться от конфов
количество коннектов на 5038 может быть ограничено.
А при наличии 50-100 номеров на пне 4 (по 2-3 линии каждый) при подобных «запросах» при «заглюченном оборудовании» коннект к 5038 может быть потерян, да и не дай бог сработает защита от доса)
Ovoshlook
Как показала 6 летняя практика- самое время отказываться. Особенно если это высоконагруженная система. Особенно если это облачное решение. Особенно если это взаимодействие с базами данных. ДА много таких вот «Особенно» я могу перечислить, исходя из опыта.
Все остальные «может» отлично обходятся с помощью прямых рук.
Что значит «заглюченное оборудование»?
Программы не умеют не работать «по настроению». Либо где то кривые руки, либо нерабочее железо. 1 исправляется поднятием компетенции любым путем, второе — заменой.
Calc
Какой нибудь pap2t может убить астериск
Глючит оборудованме, а не ПО. Атака внутри сети пользователя может откликнуться и на телефонии
С 1 и 2 согласен когда админ один, а когда их по одному на каждый номер + у сервера телефонии нет админа)
Я просто поделился опытом, за 6 лет вмешивались в работу сервера не больше 20ти раз.
Вы можете делать по своему.
Ovoshlook
Я с вами соглашусь в том что идеальные условия не всегда присутсnвуют и что компроментирование сети вполне может быть. Но как правило когда «у сервера телефонии нет админа», то решения предложенные мною, как правило выше компетенции персонала компании.
А по поводу «железо глючит» — если что-то работает не так как должно работать и это в продакшне, Будь то ПО или железо.В таких ситуациях нужно задумываться о замене или об устранении причин неработоспособности. Сливать на «глючит» — не решение.
Моя статья все же больше касается именно разработчиков сервисов телефонии, нежели админов средней руки, или непрофилирующих в VoIP. В конце концов методы взаимодейсвия с интерфейсами сервера- это больше уже стезя программистов нежели админов.
Я повторюсь: смысл написания моей статьи все же сводится не к тому как проверять транки (это не более чем пример), а к раскрытию возможностей взаимодействия с интерфейсами астериска не из внешних приложений, а из самого диалплана.