trying to get sysmobts to work and various details

I know that these commit messages aren't very good, but the code is not stable
yet, so I'm not bothering with details.

Change-Id: I2d5e5f4a5407725d71093cbd71ef97b271eb8197
diff --git a/selftest/real_suite/default-suites.conf b/selftest/real_suite/default-suites.conf
index 84a28b6..601076a 100644
--- a/selftest/real_suite/default-suites.conf
+++ b/selftest/real_suite/default-suites.conf
@@ -1 +1 @@
-- sms:trx
+- sms:sysmo
diff --git a/selftest/real_suite/default.conf b/selftest/real_suite/default.conf
index b247722..251a7b8 100644
--- a/selftest/real_suite/default.conf
+++ b/selftest/real_suite/default.conf
@@ -17,12 +17,12 @@
     timeslot_list:
     - phys_chan_config: CCCH+SDCCH4
     - phys_chan_config: SDCCH8
-    - phys_chan_config: TCH_F/TCH_H/PDCH
-    - phys_chan_config: TCH_F/TCH_H/PDCH
-    - phys_chan_config: TCH_F/TCH_H/PDCH
-    - phys_chan_config: TCH_F/TCH_H/PDCH
-    - phys_chan_config: TCH_F/TCH_H/PDCH
-    - phys_chan_config: TCH_F/TCH_H/PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
+    - phys_chan_config: TCH/F_TCH/H_PDCH
 
 osmo_bts_sysmo:
   ipa_unit_id: 1123
diff --git a/selftest/real_suite/resources.conf b/selftest/real_suite/resources.conf
index a6c396b..5fe4ca5 100644
--- a/selftest/real_suite/resources.conf
+++ b/selftest/real_suite/resources.conf
@@ -1,9 +1,9 @@
 # all hardware and interfaces available to this osmo-gsm-tester
 
 nitb_iface:
-- addr: 127.0.0.10
-- addr: 127.0.0.11
-- addr: 127.0.0.12
+- addr: 10.42.42.1
+- addr: 10.42.42.2
+- addr: 10.42.42.3
 
 bts:
 - label: sysmoBTS 1002
diff --git a/selftest/real_suite/scenarios/sysmo.conf b/selftest/real_suite/scenarios/sysmo.conf
new file mode 100644
index 0000000..5980c1a
--- /dev/null
+++ b/selftest/real_suite/scenarios/sysmo.conf
@@ -0,0 +1,3 @@
+resources:
+  bts:
+  - type: sysmo
diff --git a/selftest/real_suite/suites/sms/mo_mt_sms.py b/selftest/real_suite/suites/sms/mo_mt_sms.py
index b97d332..9c8cca9 100755
--- a/selftest/real_suite/suites/sms/mo_mt_sms.py
+++ b/selftest/real_suite/suites/sms/mo_mt_sms.py
@@ -10,7 +10,7 @@
 print('start nitb and bts...')
 nitb.bts_add(bts)
 nitb.start()
-sleep(.1)
+sleep(1)
 assert nitb.running()
 bts.start()
 
@@ -19,7 +19,7 @@
 
 ms_mo.connect(nitb)
 ms_mt.connect(nitb)
-wait(nitb.subscriber_attached, ms_mo, ms_mt)
+wait(nitb.subscriber_attached, ms_mo, ms_mt, timeout=20)
 
 sms = ms_mo.sms_send(ms_mt.msisdn)
 wait(ms_mt.sms_received, sms)
diff --git a/src/osmo_gsm_tester/bts_osmotrx.py b/src/osmo_gsm_tester/bts_osmotrx.py
index cff63ab..2bdacec 100644
--- a/src/osmo_gsm_tester/bts_osmotrx.py
+++ b/src/osmo_gsm_tester/bts_osmotrx.py
@@ -24,7 +24,6 @@
     suite_run = None
     nitb = None
     run_dir = None
-    processes = None
     inst = None
     env = None
 
