
Overview of Asterisk REST Interface (ARI)
- Tutorial
At the beginning of time, the only "supplier" of Asterisk functionality was modules, many of which expanded the arsenal of applications and dial plan features.
Then, at the beginning of time, all these commands and functions were far ahead of their time, and thanks to them, Asterisk "made" many commercial products in functionality.
If there was any need to go beyond the limits of existing applications and functions, you could write your own module in C, and this was the only way to expand the functionality and exit the existing “cell”, no matter how spacious it is.
But the development of the Asterisk module in C is hardly a trivial task. This is a very thorny path, and also a very risky one, because a critical error in its module easily led to the complete fall of Asterisk in core.
What was needed were more “softer” and simpler ways to expand functions and integrate with other systems.
So there were interfaces AGI and AMI.
Asterisk Gateway Interface (AGI) is a synchronous dialplan execution interface that is architecturally "lapped" with CGI. The AGI dialplan team started the process, and used standard input and output to receive commands and send results. Using AGI, you can solve integration problems with external systems, for example, you can go to the corporate database and find the name of the calling client by its number.
In fact, AGI provided a way to write an Asterisk dialing plan not in extensions.conf format, but in its own programming language, using the commands and functions supplied by the modules around which its own business logic is built.
Asterisk Manager Interface (AMI) is an asynchronous (event) interface that allows you to monitor the internal state of objects in Asterisk and receive information about events that occur. If the AGI is architecturally reminiscent of the CGI interface, then the AMI session is similar to a telnet session, in which a third-party application connects via TCP / IP to the Asterisk AMI port, and can send its own commands, the response to which comes after a while in the form of a response event. In addition to the responses to the commands in the AMI connection, all kinds of events occurring in Asterisk "fall", and it is up to the client to determine whether they belong to him or they can simply be ignored.
About AGI, we can say that this is a call execution mechanism, and about AMI, that it is a call control mechanism. Most often, to build your telecommunications application, you must use AGI and AMI together. There is a "smearing" of business logic for various applications, which complicates its understanding and further maintenance and development.
In addition, there are several more limitations:
- AGI: blocks the flow serving the channel.
- AGI: reaction to events (DTMF, state change) is impossible or difficult only with AGI.
- Fundamental operations are limited to what is performed on the channel. But there are other primitives: bridges, devices, states, indication of messages and media on channels, not available in AGI / AMI.
- AMI & AGI are outdated. REST, XML / JSON RPC are more familiar and convenient in today's world.
As a result, in order to break beyond the existing limitations of commands and functions, you must write your own C-module that implements a low-level telephone primitive and integrate with external systems using AGI & AMI.
That was before the Asterisk REST Interface .
Key ARI concepts:
- ARI allows you to both control the state of the call (call control) and execute logic (call execution).
- ARI is asynchronous.
- ARI "exposes" "raw" primitives - channels, bridges, devices, etc. through the REST interface.
- Object states are available through JSON events on top of WebSocket.
- ARI - not to “steer” a call to the VoiceMail application, but to create your own VoiceMail application!
Three Whales ARI:
- RESTful interface.
- WebSocket connection through which events about controlled resources are transmitted in JSON format.
- Dialplan application - Stasis, which transfers channel control to an ARI application.
Example of a dialplan transferring control to Stais:
exten => _X.,1,Stasis(myapp,arg1,arg2)
exten => _X.,n,NoOp(Left Stasis)
ARI has some limitations
- ARI does not have access to any objects, but only to those that it controls. This means that you can’t answer on channels that are not stolen in the Stasis application. However, the channel list will return all active channels, not just those that are stolen in Stasis
- Only those operations that are defined on the Asterisk side are available (which is understandable, because this Asterisk defines all REST operations).
- Stasis application is available only with a client connection. If there is no connection on WebSocket with the name of this application, Stasis will throw an error and go further on the dialplan.
Consider the categories of operations available in ARI:
- Asterisk
- Bridges
- Channels
- Devices (endpoints)
- Device states
- Events
- Mailboxes
- Playbacks
- Recordings
- Sounds
And dwell on each category in more detail.
Asterisk
- Dynamic configuration (sorcery, pjsip)
- Assembly Information
- Module management (list, loading, unloading)
- Logging and rotation management
- Global variables (read and set)
Bridges
- Getting, creating, removing bridges
- Add / Remove Channels
- Playing music while waiting
- Enable recording
Channels
- List of active channels and channel details.
- Create a channel (originate) and remove (hangup) a channel.
- Dialplan exit
- Channel redirect
- Answer, Ring, DTMF, Mute, Hold, MoH, Silence, Play, Record, Variable, Snoop
Channels
- List of active channels and channel details.
- Create a channel (originate) and remove (hangup) a channel.
- Dialplan exit
- Channel redirect
- Answer, Ring, DTMF, Mute, Hold, MoH, Silence, Play, Record, Variable, Snoop
Devices
- List of all devices
- Sending a message to the device (SIP, PJSIP, XMPP)
Device status
- List of monitored device statuses
- Setting the status (NOT_INUSE, INUSE, BUSY, INVALID, UNAVAILABLE, RINGING, RINGINUSE, ONHOLD)
See the wiki asterisk for a complete list of possible operations - https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+ARI
Events
Here is a partial list of events that are available on the web socket of the connected application:
- StasisStart / StasisEnd - sent to the socket immediately when a call hits Stasis, and the last one when a call leaves Stasis.
- ChannelCreated / ChannelDestroyed - when creating and destroying a channel.
- BridgeCreated / BridgeDestroyed - when creating and destroying a bridge.
- ChannelDtmfReceived - upon receipt of DTMF.
- ChannelStateChange - channel status has changed.
- ChannelUserevent is a custom event. A very convenient thing that allows you to build on the event model ARI.
- DeviceStateChanged - the device status has changed (NOT_INUSE, INUSE, BUSY, INVALID, UNAVAILABLE, RINGING, RINGINUSE, ONHOLD).
- EndpointStateChange - The state of the endpoint has changed.
- PlaybackStarted / PlaybackFinished - file playback started and ended.
- TextMessageReceived - received message.
- and others ( https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+REST+Data+Models )
What's New in Asterisk 14 ARI
- Retrieving Records
- Playing media from HTTP sources.
- Медиа-плейлист (асинхронность требовала ожидания окончания одного звука для запуска следующего).
Пример
Ну и в заключение приведу пример оригинации вызова при помощи 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.
- After the call is completed, the client connection is closed.
This script can be run from the console, and here is what it will return:
(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)'}
Designation of parameters:
(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
To run this script, you need to install the ari and gevent libraries:
pip install ari gevent
PS Written on the basis of the author’s speech at Asterconf 2016.
The PPS script is located here - https://gist.github.com/litnimax/2b0f9d99e49e49a07e59c45496112133