laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/40198?usp=email )
Change subject: personalization: implement reading back values from a PES
......................................................................
personalization: implement reading back values from a PES
Implement get_values_from_pes(), the reverse direction of apply_val():
read back and return values from a ProfileElementSequence. Implement for
all ConfigurableParameter subclasses.
Future: SdKey.get_values_from_pes() is reading pe.decoded[], which works
fine, but I07dfc378705eba1318e9e8652796cbde106c6a52 will change this
implementation to use the higher level ProfileElementSD members.
Implementation detail:
Implement get_values_from_pes() as classmethod that returns a generator.
Subclasses should yield all occurences of their parameter in a given
PES.
For example, the ICCID can appear in multiple places.
Iccid.get_values_from_pes() yields all of the individual values. A set()
of the results quickly tells whether the PES is consistent.
Rationales for reading back values:
This allows auditing an eSIM profile, particularly for producing an
output.csv from a batch personalization (that generated lots of random
key material which now needs to be fed to an HLR...).
Reading back from a binary result is more reliable than storing the
values that were fed into a personalization.
By auditing final DER results with this code, I discovered:
- "oh, there already was some key material in my UPP template."
- "all IMSIs ended up the same, forgot to set up the parameter."
- the SdKey.apply() implementations currently don't work, see
I07dfc378705eba1318e9e8652796cbde106c6a52 for a fix.
Change-Id: I234fc4317f0bdc1a486f0cee4fa432c1dce9b463
Jenkins: skip-card-test
---
M pySim/esim/saip/personalization.py
1 file changed, 179 insertions(+), 7 deletions(-)
Approvals:
dexter: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index fb91cb5..2b99632 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -17,13 +17,17 @@
import abc
import io
-from typing import List, Tuple
+from typing import List, Tuple, Generator, Optional
from osmocom.tlv import camel_to_snake
-from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
+from osmocom.utils import hexstr
+from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid
from pySim.esim.saip import ProfileElement, ProfileElementSequence
from pySim.ts_51_011 import EF_SMSP
+def unrpad(s: hexstr, c='f') -> hexstr:
+ return hexstr(s.rstrip(c))
+
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
return list(filter(lambda x: x[0] not in unwanted_keys, l))
@@ -200,6 +204,29 @@
pass
@classmethod
+ @abc.abstractmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence) -> Generator:
+ """This is what subclasses implement: yield all values from a decoded profile package.
+ Find all values in the pes, and yield them decoded to a valid cls.input_value format.
+ Should be a generator function, i.e. use 'yield' instead of 'return'.
+
+ Yielded value must be a dict(). Usually, an implementation will return only one key, like
+
+ { "ICCID": "1234567890123456789" }
+
+ Some implementations have more than one value to return, like
+
+ { "IMSI": "00101012345678", "IMSI-ACC" : "5" }
+
+ Implementation example:
+
+ for pe in pes:
+ if my_condition(pe):
+ yield { cls.name: b2h(my_bin_value_from(pe)) }
+ """
+ pass
+
+ @classmethod
def get_len_range(cls):
"""considering all of min_len, max_len and allow_len, get a tuple of the resulting (min, max) of permitted
value length. For example, if an input value is an int, which needs to be represented with a minimum nr of
@@ -249,6 +276,7 @@
@classmethod
def validate_val(cls, val):
val = super().validate_val(val)
+ assert isinstance(val, str)
val = ''.join('%02x' % ord(x) for x in val)
if cls.rpad is not None:
c = cls.rpad_char
@@ -256,6 +284,17 @@
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
return h2b(val)
+ @classmethod
+ def decimal_hex_to_str(cls, val):
+ """useful for get_values_from_pes() implementations of subclasses"""
+ if isinstance(val, bytes):
+ val = b2h(val)
+ assert isinstance(val, hexstr)
+ if cls.rpad is not None:
+ c = cls.rpad_char or 'f'
+ val = unrpad(val, c)
+ return val.to_bytes().decode('ascii')
+
class IntegerParam(ConfigurableParameter):
allow_types = (str, int)
allow_chars = '0123456789'
@@ -279,6 +318,14 @@
raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]')
return val
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for valdict in super().get_values_from_pes(pes):
+ for key, val in valdict.items():
+ if isinstance(val, int):
+ valdict[key] = str(val)
+ yield valdict
+
class BinaryParam(ConfigurableParameter):
allow_types = (str, io.BytesIO, bytes, bytearray)
allow_chars = '0123456789abcdefABCDEF'
@@ -322,6 +369,17 @@
# patch MF/EF.ICCID
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(val)))
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ padded = b2h(pes.get_pe_for_type('header').decoded['iccid'])
+ iccid = unrpad(padded)
+ yield { cls.name: iccid }
+
+ for pe in pes.get_pes_for_type('mf'):
+ iccid_f = pe.files.get('ef-iccid', None)
+ if iccid_f is not None:
+ yield { cls.name: dec_iccid(b2h(iccid_f.body)) }
+
class Imsi(DecimalParam):
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
the last digit of the IMSI."""
@@ -342,6 +400,18 @@
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
# TODO: DF.GSM_ACCESS if not linked?
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('usim'):
+ imsi_f = pe.files.get('ef-imsi', None)
+ acc_f = pe.files.get('ef-acc', None)
+ y = {}
+ if imsi_f:
+ y[cls.name] = dec_imsi(b2h(imsi_f.body))
+ if acc_f:
+ y[cls.name + '-ACC'] = b2h(acc_f.body)
+ yield y
+
class SmspTpScAddr(ConfigurableParameter):
"""Configurable SMSC (SMS Service Centre) TP-SC-ADDR. Expects to be a phone number in national or
international format (designated by a leading +). Automatically sets the NPI to E.164 and the TON based on
@@ -354,21 +424,39 @@
min_len = 1
example_input = '+49301234567'
- @classmethod
- def validate_val(cls, val):
- val = super().validate_val(val)
- addr_str = str(val)
+ @staticmethod
+ def str_to_tuple(addr_str):
if addr_str[0] == '+':
digits = addr_str[1:]
international = True
else:
digits = addr_str
international = False
+ return (international, digits)
+
+ @staticmethod
+ def tuple_to_str(addr_tuple):
+ international, digits = addr_tuple
+ if international:
+ ret = '+'
+ else:
+ ret = ''
+ ret += digits
+ return ret
+
+ @classmethod
+ def validate_val(cls, val):
+ val = super().validate_val(val)
+
+ addr_tuple = cls.str_to_tuple(str(val))
+
+ international, digits = addr_tuple
if len(digits) > 20:
raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}')
if not digits.isdecimal():
raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}')
- return (international, digits)
+
+ return addr_tuple
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
@@ -398,6 +486,32 @@
# re-generate the pe.decoded member from the File instance
pe.file2pe(f_smsp)
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('usim'):
+ f_smsp = pe.files['ef-smsp']
+ ef_smsp = EF_SMSP()
+ ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
+
+ tp_sc_addr = ef_smsp_dec.get('tp_sc_addr', None)
+ if not tp_sc_addr:
+ continue
+
+ digits = tp_sc_addr.get('call_number', None)
+ if not digits:
+ continue
+
+ ton_npi = tp_sc_addr.get('ton_npi', None)
+ if not ton_npi:
+ continue
+ international = ton_npi.get('type_of_number', None)
+ if international is None:
+ continue
+ international = (international == 'international')
+
+ yield { cls.name: cls.tuple_to_str((international, digits)) }
+
+
class SdKey(BinaryParam, metaclass=ClassVarMeta):
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
# these will be set by subclasses
@@ -430,6 +544,14 @@
for pe in pes.get_pes_for_type('securityDomain'):
cls._apply_sd(pe, value)
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('securityDomain'):
+ for key in pe.decoded['keyList']:
+ if key['keyIdentifier'][0] == cls.key_id and key['keyVersionNumber'][0] == cls.kvn:
+ if len(key['keyComponents']) >= 1:
+ yield { cls.name: b2h(key['keyComponents'][0]['keyData']) }
+
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
@@ -516,6 +638,14 @@
raise ValueError("input template UPP has unexpected structure:"
f" cannot find pukCode with keyReference={cls.keyReference}")
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ mf_pes = pes.pes_by_naa['mf'][0]
+ for pukCodes in obtain_all_pe_from_pelist(mf_pes, 'pukCodes'):
+ for pukCode in pukCodes.decoded['pukCodes']:
+ if pukCode['keyReference'] == cls.keyReference:
+ yield { cls.name: cls.decimal_hex_to_str(pukCode['pukValue']) }
+
class Puk1(Puk):
name = 'PUK1'
keyReference = 0x01
@@ -551,6 +681,21 @@
raise ValueError('input template UPP has unexpected structure:'
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference}')
+ @classmethod
+ def _read_all_pinvalues_from_pe(cls, pe: ProfileElement):
+ "This is a separate function because subclasses may feed different pe arguments."
+ for pinCodes in obtain_all_pe_from_pelist(pe, 'pinCodes'):
+ if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
+ continue
+
+ for pinCode in pinCodes.decoded['pinCodes'][1]:
+ if pinCode['keyReference'] == cls.keyReference:
+ yield { cls.name: cls.decimal_hex_to_str(pinCode['pinValue']) }
+
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ yield from cls._read_all_pinvalues_from_pe(pes.pes_by_naa['mf'][0])
+
class Pin1(Pin):
name = 'PIN1'
example_input = '0' * 4 # PIN are usually 4 digits
@@ -572,6 +717,14 @@
raise ValueError('input template UPP has unexpected structure:'
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference} in {naa=}')
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for naa in pes.pes_by_naa:
+ if naa not in ['usim','isim','csim','telecom']:
+ continue
+ for pe in pes.pes_by_naa[naa]:
+ yield from cls._read_all_pinvalues_from_pe(pe)
+
class Adm1(Pin):
name = 'ADM1'
keyReference = 0x0A
@@ -596,6 +749,25 @@
raise ValueError('input template UPP has unexpected structure:'
f' {cls.__name__} cannot find algoParameter with key={cls.algo_config_key}')
+ @classmethod
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('akaParameter'):
+ algoConfiguration = pe.decoded['algoConfiguration']
+ if len(algoConfiguration) < 2:
+ continue
+ if algoConfiguration[0] != 'algoParameter':
+ continue
+ if not algoConfiguration[1]:
+ continue
+ val = algoConfiguration[1].get(cls.algo_config_key, None)
+ if val is None:
+ continue
+ if isinstance(val, bytes):
+ val = b2h(val)
+ # if it is an int (algorithmID), just pass thru as int
+ yield { cls.name: val }
+
+
class AlgorithmID(DecimalParam, AlgoConfig):
algo_config_key = 'algorithmID'
allow_len = 1
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40198?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: I234fc4317f0bdc1a486f0cee4fa432c1dce9b463
Gerrit-Change-Number: 40198
Gerrit-PatchSet: 16
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr(a)sysmocom.de>
Gerrit-CC: fixeria <vyanitskiy(a)sysmocom.de>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/41845?usp=email )
Change subject: personalization: add param_source.py, add batch.py
......................................................................
personalization: add param_source.py, add batch.py
Implement pySim.esim.saip.batch.BatchPersonalization,
generating N eSIM profiles from a preset configuration.
Batch parameters can be fed by a constant, incrementing, random or from
CSV rows: add pySim.esim.saip.param_source.* classes to feed such input
to each of the BatchPersonalization's ConfigurableParameter instances.
Related: SYS#6768
Change-Id: I01ae40a06605eb205bfb409189fcd2b3a128855a
Jenkins: skip-card-test
---
A pySim/esim/saip/batch.py
A pySim/esim/saip/param_source.py
2 files changed, 323 insertions(+), 0 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
diff --git a/pySim/esim/saip/batch.py b/pySim/esim/saip/batch.py
new file mode 100644
index 0000000..92e8b1e
--- /dev/null
+++ b/pySim/esim/saip/batch.py
@@ -0,0 +1,120 @@
+"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile:
+ Run a batch of N personalizations"""
+
+# (C) 2025-2026 by sysmocom - s.f.m.c. GmbH <info(a)sysmocom.de>
+#
+# Author: nhofmeyr(a)sysmocom.de
+#
+# 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 copy
+from typing import Generator
+from pySim.esim.saip.personalization import ConfigurableParameter
+from pySim.esim.saip import param_source
+from pySim.esim.saip import ProfileElementSequence
+
+class BatchPersonalization:
+ """Produce a series of eSIM profiles from predefined parameters.
+ Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource.
+
+ Usage example:
+
+ der_input = open('some_file', 'rb').read()
+ pes = ProfileElementSequence.from_der(der_input)
+ p = BatchPersonalization(
+ n=10,
+ src_pes=pes,
+ csv_rows=get_csv_reader())
+
+ p.add_param_and_src(
+ personalization.Iccid(),
+ param_source.IncDigitSource(
+ num_digits=18,
+ first_value=123456789012340001,
+ last_value=123456789012340010))
+
+ # add more parameters here, using ConfigurableParameter and ParamSource subclass instances to define the profile
+ # ...
+
+ # generate all 10 profiles (from n=10 above)
+ for result_pes in p.generate_profiles():
+ upp = result_pes.to_der()
+ store_upp(upp)
+ """
+
+ class ParamAndSrc:
+ """tie a ConfigurableParameter to a source of actual values"""
+ def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
+ if isinstance(param, type):
+ self.param_cls = param
+ else:
+ self.param_cls = param.__class__
+ self.src = src
+
+ def __init__(self,
+ n: int,
+ src_pes: ProfileElementSequence,
+ params: list[ParamAndSrc]=None,
+ csv_rows: Generator=None,
+ ):
+ """
+ n: number of eSIM profiles to generate.
+ src_pes: a decoded eSIM profile as ProfileElementSequence, to serve as template. This is not modified, only
+ copied.
+ params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
+ profile values.
+ csv_rows: A generator (e.g. iter(list_of_rows)) producing all CSV rows one at a time, starting with a row
+ containing the column headers. This is compatible with the python csv.reader. Each row gets passed to
+ ParamSource.get_next(), such that ParamSource implementations can access the row items. See
+ param_source.CsvSource.
+ """
+ self.n = n
+ self.params = params or []
+ self.src_pes = src_pes
+ self.csv_rows = csv_rows
+
+ def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
+ self.params.append(BatchPersonalization.ParamAndSrc(param, src))
+
+ def generate_profiles(self):
+ # get first row of CSV: column names
+ csv_columns = None
+ if self.csv_rows:
+ try:
+ csv_columns = next(self.csv_rows)
+ except StopIteration as e:
+ raise ValueError('the input CSV file appears to be empty') from e
+
+ for i in range(self.n):
+ csv_row = None
+ if self.csv_rows and csv_columns:
+ try:
+ csv_row_list = next(self.csv_rows)
+ except StopIteration as e:
+ raise ValueError(f'not enough rows in the input CSV for eSIM nr {i+1} of {self.n}') from e
+
+ csv_row = dict(zip(csv_columns, csv_row_list))
+
+ pes = copy.deepcopy(self.src_pes)
+
+ for p in self.params:
+ try:
+ input_value = p.src.get_next(csv_row=csv_row)
+ assert input_value is not None
+ value = p.param_cls.validate_val(input_value)
+ p.param_cls.apply_val(pes, value)
+ except Exception as e:
+ raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
+
+ yield pes
diff --git a/pySim/esim/saip/param_source.py b/pySim/esim/saip/param_source.py
new file mode 100644
index 0000000..2083f98
--- /dev/null
+++ b/pySim/esim/saip/param_source.py
@@ -0,0 +1,203 @@
+# Implementation of SimAlliance/TCA Interoperable Profile handling: parameter sources for batch personalization.
+#
+# (C) 2025 by sysmocom - s.f.m.c. GmbH <info(a)sysmocom.de>
+#
+# Author: nhofmeyr(a)sysmocom.de
+#
+# 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 random
+import re
+from osmocom.utils import b2h
+
+class ParamSourceExn(Exception):
+ pass
+
+class ParamSourceExhaustedExn(ParamSourceExn):
+ pass
+
+class ParamSourceUndefinedExn(ParamSourceExn):
+ pass
+
+class ParamSource:
+ """abstract parameter source. For usage, see personalization.BatchPersonalization."""
+
+ # This name should be short but descriptive, useful for a user interface, like 'random decimal digits'.
+ name = "none"
+ numeric_base = None # or 10 or 16
+
+ def __init__(self, input_str:str):
+ """Subclasses should call super().__init__(input_str) before evaluating self.input_str. Each subclass __init__()
+ may in turn manipulate self.input_str to apply expansions or decodings."""
+ self.input_str = input_str
+
+ def get_next(self, csv_row:dict=None):
+ """Subclasses implement this: return the next value from the parameter source.
+ When there are no more values from the source, raise a ParamSourceExhaustedExn.
+ This default implementation is an empty source."""
+ raise ParamSourceExhaustedExn()
+
+ @classmethod
+ def from_str(cls, input_str:str):
+ """compatibility with earlier version of ParamSource. Just use the constructor."""
+ return cls(input_str)
+
+class ConstantSource(ParamSource):
+ """one value for all"""
+ name = "constant"
+
+ def get_next(self, csv_row:dict=None):
+ return self.input_str
+
+class InputExpandingParamSource(ParamSource):
+
+ def __init__(self, input_str:str):
+ super().__init__(input_str)
+ self.input_str = self.expand_input_str(self.input_str)
+
+ @classmethod
+ def expand_input_str(cls, input_str:str):
+ # user convenience syntax '0*32' becomes '00000000000000000000000000000000'
+ if "*" not in input_str:
+ return input_str
+ # re: "XX * 123" with optional spaces
+ tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", input_str)
+ if len(tokens) < 3:
+ return input_str
+ parts = []
+ for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
+ parts.append(unchanged)
+ repeat = int(repeat_str)
+ parts.append(snippet * repeat)
+
+ return "".join(parts)
+
+class DecimalRangeSource(InputExpandingParamSource):
+ """abstract: decimal numbers with a value range"""
+
+ numeric_base = 10
+
+ def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
+ """Constructor to set up values from a (user entered) string: DecimalRangeSource(input_str).
+ Constructor to set up values directly: DecimalRangeSource(num_digits=3, first_value=123, last_value=456)
+
+ num_digits produces leading zeros when first_value..last_value are shorter.
+ """
+ assert ((input_str is not None and (num_digits, first_value, last_value) == (None, None, None))
+ or (input_str is None and None not in (num_digits, first_value, last_value)))
+
+ if input_str is not None:
+ super().__init__(input_str)
+
+ input_str = self.input_str
+
+ if ".." in input_str:
+ first_str, last_str = input_str.split('..')
+ first_str = first_str.strip()
+ last_str = last_str.strip()
+ else:
+ first_str = input_str.strip()
+ last_str = None
+
+ num_digits = len(first_str)
+ first_value = int(first_str)
+ last_value = int(last_str if last_str is not None else "9" * num_digits)
+
+ assert num_digits > 0
+ assert first_value <= last_value
+ self.num_digits = num_digits
+ self.first_value = first_value
+ self.last_value = last_value
+
+ def val_to_digit(self, val:int):
+ return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
+
+class RandomDigitSource(DecimalRangeSource):
+ """return a different sequence of random decimal digits each"""
+ name = "random decimal digits"
+
+ def get_next(self, csv_row:dict=None):
+ val = random.randint(self.first_value, self.last_value) # TODO secure random source?
+ return self.val_to_digit(val)
+
+class RandomHexDigitSource(InputExpandingParamSource):
+ """return a different sequence of random hexadecimal digits each"""
+ name = "random hexadecimal digits"
+ numeric_base = 16
+
+ def __init__(self, input_str:str):
+ super().__init__(input_str)
+ input_str = self.input_str
+
+ num_digits = len(input_str.strip())
+ if num_digits < 1:
+ raise ValueError("zero number of digits")
+ # hex digits always come in two
+ if (num_digits & 1) != 0:
+ raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}")
+ self.num_digits = num_digits
+
+ def get_next(self, csv_row:dict=None):
+ val = random.randbytes(self.num_digits // 2) # TODO secure random source?
+ return b2h(val)
+
+class IncDigitSource(DecimalRangeSource):
+ """incrementing sequence of digits"""
+ name = "incrementing decimal digits"
+
+ def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
+ """input_str: the range of values to iterate. Format: 'FIRST..LAST' (e.g. '0001..9999') or
+ just 'FIRST' (iterates to the maximum value for the given digit width). Leading zeros in
+ FIRST determine the digit width and are preserved in returned values."""
+ super().__init__(input_str, num_digits, first_value, last_value)
+ self.next_val = None
+ self.reset()
+
+ def reset(self):
+ """Restart from the first value of the defined range passed to __init__()."""
+ self.next_val = self.first_value
+
+ def get_next(self, csv_row:dict=None):
+ val = self.next_val
+ if val is None:
+ raise ParamSourceExhaustedExn()
+
+ returnval = self.val_to_digit(val)
+
+ val += 1
+ if val > self.last_value:
+ self.next_val = None
+ else:
+ self.next_val = val
+
+ return returnval
+
+class CsvSource(ParamSource):
+ """apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
+ name = "from CSV"
+
+ def __init__(self, input_str:str):
+ """input_str: the CSV column name to read values from.
+ The caller passes the current CSV row to get_next(), from which CsvSource picks the column matching
+ this name."""
+ super().__init__(input_str)
+ self.csv_column = self.input_str
+
+ def get_next(self, csv_row:dict=None):
+ val = None
+ if csv_row:
+ val = csv_row.get(self.csv_column)
+ if val is None:
+ raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}")
+ return val
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41845?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: I01ae40a06605eb205bfb409189fcd2b3a128855a
Gerrit-Change-Number: 41845
Gerrit-PatchSet: 11
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-CC: dexter <pmaier(a)sysmocom.de>
Gerrit-CC: fixeria <vyanitskiy(a)sysmocom.de>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/osmo-remsim/+/42502?usp=email )
Change subject: rspro_server: use correct byte order in log message "Bankd IP/Port changed to <IP>:<Port>
......................................................................
rspro_server: use correct byte order in log message "Bankd IP/Port changed to <IP>:<Port>
The byte order is wrong and results in printing the IP as reverse.
"Bankd IP/Port changed to 1.0.0.127:9999".
Change-Id: I288ea70091d24fa95732f7035ab50100831be841
---
M src/server/rspro_server.c
1 file changed, 1 insertion(+), 1 deletion(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
pespin: Looks good to me, but someone else must approve
diff --git a/src/server/rspro_server.c b/src/server/rspro_server.c
index a6fd421..2a382d8 100644
--- a/src/server/rspro_server.c
+++ b/src/server/rspro_server.c
@@ -331,7 +331,7 @@
/* determine if IP/port of bankd have changed */
if (conn->client.bankd.port != bankd_port || conn->client.bankd.ip != bankd_ip) {
- struct in_addr ia = { .s_addr = bankd_ip };
+ struct in_addr ia = { .s_addr = htonl(bankd_ip) };
LOGPFSML(conn->fi, LOGL_NOTICE, "Bankd IP/Port changed to %s:%u\n", inet_ntoa(ia), bankd_port);
conn->client.bankd.ip = bankd_ip;
conn->client.bankd.port = bankd_port;
--
To view, visit https://gerrit.osmocom.org/c/osmo-remsim/+/42502?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: osmo-remsim
Gerrit-Branch: master
Gerrit-Change-Id: I288ea70091d24fa95732f7035ab50100831be841
Gerrit-Change-Number: 42502
Gerrit-PatchSet: 2
Gerrit-Owner: lynxis lazus <lynxis(a)fe80.eu>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: pespin <pespin(a)sysmocom.de>