@@ -37,8 +36,6 @@
         self.conf = conf
         self.set_name('osmo-bts-trx')
         self.set_log_category(log.C_RUN)
-        self.processes = {}
-        self.inst = None
         self.env = {}
 
     def start(self):
@@ -62,9 +59,6 @@
         self.suite_run.poll()
 
     def launch_process(self, binary_name, *args):
-        if self.processes.get(binary_name) is not None:
-            raise RuntimeError('Attempt to launch twice: %r' % binary_name)
-
         binary = os.path.abspath(self.inst.child('bin', binary_name))
         run_dir = self.run_dir.new_dir(binary_name)
         if not os.path.isfile(binary):
@@ -72,7 +66,6 @@
         proc = process.Process(binary_name, run_dir,
                                (binary,) + args,
                                env=self.env)
-        self.processes[binary_name] = proc
         self.suite_run.remember_to_stop(proc)
         proc.launch()
 
diff --git a/src/osmo_gsm_tester/bts_sysmo.py b/src/osmo_gsm_tester/bts_sysmo.py
index de79f65..190297f 100644
--- a/src/osmo_gsm_tester/bts_sysmo.py
+++ b/src/osmo_gsm_tester/bts_sysmo.py
@@ -17,18 +17,29 @@
 # 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/>.
 
-from . import log, config, util, template
+import os
+from . import log, config, util, template, process
 
 class SysmoBts(log.Origin):
     suite_run = None
     nitb = None
     run_dir = None
+    inst = None
+    remote_addr = None
+    remote_inst = None
+    remote_env = None
+    remote_dir = None
+
+    REMOTE_DIR = '/osmo-gsm-tester'
+    BTS_SYSMO_BIN = 'osmo-bts-sysmo'
+    BTS_SYSMO_CFG = 'osmo-bts-sysmo.cfg'
 
     def __init__(self, suite_run, conf):
         self.suite_run = suite_run
         self.conf = conf
         self.set_name('osmo-bts-sysmo')
         self.set_log_category(log.C_RUN)
+        self.remote_env = {}
 
     def start(self):
         if self.nitb is None:
@@ -36,12 +47,68 @@
         self.log('Starting sysmoBTS to connect to', self.nitb)
         self.run_dir = util.Dir(self.suite_run.trial.get_run_dir().new_dir(self.name()))
         self.configure()
-        self.err('SysmoBts is not yet implemented')
+
+        self.inst = util.Dir(os.path.abspath(self.suite_run.trial.get_inst(SysmoBts.BTS_SYSMO_BIN)))
+        lib = self.inst.child('lib')
+        if not os.path.isdir(lib):
+            self.raise_exn('No lib/ in', self.inst)
+        if not self.inst.isfile('bin', SysmoBts.BTS_SYSMO_BIN):
+            self.raise_exn('No osmo-bts-sysmo binary in', self.inst)
+
+        self.remote_dir = util.Dir(SysmoBts.REMOTE_DIR)
+        self.remote_inst = util.Dir(self.remote_dir.child(os.path.basename(str(self.inst))))
+
+        self.run_remote('rm-remote-dir', ('test', '!', '-d', SysmoBts.REMOTE_DIR, '||', 'rm', '-rf', SysmoBts.REMOTE_DIR))
+        self.run_remote('mk-remote-dir', ('mkdir', '-p', SysmoBts.REMOTE_DIR))
+        self.run_local('scp-inst-to-sysmobts',
+            ('scp', '-r', str(self.inst), '%s:%s' % (self.remote_addr, str(self.remote_inst))))
+
+        remote_run_dir = self.remote_dir.child(SysmoBts.BTS_SYSMO_BIN)
+        self.run_remote('mk-remote-run-dir', ('mkdir', '-p', remote_run_dir))
+
+        remote_config_file = self.remote_dir.child(SysmoBts.BTS_SYSMO_CFG)
+        self.run_local('scp-cfg-to-sysmobts',
+            ('scp', '-r', self.config_file, '%s:%s' % (self.remote_addr, remote_config_file)))
+
+        remote_lib = self.remote_inst.child('lib')
+        remote_binary = self.remote_inst.child('bin', 'osmo-bts-sysmo')
+        self.launch_remote('osmo-bts-sysmo',
+            ('LD_LIBRARY_PATH=%s' % remote_lib,
+             remote_binary, '-c', remote_config_file, '-r', '1'),
+            remote_cwd=remote_run_dir)
+
+    def _process_remote(self, name, popen_args, remote_cwd=None):
+        run_dir = self.run_dir.new_dir(name)
+        return process.RemoteProcess(name, run_dir, self.remote_addr, remote_cwd,
+                                     popen_args)
+
+    def run_remote(self, name, popen_args, remote_cwd=None):
+        proc = self._process_remote(name, popen_args, remote_cwd)
+        proc.launch()
+        proc.wait()
+        if proc.result != 0:
+            proc.raise_exn('Exited in error')
+
+    def launch_remote(self, name, popen_args, remote_cwd=None):
+        proc = self._process_remote(name, popen_args, remote_cwd)
+        self.suite_run.remember_to_stop(proc)
+        proc.launch()
+
+    def run_local(self, name, popen_args):
+        run_dir = self.run_dir.new_dir(name)
+        proc = process.Process(name, run_dir, popen_args)
+        proc.launch()
+        proc.wait()
+        if proc.result != 0:
+            proc.raise_exn('Exited in error')
 
     def configure(self):
         if self.nitb is None:
             raise RuntimeError('BTS needs to be added to a NITB before it can be configured')
