Тестуємо IVR на Asterisk з допомогою... Asterisk

Недавно мені знадобилося додати метрику за uptime сервісу дистанційного обслуговування для розрахунку SLA. Статистика викликів API є непрямим показником працездатності, а потрібна достовірна перевірка всіх функцій від дозвону із зовнішньої мережі, до проходження користувача по всьому меню обслуговування. В інтернеті нічого готового не бачив, тому вирішив поділитися своїми дослідженнями.
Є система дистанційного обслуговування " клієнт може зателефонувати в call-центр і перевірити/змінити налаштування облікового запису без участі оператора. Для переходу по меню і управління настройками використовуються тональні сигнали (DTMF). АТС у свою чергу взаємодіє з ядром основної системи через API, повертаючи результати користувачу у вигляді голосових повідомлень.
Завдання: налаштувати автоматизовану перевірку системи (правильно відповідає на запити/виконує потрібні команди).
Головні вимоги:
  • максимальна правдоподібність імітації користувача: тобто потрібно саме дзвонити і натискати кнопки, а не викликати методи API в обхід call-центру.
  • робота саме з тим планом набору, у який потрапляють звичайні користувачі; не можна робити спеціальний контекст для автоматизованої перевірки.

Для даної статті спростимо наш call-центр і API до неподобства: при дзвінку до call-центру користувачеві доступна єдина послуга (клавіша 1); в ній користувачеві пропонується ввести ПІН та у разі його коректності видається статус облікового запису користувача (ON/OFF); крок вліво чи вправо – видається повідомлення про помилку. Через API доступно три методу: GET ping (ініціалізація дзвінка), GET status (отримати статус), POST status (встановити статус).
Блок-схема IVR
Вирішувати задачу будемо з допомогою Asterisk. По суті нам потрібно зібрати аналогічний IVR тільки від імені клієнта: потрібно описати машину станів (чекаємо привітання, чекаємо запиту ПІН тощо), і при переході в кожну з станів виконувати певні дії.
Команди відправляти зрозуміло як – call-центр очікує тональні сигнали від користувача – значить можна скористатися командою
SendDTMF
та «натискати» потрібні кнопки від імені клієнта.
А як змінювати свій стан? Так точно так само! Для цього трохи модернізуємо dialplan нашого бойового IVR'а викликом нехитрого макросу в ключових місцях:
[macro-robot]
розширеннями => s,1,ExecIf($["${CALLERID(name)}"!="Robot"]?MacroExit())
same => n,Wait(1)
same => n,SendDTMF(${ARG1})

