fix octphy, fix conf, improve logging

Clearly separate the kinds of BTS hardware the GSM tester knows ('type') from
the NITB's bts/type config item ('osmobsc_bts_type' -- not 'osmonitb_...' to
stay in tune with future developments: it is the libbsc that needs this).

For BTS hardware kinds, use the full name of the binary for osmo driven models:
osmo-bts-sysmo, osmo-bts-trx, osmo-bts-octphy.

Change-Id: I1aa9b48e74013a93f9db1a34730f17717fb3b36c
diff --git a/src/osmo_gsm_tester/bts_octphy.py b/src/osmo_gsm_tester/bts_octphy.py
index 4396108..1f36a79 100644
--- a/src/osmo_gsm_tester/bts_octphy.py
+++ b/src/osmo_gsm_tester/bts_octphy.py
@@ -52,7 +52,7 @@
             raise RuntimeError('No lib/ in %r' % self.inst)
         self.env = { 'LD_LIBRARY_PATH': lib }
 
-        self.launch_process(OsmoBtsTrx.BIN_BTS_OCTPHY, '-r', '1', '-c', os.path.abspath(self.config_file))
+        self.launch_process(OsmoBtsOctphy.BIN_BTS_OCTPHY, '-r', '1', '-c', os.path.abspath(self.config_file))
         self.suite_run.poll()
 
     def launch_process(self, binary_name, *args):
@@ -87,8 +87,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_octphy'))
         config.overlay(values, self.conf)
-        # using type 'sysmobts' for osmo-bts-octphy
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/bts_osmotrx.py b/src/osmo_gsm_tester/bts_osmotrx.py
index 417fbf2..71cdd48 100644
--- a/src/osmo_gsm_tester/bts_osmotrx.py
+++ b/src/osmo_gsm_tester/bts_osmotrx.py
@@ -92,8 +92,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_trx'))
         config.overlay(values, self.conf)
-        # using type 'sysmobts' for osmo-bts-trx
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/bts_sysmo.py b/src/osmo_gsm_tester/bts_sysmo.py
index 64fa7f4..dd396ff 100644
--- a/src/osmo_gsm_tester/bts_sysmo.py
+++ b/src/osmo_gsm_tester/bts_sysmo.py
@@ -127,7 +127,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_sysmo'))
         config.overlay(values, self.conf)
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/config.py b/src/osmo_gsm_tester/config.py
index cbbfa6f..68bbd13 100644
--- a/src/osmo_gsm_tester/config.py
+++ b/src/osmo_gsm_tester/config.py
@@ -177,6 +177,12 @@
     defaults = read_config_file('default.conf', if_missing_return={})
     return defaults.get(for_kind, {})
 
+class Scenario(log.Origin, dict):
+    def __init__(self, name, path):
+        self.set_name(name)
+        self.set_log_category(log.C_TST)
+        self.path = path
+
 def get_scenario(name, validation_schema=None):
     scenarios_dir = get_scenarios_dir()
     if not name.endswith('.conf'):
@@ -184,7 +190,9 @@
     path = scenarios_dir.child(name)
     if not os.path.isfile(path):
         raise RuntimeError('No such scenario file: %r' % path)
-    return read(path, validation_schema=validation_schema)
+    sc = Scenario(name, path)
+    sc.update(read(path, validation_schema=validation_schema))
+    return sc
 
 def add(dest, src):
     if is_dict(dest):
diff --git a/src/osmo_gsm_tester/log.py b/src/osmo_gsm_tester/log.py
index f56d2c9..3e96999 100644
--- a/src/osmo_gsm_tester/log.py
+++ b/src/osmo_gsm_tester/log.py
@@ -186,6 +186,14 @@
             log_str = log_str + '\n'
         self.log_sink(log_str)
 
+    def large_separator(self, *msgs):
+        msg = ' '.join(msgs)
+        if not msg:
+            msg = '------------------------------------------'
+        self.log_sink('------------------------------------------\n'
+                      '%s\n'
+                      '------------------------------------------\n' % msg)
+
 targets = [ LogTarget() ]
 
 def level_str(level):
@@ -207,6 +215,10 @@
     for target in targets:
         target.log(origin, category, level, src, messages, named_items)
 
+def large_separator(*msgs):
+    for target in targets:
+        target.large_separator(*msgs)
+
 def get_src_from_caller(levels_up=1):
     caller = getframeinfo(stack()[levels_up][0])
     return '%s:%d' % (os.path.basename(caller.filename), caller.lineno)
diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py
index 5cfbeaf..b842d98 100644
--- a/src/osmo_gsm_tester/resource.py
+++ b/src/osmo_gsm_tester/resource.py
@@ -29,7 +29,7 @@
 from . import schema
 from . import ofono_client
 from . import osmo_nitb
-from . import bts_sysmo, bts_osmotrx
+from . import bts_sysmo, bts_osmotrx, bts_octphy
 
 from .util import is_dict, is_list
 
@@ -54,7 +54,8 @@
         'bts[].ipa_unit_id': schema.INT,
         'bts[].addr': schema.IPV4,
         'bts[].band': schema.BAND,
