Asterisk Manager Interface в диалплане

    Как и все А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
    • 0
      Достаточно было добавить все транки во внутреннюю БД под ключами, например Trunk/{1,2,...n} = {peer_name} и до посинения перебирать их в цикле штатными средствами.
      • 0
        1. А это не штатные средства?
        2. И как вашим методом можно вызвать сразу рабочий транк? Без попыток вызова не рабочих?
        • 0
          1. Это ненужное усложнение для решения простой задачи. Представьте себя на месте человека, который пришел после вас. А теперь представьте, что он импульсивный и склонный к насилию маньяк, который знает, где вы живете и которому не очень понравилось, что ему пришлось потратить полчаса, чтобы разобраться, как PBX находит пиру и еще полчаса, чтобы отладить, почему эта конструкция разломалась.
          2. При включенном qualify попытки набора мертвых пир будут сразу отбиваться, так что живой найдется быстро.
          • 0
            1. так я за 2 запроса к интерфейсу получаю необходимые мне данные. Вы же советуете сделать n запросов в базу. Мой метод как минимум производительнее))
            А по поводу того что кто то там не разберется или будет этт делать слишком долго: есть такой инструмент — документация. Очень помогает. А если и документация не поможет — то это уже вопрос компетенции. А само по себе ничего не ломается.
            2. Ну как бы уже ответил по поводу производительности.
            • 0
              Никакая документация не может служить оправданием для того, чтобы делать простые вещи сложно. Особенно для склонного к насилию маньяка :)
              Насчет производительности — очень спорное утверждение, внутренняя БД астериска — это крохотная беркли (или  sqlite в новых версиях), ходить в которую практически бесплатно.

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

              • 0
                2. *
                dial через макрос в цикле в диалплане
                10 каналов отбивается меньше чем за секунду. Нагрузка на сервер отсутствует.
                у вас быстрее произойдут проблемы с tcp:connect(«127.0.0.1», 5038) чем с самим диалпланом.
                • 0
                  Я про диалплан в conf файлах уже давно и думать забыл. Как и о более половины встроенных конструкций типа GotoIfTime, GotoIf, realtime диалплана, а так же макросов и подобной ерунды предложенной разработчиками, ибо скорость выполнения и удобство реализации оставляют желать лучшего.

                  tcp:connect(«127.0.0.1», 5038) не отработает только в том случае, если только навернется вся сеть на сервере. Но тогда и актуальность данной проблемы потеряет всякий смысл.
                  • 0
                    как показала 6ти летняя практика, рано пока отказываться от конфов
                    количество коннектов на 5038 может быть ограничено.
                    А при наличии 50-100 номеров на пне 4 (по 2-3 линии каждый) при подобных «запросах» при «заглюченном оборудовании» коннект к 5038 может быть потерян, да и не дай бог сработает защита от доса)
                    • 0
                      Как показала 6 летняя практика- самое время отказываться. Особенно если это высоконагруженная система. Особенно если это облачное решение. Особенно если это взаимодействие с базами данных. ДА много таких вот «Особенно» я могу перечислить, исходя из опыта.

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

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

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

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

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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.