у результаті В ключових місцях роботи IVR, якщо дзвінок надійшов від робота, канал буде відправлятися обрана нами послідовність DTMF. Затримка в 1 секунду додано, щоб наш робот встигав перейти в режим очікування введення.
Тепер нам стане в нагоді можливість Asterisk відправляти здійснюється дзвінок в потрібний нам локальний контекст – таким чином замкнемо між собою IVR call-центру і нашого робота. У найпростішому варіанті ми можемо використовувати call-файл і запускати перевірку, періодично копіюючи цей файл
/var/spool/asterisk/outgoing/
.
Процес перевірки у нас буде такий:
1. Дзвонимо в call-центр
2. чекаємо, поки можна буде вибирати послугу
3. Натискаємо «1»
4. Чекаємо, поки можна буде вводити ПІН
5. Вводимо ПІН
6. Дізнаємося стан
— При першій перевірці викликом API змінюємо стан на протилежне і заново перевіряємо стан (переходимо в п. 3)
— При другій перевірці переконуємося, що стан змінилося на протилежне
8. Якщо стан змінилося, вважаємо перевірку успішної
9. У всіх інших випадках повідомляємо про помилку
Нижче я об'єднав на одному екрані» dialplan'и обох Asterisk'ів і показав, як передається управління/зміна станів:
Схема взаємодії
Текст, а не картинкаСховав у спойлер, т. к. рве екран.
# Call-file
Channel: SIP/cc_peer/ivr >----------\
Callerid: Robot |
/--< Context: robot-test |
| Extension: s |
| |
| |
[robot-test] | | [ivr]
| |
розширеннями => s,1,NoOp(Wait for init) <--/ \-----> розширеннями => s,1,NoOp(IVR Start)
same => n,Set(STATUS=) same => n,Answer()
same => n,WaitExten(10) same => n,GotoIf($["${CURL(localhost/ping)}"!="PONG"]?err,1)
/----------------------< same => n,Macro(robot,00)
розширеннями => 00,1,NoOp(Init done) <------------------------------/ same => n,Background(welcome)
same => n,Wait(1) same => n(again),Background(press_1_for_status)
same => n,SendDTMF(1) ; 1 Press for status >----------------\ same => n,WaitExten(5)
same => n,WaitExten(10) \ same => n,Goto(err,1)
\
розширеннями => 10,1,NoOp(Status check) <---------------------------\ \--------------------> розширеннями => 1,1,NoOp(Status check) <---------------------------------\
same => n,Wait(1) \-------------------------< same => n,Macro(robot,10) |
same => n,SendDTMF(1234) ; Send pin code >----------------------------------------> same => n,Read(PIN,enter_pin,4) |
same => n,WaitExten(10) same => n,Set(STATUS=${CURL(localhost/status?pin=${PIN})}) |
same => n,GotoIf($["${STATUS}"=="ON"]?on) |
розширеннями => _1[12],1,NoOp(Status) <--------------------------------------------------\ same => n,GotoIf($["${STATUS}"=="OFF"]?off) |
same => n,ExecIf($[${РОЗШИРЕННЯМИ}==11]?MSet(CURRENT=ON NEW=OFF)) | same => n,Goto(err,1) |
same => n,ExecIf($[${РОЗШИРЕННЯМИ}==12]?MSet(CURRENT=OFF,NEW=ON)) |---< same => n(on),Macro(robot,11) |
same => n,GotoIf($["${STATUS}"==""]?toggle) | same => n,Background(status_on) |
same => n,ExecIf($["${STATUS}"=="${CURRENT}"]?System(echo GOOD >> /cc_check.log)) | same => n,Goto(s,again) |
same => n,ExecIf($["${STATUS}"!="${CURRENT}"]?System(echo BAD >> /cc_check.log)) \---< same => n(off),Macro(robot,12) |
same => n,Hangup() same => n,Background(status_off) |
same => n(toggle),NoOp(Toggle status) same => n,Goto(s,again) |
same => n,Set(STATUS=${NEW}) /--------------------------------------------------------------------------------------------/
same => n,Set(RES=${CURL(localhost/status,status=${NEW})}) / розширеннями => err,1,NoOp(Error occured)
same => n,Wait(1) / /------< same => n,Macro(robot,99)
same => n,SendDTMF(1) ; 1 Press for status >--------------------/ / same => n,Playback(error)
same => n,WaitExten(10) / same => n,Hangup()
/
розширеннями => i,1,System(echo BAD >> /cc_check.log) <---------------------------/ [macro-robot]
розширеннями => s,1,ExecIf($["${CALLERID(name)}"!="Robot"]?MacroExit())
same => n,Wait(1)
same => n,SendDTMF(${ARG1})

У нашому прикладі результат перевірки виконання записується у файл
/cc_check.log
. У бойовій системі ви звичайно ж ці результати будете складати в свою систему моніторингу.
В реальній системі простого CURL-запиту до API для перевірки всієї системи дистанційного обслуговування швидше за все не вистачить, тому рішення можна розширити для контролю дзвінка робота через AMI. Для цього треба модифікувати dialplan робота, щоб він відправляв
UserEvent's
в AMI. Нашу демонстраційну конфігурацію можна змінити наступним чином:
robot-ami.conf
[robot-test-ami]
розширеннями => s,1,UserEvent(CC_ROBOT_WAIT_INIT,RobotId: ${RobotId})
same => n,Set(STATUS=)
same => n,WaitExten(10)

розширеннями => 00,1,UserEvent(CC_ROBOT_WAIT_SERVICE,RobotId: ${RobotId})
same => n,Wait(1)
same => n,UserEvent(CC_ROBOT_WAIT_SERVICE,RobotId: ${RobotId},Data: Will 1 press now)
same => n,SendDTMF(1) ; 1 Press for status
same => n,WaitExten(10)

розширеннями => 10,1,UserEvent(CC_ROBOT_WAIT_PIN,RobotId: ${RobotId})
same => n,Wait(1)
same => n,UserEvent(CC_ROBOT_WAIT_PIN,RobotId: ${RobotId},Data: Will send pin (1234) now)
same => n,SendDTMF(1234) ; Send pin code
same => n,WaitExten(10)

розширеннями => _1[12],1,UserEvent(CC_ROBOT_STATUS_CHECK,RobotId: ${RobotId})
same => n,ExecIf($[${РОЗШИРЕННЯМИ}==11]?MSet(CURRENT=ON NEW=OFF))
same => n,ExecIf($[${РОЗШИРЕННЯМИ}==12]?MSet(CURRENT=OFF,NEW=ON))
same => n,UserEvent(CC_ROBOT_STATUS_CHECK,RobotId: ${RobotId},Data: Current status is '${CURRENT}')
same => n,GotoIf($["${STATUS}"==""]?toggle)
same => n,ExecIf($["${STATUS}"=="${CURRENT}"]?UserEvent(CC_ROBOT_RESULT,RobotId: ${RobotId},Data: GOOD))
same => n,ExecIf($["${STATUS}"!="${CURRENT}"]?UserEvent(CC_ROBOT_RESULT,RobotId: ${RobotId},Data: BAD))
same => n,Hangup()
same => n(toggle),UserEvent(CC_ROBOT_STATUS_CHECK,RobotId: ${RobotId},Data: Need to toggle state)
same => n,Set(STATUS=${NEW})
same => n,UserEvent(CC_ROBOT_TOGGLE,RobotId: ${RobotId},Data: ${CURRENT})
same => n,Wait(2)
same => n,SendDTMF(1)
same => n,WaitExten(10)

