Change in osmo-gsm-tester[master]: Generate schemas dynamically from pieces provided by each object class

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

pespin gerrit-no-reply at lists.osmocom.org
Mon May 4 14:39:03 UTC 2020


pespin has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-gsm-tester/+/18023 )


Change subject: Generate schemas dynamically from pieces provided by each object class
......................................................................

Generate schemas dynamically from pieces provided by each object class

This way we benefit from:
* knowing which attributes are used/required by each object class and
  subclass
* Having validation function definitions near the class going to use them

Change-Id: I8fd6773c51d19405a585977af4ed72cad2b21db1
---
M selftest/config_test.py
M selftest/resource_test.py
M selftest/suite_test.py
M src/osmo-gsm-tester.py
M src/osmo_gsm_tester/core/config.py
M src/osmo_gsm_tester/core/schema.py
M src/osmo_gsm_tester/core/util.py
M src/osmo_gsm_tester/obj/bsc_osmo.py
M src/osmo_gsm_tester/obj/bts.py
M src/osmo_gsm_tester/obj/bts_osmo.py
M src/osmo_gsm_tester/obj/bts_osmotrx.py
M src/osmo_gsm_tester/obj/enb.py
M src/osmo_gsm_tester/obj/enb_amarisoft.py
M src/osmo_gsm_tester/obj/enb_srs.py
M src/osmo_gsm_tester/obj/epc.py
M src/osmo_gsm_tester/obj/epc_amarisoft.py
M src/osmo_gsm_tester/obj/epc_srs.py
M src/osmo_gsm_tester/obj/iperf3.py
M src/osmo_gsm_tester/obj/ms.py
M src/osmo_gsm_tester/obj/ms_amarisoft.py
M src/osmo_gsm_tester/obj/ms_srs.py
M src/osmo_gsm_tester/obj/msc_osmo.py
M src/osmo_gsm_tester/obj/osmocon.py
M src/osmo_gsm_tester/obj/run_node.py
M src/osmo_gsm_tester/resource.py
M src/osmo_gsm_tester/suite.py
26 files changed, 418 insertions(+), 208 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-gsm-tester refs/changes/23/18023/1

