osmith submitted this change.
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(-)
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));
+}
+
+}
To view, visit change 41227. To unsubscribe, or for help writing mail filters, visit settings.