pespin has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/36594?usp=email )
Change subject: Asterisk: Initial AMI support ......................................................................
Asterisk: Initial AMI support
Introduce config and functions around TELNETasp_PT to implement an AMI interface client to interact with Asterisk.
So far only the "Action: Login" case is implemented.
Change-Id: I2c570e4d04e7ab8c44962cf484e4bbc946209aee --- A asterisk/AMI_Functions.ttcn M asterisk/Asterisk_Tests.default M asterisk/Asterisk_Tests.ttcn M asterisk/gen_links.sh M asterisk/regen_makefile.sh 5 files changed, 257 insertions(+), 1 deletion(-)
Approvals: osmith: Looks good to me, but someone else must approve pespin: Looks good to me, approved Jenkins Builder: Verified jolly: Looks good to me, but someone else must approve
diff --git a/asterisk/AMI_Functions.ttcn b/asterisk/AMI_Functions.ttcn new file mode 100644 index 0000000..53a7964 --- /dev/null +++ b/asterisk/AMI_Functions.ttcn @@ -0,0 +1,212 @@ +/* Asterisk's AMI interface functions in TTCN-3 + * (C) 2024 by sysmocom - s.f.m.c. GmbH info@sysmocom.de + * Author: Pau Espin Pedrol pespin@sysmocom.de + * All rights reserved. + * + * Released under the terms of GNU General Public License, Version 2 or + * (at your option) any later version. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* + * https://docs.asterisk.org/Configuration/Interfaces/Asterisk-Manager-Interfac... + */ +module AMI_Functions { + +import from Misc_Helpers all; +import from TELNETasp_PortType all; +import from Osmocom_Types all; +import from TCCConversion_Functions all; +import from Socket_API_Definitions all; + +modulepar { + float mp_ami_prompt_timeout := 10.0; +} + +const charstring AMI_FIELD_ACTION := "Action"; +const charstring AMI_FIELD_USERNAME := "Username"; +const charstring AMI_FIELD_SECRET := "Secret"; +const charstring AMI_FIELD_RESPONSE := "Response"; + +type record AMI_Field { + charstring key, + charstring val +}; +type set of AMI_Field AMI_Msg; + +template (value) AMI_Field +ts_AMI_Field(template (value) charstring key, + template (value) charstring val) := { + key := key, + val := val +}; + +template (present) AMI_Field +tr_AMI_Field(template (present) charstring key := ?, + template (present) charstring val := ?) := { + key := key, + val := val +}; + +/* + * Field Templates: + */ + +template (value) AMI_Field +ts_AMI_Field_Action(template (value) charstring val) := ts_AMI_Field(AMI_FIELD_ACTION, val); +template (value) AMI_Field +ts_AMI_Field_Username(template (value) charstring val) := ts_AMI_Field(AMI_FIELD_USERNAME, val); +template (value) AMI_Field +ts_AMI_Field_Secret(template (value) charstring val) := ts_AMI_Field(AMI_FIELD_SECRET, val); + +template (present) AMI_Field +tr_AMI_Field_Action(template (present) charstring val := ?) := tr_AMI_Field(AMI_FIELD_ACTION, val); +template (present) AMI_Field +tr_AMI_Field_Username(template (present) charstring val := ?) := tr_AMI_Field(AMI_FIELD_USERNAME, val); +template (present) AMI_Field +tr_AMI_Field_Secret(template (present) charstring val := ?) := tr_AMI_Field(AMI_FIELD_SECRET, val); +template (present) AMI_Field +tr_AMI_Field_Response(template (present) charstring val := ?) := tr_AMI_Field(AMI_FIELD_RESPONSE, val); + + +template (present) AMI_Field +tr_AMI_Field_ResponseSuccess := tr_AMI_Field(AMI_FIELD_RESPONSE, "Success"); + + +/* + * Message Templates: + */ + +template (value) AMI_Msg +ts_AMI_Action_Login(charstring username, charstring secret) := { + ts_AMI_Field_Action("Login"), + ts_AMI_Field_Username(username), + ts_AMI_Field_Secret(secret) +}; + +template (present) AMI_Msg +tr_AMI_Action_Login(template(present) charstring username := ?, + template(present) charstring secret := ?) := superset( + tr_AMI_Field_Action("Login"), + tr_AMI_Field_Username(username), + tr_AMI_Field_Secret(secret) +); + +template (present) AMI_Msg +tr_AMI_Response_Success := superset( + tr_AMI_Field_ResponseSuccess +); + +/* + * Functions: + */ + +function f_AMI_Field_from_str(charstring str) return AMI_Field { + var AMI_Field field; + /* "each field is a key value pair delineated by a ':'. + * A single space MUST follow the ':' and precede the value. "*/ + var integer pos := f_strstr(str, ": ", 0); + if (pos < 0) { + Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, + log2str("Failed parsing AMI_Field: ", str)); + } + field.key := substr(str, 0, pos); + /* skip ": " */ + pos := pos + 2; + field.val := substr(str, pos, lengthof(str) - pos); + return field; +} + +function f_AMI_Msg_from_str(charstring str) return AMI_Msg { + var AMI_Msg msg := {}; + var Misc_Helpers.ro_charstring lines := f_str_split(str, "\n"); + + for (var integer i := 0; i < lengthof(lines); i := i + 1) { + var charstring line := lines[i]; + var AMI_Field field := f_AMI_Field_from_str(lines[i]); + msg := msg & { field }; + } + return msg; +} + +function f_AMI_Field_to_str(AMI_Field field) return charstring { + return field.key & ": " & field.val; +} + +function f_AMI_Msg_to_str(AMI_Msg msg) return charstring { + var charstring str := ""; + + for (var integer i := 0; i < lengthof(msg); i := i + 1) { + str := str & f_AMI_Field_to_str(msg[i]) & "\r\n"; + } + + str := str & "\r\n"; + return str; +} + +private function f_ami_wait_for_prompt_str(TELNETasp_PT pt, charstring log_label := "(?)") +return charstring { + var charstring rx, buf := ""; + var integer fd; + timer T; + + T.start(mp_ami_prompt_timeout); + alt { + [] pt.receive(pattern "\n") { }; + [] pt.receive(charstring:?) -> value rx { buf := buf & rx; repeat }; + [] pt.receive(integer:?) -> value fd { + if (fd == -1) { + Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, + "AMI Telnet Connection Failure: " & log_label); + } else { + repeat; /* telnet connection succeeded */ + } + } + [] T.timeout { + Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, + "AMI Timeout for prompt: " & log_label); + }; + } + T.stop; + return buf; +} + +function f_ami_wait_for_prompt(TELNETasp_PT pt, charstring log_label := "(?)") return AMI_Msg { + var charstring buf := f_ami_wait_for_prompt_str(pt, log_label); + var AMI_Msg msg := f_AMI_Msg_from_str(buf); + return msg; +} + +/* send a AMI command and obtain response until prompt is received */ +private function f_ami_transceive_ret_str(TELNETasp_PT pt, charstring tx) return charstring { + pt.send(tx); + return f_ami_wait_for_prompt_str(pt, tx); +} + +function f_ami_transceive_ret(TELNETasp_PT pt, template (value) AMI_Msg tx_msg) return AMI_Msg { + var charstring tx_txt := f_AMI_Msg_to_str(valueof(tx_msg)); + var charstring resp_txt := f_ami_transceive_ret_str(pt, tx_txt); + return f_AMI_Msg_from_str(resp_txt); +} + +function f_ami_transceive_match(TELNETasp_PT pt, + template (value) AMI_Msg tx_msg, + template (present) AMI_Msg exp_ret := ?) { + var AMI_Msg ret := f_ami_transceive_ret(pt, tx_msg); + if (not match(ret, exp_ret)) { + Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, + log2str("Non-matching AMI response: ", ret, " vs exp: ", exp_ret)); + } +} + +function f_ami_transceive_match_response_success(TELNETasp_PT pt, + template (value) AMI_Msg tx_msg) { + f_ami_transceive_match(pt, tx_msg, tr_AMI_Response_Success); +} + +function f_ami_action_login(TELNETasp_PT pt, charstring username, charstring secret) { + f_ami_transceive_match_response_success(pt, ts_AMI_Action_Login(username, secret)); +} + +} diff --git a/asterisk/Asterisk_Tests.default b/asterisk/Asterisk_Tests.default index a2fda0f..0996545 100644 --- a/asterisk/Asterisk_Tests.default +++ b/asterisk/Asterisk_Tests.default @@ -4,13 +4,24 @@ mtc.FileMask := ERROR | WARNING | PARALLEL | VERDICTOP;
[TESTPORT_PARAMETERS] +*.*.DEBUG := "yes" +*.AMI.PROMPT1 := "Asterisk Call Manager/9.0.0\n" +*.AMI.PROMPT2 := "\n" +#*.AMI.REGEX_PROMPT1 := "^Asterisk Call Manager.*$" +*.AMI.CTRL_MODE := "client" +*.AMI.CTRL_HOSTNAME := "127.0.0.1" +*.AMI.CTRL_PORTNUM := "5038" +*.AMI.CTRL_LOGIN_SKIPPED := "yes" +*.AMI.CTRL_DETECT_SERVER_DISCONNECTED := "yes" +*.AMI.CTRL_READMODE := "buffered" +*.AMI.CTRL_CLIENT_CLEANUP_LINEFEED := "yes" +*.AMI.CTRL_CRLF := "yes" *.SIP.local_sip_port := "5060" *.SIP.default_local_address := "127.0.0.2" *.SIP.default_sip_protocol := "UDP" *.SIP.default_dest_port := "5060" *.SIP.default_dest_address := "127.0.0.1"
- [MODULE_PARAMETERS]
[MAIN_CONTROLLER] diff --git a/asterisk/Asterisk_Tests.ttcn b/asterisk/Asterisk_Tests.ttcn index 151041d..c883bac 100644 --- a/asterisk/Asterisk_Tests.ttcn +++ b/asterisk/Asterisk_Tests.ttcn @@ -16,6 +16,8 @@ import from Osmocom_Types all; import from Native_Functions all; import from Misc_Helpers all; +import from TELNETasp_PortType all; +import from AMI_Functions all;
import from SDP_Types all; import from SDP_Templates all; @@ -29,6 +31,10 @@ integer mp_local_sip_port := 5060; charstring mp_remote_sip_host := "127.0.0.1"; integer mp_remote_sip_port := 5060; + + /* Asterisk AMI: */ + charstring mp_ami_user := "test_user"; + charstring mp_ami_secret := "1234"; }
type port Coord_PT message @@ -44,6 +50,7 @@
type component test_CT { var SIP_Emulation_CT vc_SIP; + port TELNETasp_PT AMI; port Coord_PT COORD; }
@@ -159,7 +166,14 @@ mt := t_CallParsMT }
+/* Initialize connection towards Asterisk AMI */ +private function f_init_ami() runs on test_CT { + map(self:AMI, system:AMI); + f_ami_action_login(AMI, mp_ami_user, mp_ami_secret); +} + function f_init() runs on test_CT { + f_init_ami(); f_init_sip(vc_SIP, "Asterisk_Test"); log("end of f_init"); } diff --git a/asterisk/gen_links.sh b/asterisk/gen_links.sh index 7394b64..97df3a2 100755 --- a/asterisk/gen_links.sh +++ b/asterisk/gen_links.sh @@ -18,6 +18,10 @@ FILES="IPL4asp_Functions.ttcn IPL4asp_PT.cc IPL4asp_PT.hh IPL4asp_PortType.ttcn IPL4asp_Types.ttcn IPL4asp_discovery.cc IPL4asp_protocol_L234.hh" gen_links $DIR $FILES
+DIR=$BASEDIR/titan.TestPorts.TELNETasp/src +FILES="TELNETasp_PT.cc TELNETasp_PT.hh TELNETasp_PortType.ttcn" +gen_links $DIR $FILES + DIR=$BASEDIR/titan.ProtocolModules.SDP/src FILES="SDP_EncDec.cc SDP_Types.ttcn SDP_parse_.tab.c SDP_parse_.tab.h SDP_parse_parser.h SDP_parser.l SDP_parser.y lex.SDP_parse_.c" diff --git a/asterisk/regen_makefile.sh b/asterisk/regen_makefile.sh index 3cd0a66..1aa18ae 100755 --- a/asterisk/regen_makefile.sh +++ b/asterisk/regen_makefile.sh @@ -15,6 +15,7 @@ TCCConversion.cc TCCInterface.cc TCCOpenSecurity.cc + TELNETasp_PT.cc "
../regen-makefile.sh -e $NAME $FILES