-        'bts[].trx[].hw_addr': schema.HWADDR,
+        'bts[].trx_list[].hw_addr': schema.HWADDR,
+        'bts[].trx_list[].net_device': schema.STR,
         'arfcn[].arfcn': schema.INT,
         'arfcn[].band': schema.BAND,
         'modem[].label': schema.STR,
@@ -68,8 +69,9 @@
     RESOURCES_SCHEMA)
 
 KNOWN_BTS_TYPES = {
-        'sysmo': bts_sysmo.SysmoBts,
-        'osmotrx': bts_osmotrx.OsmoBtsTrx,
+        'osmo-bts-sysmo': bts_sysmo.SysmoBts,
+        'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
+        'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
     }
 
 def register_bts_type(name, clazz):
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index 08965b5..74c8b28 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -20,6 +20,7 @@
 import os
 import sys
 import time
+import copy
 from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb
 from . import test
 
@@ -56,7 +57,6 @@
                                     SuiteDefinition.CONF_SCHEMA)
             self.load_tests()
 
-
     def load_tests(self):
         with self:
             self.tests = []
@@ -122,22 +122,27 @@
     _config = None
     _processes = None
 
-    def __init__(self, current_trial, suite_definition, scenarios=[]):
+    def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
         self.trial = current_trial
         self.definition = suite_definition
         self.scenarios = scenarios
-        self.set_name(suite_definition.name())
+        self.set_name(suite_scenario_str)
         self.set_log_category(log.C_TST)
         self.resources_pool = resource.ResourcesPool()
 
     def combined(self, conf_name):
-        combination = self.definition.conf.get(conf_name) or {}
-        for scenario in self.scenarios:
-            c = scenario.get(conf_name)
-            if c is None:
-                continue
-            config.combine(combination, c)
-        return combination
+        self.dbg(combining=conf_name)
+        with log.Origin(combining_scenarios=conf_name):
+            combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
+            self.dbg(definition_conf=combination)
+            for scenario in self.scenarios:
+                with scenario:
+                    c = scenario.get(conf_name)
+                    self.dbg(scenario=scenario.name(), conf=c)
+                    if c is None:
+                        continue
+                    config.combine(combination, c)
+            return combination
 
     def resource_requirements(self):
         if self._resource_requirements is None:
@@ -183,6 +188,7 @@
             self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
 
     def run_tests(self, names=None):
+        self.log('Suite run start')
         if not self.reserved_resources:
             self.reserve_resources()
         results = SuiteRun.Results()
@@ -281,7 +287,6 @@
         self.log('prompt entered:', entered)
         return entered
 
-
 loaded_suite_definitions = {}
 
 def load(suite_name):
@@ -319,7 +324,7 @@
     suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
     suite = load(suite_name)
     scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
-    return (suite, scenarios)
+    return (suite_scenario_str, suite, scenarios)
 
 def bts_obj(suite_run, conf):
     bts_type = conf.get('type')
diff --git a/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
index 90d6092..bf6adf9 100644
--- a/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
@@ -20,13 +20,17 @@
 line vty
  no login
 !
-phy 0
- octphy hw-addr ${osmo_bts_octphy.hw_addr}
- octphy net-device ${osmo_bts_octphy.net_device}
+%for trx in osmo_bts_octphy.trx_list:
+phy ${loop.index}
+ octphy hw-addr ${trx.hw_addr}
+ octphy net-device ${trx.net_device}
  instance 0
+%endfor
 bts 0
  band ${osmo_bts_octphy.band}
  ipa unit-id ${osmo_bts_octphy.ipa_unit_id} 0
  oml remote-ip ${osmo_bts_octphy.oml_remote_ip}
- trx 0
-  phy 0 instance 0
+%for trx in osmo_bts_octphy.trx_list:
+ trx ${loop.index}
+  phy ${loop.index} instance 0
+%endfor
diff --git a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
index e7dc119..28cf61c 100644
--- a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
@@ -46,7 +46,7 @@
  timer t3141 0
 %for bts in nitb.net.bts_list:
  bts ${loop.index}
-  type ${bts.type}
+  type ${bts.osmobsc_bts_type}
   band ${bts.band}
   cell_identity 0
   location_area_code ${bts.location_area_code}
diff --git a/src/osmo_gsm_tester/util.py b/src/osmo_gsm_tester/util.py
index 61d0f6e..e132e21 100644
--- a/src/osmo_gsm_tester/util.py
+++ b/src/osmo_gsm_tester/util.py
@@ -74,6 +74,7 @@
         return dict2obj(self.obj[key])
 
     def __getattr__(self, key):
+        'provide error information to know which template item was missing'
         try:
             return dict2obj(getattr(self.obj, key))
         except AttributeError:
@@ -82,19 +83,9 @@
             except KeyError:
                 raise AttributeError(key)
 
-class ListProxy:
-    'allow nesting for DictProxy'
-    def __init__(self, obj):
-        self.obj = obj
-
-    def __getitem__(self, key):
-        return dict2obj(self.obj[key])
-
 def dict2obj(value):
-    if isinstance(value, dict):
+    if is_list(value) or is_dict(value):
         return DictProxy(value)
-    if isinstance(value, (tuple, list)):
-        return ListProxy(value)
     return value