В начале времен единственным "поставщиком" функционала Asterisk были модули, многие из которых расширяли арсенал приложений и функций плана набора.
Тогда, в начале времен, все эти команды и функции далеко опережали свое время, и благодаря им Asterisk "уделывал" по функционалу многие коммерческие продукты.
Если возникала какая-нибудь необходимость в выходе за пределы имеющихся приложений и функций, можно было написать свой собственный модуль на языке С, и это был единственный способ расширения функционала и выхода из имеющейся "клетки", какой бы просторной она ни была.
Но разработку модуля Астериск на языке С сложно назвать тривиальной задачей. Это весьма тернистый путь, к тому же весьма рискованный, ведь критическая ошибка в своем модуле запросто приводила к полному падению Asterisk в core.
Нужны были более "мягкие" и простые способы для расширения функций и интеграции с другими системами.
Так появились интерфейсы AGI и AMI.
Asterisk Gateway Interface (AGI) — это синхронный интерфейс выполнения диалплана, архитектурно "слизанный" с CGI. Команда диалплана AGI запускала процесс, и использовала стандартный ввод и вывод для получения команд и передачи результатов. При помощи AGI можно решать задачи интеграции с внешними системами, например, можно отправиться в корпоративную базу данных и найти имя звонящего клиента по его номеру.
По сути, AGI предоставлял способ написать план набора Asterisk не в формате extensions.conf, а на своем языке программирования, используя поставляемые модулями команды и функции, вокруг которых строится своя бизнес-логика.
Asterisk Manager Interface (AMI) — это асинхронный (событийный) интерфейс, позволяющий контролировать внутреннее состояние объектов в Asterisk, и получать информацию о происходящих событиях. Если AGI архитектурно напоминает CGI интерфейс, то AMI сессия похожа на телнет-сессию, в рамках которой стороннее приложение подключается по TCP/IP к AMI порту Asterisk, и может отправлять свои команды, ответ на которые приходит через некоторое время в виде события-ответа. Помимо ответов на команды в AMI соединение "валятся" всевозможные события, происходящие в Asterisk, и дело клиента определить, относятся они к нему или их можно просто игнорировать.
Про AGI можно сказать, что это call execution механизм, а про AMI — что это call control механизм. Чаще всего для построения своего телекоммуникационного приложения необходимо использовать сразу AGI и AMI вместе. Происходит "размазывание" бизнес логики по разным приложениям, что затрудняет его понимание и дальнейшее сопровождение и развитие.
Помимо этого, существует еще несколько ограничений:
- AGI: блокирует поток, обслуживающий канал.
- AGI: реакция на события (DTMF, изменение состояния) невозможна или затруднена только с AGI.
- Фундаментальные операции ограничены тем, что выполняется на канале. Но есть и другие примитивы: мосты, устройства, состояния, индикации сообщений и медиа на каналах, недоступные в AGI/AMI.
- AMI & AGI — морально устарели. REST, XML/JSON RPC более привычны и удобны в сегодняшнем мире.
В результате, чтобы вырваться за рамки существующих ограничений команд и функций, надо и писать свой С-модуль, реализующий низкоуровневый телефонный примитив, и интегрироваться с внешними системами при помощи AGI & AMI.
Так было до появления Asterisk REST Interface.
Основные концепции ARI:
- ARI позволяет как управлять состоянием звонка (call control), так и выполнять логику (call execution).
- ARI асинхронен.
- ARI «выставляет» «сырые» примитивы — каналы, мосты, устройства и т.п. через REST интерфейс.
- Состояния объектов доступны через JSON события поверх WebSocket.
- ARI — не для того, чтобы «зарулить» звонок в приложение VoiceMail, а для того, чтобы создать свое собственное приложение VoiceMail!
"Три кита" ARI:
- RESTful интерфейс.
- WebSocket подключение, по которому передаются события о контролируемых ресурсах в JSON формате.
- Приложение диалплана — Stasis, передающее управление каналом в ARI приложение.
Пример диалплана, передающего управление в Stais:
exten => _X.,1,Stasis(myapp,arg1,arg2)
exten => _X.,n,NoOp(Left Stasis)
ARI имеет некоторые ограничения
- ARI не имеет доступа к любым объектам, а только к тем, которые контролирует. Это значит, что нельзя сделать answer на канале, которые не зарулен в Stasis приложение. Однако, channel list вернет все активные каналы, а не только те, что зарулены в Stasis
- Доступны только те операции, которые определены на стороне Asterisk (что понятно, ведь это Asterisk определяет все REST операции).
- Stasis приложение доступно только при установленном клиентском соединении. Если нет соединения на WebSocket с именем данного приложения, Stasis выдаст ошибку и пойдет дальше по диалплану.
Рассмотрим категории операций, доступных в ARI:
- Asterisk
- Мосты (bridges)
- Каналы (channels)
- Устройства (endpoints)
- Состояния устройств (device states)
- События (events)
- Почтовые ящики (mailboxes)
- Воспроизведения (playbacks)
- Записи (recordings)
- Звуки (sounds)
И остановимся на каждой категории подробнее.
Asterisk
- Динамическая конфигурация (sorcery, pjsip)
- Информация о сборке
- Управление модулями (список, загрузка, выгрузка)
- Управление логированием и ротацией логов
- Глобальные переменные (чтение и установка)
Мосты
- Получение, создание, удаление мостов
- Добавление / удаление каналов
- Проигрывание музыки на ожидании
- Включение записи
Каналы
- Список активных каналов и подробные данные канала.
- Создание канала (originate) и удаление (hangup) канала.
- Выход в диалплан
- Редирект канала
- Answer, Ring, DTMF, Mute, Hold, MoH, Silence, Play, Record, Variable, Snoop
Каналы
- Список активных каналов и подробные данные канала.
- Создание канала (originate) и удаление (hangup) канала.
- Выход в диалплан
- Редирект канала
- Answer, Ring, DTMF, Mute, Hold, MoH, Silence, Play, Record, Variable, Snoop
Устройства
- Список всех устройств
- Отправка сообщения на устройство (SIP, PJSIP, XMPP)
Состояние устройств
- Список статусов контролируемых устройств
- Установка статуса (NOT_INUSE, INUSE, BUSY, INVALID, UNAVAILABLE, RINGING, RINGINUSE, ONHOLD)
Полный список возможных операций смотрите на wiki asterisk — https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+ARI
События
Приведу частичный список событий, которые доступны на веб-сокете подключенного приложения:
- StasisStart / StasisEnd — посылается в сокет сразу при попадании звонка в Stasis, и последним при выходе звонка из Стасиса.
- ChannelCreated / ChannelDestroyed — при создании и разрушении канала.
- BridgeCreated / BridgeDestroyed — при создании и разрушении моста.
- ChannelDtmfReceived — при получении DTMF.
- ChannelStateChange — изменилось состояние канала.
- ChannelUserevent — пользовательское событие. Очень удобная штука, которая позволяет надстраиваться над событийной моделью ARI.
- DeviceStateChanged — изменилось состояние устройства (NOT_INUSE, INUSE, BUSY, INVALID, UNAVAILABLE, RINGING, RINGINUSE, ONHOLD).
- EndpointStateChange — изменилось состояние конечной точки.
- PlaybackStarted / PlaybackFinished — началось и закончилось проигрывание файла.
- TextMessageReceived — получено сообщение.
- и другие (https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+REST+Data+Models)
Что нового в Asterisk 14 ARI
- Получение записей
- Проигрывание медиа из HTTP источников.
- Медиа-плейлист (асинхронность требовала ожидания окончания одного звука для запуска следующего).
Пример
Ну и в заключение приведу пример оригинации вызова при помощи Python ARI библиотеки.
В этом примере делается оригинация по указанному пиру, и возвращается cause code:
#!/usr/bin/env python2.7
# Requirements: pip install ari gevent
import argparse
import ari
import gevent
from gevent.monkey import patch_all; patch_all()
from gevent.event import Event
import logging
from requests.exceptions import HTTPError, ConnectionError
import socket
import time
logging.basicConfig() # Important!
# Otherwise you get No handlers could be found for
# logger "ari.client"
ARI_URL = 'http://192.168.56.101:8088/ari'
ARI_USER = 'test'
ARI_PASSWORD = 'test'
client = ari.connect(ARI_URL, ARI_USER, ARI_PASSWORD)
def run():
try:
client.run('originator')
except socket.error as e:
if e.errno == 32: # Broken pipe as we close the client.
pass
except ValueError as e:
if e.message == 'No JSON object could be decoded': # client.close()
pass
def originate(endpoint=None, callerid=None, context=None, extension=None,
priority=None, timeout=None):
# Go!
evt = Event() # Wait flag for origination
result = {}
gevent.sleep(0.1) # Hack to let run() arrange all.
start_time = time.time()
try:
channel = client.channels.originate(
endpoint=endpoint,
callerId=callerid,
app='originator',
timeout=timeout
)
def state_change(channel, event):
state = event['channel']['state']
if state == 'Up':
channel = channel.continueInDialplan(
context=context, extension=extension, priority=priority)
def destroyed(channel, event):
end_time = time.time()
result['status'] = 'success'
result['message'] = '%s (%s)' % (
event.get('cause_txt'),
event.get('cause'))
result['duration'] = '%0.2f' % (end_time - start_time)
evt.set()
channel.on_event('ChannelDestroyed', destroyed)
channel.on_event('ChannelStateChange', state_change)
# Wait until we get origination result
evt.wait()
client.close()
return
except HTTPError as e:
result['status'] = 'error'
try:
error = e.response.json().get('error')
result['message'] = e.response.json().get('error')
except Exception:
result['message'] = e.response.content
finally:
print result
client.close()
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('endpoint', type=str, help='Endpoint, e.g. SIP/operator/123456789')
parser.add_argument('callerid', type=str, help='CallerID, e.g. 111111')
parser.add_argument('context', type=str, help='Asterisk context to connect call, e.g. default')
parser.add_argument('extension', type=str, help='Context\'s extension, e.g. s')
parser.add_argument('priority', type=str, help='Context\'s priority, e.g. 1')
parser.add_argument('timeout', type=int, help='Originate timeout, e.g. 60')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
runner = gevent.spawn(run)
originator = gevent.spawn(originate, endpoint=args.endpoint, callerid=args.callerid,
context=args.context, extension=args.extension,
priority=args.priority, timeout=args.timeout
)
gevent.joinall([originator, runner])
Комментарии по скрипту
- Используется асинхронный фреймворк gevent для того, чтобы в рамках одного потока как установить соединение на websocket и принимать входящие сообщения, так и для того чтобы соригинировать вызов.
- Чтобы получить статус звонка и его продолжительность, необходимо подключенный звонок зарулить в Stasis приложение originator, в рамках которого будет вызвано событие ChannelDestroyed, уже в рамках которого произойдет обработка кода завершения.
- После соединения канал перейдет в состояние up, и в этом случае будет переброшен на указанный context, extension, priority.
- После завершения звонка закрывается client соединение.
Данный скрипт можно запустить из консоли, и вот что он вернет:
(env)MacBook-Pro-Max:barrier max$ ./ari_originate.py SIP/operator 11111 default s 1 4
{'status': 'success', 'duration': '2.54', 'message': u'Normal Clearing (16)'}
Обозначения параметров:
(env)MacBook-Pro-Max:barrier max$ ./ari_originate.py -h
usage: ari_originate.py [-h]
endpoint callerid context extension priority timeout
positional arguments:
endpoint Endpoint, e.g. SIP/operator/123456789
callerid CallerID, e.g. 111111
context Asterisk context to connect call, e.g. default
extension Context's extension, e.g. s
priority Context's priority, e.g. 1
timeout Originate timeout, e.g. 60
optional arguments:
-h, --help show this help message and exit
Чтобы запустить данный скрипт, надо установить библиотеки ari и gevent:
pip install ari gevent
P.S. Написано по материалам выступления автора на Asterconf 2016.
P.P.S. Скрипт находится тут — https://gist.github.com/litnimax/2b0f9d99e49e49a07e59c45496112133
Комментарии (12)
Ovoshlook
30.08.2016 18:01На конференции Kamailio 2016 выступали ребята, которым нужно было создать сценарий обработки очередь не такой как у всех.
Они не использовали ARI аргументируя следующим:
«Если вы возметесь переписывать какой либо из модулей, то вы его закончите но не в этом веке»
и в этом есть доля правды, так как действительно, очень много работы предстоит проделать.
К слову VoiceMail аппликейшн мне приходилось переписывать из за того, что аппликейшн лочил директоррию на время записи.
В multitenant системе этого допускать нельзя.
Только переписал на lua)) используя уже существующие аппликейшны типа read и record (лень лезть в код)
Сделал за 4 дня ;-)xmaster83
31.08.2016 08:00kamalio 2016? это та конфа которая в Берлине была?
Ovoshlook
31.08.2016 10:45Да
Видео на ютюбе есть
litnimax
01.09.2016 12:52Я только не понял, они реализовали эти функции типа get_members на С и выставили их в Lua функции диалплана, или сделали так что в в app_queue оказались вмонтированы Lua функции? Что значит script hooks?
Ovoshlook
01.09.2016 19:35как я понял они взяли существующий app_queue и дописали его с помощью lua (не на С)
А конфигурить его можно так же как очередь. Через файл.
ssh24
02.09.2016 13:25>Написано по материалам выступления автора на Asterconf 2016.
да, спасибо.
а то презентациий от туда до сих пор еще не было
IgorG
Мало кто пишет в статьях, где настраиваются в asterisk параметры подключения к ARI. У тебя в статье тоже не заметил. Какой URL, логин и пароль по-умолчанию?
litnimax
Игорь, я сейчас сделаю готовый скрипт для вызова из шелла. И вложу сюда его код.
litnimax
Игорь, запилил полностью рабочий скрипт.
Не забудь сделать pip install ari gevent