laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/41873?usp=email )
Change subject: esim/http_json_api: add missing check
......................................................................
esim/http_json_api: add missing check
The line actual_sec = func_ex_status.get('statusCodeData', None) suggests
that 'statusCodeData' may be None under normal circumstances. So let's guard
sec.update(actual_sec) so that we won't run into an exception in case
'statusCodeData' is not in func_ex_status.
Related: SYS#7825
Change-Id: I8a1a3cd5e029dba4a3aec1a64702e19b0d694ae2
---
M pySim/esim/http_json_api.py
1 file changed, 2 insertions(+), 1 deletion(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
diff --git a/pySim/esim/http_json_api.py b/pySim/esim/http_json_api.py
index 8324bb9..bb41df3 100644
--- a/pySim/esim/http_json_api.py
+++ b/pySim/esim/http_json_api.py
@@ -149,7 +149,8 @@
'message': None,
}
actual_sec = func_ex_status.get('statusCodeData', None)
- sec.update(actual_sec)
+ if actual_sec:
+ sec.update(actual_sec)
self.subject_code = sec['subjectCode']
self.reason_code = sec['reasonCode']
self.subject_id = sec['subjectIdentifier']
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41873?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I8a1a3cd5e029dba4a3aec1a64702e19b0d694ae2
Gerrit-Change-Number: 41873
Gerrit-PatchSet: 2
Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/41875?usp=email )
Change subject: contrib: add utility to receive ES2+handleDownloadProgressInfo calls
......................................................................
contrib: add utility to receive ES2+handleDownloadProgressInfo calls
We already have a tool to work with the ES2+ API provided by an SMDP+
(es2p_client.py) With this tool we can only make API calls towards
an SMDP+. However, SGP.22 also defines a "reverse direction" ES2+
interface through wich the SMDP+ may make API calls towards the MNO.
At the moment the only possible MNO originated API call is
ES2+handleDownloadProgressInfo. Let's add a simple tool that runs a
HTTP server to receive and log the ES2+handleDownloadProgressInfo
requests.
Related: SYS#7825
Change-Id: I95af30cebae31f7dc682617b1866f4a2dc9b760c
---
A contrib/es2p_server.py
1 file changed, 99 insertions(+), 0 deletions(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/contrib/es2p_server.py b/contrib/es2p_server.py
new file mode 100755
index 0000000..6d71689
--- /dev/null
+++ b/contrib/es2p_server.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+
+# (C) 2026 by sysmocom - s.f.m.c. GmbH
+# All Rights Reserved
+#
+# Author: Philipp Maier
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import argparse
+import logging
+import json
+import asn1tools
+import asn1tools.codecs.ber
+import asn1tools.codecs.der
+import pySim.esim.rsp as rsp
+import pySim.esim.saip as saip
+from pySim.esim.es2p import param, Es2pApiServerMno, Es2pApiServerHandlerMno
+from osmocom.utils import b2h
+from datetime import datetime
+from analyze_simaResponse import split_sima_response
+
+logger = logging.getLogger(__name__)
+
+parser = argparse.ArgumentParser(description="""
+Utility to receive and log requests against the ES2+ API of an SM-DP+ according to GSMA SGP.22.""")
+parser.add_argument("--host", help="Host/IP to bind HTTP(S) to", default="localhost")
+parser.add_argument("--port", help="TCP port to bind HTTP(S) to", default=443, type=int)
+parser.add_argument('--server-cert', help='X.509 server certificate used to provide the ES2+ HTTPs service')
+parser.add_argument('--client-ca-cert', help='X.509 CA certificates to authenticate the requesting client(s)')
+parser.add_argument("-v", "--verbose", help="enable debug output", action='store_true', default=False)
+
+def decode_sima_response(sima_response):
+ decoded = []
+ euicc_response_list = split_sima_response(sima_response)
+ for euicc_response in euicc_response_list:
+ decoded.append(saip.asn1.decode('EUICCResponse', euicc_response))
+ return decoded
+
+def decode_result_data(result_data):
+ return rsp.asn1.decode('PendingNotification', result_data)
+
+def decode(data, path="/"):
+ if data is None:
+ return 'none'
+ elif type(data) is datetime:
+ return data.isoformat()
+ elif type(data) is tuple:
+ return {str(data[0]) : decode(data[1], path + str(data[0]) + "/")}
+ elif type(data) is list:
+ new_data = []
+ for item in data:
+ new_data.append(decode(item, path))
+ return new_data
+ elif type(data) is bytes:
+ return b2h(data)
+ elif type(data) is dict:
+ new_data = {}
+ for key, item in data.items():
+ new_key = str(key)
+ if path == '/' and new_key == 'resultData':
+ new_item = decode_result_data(item)
+ elif (path == '/resultData/profileInstallationResult/profileInstallationResultData/finalResult/successResult/' \
+ or path == '/resultData/profileInstallationResult/profileInstallationResultData/finalResult/errorResult/') \
+ and new_key == 'simaResponse':
+ new_item = decode_sima_response(item)
+ else:
+ new_item = item
+ new_data[new_key] = decode(new_item, path + new_key + "/")
+ return new_data
+ else:
+ return data
+
+class Es2pApiServerHandlerForLogging(Es2pApiServerHandlerMno):
+ def call_handleDownloadProgressInfo(self, data: dict) -> (dict, str):
+ logging.info("ES2+:handleDownloadProgressInfo: %s" % json.dumps(decode(data)))
+ return {}, None
+
+if __name__ == "__main__":
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING,
+ format='%(asctime)s %(levelname)s %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S')
+
+ Es2pApiServerMno(args.port, args.host, Es2pApiServerHandlerForLogging(), args.server_cert, args.client_ca_cert)
+
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41875?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I95af30cebae31f7dc682617b1866f4a2dc9b760c
Gerrit-Change-Number: 41875
Gerrit-PatchSet: 3
Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/41874?usp=email )
Change subject: esim/http_json_api: extend JSON API with server functionality
......................................................................
esim/http_json_api: extend JSON API with server functionality
At the moment http_json_api only supports the client role. Let's also add
support for the server role.
This patch refactors the existing client code. This in particular means
that the following preperations have to be made:
- To use the existing JsonHttpApiFunction definitions in the client and
server the scheme has to be symetric. It already is for the most part,
but it treads the header field differently. So let's just treat the
header field like any other mandatory field and add it input_params.
(this does not affect the es9p.py code since in ES9+ the requests have
no header messages, see also SGP.22, section 6.5.1.1)
- The JsonHttpApiFunction class currently also has the code to perform
the client requests. Let's seperate that code in a JsonHttpApiClient
class to which we pass an JsonHttpApiFunction object.
- The code that does the encoding and decoding in the client role has
lots of conditions the treat the header differently. Let's do the
decisions about the header in the JsonHttpApiClient. The encoder
and decoder function should do the generic encoding and decoding
only. (however, some generic header specific conditions will remain).
The code for the server role logically mirrors the code for the client
role. We add a JsonHttpApiServer class that can be used to create
API endpoints. The API user has to pass in a call_handler through which
the application logic is defined. Above that we add an Es2pApiServer
class in es2p. In this class we implement the logic that runs the
HTTP server and receives the requests. The Es2pApiServer supports all
ES2+ functions defined by GSMA SGP.22. The user may use the provided
Es2pApiServerHandler base class to define the application logic for each
ES2+ function.
Related: SYS#7825
Change-Id: I277aa90fddb5171c4bf6c3436259aa371d30d092
---
M pySim/esim/es2p.py
M pySim/esim/es9p.py
M pySim/esim/http_json_api.py
3 files changed, 321 insertions(+), 49 deletions(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/pySim/esim/es2p.py b/pySim/esim/es2p.py
index b917b47..36a3a0c 100644
--- a/pySim/esim/es2p.py
+++ b/pySim/esim/es2p.py
@@ -16,6 +16,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
+from klein import Klein
+from twisted.internet import defer, protocol, ssl, task, endpoints, reactor
+from twisted.internet.posixbase import PosixReactorBase
+from pathlib import Path
+from twisted.web.server import Site, Request
+
import logging
from datetime import datetime
import time
@@ -123,10 +129,12 @@
class DownloadOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/downloadOrder'
input_params = {
+ 'header': JsonRequestHeader,
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType
}
+ input_mandatory = ['header']
output_params = {
'header': JsonResponseHeader,
'iccid': param.Iccid,
@@ -137,6 +145,7 @@
class ConfirmOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/confirmOrder'
input_params = {
+ 'header': JsonRequestHeader,
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
@@ -144,7 +153,7 @@
'smdsAddress': param.SmdsAddress,
'releaseFlag': param.ReleaseFlag,
}
- input_mandatory = ['iccid', 'releaseFlag']
+ input_mandatory = ['header', 'iccid', 'releaseFlag']
output_params = {
'header': JsonResponseHeader,
'eid': param.Eid,
@@ -157,12 +166,13 @@
class CancelOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/cancelOrder'
input_params = {
+ 'header': JsonRequestHeader,
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
}
- input_mandatory = ['finalProfileStatusIndicator', 'iccid']
+ input_mandatory = ['header', 'finalProfileStatusIndicator', 'iccid']
output_params = {
'header': JsonResponseHeader,
}
@@ -172,9 +182,10 @@
class ReleaseProfile(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/releaseProfile'
input_params = {
+ 'header': JsonRequestHeader,
'iccid': param.Iccid,
}
- input_mandatory = ['iccid']
+ input_mandatory = ['header', 'iccid']
output_params = {
'header': JsonResponseHeader,
}
@@ -184,6 +195,7 @@
class HandleDownloadProgressInfo(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
input_params = {
+ 'header': JsonRequestHeader,
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType,
@@ -192,10 +204,9 @@
'notificationPointStatus': param.NotificationPointStatus,
'resultData': param.ResultData,
}
- input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
+ input_mandatory = ['header', 'iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
expected_http_status = 204
-
class Es2pApiClient:
"""Main class representing a full ES2+ API client. Has one method for each API function."""
def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
@@ -206,18 +217,17 @@
if client_cert:
self.session.cert = client_cert
- self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
- self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
- self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
- self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
- self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
+ self.downloadOrder = JsonHttpApiClient(DownloadOrder(), url_prefix, func_req_id, self.session)
+ self.confirmOrder = JsonHttpApiClient(ConfirmOrder(), url_prefix, func_req_id, self.session)
+ self.cancelOrder = JsonHttpApiClient(CancelOrder(), url_prefix, func_req_id, self.session)
+ self.releaseProfile = JsonHttpApiClient(ReleaseProfile(), url_prefix, func_req_id, self.session)
+ self.handleDownloadProgressInfo = JsonHttpApiClient(HandleDownloadProgressInfo(), url_prefix, func_req_id, self.session)
def _gen_func_id(self) -> str:
"""Generate the next function call id."""
self.func_id += 1
return 'FCI-%u-%u' % (time.time(), self.func_id)
-
def call_downloadOrder(self, data: dict) -> dict:
"""Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
return self.downloadOrder.call(data, self._gen_func_id())
@@ -237,3 +247,116 @@
def call_handleDownloadProgressInfo(self, data: dict) -> dict:
"""Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
return self.handleDownloadProgressInfo.call(data, self._gen_func_id())
+
+class Es2pApiServerHandlerSmdpp(abc.ABC):
+ """ES2+ (SMDP+ side) API Server handler class. The API user is expected to override the contained methods."""
+
+ @abc.abstractmethod
+ def call_downloadOrder(self, data: dict) -> (dict, str):
+ """Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
+ pass
+
+ @abc.abstractmethod
+ def call_confirmOrder(self, data: dict) -> (dict, str):
+ """Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
+ pass
+
+ @abc.abstractmethod
+ def call_cancelOrder(self, data: dict) -> (dict, str):
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
+ pass
+
+ @abc.abstractmethod
+ def call_releaseProfile(self, data: dict) -> (dict, str):
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
+ pass
+
+class Es2pApiServerHandlerMno(abc.ABC):
+ """ES2+ (MNO side) API Server handler class. The API user is expected to override the contained methods."""
+
+ @abc.abstractmethod
+ def call_handleDownloadProgressInfo(self, data: dict) -> (dict, str):
+ """Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
+ pass
+
+class Es2pApiServer(abc.ABC):
+ """Main class representing a full ES2+ API server. Has one method for each API function."""
+ app = None
+
+ def __init__(self, port: int, interface: str, server_cert: str = None, client_cert_verify: str = None):
+ logger.debug("HTTP SRV: starting ES2+ API server on %s:%s" % (interface, port))
+ self.port = port
+ self.interface = interface
+ if server_cert:
+ self.server_cert = ssl.PrivateCertificate.loadPEM(Path(server_cert).read_text())
+ else:
+ self.server_cert = None
+ if client_cert_verify:
+ self.client_cert_verify = ssl.Certificate.loadPEM(Path(client_cert_verify).read_text())
+ else:
+ self.client_cert_verify = None
+
+ def reactor(self, reactor: PosixReactorBase):
+ logger.debug("HTTP SRV: listen on %s:%s" % (self.interface, self.port))
+ if self.server_cert:
+ if self.client_cert_verify:
+ reactor.listenSSL(self.port, Site(self.app.resource()), self.server_cert.options(self.client_cert_verify),
+ interface=self.interface)
+ else:
+ reactor.listenSSL(self.port, Site(self.app.resource()), self.server_cert.options(),
+ interface=self.interface)
+ else:
+ reactor.listenTCP(self.port, Site(self.app.resource()), interface=self.interface)
+ return defer.Deferred()
+
+class Es2pApiServerSmdpp(Es2pApiServer):
+ """ES2+ (SMDP+ side) API Server."""
+ app = Klein()
+
+ def __init__(self, port: int, interface: str, handler: Es2pApiServerHandlerSmdpp,
+ server_cert: str = None, client_cert_verify: str = None):
+ super().__init__(port, interface, server_cert, client_cert_verify)
+ self.handler = handler
+ self.downloadOrder = JsonHttpApiServer(DownloadOrder(), handler.call_downloadOrder)
+ self.confirmOrder = JsonHttpApiServer(ConfirmOrder(), handler.call_confirmOrder)
+ self.cancelOrder = JsonHttpApiServer(CancelOrder(), handler.call_cancelOrder)
+ self.releaseProfile = JsonHttpApiServer(ReleaseProfile(), handler.call_releaseProfile)
+ task.react(self.reactor)
+
+ @app.route(DownloadOrder.path)
+ def call_downloadOrder(self, request: Request) -> dict:
+ """Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
+ return self.downloadOrder.call(request)
+
+ @app.route(ConfirmOrder.path)
+ def call_confirmOrder(self, request: Request) -> dict:
+ """Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
+ return self.confirmOrder.call(request)
+
+ @app.route(CancelOrder.path)
+ def call_cancelOrder(self, request: Request) -> dict:
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
+ return self.cancelOrder.call(request)
+
+ @app.route(ReleaseProfile.path)
+ def call_releaseProfile(self, request: Request) -> dict:
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
+ return self.releaseProfile.call(request)
+
+class Es2pApiServerMno(Es2pApiServer):
+ """ES2+ (MNO side) API Server."""
+
+ app = Klein()
+
+ def __init__(self, port: int, interface: str, handler: Es2pApiServerHandlerMno,
+ server_cert: str = None, client_cert_verify: str = None):
+ super().__init__(port, interface, server_cert, client_cert_verify)
+ self.handler = handler
+ self.handleDownloadProgressInfo = JsonHttpApiServer(HandleDownloadProgressInfo(),
+ handler.call_handleDownloadProgressInfo)
+ task.react(self.reactor)
+
+ @app.route(HandleDownloadProgressInfo.path)
+ def call_handleDownloadProgressInfo(self, request: Request) -> dict:
+ """Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
+ return self.handleDownloadProgressInfo.call(request)
diff --git a/pySim/esim/es9p.py b/pySim/esim/es9p.py
index 0d22219..7e0c416 100644
--- a/pySim/esim/es9p.py
+++ b/pySim/esim/es9p.py
@@ -155,11 +155,11 @@
if server_cert_verify:
self.session.verify = server_cert_verify
- self.initiateAuthentication = InitiateAuthentication(url_prefix, '', self.session)
- self.authenticateClient = AuthenticateClient(url_prefix, '', self.session)
- self.getBoundProfilePackage = GetBoundProfilePackage(url_prefix, '', self.session)
- self.handleNotification = HandleNotification(url_prefix, '', self.session)
- self.cancelSession = CancelSession(url_prefix, '', self.session)
+ self.initiateAuthentication = JsonHttpApiClient(InitiateAuthentication(), url_prefix, '', self.session)
+ self.authenticateClient = JsonHttpApiClient(AuthenticateClient(), url_prefix, '', self.session)
+ self.getBoundProfilePackage = JsonHttpApiClient(GetBoundProfilePackage(), url_prefix, '', self.session)
+ self.handleNotification = JsonHttpApiClient(HandleNotification(), url_prefix, '', self.session)
+ self.cancelSession = JsonHttpApiClient(CancelSession(), url_prefix, '', self.session)
def call_initiateAuthentication(self, data: dict) -> dict:
return self.initiateAuthentication.call(data)
diff --git a/pySim/esim/http_json_api.py b/pySim/esim/http_json_api.py
index bb41df3..e640b3f 100644
--- a/pySim/esim/http_json_api.py
+++ b/pySim/esim/http_json_api.py
@@ -21,6 +21,8 @@
import json
from typing import Optional
import base64
+from twisted.web.server import Request
+
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
@@ -131,6 +133,16 @@
if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
raise ValueError('Unknown/unspecified status "%s"' % status)
+class JsonRequestHeader(ApiParam):
+ """SGP.22 section 6.5.1.3."""
+ @classmethod
+ def verify_decoded(cls, data):
+ func_req_id = data.get('functionRequesterIdentifier')
+ if not func_req_id:
+ raise ValueError('Missing mandatory functionRequesterIdentifier in header')
+ func_call_id = data.get('functionCallIdentifier')
+ if not func_call_id:
+ raise ValueError('Missing mandatory functionCallIdentifier in header')
class HttpStatusError(Exception):
pass
@@ -161,36 +173,46 @@
class JsonHttpApiFunction(abc.ABC):
"""Base class for representing an HTTP[s] API Function."""
- # the below class variables are expected to be overridden in derived classes
+ # The below class variables are used to describe the properties of the API function. Derived classes are expected
+ # to orverride those class properties with useful values. The prefixes "input_" and "output_" refer to the API
+ # function from an abstract point of view. Seen from the client perspective, "input_" will refer to parameters the
+ # client sends to a HTTP server. Seen from the server perspective, "input_" will refer to parameters the server
+ # receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
+ # prefix.
+ # path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder')
path = None
+
# dictionary of input parameters. key is parameter name, value is ApiParam class
input_params = {}
+
# list of mandatory input parameters
input_mandatory = []
+
# dictionary of output parameters. key is parameter name, value is ApiParam class
output_params = {}
+
# list of mandatory output parameters (for successful response)
output_mandatory = []
+
+ # list of mandatory output parameters (for failed response)
+ output_mandatory_failed = []
+
# expected HTTP status code of the response
expected_http_status = 200
+
# the HTTP method used (GET, OPTIONS, HEAD, POST, PUT, PATCH or DELETE)
http_method = 'POST'
+
+ # additional custom HTTP headers (client requests)
extra_http_req_headers = {}
- def __init__(self, url_prefix: str, func_req_id: Optional[str], session: requests.Session):
- self.url_prefix = url_prefix
- self.func_req_id = func_req_id
- self.session = session
+ # additional custom HTTP headers (server responses)
+ extra_http_res_headers = {}
- def encode(self, data: dict, func_call_id: Optional[str] = None) -> dict:
+ def encode_client(self, data: dict) -> dict:
"""Validate an encode input dict into JSON-serializable dict for request body."""
output = {}
- if func_call_id:
- output['header'] = {
- 'functionRequesterIdentifier': self.func_req_id,
- 'functionCallIdentifier': func_call_id
- }
for p in self.input_mandatory:
if not p in data:
@@ -204,22 +226,19 @@
output[p] = p_class.encode(v)
return output
- def decode(self, data: dict) -> dict:
+ def decode_client(self, data: dict) -> dict:
"""[further] Decode and validate the JSON-Dict of the response body."""
output = {}
- if 'header' in self.output_params:
- # let's first do the header, it's special
- if not 'header' in data:
- raise ValueError('Mandatory output parameter "header" missing')
- hdr_class = self.output_params.get('header')
- output['header'] = hdr_class.decode(data['header'])
+ output_mandatory = self.output_mandatory
- if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
- raise ApiError(output['header']['functionExecutionStatus'])
- # we can only expect mandatory parameters to be present in case of successful execution
- for p in self.output_mandatory:
- if p == 'header':
- continue
+ # In case a provided header (may be optional) indicates that the API function call was unsuccessful, a
+ # different set of mandatory parameters applies.
+ header = data.get('header')
+ if header:
+ if data['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
+ output_mandatory = self.output_mandatory_failed
+
+ for p in output_mandatory:
if not p in data:
raise ValueError('Mandatory output parameter "%s" missing' % p)
for p, v in data.items():
@@ -231,30 +250,160 @@
output[p] = p_class.decode(v)
return output
+ def encode_server(self, data: dict) -> dict:
+ """Validate an encode input dict into JSON-serializable dict for response body."""
+ output = {}
+ output_mandatory = self.output_mandatory
+
+ # In case a provided header (may be optional) indicates that the API function call was unsuccessful, a
+ # different set of mandatory parameters applies.
+ header = data.get('header')
+ if header:
+ if data['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
+ output_mandatory = self.output_mandatory_failed
+
+ for p in output_mandatory:
+ if not p in data:
+ raise ValueError('Mandatory output parameter %s missing' % p)
+ for p, v in data.items():
+ p_class = self.output_params.get(p)
+ if not p_class:
+ logger.warning('Unexpected/unsupported output parameter %s=%s', p, v)
+ output[p] = v
+ else:
+ output[p] = p_class.encode(v)
+ return output
+
+ def decode_server(self, data: dict) -> dict:
+ """[further] Decode and validate the JSON-Dict of the request body."""
+ output = {}
+
+ for p in self.input_mandatory:
+ if not p in data:
+ raise ValueError('Mandatory input parameter "%s" missing' % p)
+ for p, v in data.items():
+ p_class = self.input_params.get(p)
+ if not p_class:
+ logger.warning('Unexpected/unsupported input parameter "%s"="%s"', p, v)
+ output[p] = v
+ else:
+ output[p] = p_class.decode(v)
+ return output
+
+class JsonHttpApiClient():
+ def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
+ session: requests.Session):
+ self.api_func = api_func
+ self.url_prefix = url_prefix
+ self.func_req_id = func_req_id
+ self.session = session
+
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
- """Make an API call to the HTTP API endpoint represented by this object.
- Input data is passed in `data` as json-serializable dict. Output data
- is returned as json-deserialized dict."""
- url = self.url_prefix + self.path
- encoded = json.dumps(self.encode(data, func_call_id))
+ """Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
+ json-serializable dict. Output data is returned as json-deserialized dict."""
+
+ # In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
+ # and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
+ # field is checked by the encode_client method)
+ if func_call_id:
+ data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
+ 'functionCallIdentifier': func_call_id}} | data
+
+ # Encode the message (the presence of mandatory fields is checked during encoding)
+ encoded = json.dumps(self.api_func.encode_client(data))
+
+ # Apply HTTP request headers according to SGP.22, section 6.5.1
req_headers = {
'Content-Type': 'application/json',
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
}
- req_headers.update(self.extra_http_req_headers)
+ req_headers.update(self.api_func.extra_http_req_headers)
+ # Perform HTTP request
+ url = self.url_prefix + self.api_func.path
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
- response = self.session.request(self.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
+ response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
logger.debug("HTTP RSP: %s" % (response.content))
- if response.status_code != self.expected_http_status:
+ # Check HTTP response status code and make sure that the returned HTTP headers look plausible (according to
+ # SGP.22, section 6.5.1)
+ if response.status_code != self.api_func.expected_http_status:
raise HttpStatusError(response)
if not response.headers.get('Content-Type').startswith(req_headers['Content-Type']):
raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response)
+ # Decode response and return the result back to the caller
if response.content:
- return self.decode(response.json())
+ output = self.api_func.decode_client(response.json())
+ # In case the response contains a header, check it to make sure that the API call was executed successfully
+ # (the presence of the header field is checked by the decode_client method)
+ if 'header' in output:
+ if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
+ raise ApiError(output['header']['functionExecutionStatus'])
+ return output
return None
+
+class JsonHttpApiServer():
+ def __init__(self, api_func: JsonHttpApiFunction, call_handler = None):
+ """
+ Args:
+ api_func : API function definition (JsonHttpApiFunction)
+ call_handler : handler function to process the request. This function must accept the
+ decoded request as a dictionary. The handler function must return a tuple consisting
+ of the response in the form of a dictionary (may be empty), and a function execution
+ status string ('Executed-Success', 'Executed-WithWarning', 'Failed' or 'Expired')
+ """
+ self.api_func = api_func
+ if call_handler:
+ self.call_handler = call_handler
+ else:
+ self.call_handler = self.default_handler
+
+ def default_handler(self, data: dict) -> (dict, str):
+ """default handler, used in case no call handler is provided."""
+ logger.error("no handler function for request: %s" % str(data))
+ return {}, 'Failed'
+
+ def call(self, request: Request) -> str:
+ """ Process an incoming request.
+ Args:
+ request : request object as received using twisted.web.server
+ Returns:
+ encoded JSON string (HTTP response code and headers are set by calling the appropriate methods on the
+ provided the request object)
+ """
+
+ # Make sure the request is done with the correct HTTP method
+ if (request.method.decode() != self.api_func.http_method):
+ raise ValueError('Wrong HTTP method %s!=%s' % (request.method.decode(), self.api_func.http_method))
+
+ # Decode the request
+ decoded_request = self.api_func.decode_server(json.loads(request.content.read()))
+
+ # Run call handler (see above)
+ data, fe_status = self.call_handler(decoded_request)
+
+ # In case a function execution status is returned, use it to generate and prepend the header field according to
+ # SGP.22, section 6.5.1.2 and 6.5.1.4 (the presence of the header filed is checked by the encode_server method)
+ if fe_status:
+ data = {'header' : {'functionExecutionStatus': {'status' : fe_status}}} | data
+
+ # Encode the message (the presence of mandatory fields is checked during encoding)
+ encoded = json.dumps(self.api_func.encode_server(data))
+
+ # Apply HTTP request headers according to SGP.22, section 6.5.1
+ res_headers = {
+ 'Content-Type': 'application/json',
+ 'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
+ }
+ res_headers.update(self.api_func.extra_http_res_headers)
+ for header, value in res_headers.items():
+ request.setHeader(header, value)
+ request.setResponseCode(self.api_func.expected_http_status)
+
+ # Return the encoded result back to the caller for sending (using twisted/klein)
+ return encoded
+
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41874?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I277aa90fddb5171c4bf6c3436259aa371d30d092
Gerrit-Change-Number: 41874
Gerrit-PatchSet: 3
Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Attention is currently required from: dexter.
laforge has posted comments on this change by dexter. ( https://gerrit.osmocom.org/c/pysim/+/41874?usp=email )
Change subject: esim/http_json_api: extend JSON API with server functionality
......................................................................
Patch Set 3: Code-Review+2
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41874?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: comment
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I277aa90fddb5171c4bf6c3436259aa371d30d092
Gerrit-Change-Number: 41874
Gerrit-PatchSet: 3
Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Attention: dexter <pmaier(a)sysmocom.de>
Gerrit-Comment-Date: Fri, 23 Jan 2026 12:37:16 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
Attention is currently required from: fixeria.
laforge has posted comments on this change by fixeria. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41897?usp=email )
Change subject: bts: use existing API in as_l1_sacch_loop()
......................................................................
Patch Set 2: Code-Review+2
--
To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41897?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: comment
Gerrit-Project: osmo-ttcn3-hacks
Gerrit-Branch: master
Gerrit-Change-Id: I5ab2988152b948e457c3cfa4930df484c170d053
Gerrit-Change-Number: 41897
Gerrit-PatchSet: 2
Gerrit-Owner: fixeria <vyanitskiy(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: pespin <pespin(a)sysmocom.de>
Gerrit-Attention: fixeria <vyanitskiy(a)sysmocom.de>
Gerrit-Comment-Date: Fri, 23 Jan 2026 12:37:01 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
Attention is currently required from: pespin.
laforge has posted comments on this change by pespin. ( https://gerrit.osmocom.org/c/libosmocore/+/41894?usp=email )
Change subject: logging: Move log target file to its own file
......................................................................
Patch Set 3: Code-Review+1
--
To view, visit https://gerrit.osmocom.org/c/libosmocore/+/41894?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: comment
Gerrit-Project: libosmocore
Gerrit-Branch: master
Gerrit-Change-Id: I8e32e31c75b66ff0649d92c2f469f8895689fbad
Gerrit-Change-Number: 41894
Gerrit-PatchSet: 3
Gerrit-Owner: pespin <pespin(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Attention: pespin <pespin(a)sysmocom.de>
Gerrit-Comment-Date: Fri, 23 Jan 2026 12:34:32 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
laforge has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41926?usp=email )
Change subject: bts: limit stderr logging to NOTICE to avoid long write() to ext4 fs
......................................................................
bts: limit stderr logging to NOTICE to avoid long write() to ext4 fs
stderr being redirected to a file in the ext4 filesystem sometimes ends
up in write() syscall taking >500ms, which means the entire osmo-bts
process stalls during that time and we miss clock updates up to a
threshold which makes osmo-bts process exit with an error.
Let's decrease logging verbosity to NOTICE for now with the aim to not
run into those stalls during normal operation, while we can re-eanble
this manually when debugging the write() issue.
Related: OS#6794
Change-Id: I74c4abf7571d2a0f9ee22402a949dbde02896d7d
---
M bts/osmo-bts.cfg
1 file changed, 11 insertions(+), 10 deletions(-)
Approvals:
fixeria: Looks good to me, but someone else must approve
Jenkins Builder: Verified
laforge: Looks good to me, approved
osmith: Looks good to me, approved
diff --git a/bts/osmo-bts.cfg b/bts/osmo-bts.cfg
index c583d32..7ac38a1 100644
--- a/bts/osmo-bts.cfg
+++ b/bts/osmo-bts.cfg
@@ -17,16 +17,17 @@
logging print level 1
!
logging level set-all notice
- logging level rsl info
- logging level meas info
- logging level pag info
- logging level l1c info
- logging level dsp info
- logging level pcu info
- logging level trx info
- logging level osmux info
- logging level lmib info
- logging level lmux info
+ !Disabled due to stderr redirect to ext4 filesystem creating long writes of >500ms (OS#6794):
+ !logging level rsl info
+ !logging level meas info
+ !logging level pag info
+ !logging level l1c info
+ !logging level dsp info
+ !logging level pcu info
+ !logging level trx info
+ !logging level osmux info
+ !logging level lmib info
+ !logging level lmux info
!
line vty
no login
--
To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41926?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: osmo-ttcn3-hacks
Gerrit-Branch: master
Gerrit-Change-Id: I74c4abf7571d2a0f9ee22402a949dbde02896d7d
Gerrit-Change-Number: 41926
Gerrit-PatchSet: 1
Gerrit-Owner: pespin <pespin(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: fixeria <vyanitskiy(a)sysmocom.de>
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>