розширеннями => i,1,UserEvent(CC_ROBOT_RESULT,RobotId: ${RobotId},Data: BAD)

Для взаємодії з таким dialplan'ом необхідно підключитися до Asterisk'у, з якого ініціюється дзвінок робота, через AMI, зробити
Originate
і далі діяти згідно з приходять
UserEvent'ами
. Приклад реалізації скрипта нашої demo-перевірки на Python:
test_call.py
#!/usr/bin/python

import os
import time
import string
import random
import sys
import requests

from asterisk.ami import Action, AMIClient

seconds_to_wait = 30
test_result = 'unknown'
host = 'localhost' # Asterisk with AMI and test dialplan
user = 'robot' # AMI user
password = 'MrRobot' # AMI password
call_to = 'Local/ivr' # Call-center
context = { # Robot dialplan context
"context": "robot-test-ami",
"extension": "s",
"priority": 1
}

def toggle_state(new_state):
print 'Will try to toggle state to {}'.format(new_state)
r = requests.post('http://localhost/status', data = {'status':new_state.upper()})
print 'Done! Actual state now: {}'.format(r.text)

def event_notification(source, event):
global test_result
keys = event.keys
if 'RobotId' in keys:
if keys['RobotId'] == robot_id: # it's our RobotId

if 'Data' in keys:
data = keys['Data']
else:
data = 'unknown'
if 'UserEvent' in keys:
name = keys['UserEvent']
else:
name = 'unknown'

if name.startswith('CC_ROBOT'):
print '{}: {}'.format(name, data)

if name == 'CC_ROBOT_TOGGLE':
if data.lower() in ['on','off']:
if data.lower() == 'on':
toggle_state('off')
else:
toggle_state('on')
else:
print 'Unknown state {}'.format(data)

if name == 'CC_ROBOT_RESULT':
test_result = data

# Generate uniq RobotId to distinguish events from different robots
robot_id = ".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))
print 'Current RobotId: {}'.format(robot_id)

# Action to enable user events
aEnableEvents = Action('Events', keys={'EventMask':'user'})

# Action to call originate
aOriginateCall = Action('Originate',
keys={'Channel':call_to, 'Context':context'context'], 'Розширеннями':context'extension'], 'Priority':context'priority'], 'CallerId':'Robot'},
variables={'RobotId':robot_id}
)

# Init AMI client and try to login
client = AMIClient(host)

# Register our event listener
client.add_event_listener(event_notification)

try:
future = client.login(user, password)
# This will wait for 1 second or fail
if future.response.is_error():
raise Exception(str(future.response.keys['Message']))
except as Exception err:
client.logoff()
sys.exit('Error: {}'.format(err.message))

print 'Spawned AMI session to: {}'.format(host)

try:
# Try to enable user events coming
future = client.send_action(aEnableEvents,None)
if future.response.is_error():
raise Exception(str(future.response.keys['Message']))
print 'Logged in as {}'.format(user)

# Try to call originate
future = client.send_action(aOriginateCall,None)
if future.response.is_error():
raise Exception(str(future.response.keys['Message']))
print 'Originated test call'

except as Exception err:
client.logoff()
sys.exit('Error: {}'.format(err.message))

print 'Waiting for events...'

# Wait for events during timelimit interval
for i in range(seconds_to_wait):
time.sleep(1)
# If test_result is changed (via events), then stop waiting
if test_result != 'unknown':
break;
else:
client.logoff()
sys.exit('Error: time limit exceeded')

# Logoff if we still here
client.logoff()

print 'Test result: {}'.format(test_result)

Дії виконуються аналогічні описаним вище сценарієм з call-файлом, але дзвінок ініціюється по команді скрипта і виклик API теж здійснюється з скрипта при отриманні події
CC_ROBOT_TOGGLE
. Результат перевірки приходить разом з подією
CC_ROBOT_RESULT
.
У підсумку ми вирішили завдання в рамках необхідних умов: дзвонимо і «натискаємо кнопки», робимо перевірки в бойовому dialplan'е (видаючи тони по ходу дзвінка, якщо дзвонить робот).
Вдалого тестування!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.