Как и все А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. Получить их статусы

И так. Первым делом цепляем библиотеку сокетов:

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. Только вот с документацией там… никак.

Комментарии (12)


  1. miga
    17.08.2015 15:49

    Достаточно было добавить все транки во внутреннюю БД под ключами, например Trunk/{1,2,...n} = {peer_name} и до посинения перебирать их в цикле штатными средствами.


    1. Ovoshlook
      17.08.2015 18:19

      1. А это не штатные средства?
      2. И как вашим методом можно вызвать сразу рабочий транк? Без попыток вызова не рабочих?


      1. miga
        17.08.2015 18:53

        1. Это ненужное усложнение для решения простой задачи. Представьте себя на месте человека, который пришел после вас. А теперь представьте, что он импульсивный и склонный к насилию маньяк, который знает, где вы живете и которому не очень понравилось, что ему пришлось потратить полчаса, чтобы разобраться, как PBX находит пиру и еще полчаса, чтобы отладить, почему эта конструкция разломалась.
        2. При включенном qualify попытки набора мертвых пир будут сразу отбиваться, так что живой найдется быстро.


        1. Ovoshlook
          17.08.2015 20:30

          1. так я за 2 запроса к интерфейсу получаю необходимые мне данные. Вы же советуете сделать n запросов в базу. Мой метод как минимум производительнее))
          А по поводу того что кто то там не разберется или будет этт делать слишком долго: есть такой инструмент — документация. Очень помогает. А если и документация не поможет — то это уже вопрос компетенции. А само по себе ничего не ломается.
          2. Ну как бы уже ответил по поводу производительности.


          1. miga
            17.08.2015 20:52

            Никакая документация не может служить оправданием для того, чтобы делать простые вещи сложно. Особенно для склонного к насилию маньяка :)
            Насчет производительности — очень спорное утверждение, внутренняя БД астериска — это крохотная беркли (или  sqlite в новых версиях), ходить в которую практически бесплатно.

            Городить такую штуку имеет смысл, если нужна еще какая-то сложная бизнес-логика поверх простого вызова (нетривиальная IVR, например) — тогда да, можно сесть, покурить AMI, написать хороший скрипт с обработкой ошибок, логированием и проч.


            1. Ovoshlook
              17.08.2015 21:11

              По поводу маньяка оставлю без комментариев).
              А по поводу производительности — как бы то бесплатно ни было — это лишние шаги в обращении данным которые сыплятся сами. Надо только слушать. Ну тут можно долго спорить. Можно РОСТО взять как нибудь и замерить скорость и нагрузку. Тогда будет понятно.
              Я не говорю что ваш метод не имеет права на существование. И вполне жизнепригоден, и не лишен изящества. Моей задачей было в общем то показать как можно напрямую из диалплана работать с AMI. Не используя при этом AGI во всех его интерпритациях. С данной задачей я справился вполне.


          1. Calc
            18.08.2015 13:10

            2. *
            dial через макрос в цикле в диалплане
            10 каналов отбивается меньше чем за секунду. Нагрузка на сервер отсутствует.
            у вас быстрее произойдут проблемы с tcp:connect(«127.0.0.1», 5038) чем с самим диалпланом.


            1. Ovoshlook
              18.08.2015 13:27

              Я про диалплан в conf файлах уже давно и думать забыл. Как и о более половины встроенных конструкций типа GotoIfTime, GotoIf, realtime диалплана, а так же макросов и подобной ерунды предложенной разработчиками, ибо скорость выполнения и удобство реализации оставляют желать лучшего.

              tcp:connect(«127.0.0.1», 5038) не отработает только в том случае, если только навернется вся сеть на сервере. Но тогда и актуальность данной проблемы потеряет всякий смысл.


              1. Calc
                18.08.2015 13:47

                как показала 6ти летняя практика, рано пока отказываться от конфов
                количество коннектов на 5038 может быть ограничено.
                А при наличии 50-100 номеров на пне 4 (по 2-3 линии каждый) при подобных «запросах» при «заглюченном оборудовании» коннект к 5038 может быть потерян, да и не дай бог сработает защита от доса)


                1. Ovoshlook
                  18.08.2015 14:09

                  Как показала 6 летняя практика- самое время отказываться. Особенно если это высоконагруженная система. Особенно если это облачное решение. Особенно если это взаимодействие с базами данных. ДА много таких вот «Особенно» я могу перечислить, исходя из опыта.

                  Все остальные «может» отлично обходятся с помощью прямых рук.
                  Что значит «заглюченное оборудование»?
                  Программы не умеют не работать «по настроению». Либо где то кривые руки, либо нерабочее железо. 1 исправляется поднятием компетенции любым путем, второе — заменой.


                  1. Calc
                    18.08.2015 14:27

                    Какой нибудь pap2t может убить астериск
                    Глючит оборудованме, а не ПО. Атака внутри сети пользователя может откликнуться и на телефонии
                    С 1 и 2 согласен когда админ один, а когда их по одному на каждый номер + у сервера телефонии нет админа)
                    Я просто поделился опытом, за 6 лет вмешивались в работу сервера не больше 20ти раз.
                    Вы можете делать по своему.


                    1. Ovoshlook
                      18.08.2015 16:20

                      Я с вами соглашусь в том что идеальные условия не всегда присутсnвуют и что компроментирование сети вполне может быть. Но как правило когда «у сервера телефонии нет админа», то решения предложенные мною, как правило выше компетенции персонала компании.

                      А по поводу «железо глючит» — если что-то работает не так как должно работать и это в продакшне, Будь то ПО или железо.В таких ситуациях нужно задумываться о замене или об устранении причин неработоспособности. Сливать на «глючит» — не решение.

                      Моя статья все же больше касается именно разработчиков сервисов телефонии, нежели админов средней руки, или непрофилирующих в VoIP. В конце концов методы взаимодейсвия с интерфейсами сервера- это больше уже стезя программистов нежели админов.

                      Я повторюсь: смысл написания моей статьи все же сводится не к тому как проверять транки (это не более чем пример), а к раскрытию возможностей взаимодействия с интерфейсами астериска не из внешних приложений, а из самого диалплана.