Pull to refresh

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

Reading time 4 min
Views 14K
Как и все А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. Только вот с документацией там… никак.
Tags:
Hubs:
+7
Comments 12
Comments Comments 12

Articles