diff --git a/selftest/config_test.py b/selftest/config_test.py
index 83a8d06..c26ebd1 100755
--- a/selftest/config_test.py
+++ b/selftest/config_test.py
@@ -116,35 +116,35 @@
 a = {'times': '2'}
 b = {'type': 'osmo-bts-trx'}
 res = {'times': '2', 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine dicts 2:')
 a = {'times': '1', 'label': 'foo', 'type': 'osmo-bts-trx'}
 b = {'type': 'osmo-bts-trx'}
 res = {'times': '1', 'label': 'foo', 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists:')
 a = { 'a_list': ['x', 'y', 'z'] }
 b = { 'a_list': ['y'] }
 res = {'a_list': ['x', 'y', 'z']}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 2:')
 a = { 'a_list': ['x'] }
 b = { 'a_list': ['w', 'u', 'x', 'y', 'z'] }
 res = {'a_list': ['x', 'w', 'u', 'y', 'z']}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 3:')
 a = { 'a_list': ['x', 3] }
 b = { 'a_list': ['y', 'z'] }
 try:
-    config.combine(a, b)
+    schema.combine(a, b)
 except ValueError:
     print("ValueError expected")
 
@@ -152,7 +152,7 @@
 a = { 'a_list': [2, 3] }
 b = { 'a_list': ['y', 'z'] }
 try:
-    config.combine(a, b)
+    schema.combine(a, b)
 except ValueError:
     print("ValueError expected")
 
@@ -160,7 +160,7 @@
 a = { 'a_list': [{}, {}] }
 b = { 'a_list': ['y', 'z'] }
 try:
-    config.combine(a, b)
+    schema.combine(a, b)
 except ValueError:
     print("ValueError expected")
 
@@ -168,49 +168,49 @@
 a = { 'a_list': [{}, {}] }
 b = { 'a_list': [{}] }
 res = {'a_list': [{}, {}]}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 7:')
 a = { 'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 b = { 'type': 'osmo-bts-trx', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 res = {'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 8:')
 a = { 'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}] }
 b = { 'type': 'osmo-bts-trx', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 res = {'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 9:')
 a = { 'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 b = { 'type': 'osmo-bts-trx', 'trx': [{'nominal power': '10'}] }
 res = {'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 10:')
 a = { 'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 b = { 'type': 'osmo-bts-trx', 'trx': [{}, {'nominal power': '12'}] }
 res = {'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 13:')
 a = { 'times': '1', 'label': 'foo', 'trx': [{}, {'nominal power': '12'}] }
 b = { 'type': 'osmo-bts-trx', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}] }
 res = {'times': '1', 'label': 'foo', 'trx': [{'nominal power': '10'}, {'nominal power': '12'}], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 print('- Combine lists 14:')
 a = { 'times': '1', 'label': 'foo', 'trx': [] }
 b = { 'type': 'osmo-bts-trx', 'trx': [] }
 res = {'times': '1', 'label': 'foo', 'trx': [], 'type': 'osmo-bts-trx'}
-config.combine(a, b)
+schema.combine(a, b)
 assert a == res
 
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/resource_test.py b/selftest/resource_test.py
index f399e20..ecbeb24 100755
--- a/selftest/resource_test.py
+++ b/selftest/resource_test.py
@@ -7,6 +7,7 @@
 import atexit
 import _prep
 from osmo_gsm_tester.core import config, log, util
+from osmo_gsm_tester.core.schema import generate_schemas
 from osmo_gsm_tester import resource
 
 workdir = util.get_tempdir()
@@ -16,6 +17,9 @@
 
 log.get_process_id = lambda: '123-1490837279'
 
+# Generate supported schemas dynamically from objects:
+generate_schemas()
+
 print('- expect solutions:')
 pprint.pprint(
     resource.solve([ [0, 1, 2],
diff --git a/selftest/suite_test.py b/selftest/suite_test.py
index c4dd5bf..1fb95ec 100755
--- a/selftest/suite_test.py
+++ b/selftest/suite_test.py
@@ -3,6 +3,7 @@
 import _prep
 import shutil
 from osmo_gsm_tester.core import log, config, util
+from osmo_gsm_tester.core.schema import generate_schemas
 from osmo_gsm_tester import suite, report
 
 config.ENV_CONF = './suite_test'
@@ -24,6 +25,9 @@
 
 #log.style_change(trace=True)
 
+# Generate supported schemas dynamically from objects:
+generate_schemas()
+
 print('- non-existing suite dir')
 assert(log.run_logging_exceptions(suite.load, 'does_not_exist') == None)
 
diff --git a/src/osmo-gsm-tester.py b/src/osmo-gsm-tester.py
index df87957..af66b32 100755
--- a/src/osmo-gsm-tester.py
+++ b/src/osmo-gsm-tester.py
@@ -70,7 +70,9 @@
 import argparse
 from signal import *
 from osmo_gsm_tester import __version__
-from osmo_gsm_tester.core import log, config
+from osmo_gsm_tester.core import log
+from osmo_gsm_tester.core.config import read_config_file, DEFAULT_SUITES_CONF
+from osmo_gsm_tester.core.schema import generate_schemas
 from osmo_gsm_tester import trial, suite
 
 def sig_handler_cleanup(signum, frame):
@@ -162,6 +164,9 @@
     if not combination_strs:
         raise RuntimeError('Need at least one suite:scenario or series to run')
 
+    # Generate supported schemas dynamically from objects:
+    generate_schemas()
+
     # make sure all suite:scenarios exist
     suite_scenarios = []
     for combination_str in combination_strs:
diff --git a/src/osmo_gsm_tester/core/config.py b/src/osmo_gsm_tester/core/config.py
index 9333601..6730807 100644
--- a/src/osmo_gsm_tester/core/config.py
+++ b/src/osmo_gsm_tester/core/config.py
@@ -54,7 +54,8 @@
 import os
 import copy
 
-from . import log, schema, util, template
+from . import log, util, template
+from . import schema
 from .util import is_dict, is_list, Dir, get_tempdir
 
 ENV_PREFIX = 'OSMO_GSM_TESTER_'
@@ -288,68 +289,6 @@
     sc.read_from_file(validation_schema)
     return sc
 
-def add(dest, src):
-    if is_dict(dest):
-        if not is_dict(src):
-            raise ValueError('cannot add to dict a value of type: %r' % type(src))
-
-        for key, val in src.items():
-            dest_val = dest.get(key)
-            if dest_val is None:
-                dest[key] = val
-            else:
-                log.ctx(key=key)
-                add(dest_val, val)
-        return
-    if is_list(dest):
-        if not is_list(src):
-            raise ValueError('cannot add to list a value of type: %r' % type(src))
-        dest.extend(src)
-        return
-    if dest == src:
-        return
-    raise ValueError('cannot add dicts, conflicting items (values %r and %r)'
-                     % (dest, src))
-
-def combine(dest, src):
-    if is_dict(dest):
-        if not is_dict(src):
-            raise ValueError('cannot combine dict with a value of type: %r' % type(src))
-
-        for key, val in src.items():
-            log.ctx(key=key)
-            dest_val = dest.get(key)
-            if dest_val is None:
-                dest[key] = val
-            else:
-                combine(dest_val, val)
-        return
-    if is_list(dest):
-        if not is_list(src):
-            raise ValueError('cannot combine list with a value of type: %r' % type(src))
-        # Validate that all elements in both lists are of the same type:
-        t = util.list_validate_same_elem_type(src + dest)
-        if t is None:
-            return # both lists are empty, return
-        # For lists of complex objects, we expect them to be sorted lists:
-        if t in (dict, list, tuple):
-            for i in range(len(dest)):
-                log.ctx(idx=i)
-                src_it = src[i] if i < len(src) else util.empty_instance_type(t)
-                combine(dest[i], src_it)
-            for i in range(len(dest), len(src)):
-                log.ctx(idx=i)
-                dest.append(src[i])
-        else: # for lists of basic elements, we handle them as unsorted sets:
-            for elem in src:
-                if elem not in dest:
-                    dest.append(elem)
-        return
-    if dest == src:
-        return
-    raise ValueError('cannot combine dicts, conflicting items (values %r and %r)'
-                     % (dest, src))
-
 def overlay(dest, src):
     if is_dict(dest):
         if not is_dict(src):
diff --git a/src/osmo_gsm_tester/core/schema.py b/src/osmo_gsm_tester/core/schema.py
index d343bef..588c432 100644
--- a/src/osmo_gsm_tester/core/schema.py
+++ b/src/osmo_gsm_tester/core/schema.py
@@ -18,9 +18,10 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
+import os
 
 from . import log
-from .util import is_dict, is_list, str2bool, ENUM_OSMO_AUTH_ALGO
+from . import util
 
 KEY_RE = re.compile('[a-zA-Z][a-zA-Z0-9_]*')
 IPV4_RE = re.compile('([0-9]{1,3}.){3}[0-9]{1,3}')
@@ -62,7 +63,7 @@
     match_re('MSISDN', MSISDN_RE, val)
 
 def auth_algo(val):
-    if val not in ENUM_OSMO_AUTH_ALGO:
+    if val not in util.ENUM_OSMO_AUTH_ALGO:
         raise ValueError('Unknown Authentication Algorithm: %r' % val)
 
 def uint(val):
@@ -162,7 +163,7 @@
         INT: int,
         STR: str,
         UINT: uint,
-        BOOL_STR: str2bool,
+        BOOL_STR: util.str2bool,
         BAND: band,
         IPV4: ipv4,
         HWADDR: hwaddr,
@@ -182,6 +183,87 @@
         DURATION: duration,
     }
 
+def add(dest, src):
+    if util.is_dict(dest):
+        if not util.is_dict(src):
+            raise ValueError('cannot add to dict a value of type: %r' % type(src))
+
+        for key, val in src.items():
+            dest_val = dest.get(key)
+            if dest_val is None:
+                dest[key] = val
+            else:
+                log.ctx(key=key)
+                add(dest_val, val)
+        return
+    if util.is_list(dest):
+        if not util.is_list(src):
+            raise ValueError('cannot add to list a value of type: %r' % type(src))
+        dest.extend(src)
+        return
+    if dest == src:
+        return
+    raise ValueError('cannot add dicts, conflicting items (values %r and %r)'
+                     % (dest, src))
+
+def combine(dest, src):
+    if util.is_dict(dest):
+        if not util.is_dict(src):
+            raise ValueError('cannot combine dict with a value of type: %r' % type(src))
+
+        for key, val in src.items():
+            log.ctx(key=key)
+            dest_val = dest.get(key)
+            if dest_val is None:
+                dest[key] = val
+            else:
+                combine(dest_val, val)
+        return
+    if util.is_list(dest):
+        if not util.is_list(src):
+            raise ValueError('cannot combine list with a value of type: %r' % type(src))
+        # Validate that all elements in both lists are of the same type:
+        t = util.list_validate_same_elem_type(src + dest)
+        if t is None:
+            return # both lists are empty, return
+        # For lists of complex objects, we expect them to be sorted lists:
+        if t in (dict, list, tuple):
+            for i in range(len(dest)):
+                log.ctx(idx=i)
+                src_it = src[i] if i < len(src) else util.empty_instance_type(t)
+                combine(dest[i], src_it)
+            for i in range(len(dest), len(src)):
+                log.ctx(idx=i)
+                dest.append(src[i])
+        else: # for lists of basic elements, we handle them as unsorted sets:
+            for elem in src:
+                if elem not in dest:
+                    dest.append(elem)
+        return
+    if dest == src:
+        return
+    raise ValueError('cannot combine dicts, conflicting items (values %r and %r)'
+                     % (dest, src))
+
+def replicate_times(d):
+    '''
+    replicate items that have a "times" > 1
+
+    'd' is a dict matching WANT_SCHEMA, which is the same as
+    the RESOURCES_SCHEMA, except each entity that can be reserved has a 'times'
+    field added, to indicate how many of those should be reserved.
+    '''
+    d = copy.deepcopy(d)
+    for key, item_list in d.items():
+        idx = 0
+        while idx < len(item_list):
+            item = item_list[idx]
+            times = int(item.pop('times', 1))
+            for j in range(1, times):
+                item_list.insert(idx + j, copy.deepcopy(item))
+            idx += times
+    return d
+
 def validate(config, schema):
     '''Make sure the given config dict adheres to the schema.
        The schema is a dict of 'dict paths' in dot-notation with permitted
@@ -198,17 +280,17 @@
     def validate_item(path, value, schema):
         want_type = schema.get(path)
 
-        if is_list(value):
+        if util.is_list(value):
             if want_type:
                 raise ValueError('config item is a list, should be %r: %r' % (want_type, path))
             path = path + '[]'
             want_type = schema.get(path)
 
         if not want_type:
-            if is_dict(value):
+            if util.is_dict(value):
                 nest(path, value, schema)
                 return
-            if is_list(value) and value:
+            if util.is_list(value) and value:
                 for list_v in value:
                     validate_item(path, list_v, schema)
                 return
@@ -217,11 +299,11 @@
         if want_type not in SCHEMA_TYPES:
             raise ValueError('unknown type %r at %r' % (want_type, path))
 
-        if is_dict(value):
+        if util.is_dict(value):
             raise ValueError('config item is dict but should be a leaf node of type %r: %r'
                              % (want_type, path))
 
-        if is_list(value):
+        if util.is_list(value):
             for list_v in value:
                 validate_item(path, list_v, schema)
             return
@@ -243,4 +325,73 @@
 
     nest(None, config, schema)
 
+def generate_schemas():
+    "Generate supported schemas dynamically from objects"
+    obj_dir = '%s/../obj/' % os.path.dirname(os.path.abspath(__file__))
+    for filename in os.listdir(obj_dir):
+        if not filename.endswith(".py"):
+            continue
+        module_name = 'osmo_gsm_tester.obj.%s' % filename[:-3]
+        util.run_python_file_method(module_name, 'on_register_schemas', False)
+
+
+_RESOURCE_TYPES = ['ip_address', 'arfcn']
+
+_RESOURCES_SCHEMA = {
+        'ip_address[].addr': IPV4,
+        'arfcn[].arfcn': INT,
+        'arfcn[].band': BAND,
+    }
+
+_CONFIG_SCHEMA = {}
+
+_WANT_SCHEMA = None
+_ALL_SCHEMA = None
+
+def register_resource_schema(obj_class_str, obj_attr_dict):
+    """Register schema attributes for a resource type.
+       For instance: register_resource_schema_attributes('modem', {'type': schema.STR, 'ki': schema.KI})
+    """
+    global _RESOURCES_SCHEMA
+    global _RESOURCE_TYPES
+    tmpdict = {}
+    for key, val in obj_attr_dict.items():
+        new_key = '%s[].%s' % (obj_class_str, key)
+        tmpdict[new_key] = val
+    combine(_RESOURCES_SCHEMA, tmpdict)
+    if obj_class_str not in _RESOURCE_TYPES:
+        _RESOURCE_TYPES.append(obj_class_str)
+
+def register_config_schema(obj_class_str, obj_attr_dict):
+    """Register schema attributes to configure all instances of an object class.
+       For instance: register_resource_schema_attributes('bsc', {'net.codec_list[]': schema.CODEC})
+    """
+    global _CONFIG_SCHEMA
+    tmpdict = {}
+    for key, val in obj_attr_dict.items():
+        new_key = '%s.%s' % (obj_class_str, key)
+        tmpdict[new_key] = val
+    combine(_CONFIG_SCHEMA, tmpdict)
+
+def get_resources_schema():
+    return _RESOURCES_SCHEMA;
+
+def get_want_schema():
+    global _WANT_SCHEMA
+    if _WANT_SCHEMA is None:
+        _WANT_SCHEMA = util.dict_add(
+            dict([('%s[].times' % r, TIMES) for r in _RESOURCE_TYPES]),
+            get_resources_schema())
+    return _WANT_SCHEMA
+
+def get_all_schema():
+    global _ALL_SCHEMA
+    if _ALL_SCHEMA is None:
+        want_schema = get_want_schema()
+        _ALL_SCHEMA = util.dict_add({ 'defaults.timeout': STR },
+                        dict([('config.%s' % key, val) for key, val in _CONFIG_SCHEMA.items()]),
+                        dict([('resources.%s' % key, val) for key, val in want_schema.items()]),
+                        dict([('modifiers.%s' % key, val) for key, val in want_schema.items()]))
+    return _ALL_SCHEMA
+
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/core/util.py b/src/osmo_gsm_tester/core/util.py
index a5b2bbf..4c7b1dd 100644
--- a/src/osmo_gsm_tester/core/util.py
+++ b/src/osmo_gsm_tester/core/util.py
@@ -370,6 +370,17 @@
     def run_python_file(module_name, path):
         SourceFileLoader(module_name, path).load_module()
 
+def run_python_file_method(module_name, func_name, fail_if_missing=True):
+    module_obj = __import__(module_name, globals(), locals(), [func_name])
+    try:
+        func = getattr(module_obj, func_name)
+    except AttributeError as e:
+        if fail_if_missing:
+            raise RuntimeError('function %s not found in %s (%s)' % (func_name, module_name))
+        else:
+            return None
+    return func()
+
 def msisdn_inc(msisdn_str):
     'add 1 and preserve leading zeros'
     return ('%%0%dd' % len(msisdn_str)) % (int(msisdn_str) + 1)
diff --git a/src/osmo_gsm_tester/obj/bsc_osmo.py b/src/osmo_gsm_tester/obj/bsc_osmo.py
index 25cc780..046ef94 100644
--- a/src/osmo_gsm_tester/obj/bsc_osmo.py
+++ b/src/osmo_gsm_tester/obj/bsc_osmo.py
@@ -22,8 +22,16 @@
 import pprint
 
 from ..core import log, util, config, template, process
+from ..core import schema
 from . import osmo_ctrl, pcap_recorder
 
+def on_register_schemas():
+    config_schema = {
+        'net.codec_list[]': schema.CODEC,
+        }
+    schema.register_config_schema('bsc', config_schema)
+
+
 class OsmoBsc(log.Origin):
 
     def __init__(self, suite_run, msc, mgw, stp, ip_address):
diff --git a/src/osmo_gsm_tester/obj/bts.py b/src/osmo_gsm_tester/obj/bts.py
index 515b42b..8b05ea0 100644
--- a/src/osmo_gsm_tester/obj/bts.py
+++ b/src/osmo_gsm_tester/obj/bts.py
@@ -21,6 +21,30 @@
 from abc import ABCMeta, abstractmethod
 from ..core import log, config, schema
 
+def on_register_schemas():
+    resource_schema = {
+        'label': schema.STR,
+        'type': schema.STR,
+        'addr': schema.IPV4,
+        'band': schema.BAND,
+        'direct_pcu': schema.BOOL_STR,
+        'ciphers[]': schema.CIPHER,
+        'channel_allocator': schema.CHAN_ALLOCATOR,
+        'gprs_mode': schema.GPRS_MODE,
+        'num_trx': schema.UINT,
+        'max_trx': schema.UINT,
+        'trx_list[].addr': schema.IPV4,
+        'trx_list[].hw_addr': schema.HWADDR,
+        'trx_list[].net_device': schema.STR,
+        'trx_list[].nominal_power': schema.UINT,
+        'trx_list[].max_power_red': schema.UINT,
+        'trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
+        'trx_list[].power_supply.type': schema.STR,
+        'trx_list[].power_supply.device': schema.STR,
+        'trx_list[].power_supply.port': schema.STR,
+        }
+    schema.register_resource_schema('bts', resource_schema)
+
 class Bts(log.Origin, metaclass=ABCMeta):
 
 ##############
diff --git a/src/osmo_gsm_tester/obj/bts_osmo.py b/src/osmo_gsm_tester/obj/bts_osmo.py
index 74f3ec7..a182c47 100644
--- a/src/osmo_gsm_tester/obj/bts_osmo.py
+++ b/src/osmo_gsm_tester/obj/bts_osmo.py
@@ -21,8 +21,18 @@
 import tempfile
 from abc import ABCMeta, abstractmethod
 from ..core import log
+from ..core import schema
 from . import bts, pcu_osmo
 
+def on_register_schemas():
+    resource_schema = {
+        'ipa_unit_id': schema.UINT,
+        'direct_pcu': schema.BOOL_STR,
+        'channel_allocator': schema.CHAN_ALLOCATOR,
+        'gprs_mode': schema.GPRS_MODE,
+        }
+    schema.register_resource_schema('bts', resource_schema)
+
 class OsmoBts(bts.Bts, metaclass=ABCMeta):
 
 ##############
diff --git a/src/osmo_gsm_tester/obj/bts_osmotrx.py b/src/osmo_gsm_tester/obj/bts_osmotrx.py
index 5339946..9234fc8 100644
--- a/src/osmo_gsm_tester/obj/bts_osmotrx.py
+++ b/src/osmo_gsm_tester/obj/bts_osmotrx.py
@@ -21,9 +21,25 @@
 import pprint
 from abc import ABCMeta, abstractmethod
 from ..core import log, config, util, template, process, remote
+from ..core import schema
 from ..core.event_loop import MainLoop
 from . import powersupply, bts_osmo
 
+def on_register_schemas():
+    resource_schema = {
+        'osmo_trx.launch_trx': schema.BOOL_STR,
+        'osmo_trx.type': schema.STR,
+        'osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
+        'osmo_trx.trx_ip': schema.IPV4,
+        'osmo_trx.remote_user': schema.STR,
+        'osmo_trx.dev_args': schema.STR,
+        'osmo_trx.multi_arfcn': schema.BOOL_STR,
+        'osmo_trx.max_trxd_version': schema.UINT,
+        'osmo_trx.channels[].rx_path': schema.STR,
+        'osmo_trx.channels[].tx_path': schema.STR,
+        }
+    schema.register_resource_schema('bts', resource_schema)
+
 class OsmoBtsTrx(bts_osmo.OsmoBtsMainUnit):
 ##############
 # PROTECTED
diff --git a/src/osmo_gsm_tester/obj/enb.py b/src/osmo_gsm_tester/obj/enb.py
index 3c1f771..c652761 100644
--- a/src/osmo_gsm_tester/obj/enb.py
+++ b/src/osmo_gsm_tester/obj/enb.py
@@ -19,7 +19,47 @@
 
 from abc import ABCMeta, abstractmethod
 from ..core import log, config
+from ..core import schema
 
+def on_register_schemas():
+    resource_schema = {
+        'label': schema.STR,
+        'type': schema.STR,
+        'remote_user': schema.STR,
+        'addr': schema.IPV4,
+        'gtp_bind_addr': schema.IPV4,
+        'id': schema.UINT,
+        'num_prb': schema.UINT,
+        'transmission_mode': schema.LTE_TRANSMISSION_MODE,
+        'tx_gain': schema.UINT,
+        'rx_gain': schema.UINT,
+        'rf_dev_type': schema.STR,
+        'rf_dev_args': schema.STR,
+        'additional_args': schema.STR,
+        'enable_measurements': schema.BOOL_STR,
+        'a1_report_type': schema.STR,
+        'a1_report_value': schema.INT,
+        'a1_hysteresis': schema.INT,
+        'a1_time_to_trigger': schema.INT,
+        'a2_report_type': schema.STR,
+        'a2_report_value': schema.INT,
+        'a2_hysteresis': schema.INT,
+        'a2_time_to_trigger': schema.INT,
+        'a3_report_type': schema.STR,
+        'a3_report_value': schema.INT,
+        'a3_hysteresis': schema.INT,
+        'a3_time_to_trigger': schema.INT,
+        'num_cells': schema.UINT,
+        'cell_list[].cell_id': schema.UINT,
+        'cell_list[].pci': schema.UINT,
+        'cell_list[].ncell_list[]': schema.UINT,
+        'cell_list[].scell_list[]': schema.UINT,
+        'cell_list[].dl_earfcn': schema.UINT,
+        'cell_list[].dl_rfemu.type': schema.STR,
+        'cell_list[].dl_rfemu.addr': schema.IPV4,
+        'cell_list[].dl_rfemu.ports[]': schema.UINT,
+        }
+    schema.register_resource_schema('enb', resource_schema)
 
 class eNodeB(log.Origin, metaclass=ABCMeta):
 
diff --git a/src/osmo_gsm_tester/obj/enb_amarisoft.py b/src/osmo_gsm_tester/obj/enb_amarisoft.py
index ec7063c..6f7080a 100644
--- a/src/osmo_gsm_tester/obj/enb_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/enb_amarisoft.py
@@ -21,9 +21,16 @@
 import pprint
 
 from ..core import log, util, config, template, process, remote
+from ..core import schema
 from . import enb
 from . import rfemu
 
+def on_register_schemas():
+    config_schema = {
+        'license_server_addr': schema.IPV4,
+        }
+    schema.register_config_schema('amarisoft', config_schema)
+
 def rf_type_valid(rf_type_str):
     return rf_type_str in ('uhd', 'zmq')
 
diff --git a/src/osmo_gsm_tester/obj/enb_srs.py b/src/osmo_gsm_tester/obj/enb_srs.py
index 243ffaa..0816ca6 100644
--- a/src/osmo_gsm_tester/obj/enb_srs.py
+++ b/src/osmo_gsm_tester/obj/enb_srs.py
@@ -24,6 +24,14 @@
 from . import enb
 from . import rfemu
 
+from ..core import schema
+
+def on_register_schemas():
+    config_schema = {
+        'enable_pcap': schema.BOOL_STR,
+        }
+    schema.register_config_schema('enb', config_schema)
+
 def rf_type_valid(rf_type_str):
     return rf_type_str in ('zmq', 'uhd', 'soapy', 'bladerf')
 
diff --git a/src/osmo_gsm_tester/obj/epc.py b/src/osmo_gsm_tester/obj/epc.py
index f6bddea..c725f76 100644
--- a/src/osmo_gsm_tester/obj/epc.py
+++ b/src/osmo_gsm_tester/obj/epc.py
@@ -19,7 +19,14 @@
 
 from abc import ABCMeta, abstractmethod
 from ..core import log, config
+from ..core import schema
 
+def on_register_schemas():
+    config_schema = {
+        'type': schema.STR,
+        'qci': schema.UINT,
+        }
+    schema.register_config_schema('epc', config_schema)
 
 class EPC(log.Origin, metaclass=ABCMeta):
 
diff --git a/src/osmo_gsm_tester/obj/epc_amarisoft.py b/src/osmo_gsm_tester/obj/epc_amarisoft.py
index afd8aa4..8606e57 100644
--- a/src/osmo_gsm_tester/obj/epc_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/epc_amarisoft.py
@@ -21,8 +21,15 @@
 import pprint
 
 from ..core import log, util, config, template, process, remote
+from ..core import schema
 from . import epc
 
+def on_register_schemas():
+    config_schema = {
+        'license_server_addr': schema.IPV4,
+        }
+    schema.register_config_schema('amarisoft', config_schema)
+
 class AmarisoftEPC(epc.EPC):
 
     REMOTE_DIR = '/osmo-gsm-tester-amarisoftepc'
diff --git a/src/osmo_gsm_tester/obj/epc_srs.py b/src/osmo_gsm_tester/obj/epc_srs.py
index ec9dc44..f859df0 100644
--- a/src/osmo_gsm_tester/obj/epc_srs.py
+++ b/src/osmo_gsm_tester/obj/epc_srs.py
@@ -21,8 +21,15 @@
 import pprint
 
 from ..core import log, util, config, template, process, remote
+from ..core import schema
 from . import epc
 
+def on_register_schemas():
+    config_schema = {
+        'enable_pcap': schema.BOOL_STR,
+        }
+    schema.register_config_schema('epc', config_schema)
+
 class srsEPC(epc.EPC):
 
     REMOTE_DIR = '/osmo-gsm-tester-srsepc'
diff --git a/src/osmo_gsm_tester/obj/iperf3.py b/src/osmo_gsm_tester/obj/iperf3.py
index 9427770..b9fdfe8 100644
--- a/src/osmo_gsm_tester/obj/iperf3.py
+++ b/src/osmo_gsm_tester/obj/iperf3.py
@@ -21,8 +21,15 @@
 import json
 
 from ..core import log, util, config, process, remote
+from ..core import schema
 from . import pcap_recorder, run_node
 
+def on_register_schemas():
+    config_schema = {
+        'time': schema.DURATION,
+        }
+    schema.register_config_schema('iperf3cli', config_schema)
+
 def iperf3_result_to_json(log_obj, data):
     try:
         # Drop non-interesting self-generated output before json:
diff --git a/src/osmo_gsm_tester/obj/ms.py b/src/osmo_gsm_tester/obj/ms.py
index 3dcea7b..b72333a 100644
--- a/src/osmo_gsm_tester/obj/ms.py
+++ b/src/osmo_gsm_tester/obj/ms.py
@@ -19,6 +19,21 @@
 
 from abc import ABCMeta, abstractmethod
 from ..core import log
+from ..core import schema
+
+def on_register_schemas():
+    resource_schema = {
+        'type': schema.STR,
+        'label': schema.STR,
+        'path': schema.STR,
+        'imsi': schema.IMSI,
+        'ki': schema.KI,
+        'auth_algo': schema.AUTH_ALGO,
+        'apn_ipaddr': schema.IPV4,
+        'ciphers[]': schema.CIPHER,
+        'features[]': schema.MODEM_FEATURE
+        }
+    schema.register_resource_schema('modem', resource_schema)
 
 class MS(log.Origin, metaclass=ABCMeta):
     """Base for everything about mobile/modem and SIMs."""
diff --git a/src/osmo_gsm_tester/obj/ms_amarisoft.py b/src/osmo_gsm_tester/obj/ms_amarisoft.py
index 6fd80ee..46a92dc 100644
--- a/src/osmo_gsm_tester/obj/ms_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/ms_amarisoft.py
@@ -21,10 +21,17 @@
 import pprint
 
 from ..core import log, util, config, template, process, remote
+from ..core import schema
 from ..core.event_loop import MainLoop
 from .run_node import RunNode
 from .ms import MS
 
+def on_register_schemas():
+    config_schema = {
+        'license_server_addr': schema.IPV4,
+        }
+    schema.register_config_schema('amarisoft', config_schema)
+
 def rf_type_valid(rf_type_str):
     return rf_type_str in ('uhd', 'zmq')
 
diff --git a/src/osmo_gsm_tester/obj/ms_srs.py b/src/osmo_gsm_tester/obj/ms_srs.py
index cdc8d18..7d08b66 100644
--- a/src/osmo_gsm_tester/obj/ms_srs.py
+++ b/src/osmo_gsm_tester/obj/ms_srs.py
@@ -21,6 +21,7 @@
 import pprint
 
 from ..core import log, util, config, template, process, remote
+from ..core import schema
 from .run_node import RunNode
 from ..core.event_loop import MainLoop
 from .ms import MS
@@ -28,6 +29,26 @@
 def rf_type_valid(rf_type_str):
     return rf_type_str in ('zmq', 'uhd', 'soapy', 'bladerf')
 
+def on_register_schemas():
+    resource_schema = {
+        'remote_user': schema.STR,
+        'addr': schema.IPV4,
+        'rf_dev_type': schema.STR,
+        'rf_dev_args': schema.STR,
+        'num_carriers': schema.UINT,
+        'additional_args': schema.STR,
+        'airplane_t_on_ms': schema.INT,
+        'airplane_t_off_ms': schema.INT,
+        'tx_gain': schema.UINT,
+        'rx_gain': schema.UINT,
+        }
+    schema.register_resource_schema('modem', resource_schema)
+
+    config_schema = {
+        'enable_pcap': schema.BOOL_STR,
+        }
+    schema.register_config_schema('modem', config_schema)
+
 #reference: srsLTE.git srslte_symbol_sz()
 def num_prb2symbol_sz(num_prb):
     if num_prb <= 6:
diff --git a/src/osmo_gsm_tester/obj/msc_osmo.py b/src/osmo_gsm_tester/obj/msc_osmo.py
index cb8894f..048934e 100644
--- a/src/osmo_gsm_tester/obj/msc_osmo.py
+++ b/src/osmo_gsm_tester/obj/msc_osmo.py
@@ -21,8 +21,15 @@
 import pprint
 
 from ..core import log, util, config, template, process
+from ..core import schema
 from . import osmo_ctrl, pcap_recorder, smsc
 
+def on_register_schemas():
+    resource_schema = {
+        'path': schema.STR,
+        }
+    schema.register_resource_schema('modem', resource_schema)
+
 class OsmoMsc(log.Origin):
 
     def __init__(self, suite_run, hlr, mgw, stp, ip_address):
diff --git a/src/osmo_gsm_tester/obj/osmocon.py b/src/osmo_gsm_tester/obj/osmocon.py
index 1fad239..6f6ac2a 100644
--- a/src/osmo_gsm_tester/obj/osmocon.py
+++ b/src/osmo_gsm_tester/obj/osmocon.py
@@ -21,8 +21,16 @@
 import tempfile
 
 from ..core import log, util, process
+from ..core import schema
 from ..core.event_loop import MainLoop
 
+def on_register_schemas():
+    resource_schema = {
+        'serial_device': schema.STR,
+        }
+    schema.register_resource_schema('osmocon_phone', resource_schema)
+
+
 class Osmocon(log.Origin):
 
     FIRMWARE_FILE="opt/osmocom-bb/target/firmware/board/compal_e88/layer1.compalram.bin"
diff --git a/src/osmo_gsm_tester/obj/run_node.py b/src/osmo_gsm_tester/obj/run_node.py
index e9f43a1..26c85df 100644
--- a/src/osmo_gsm_tester/obj/run_node.py
+++ b/src/osmo_gsm_tester/obj/run_node.py
@@ -18,6 +18,17 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from ..core import log
+from ..core import schema
+
+def on_register_schemas():
+    resource_schema = {
+        'run_type': schema.STR,
+        'run_addr': schema.IPV4,
+        'ssh_user': schema.STR,
+        'ssh_addr': schema.IPV4,
+        }
+    schema.register_resource_schema('run_node', resource_schema)
+
 
 class RunNode(log.Origin):
 
diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py
index 20302d3..b216c50 100644
--- a/src/osmo_gsm_tester/resource.py
+++ b/src/osmo_gsm_tester/resource.py
@@ -46,120 +46,6 @@
 R_MODEM = 'modem'
 R_OSMOCON = 'osmocon_phone'
 R_ENB = 'enb'
-R_ALL = (R_IP_ADDRESS, R_RUN_NODE, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON, R_ENB)
-
-RESOURCES_SCHEMA = {
-        'ip_address[].addr': schema.IPV4,
-        'run_node[].run_type': schema.STR,
-        'run_node[].run_addr': schema.IPV4,
-        'run_node[].ssh_user': schema.STR,
-        'run_node[].ssh_addr': schema.IPV4,
-        'bts[].label': schema.STR,
-        'bts[].type': schema.STR,
-        'bts[].ipa_unit_id': schema.UINT,
-        'bts[].addr': schema.IPV4,
-        'bts[].band': schema.BAND,
-        'bts[].direct_pcu': schema.BOOL_STR,
-        'bts[].ciphers[]': schema.CIPHER,
-        'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
-        'bts[].gprs_mode': schema.GPRS_MODE,
-        'bts[].num_trx': schema.UINT,
-        'bts[].max_trx': schema.UINT,
-        'bts[].trx_list[].addr': schema.IPV4,
-        'bts[].trx_list[].hw_addr': schema.HWADDR,
-        'bts[].trx_list[].net_device': schema.STR,
-        'bts[].trx_list[].nominal_power': schema.UINT,
-        'bts[].trx_list[].max_power_red': schema.UINT,
-        'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
-        'bts[].trx_list[].power_supply.type': schema.STR,
-        'bts[].trx_list[].power_supply.device': schema.STR,
-        'bts[].trx_list[].power_supply.port': schema.STR,
-        'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
-        'bts[].osmo_trx.type': schema.STR,
-        'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
-        'bts[].osmo_trx.trx_ip': schema.IPV4,
-        'bts[].osmo_trx.remote_user': schema.STR,
-        'bts[].osmo_trx.dev_args': schema.STR,
-        'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
-        'bts[].osmo_trx.max_trxd_version': schema.UINT,
-        'bts[].osmo_trx.channels[].rx_path': schema.STR,
-        'bts[].osmo_trx.channels[].tx_path': schema.STR,
-        'enb[].label': schema.STR,
-        'enb[].type': schema.STR,
-        'enb[].remote_user': schema.STR,
-        'enb[].addr': schema.IPV4,
-        'enb[].gtp_bind_addr': schema.IPV4,
-        'enb[].id': schema.UINT,
-        'enb[].num_prb': schema.UINT,
-        'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
-        'enb[].tx_gain': schema.UINT,
-        'enb[].rx_gain': schema.UINT,
-        'enb[].rf_dev_type': schema.STR,
-        'enb[].rf_dev_args': schema.STR,
-        'enb[].additional_args': schema.STR,
-        'enb[].enable_measurements': schema.BOOL_STR,
-        'enb[].a1_report_type': schema.STR,
-        'enb[].a1_report_value': schema.INT,
-        'enb[].a1_hysteresis': schema.INT,
-        'enb[].a1_time_to_trigger': schema.INT,
-        'enb[].a2_report_type': schema.STR,
-        'enb[].a2_report_value': schema.INT,
-        'enb[].a2_hysteresis': schema.INT,
-        'enb[].a2_time_to_trigger': schema.INT,
-        'enb[].a3_report_type': schema.STR,
-        'enb[].a3_report_value': schema.INT,
-        'enb[].a3_hysteresis': schema.INT,
-        'enb[].a3_time_to_trigger': schema.INT,
-        'enb[].num_cells': schema.UINT,
-        'enb[].cell_list[].cell_id': schema.UINT,
-        'enb[].cell_list[].pci': schema.UINT,
-        'enb[].cell_list[].ncell_list[]': schema.UINT,
-        'enb[].cell_list[].scell_list[]': schema.UINT,
-        'enb[].cell_list[].dl_earfcn': schema.UINT,
-        'enb[].cell_list[].dl_rfemu.type': schema.STR,
-        'enb[].cell_list[].dl_rfemu.addr': schema.IPV4,
-        'enb[].cell_list[].dl_rfemu.ports[]': schema.UINT,
-        'arfcn[].arfcn': schema.INT,
-        'arfcn[].band': schema.BAND,
-        'modem[].type': schema.STR,
-        'modem[].label': schema.STR,
-        'modem[].path': schema.STR,
-        'modem[].imsi': schema.IMSI,
-        'modem[].ki': schema.KI,
-        'modem[].auth_algo': schema.AUTH_ALGO,
-        'modem[].apn_ipaddr': schema.IPV4,
-        'modem[].remote_user': schema.STR,
-        'modem[].addr': schema.IPV4,
-        'modem[].ciphers[]': schema.CIPHER,
-        'modem[].features[]': schema.MODEM_FEATURE,
-        'modem[].rf_dev_type': schema.STR,
-        'modem[].rf_dev_args': schema.STR,
-        'modem[].num_carriers': schema.UINT,
-        'modem[].additional_args': schema.STR,
-        'modem[].airplane_t_on_ms': schema.INT,
-        'modem[].airplane_t_off_ms': schema.INT,
-        'modem[].tx_gain': schema.UINT,
-        'modem[].rx_gain': schema.UINT,
-        'osmocon_phone[].serial_device': schema.STR,
-    }
-
-WANT_SCHEMA = util.dict_add(
-    dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
-    RESOURCES_SCHEMA)
-
-CONF_SCHEMA = util.dict_add(
-    { 'defaults.timeout': schema.STR,
-      'config.bsc.net.codec_list[]': schema.CODEC,
-      'config.enb.enable_pcap': schema.BOOL_STR,
-      'config.epc.type': schema.STR,
-      'config.epc.qci': schema.UINT,
-      'config.epc.enable_pcap': schema.BOOL_STR,
-      'config.modem.enable_pcap': schema.BOOL_STR,
-      'config.amarisoft.license_server_addr': schema.IPV4,
-      'config.iperf3cli.time': schema.DURATION,
-    },
-    dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
-    dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
 
 KNOWN_BTS_TYPES = {
         'osmo-bts-sysmo': bts_sysmo.SysmoBts,
@@ -204,7 +90,7 @@
         self.read_conf()
 
     def read_conf(self):
-        self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
+        self.all_resources = Resources(config.read(self.config_path, schema.get_resources_schema()))
         self.all_resources.set_hashes()
 
     def reserve(self, origin, want, modifiers):
@@ -239,8 +125,8 @@
            'modem': [ {}, {} ],
          }
         '''
-        schema.validate(want, RESOURCES_SCHEMA)
-        schema.validate(modifiers, RESOURCES_SCHEMA)
+        schema.validate(want, schema.get_resources_schema())
+        schema.validate(modifiers, schema.get_resources_schema())
 
         origin_id = origin.origin_id()
 
@@ -480,7 +366,7 @@
     def add(self, more):
         if more is self:
             raise RuntimeError('adding a list of resources to itself?')
-        config.add(self, copy.deepcopy(more))
+        schema.add(self, copy.deepcopy(more))
 
     def mark_reserved_by(self, origin_id):
         for key, item_list in self.items():
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index fecb7a6..6d2916c 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -21,7 +21,7 @@
 import sys
 import time
 import pprint
-from .core import config, log, util, process
+from .core import config, log, util, process, schema
 from .core.event_loop import MainLoop
 from .obj import nitb_osmo, hlr_osmo, mgcpgw_osmo, mgw_osmo, msc_osmo, bsc_osmo, stp_osmo, ggsn_osmo, sgsn_osmo, esme, osmocon, ms_driver, iperf3
 from .obj import run_node
@@ -50,7 +50,7 @@
             raise RuntimeError('No such directory: %r' % self.suite_dir)
         self.conf = config.read(os.path.join(self.suite_dir,
                                              SuiteDefinition.CONF_FILENAME),
-                                resource.CONF_SCHEMA)
+                                schema.get_all_schema())
         self.load_test_basenames()
 
     def load_test_basenames(self):
@@ -143,7 +143,7 @@
             log.dbg(scenario=scenario.name(), conf=c)
             if c is None:
                 continue
-            config.combine(combination, c)
+            schema.combine(combination, c)
         return combination
 
     def get_run_dir(self):
@@ -486,7 +486,7 @@
 def load_suite_scenario_str(suite_scenario_str):
     suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
     suite = load(suite_name)
-    scenarios = [config.get_scenario(scenario_name, resource.CONF_SCHEMA) for scenario_name in scenario_names]
+    scenarios = [config.get_scenario(scenario_name, schema.get_all_schema()) for scenario_name in scenario_names]
     return (suite_scenario_str, suite, scenarios)
 
 def bts_obj(suite_run, conf):

-- 
To view, visit https://gerrit.osmocom.org/c/osmo-gsm-tester/+/18023
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-gsm-tester
Gerrit-Branch: master
Gerrit-Change-Id: I8fd6773c51d19405a585977af4ed72cad2b21db1
Gerrit-Change-Number: 18023
Gerrit-PatchSet: 1
Gerrit-Owner: pespin <pespin at sysmocom.de>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20200504/6e8a0907/attachment.htm>


More information about the gerrit-log mailing list