📜 ⬆️ ⬇️

Testing IVR on Asterisk with ... Asterisk

Recently, I needed to add a metric for the uptime of the remote maintenance service to calculate the SLA. Statistics on API calls is an indirect indicator of performance, and you need a reliable test of all functions from dialing from the external network to the user's passage through the entire maintenance menu. I did not see anything ready on the Internet, so I decided to share my research.


There is a remote service system - the client can call the call-center and check / change the settings of his account without the participation of the operator. Tones (DTMF) are used to navigate through the menus and control settings. The PBX in turn interacts with the core of the main system through the API, returning the results to the user in the form of voice messages.


Task : set up an automated system check (correctly responds to requests / executes the necessary commands).
Main requirements:



For this article, simplify our call-center and API to disgrace: when you call the call-center, the only service available to the user (key 1); in it, the user is prompted to enter a PIN and, if it is correct, the status of the user account is issued (ON / OFF); step left or right - an error message is displayed. Three methods are available through the API: GET ping (call initialization), GET status (get status), POST status (set status).


IVR Flow Chart


We will solve the problem with the help of Asterisk. In fact, we need to collect a similar IVR only on behalf of the client: we need to describe the state machine (waiting for a greeting, waiting for a PIN request, etc.), and when moving to each of the states, perform certain actions.


It is clear how to send commands — the call center expects tones from the user — then you can use the SendDTMF command and “press” the necessary buttons on behalf of the client.


How to change your state? Yes, exactly the same! To do this, we will slightly modernize the dialplan of our combat IVR by calling a simple macro in key places:


 [macro-robot] exten => s,1,ExecIf($["${CALLERID(name)}"!="Robot"]?MacroExit()) same => n,Wait(1) same => n,SendDTMF(${ARG1}) 

As a result, in key IVR locations, if the call came from a robot, the selected DTMF sequence will be sent to the channel. A delay of 1 second is added, so that our robot has time to go into the input mode.


Now we can use the ability of Asterisk to send the call made to the local context we need - thus closing the IVR of the call-center and our robot between us. In the simplest version, we can use the call-file and run the check, periodically copying this file to /var/spool/asterisk/outgoing/ .


The verification process will be as follows:


1. Call the call center


2. we wait, while it will be possible to choose service


3. Push "1"


4. We are waiting for the PIN to be entered.


5. Enter the PIN


6. We learn a condition


- At the first check with an API call, we change the state to the opposite and re-check the state (go to step 3)


- At the second check we are convinced that the state has changed to the opposite


8. If the status has changed, we consider the check successful.


9. In all other cases, we report an error.


Below, I combined the dialplans of both Asterisks on one screen and showed how control / state changes are transmitted:


Interaction scheme


Text, not a picture

Hid in the spoiler, tk. tearing the screen.


  # Call-file Channel: SIP/cc_peer/ivr >----------\ Callerid: Robot | /--< Context: robot-test | | Extension: s | | | | | [robot-test] | | [ivr] | | exten => s,1,NoOp(Wait for init) <--/ \-----> exten => 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) exten => 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) ; Press 1 for status >----------------\ same => n,WaitExten(5) same => n,WaitExten(10) \ same => n,Goto(err,1) \ exten => 10,1,NoOp(Status check) <---------------------------\ \--------------------> exten => 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) | exten => _1[12],1,NoOp(Status) <--------------------------------------------------\ same => n,GotoIf($["${STATUS}"=="OFF"]?off) | same => n,ExecIf($[${EXTEN}==11]?MSet(CURRENT=ON,NEW=OFF)) | same => n,Goto(err,1) | same => n,ExecIf($[${EXTEN}==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})}) / exten => err,1,NoOp(Error occured) same => n,Wait(1) / /------< same => n,Macro(robot,99) same => n,SendDTMF(1) ; Press 1 for status >--------------------/ / same => n,Playback(error) same => n,WaitExten(10) / same => n,Hangup() / exten => i,1,System(echo BAD >> /cc_check.log) <---------------------------/ [macro-robot] exten => s,1,ExecIf($["${CALLERID(name)}"!="Robot"]?MacroExit()) same => n,Wait(1) same => n,SendDTMF(${ARG1}) 

In our example, the result of the check is written to the file /cc_check.log In the combat system, you will of course add these results to your monitoring system.


In a real system, a simple CURL request to the API for testing the entire remote maintenance system is most likely not enough, so the solution can be expanded to monitor the robot’s call through AMI. To do this, you need to modify the dialplan of the robot so that it sends UserEvent' to AMI. Our demo configuration can be changed as follows:


robot-ami.conf
 [robot-test-ami] exten => s,1,UserEvent(CC_ROBOT_WAIT_INIT,RobotId: ${RobotId}) same => n,Set(STATUS=) same => n,WaitExten(10) exten => 00,1,UserEvent(CC_ROBOT_WAIT_SERVICE,RobotId: ${RobotId}) same => n,Wait(1) same => n,UserEvent(CC_ROBOT_WAIT_SERVICE,RobotId: ${RobotId},Data: Will press 1 now) same => n,SendDTMF(1) ; Press 1 for status same => n,WaitExten(10) exten => 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) exten => _1[12],1,UserEvent(CC_ROBOT_STATUS_CHECK,RobotId: ${RobotId}) same => n,ExecIf($[${EXTEN}==11]?MSet(CURRENT=ON,NEW=OFF)) same => n,ExecIf($[${EXTEN}==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) exten => i,1,UserEvent(CC_ROBOT_RESULT,RobotId: ${RobotId},Data: BAD) 

To interact with such a dialplan, you need to connect to Asterisk, from which the robot's call is initiated, via AMI, make Originate and continue to act in accordance with incoming UserEvent' . An example of the implementation of the script of our demo-test in 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 originate call aOriginateCall = Action('Originate', keys={'Channel':call_to, 'Context':context['context'], 'Exten':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 Exception as 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 originate call future = client.send_action(aOriginateCall,None) if future.response.is_error(): raise Exception(str(future.response.keys['Message'])) print 'Originated test call' except Exception as 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) 

Actions are performed similar to the above-described script with a call-file, but the call is initiated by a script command and the API call is also made from the script when the CC_ROBOT_TOGGLE event is CC_ROBOT_TOGGLE . The result of the check comes with the event CC_ROBOT_RESULT .


As a result, we solved the task within the framework of the required conditions: we call and “press buttons”, we make checks in the combat dialplan (giving out tones during the call if the robot calls).


Have a good test!


')

Source: https://habr.com/ru/post/309156/


All Articles