osmith has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41227?usp=email )
Change subject: hlr: pyhss: create/delete subscribers ......................................................................
hlr: pyhss: create/delete subscribers
Run the PyHSS API service, and fill it with a default APN on startup. Having one APN entry in the database is required for creating subscribers.
Talk to the API service for creating and deleting subscribers. Do this with a new script pyhss_api_helper.py. Deleting subscribers requires reading JSON returned from the server (to get the AUC and subscriber ID from the IMSI). I have first attempted to do this via HTTP_Adapter instead of using a helper script, but this was a lot more complex and would have required to have the JSON structure in the TTCN3 files. The eim testsuite also runs an external script for REST requests.
With this change and additional fixes in PyHSS, more tests pass: * HLR_Tests.TC_gsup_sai * HLR_Tests.TC_gsup_sai_num_auth_vectors * HLR_Tests.TC_gsup_ul * HLR_Tests.TC_gsup_purge_cs * HLR_Tests.TC_gsup_purge_ps
Related: OS#6862 Change-Id: Ic924dabbc813459f73d6646ee17b79cb11d39a76 --- M _testenv/data/podman/Dockerfile A _testenv/data/scripts/pyhss_api_helper.py M hlr/HLR_Tests.ttcn M hlr/README.md M hlr/gen_links.sh A hlr/pyhss/setup_db.sh M hlr/regen_makefile.sh M hlr/testenv_pyhss.cfg A library/PyHSS_REST_Functions.ttcn 9 files changed, 303 insertions(+), 5 deletions(-)
Approvals: laforge: Looks good to me, approved Jenkins Builder: Verified
diff --git a/_testenv/data/podman/Dockerfile b/_testenv/data/podman/Dockerfile index f1deb29..9044f1f 100644 --- a/_testenv/data/podman/Dockerfile +++ b/_testenv/data/podman/Dockerfile @@ -80,6 +80,7 @@ python3-dev \ python3-pip \ python3-poetry-core \ + python3-requests \ python3-venv \ qemu-system-x86 \ rsync \ diff --git a/_testenv/data/scripts/pyhss_api_helper.py b/_testenv/data/scripts/pyhss_api_helper.py new file mode 100755 index 0000000..a20af74 --- /dev/null +++ b/_testenv/data/scripts/pyhss_api_helper.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# Copyright 2025 sysmocom - s.f.m.c. GmbH +# SPDX-License-Identifier: GPL-3.0-or-later +import argparse +import requests +import sys + +args = None +api = "http://127.0.0.1:8080" +session = requests.Session() + + +def parse_args(): + global args + + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="action", dest="action", required=True) + + subparser = subparsers.add_parser("add_default_apn") + + subparser = subparsers.add_parser("add_subscr") + subparser.add_argument("--imsi", required=True) + subparser.add_argument("--msisdn", required=True) + subparser.add_argument("--auc-id", required=True) + subparser.add_argument("--algo", required=True) + subparser.add_argument("--ki", required=True) + subparser.add_argument("--opc", required=True) + + subparser = subparsers.add_parser("del_subscr") + subparser.add_argument("--imsi", required=True) + + args = parser.parse_args() + + +def add_default_apn(): + url = f"{api}/apn/" + print(f"PUT {url}") + payload = { + "apn_id": 1, + "apn": "internet", + "ip_version": 0, + "charging_characteristics": "0800", + "apn_ambr_dl": 0, + "apn_ambr_ul": 0, + "qci": 9, + "arp_priority": 4, + "arp_preemption_capability": 0, + "arp_preemption_vulnerability": 1, + } + session.put(url, json=payload) + + +def add_subscr(): + # Previous tests may have left an entry in the AUC table + url = f"{api}/auc/{args.auc_id}" + print(f"DELETE {url}") + session.delete(url) + + url = f"{api}/auc/" + print(f"PUT {url}") + payload = { + "auc_id": args.auc_id, + "ki": args.ki, + "opc": args.opc, + "amf": "8000", + "sqn": "0", + "imsi": args.imsi, + "algo": args.algo, + } + session.put(url, json=payload) + + url = f"{api}/subscriber/" + print(f"PUT {url}") + payload = { + "auc_id": args.auc_id, + "default_apn": "Internet", + "apn_list": "1", + "imsi": args.imsi, + "msisdn": args.msisdn, + } + session.put(url, json=payload) + + +def get_subscr_by_imsi(): + url = f"{api}/subscriber/imsi/{args.imsi}" + print(f"GET {url}") + ret = session.get(url).json() + + if not ret: + print("ERROR: subscriber does not exist") + sys.exit(1) + + return ret + + +def del_subscr(): + subscr = get_subscr_by_imsi() + + url = f"{api}/auc/{subscr['auc_id']}" + print(f"DELETE {url}") + session.delete(url) + + url = f"{api}/subscriber/{subscr['subscriber_id']}" + print(f"DELETE {url}") + session.delete(url) + + +if __name__ == "__main__": + parse_args() + globals()[args.action]() diff --git a/hlr/HLR_Tests.ttcn b/hlr/HLR_Tests.ttcn index d324e08..c69efc3 100644 --- a/hlr/HLR_Tests.ttcn +++ b/hlr/HLR_Tests.ttcn @@ -37,12 +37,14 @@ import from MSLookup_mDNS_Templates all; import from DNS_Helpers all;
+import from PyHSS_REST_Functions all; + type enumerated HLR_Impl { HLR_IMPL_OSMOCOM, HLR_IMPL_PYHSS };
-type component test_CT extends CTRL_Adapter_CT { +type component test_CT extends CTRL_Adapter_CT, PyHSS_REST_CT { /* emulated GSUP client (MSC/SGSN) */ var IPA_Emulation_CT vc_IPA; var IPA_CCM_Parameters ccm_pars; @@ -101,7 +103,7 @@
type record of HlrSubscriber HlrSubscriberList;
-type component HLR_ConnHdlr extends GSUP_ConnHdlr { +type component HLR_ConnHdlr extends GSUP_ConnHdlr, PyHSS_REST_CT { timer g_Tguard := 10.0; var HLR_ConnHdlrPars g_pars; port TELNETasp_PT VTY; @@ -498,6 +500,42 @@ var charstring prefix := valueof(t_subscr_prefix(sub.imsi)); f_vty_transceive_nomatch(VTY, prefix & "show", exp); } +/*********************************************************************** + * Subscriber creation via PyHSS REST API + ***********************************************************************/ +function f_rest_subscr_create(HlrSubscriber sub) +runs on PyHSS_REST_CT { + /* Notes on 3G K, OP/ OPc: + * - OPc is derived from K and OPc (3GPP TS 35.206 § 4.1). + * - HlrSubscrAud3G has "k", "op", "op_is_opc" + * - OsmoHLR DB has "k", "op" and "opc" + * - PyHSS DB has "opc" + * As PyHSS only accepts OPc, and the values are dummys anyway, ignore + * "op_is_opc" and just set sub.aud3g.op as OPc. */ + if (ispresent(sub.aud2g) and ispresent(sub.aud3g)) { + f_pyhss_rest_subscr_create(sub.imsi, + sub.msisdn, + sub.aud2g.algo, + sub.aud2g.ki, + sub.aud3g.algo, + sub.aud3g.op); + } else if (ispresent(sub.aud2g)) { + f_pyhss_rest_subscr_create(sub.imsi, + sub.msisdn, + sub.aud2g.algo, + sub.aud2g.ki); + } else if (ispresent(sub.aud3g)) { + f_pyhss_rest_subscr_create(sub.imsi, + sub.msisdn, + omit, + omit, + sub.aud3g.algo, + sub.aud3g.op); + } else { + setverdict(fail, "f_rest_subscr_create: algo2g and algo3g are unset"); + mtc.stop; + } +}
/*********************************************************************** * Subscriber creation wrappers @@ -507,6 +545,8 @@ runs on test_CT { if (m_hlr_impl == HLR_IMPL_OSMOCOM) { f_vty_subscr_create(VTY, sub); + } else if (m_hlr_impl == HLR_IMPL_PYHSS) { + f_rest_subscr_create(sub); } else { setverdict(fail, "f_subscr_create is not implemented for ", m_hlr_impl); mtc.stop; @@ -517,6 +557,8 @@ runs on HLR_ConnHdlr { if (m_hlr_impl == HLR_IMPL_OSMOCOM) { f_vty_subscr_create(VTY, sub); + } else if (m_hlr_impl == HLR_IMPL_PYHSS) { + f_rest_subscr_create(sub); } else { setverdict(fail, "f_ConnHdlr_subscr_create is not implemented for ", m_hlr_impl); mtc.stop; @@ -527,6 +569,8 @@ runs on test_CT { if (m_hlr_impl == HLR_IMPL_OSMOCOM) { f_vty_subscr_delete(VTY, sub); + } else if (m_hlr_impl == HLR_IMPL_PYHSS) { + f_pyhss_rest_subscr_delete(sub.imsi); } else { setverdict(fail, "f_subscr_delete is not implemented for ", m_hlr_impl); mtc.stop; @@ -537,6 +581,8 @@ runs on HLR_ConnHdlr { if (m_hlr_impl == HLR_IMPL_OSMOCOM) { f_vty_subscr_delete(VTY, sub); + } else if (m_hlr_impl == HLR_IMPL_PYHSS) { + f_pyhss_rest_subscr_delete(sub.imsi); } else { setverdict(fail, "f_ConnHdlr_subscr_delete is not implemented for ", m_hlr_impl); mtc.stop; diff --git a/hlr/README.md b/hlr/README.md index a08c03f..0bf29ad 100644 --- a/hlr/README.md +++ b/hlr/README.md @@ -1,8 +1,14 @@ # HLR_Tests.ttcn
-* external interfaces - * GSUP (emulates VLR/SGSN side) - * VTY +External interfaces: +* GSUP (emulates VLR/SGSN side) +* VTY / PyHSS REST API client + +PyHSS notes: +* Temporary branch that has all necessary patches: + https://gitea.osmocom.org/osmith/pyhss/src/branch/osmith/ttcn3 +* Patches are being upstreamed to: + https://github.com/nickvsnetworking/pyhss
{% dot hlr_tests.svg digraph G { diff --git a/hlr/gen_links.sh b/hlr/gen_links.sh index a077937..621fe05 100755 --- a/hlr/gen_links.sh +++ b/hlr/gen_links.sh @@ -43,6 +43,10 @@ FILES="UDPasp_PT.cc UDPasp_PT.hh UDPasp_PortType.ttcn UDPasp_Types.ttcn" gen_links $DIR $FILES
+DIR=$BASEDIR/titan.TestPorts.PIPEasp/src +FILES="PIPEasp_PT.cc PIPEasp_PT.hh PIPEasp_Types.ttcn PIPEasp_PortType.ttcn " +gen_links $DIR $FILES + DIR=../library FILES="Native_Functions.ttcn Native_FunctionDefs.cc Misc_Helpers.ttcn General_Types.ttcn Osmocom_Types.ttcn GSM_Types.ttcn IPA_Types.ttcn IPA_CodecPort.ttcn IPA_CodecPort_CtrlFunct.ttcn IPA_CodecPort_CtrlFunctDef.cc IPA_Emulation.ttcnpp " FILES+="PCO_Types.ttcn GSUP_Types.ttcn GSUP_Templates.ttcn GSUP_Emulation.ttcn " @@ -50,6 +54,8 @@ FILES+="Osmocom_VTY_Functions.ttcn " FILES+="SS_Templates.ttcn USSD_Helpers.ttcn " FILES+="MSLookup_mDNS_Types.ttcn MSLookup_mDNS_Emulation.ttcn MSLookup_mDNS_Templates.ttcn DNS_Helpers.ttcn " +FILES+="PyHSS_REST_Functions.ttcn " +FILES+="PIPEasp_Templates.ttcn "
gen_links $DIR $FILES diff --git a/hlr/pyhss/setup_db.sh b/hlr/pyhss/setup_db.sh new file mode 100755 index 0000000..5fe0a71 --- /dev/null +++ b/hlr/pyhss/setup_db.sh @@ -0,0 +1,5 @@ +#!/bin/sh -ex + +wait_for_port.py -p 8080 + +pyhss_api_helper.py add_default_apn diff --git a/hlr/regen_makefile.sh b/hlr/regen_makefile.sh index 721599c..ac58c84 100755 --- a/hlr/regen_makefile.sh +++ b/hlr/regen_makefile.sh @@ -12,6 +12,7 @@ IPL4asp_discovery.cc Native_FunctionDefs.cc MAP_EncDec.cc + PIPEasp_PT.cc SS_EncDec.cc TCCConversion.cc TCCEncoding.cc @@ -26,3 +27,6 @@ "
. ../_buildsystem/regen_makefile.inc.sh + +# required for forkpty(3) used by PIPEasp +sed -i -e '/^LINUX_LIBS/ s/$/ -lutil/' Makefile diff --git a/hlr/testenv_pyhss.cfg b/hlr/testenv_pyhss.cfg index 1fcd75b..4666d2d 100644 --- a/hlr/testenv_pyhss.cfg +++ b/hlr/testenv_pyhss.cfg @@ -10,3 +10,10 @@ make=pyhss package=pyhss copy=pyhss/config.yaml + +[pyhss_api] +program=cd ../pyhss_gsup && PYHSS_CONFIG=config.yaml run_in_venv.sh pyhss_api +setup=./setup_db.sh +make=pyhss +package=pyhss +copy=pyhss/setup_db.sh diff --git a/library/PyHSS_REST_Functions.ttcn b/library/PyHSS_REST_Functions.ttcn new file mode 100644 index 0000000..10dc341 --- /dev/null +++ b/library/PyHSS_REST_Functions.ttcn @@ -0,0 +1,113 @@ +/* REST interface API for PyHSS + * + * (C) 2025 by sysmocom - s.f.m.c. GmbH info@sysmocom.de + * Author: Oliver Smith osmith@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 + */ + +module PyHSS_REST_Functions { + +import from General_Types all; + +import from PIPEasp_PortType all; +import from PIPEasp_Types all; +import from PIPEasp_Templates all; + + +type component PyHSS_REST_CT { + var boolean g_pipe_initialized := false; + var integer g_next_auc_id := 1; + port PIPEasp_PT PIPE; +} + +private function f_pyhss_api_helper(charstring args) +runs on PyHSS_REST_CT { + if (not g_pipe_initialized) { + map(self:PIPE, system:PIPE); + g_pipe_initialized := true; + } + f_PIPEasp_exec_sync(PIPE, "pyhss_api_helper.py " & args) +} + +/* Get the algorithm value (algo), from PyHSS lib/database.py: + * 1 = Comp128v1 + * 2 = Comp128v2 + * 3 = Comp128v3 + * All Other = 3G auth with 2g keys from 3G Milenage */ +private function f_pyhss_algo(template (omit) charstring algo2g := omit, + template (omit) charstring algo3g := omit) +return integer { + if (not istemplatekind(algo2g, "omit")) { + if (valueof(algo2g) == "comp128v1") { + return 1; + } else if (valueof(algo2g) == "comp128v2") { + return 2; + } else if (valueof(algo2g) == "comp128v3") { + return 3; + } else { + setverdict(fail, "f_pyhss_algo: Unexpected algo2g: ", algo2g); + mtc.stop; + } + } + + if (not istemplatekind(algo3g, "omit")) { + if (valueof(algo3g) == "milenage") { + return 4; + } else { + setverdict(fail, "f_pyhss_algo: Unexpected algo3g: ", algo3g); + mtc.stop; + } + } + + setverdict(fail, "f_pyhss_algo: algo2g and algo3g are unset"); + mtc.stop; + + return -1; +} + +/* Create a subscriber */ +function f_pyhss_rest_subscr_create(hexstring imsi, + hexstring msisdn, + /* 2g */ + template (omit) charstring algo2g := omit, + template (omit) OCT16 ki := omit, + /* 3g */ + template (omit) charstring algo3g := omit, + template (omit) OCT16 opc := omit) +runs on PyHSS_REST_CT { + var integer algo := f_pyhss_algo(algo2g, algo3g); + + /* ki and opc cannot be NULL in the PyHSS DB */ + if (istemplatekind(ki, "omit")) { + ki := '11111111111111111111111111111111'O; + } + if (istemplatekind(opc, "omit")) { + opc := '22222222222222222222222222222222'O; + } + + f_pyhss_api_helper( + "add_subscr" + & " --imsi " & hex2str(imsi) + & " --msisdn " & hex2str(msisdn) + & " --auc-id " & int2str(g_next_auc_id) + & " --algo " & int2str(algo) + & " --ki " & oct2str(valueof(ki)) + & " --opc " & oct2str(valueof(opc)) + ); + + g_next_auc_id := g_next_auc_id + 1; +} + +/* Delete a subscriber */ +function f_pyhss_rest_subscr_delete(hexstring imsi) +runs on PyHSS_REST_CT{ + f_pyhss_api_helper("del_subscr --imsi " & hex2str(imsi)); +} + +}