-        self.config_file = self.run_dir.new_file('osmo-bts-sysmo.cfg')
+
+        self.remote_addr = self.conf.get('addr')
+
+        self.config_file = self.run_dir.new_file(SysmoBts.BTS_SYSMO_CFG)
         self.dbg(config_file=self.config_file)
 
         values = { 'osmo_bts_sysmo': config.get_defaults('osmo_bts_sysmo') }
@@ -51,7 +118,7 @@
         self.dbg(conf=values)
 
         with open(self.config_file, 'w') as f:
-            r = template.render('osmo-bts-sysmo.cfg', values)
+            r = template.render(SysmoBts.BTS_SYSMO_CFG, values)
             self.dbg(r)
             f.write(r)
 
diff --git a/src/osmo_gsm_tester/log.py b/src/osmo_gsm_tester/log.py
index a4f78df..f56d2c9 100644
--- a/src/osmo_gsm_tester/log.py
+++ b/src/osmo_gsm_tester/log.py
@@ -66,7 +66,7 @@
     get_time_str = lambda self: time.strftime(self.log_time_fmt)
 
     # sink that gets each complete logging line
-    log_sink = sys.stderr.write
+    log_sink = sys.__stdout__.write
 
     category_levels = None
 
@@ -182,8 +182,9 @@
                               loglevel,
                               ' '.join(log_line))
 
-        self.log_sink(log_str.strip() + '\n')
-
+        if not log_str.endswith('\n'):
+            log_str = log_str + '\n'
+        self.log_sink(log_str)
 
 targets = [ LogTarget() ]
 
diff --git a/src/osmo_gsm_tester/osmo_ctrl.py b/src/osmo_gsm_tester/osmo_ctrl.py
index c3a09db..736c943 100644
--- a/src/osmo_gsm_tester/osmo_ctrl.py
+++ b/src/osmo_gsm_tester/osmo_ctrl.py
@@ -79,10 +79,12 @@
         self._send(getmsg)
 
     def __enter__(self):
+        super().__enter__()
         self.connect()
         return self
 
     def __exit__(self, *exc_info):
         self.disconnect()
+        super().__exit__(*exc_info)
 
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/process.py b/src/osmo_gsm_tester/process.py
index 8152ff0..78814c0 100644
--- a/src/osmo_gsm_tester/process.py
+++ b/src/osmo_gsm_tester/process.py
@@ -22,7 +22,7 @@
 import subprocess
 import signal
 
-from . import log
+from . import log, test
 from .util import Dir
 
 class Process(log.Origin):
@@ -166,7 +166,9 @@
         if self.result is not None:
             self.cleanup()
 
-    def is_running(self):
+    def is_running(self, poll_first=True):
+        if poll_first:
+            self.poll()
         return self.process_obj is not None and self.result is None
 
     def get_output(self, which):
@@ -178,9 +180,12 @@
             return f2.read()
 
     def get_output_tail(self, which, tail=10, prefix=''):
-        out = self.get_output(which).splitlines()
+        out = self.get_output(which)
+        if not out:
+            return None
+        out = out.splitlines()
         tail = min(len(out), tail)
-        return ('\n' + prefix).join(out[-tail:])
+        return prefix + ('\n' + prefix).join(out[-tail:])
 
     def get_stdout(self):
         return self.get_output('stdout')
@@ -194,28 +199,32 @@
     def get_stderr_tail(self, tail=10, prefix=''):
         return self.get_output_tail('stderr', tail, prefix)
 
-    def terminated(self):
-        self.poll()
+    def terminated(self, poll_first=True):
+        if poll_first:
+            self.poll()
         return self.result is not None
 
-    def wait(self):
-        self.process_obj.wait()
-        self.poll()
+    def wait(self, timeout=300):
+        test.wait(self.terminated, timeout=timeout)
 
 
 class RemoteProcess(Process):
 
-    def __init__(self, remote_host, remote_cwd, *process_args, **process_kwargs):
-        super().__init__(*process_args, **process_kwargs)
+    def __init__(self, name, run_dir, remote_host, remote_cwd, popen_args, **popen_kwargs):
+        super().__init__(name, run_dir, popen_args, **popen_kwargs)
         self.remote_host = remote_host
         self.remote_cwd = remote_cwd
 
         # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
         # over the ssh link, we should probably run on the remote side,
         # monitoring the process remotely.
-        self.popen_args = ['ssh', '-t', self.remote_host,
-                           'cd "%s"; %s' % (self.remote_cwd,
-                                            ' '.join(['"%s"' % arg for arg in self.popen_args]))]
+        if self.remote_cwd:
+            cd = 'cd "%s"; ' % self.remote_cwd
+        else:
+            cd = ''
+        self.popen_args = ['ssh', self.remote_host,
+                           '%s%s' % (cd,
+                                     ' '.join(self.popen_args))]
         self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
 
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index 2d6c67b..08965b5 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -232,9 +232,11 @@
         self.log('using MSISDN', msisdn)
         return msisdn
 
-    def wait(self, condition, *condition_args, timeout=300, **condition_kwargs):
+    def _wait(self, condition, condition_args, condition_kwargs, timeout, timestep):
         if not timeout or timeout < 0:
             raise RuntimeError('wait() *must* time out at some point. timeout=%r' % timeout)
+        if timestep < 0.1:
+            timestep = 0.1
 
         started = time.time()
         while True:
@@ -244,17 +246,21 @@
             waited = time.time() - started
             if waited > timeout:
                 return False
-            time.sleep(.1)
+            time.sleep(timestep)
+
+    def wait(self, condition, *condition_args, timeout=300, timestep=1, **condition_kwargs):
+        if not self._wait(condition, condition_args, condition_kwargs, timeout, timestep):
+            raise RuntimeError('Timeout expired')
 
     def sleep(self, seconds):
-        self.wait(lambda: False, timeout=seconds)
+        assert seconds > 0.
+        self._wait(lambda: False, [], {}, timeout=seconds, timestep=min(seconds, 1))
 
     def poll(self):
         ofono_client.poll()
         if self._processes:
             for process in self._processes:
-                process.poll()
-                if not process.is_running():
+                if process.terminated():
                     process.log_stdout_tail()
                     process.log_stderr_tail()
                     process.raise_exn('Process ended prematurely')