Change in osmo-gsm-tester[master]: Introduce Android UEs as new modems

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/.

ninjab3s gerrit-no-reply at lists.osmocom.org
Mon Nov 23 15:00:55 UTC 2020


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


Change subject: Introduce Android UEs as new modems
......................................................................

Introduce Android UEs as new modems

To expand the test capacities we would like to introduce
Android UEs as new modems. Currently the following tests
are supported:
- Ping
- iPerf3 DL/UL
- RRC Mobile MT Ping

In the following is a small description.

Prerequisites:
    - Android UE
        - Rooted (Ping, iPerf, RRC Idle MT Ping)
        - Qualcomm baseband with working diag_mdlog (RRC Idle MT Ping)
        - iPerf3
        - Dropbear
    - OGT Slave Unit
        - ADB (sudo apt-get install adb)
        - Pycrate (https://github.com/P1sec/pycrate)
        - SCAT
            clone https://github.com/bedrankara/scat/ & install dependencies
            checkout branch ogt
            symlink scat (ln -s ~/scat/scat.py /usr/local/bin/scat)

Infrastructure explaination:
The Android UEs are connected to the OGT Units via USB. We
activate tethering and set up a SSH server (with Dropbear).
We chose tethering over WiFi to have a more stable route
for the ssh connection. We forward incoming connections to
the OGT unit hosting the Android UE(s) on specific ports
to the UEs via iptables. This enables OGT to issue commands
directly to the UEs. In case of local execution we use ADB
to issue commands to the AndroidUE. The set up was tested
with 5 Android UEs connected in parallel but it should be
scalable to the number of available IPs in the respective
subnet. Furthermore, we need to cross compile Dropbear
and iPerf3 to use them on the UEs. These tools have to be
added to the $PATH variable of the UEs.

Examplary set up:
In this example we have two separate OGT units (master
and slave) and two Android UEs that are connected to the
slave unit. An illustration may be found here: https://ibb.co/6BXSP2C

On UE 1:
ip address add 192.168.42.130/24 dev rndis0
ip route add 192.168.42.0/24 dev rndis0 table local_network
dropbearmulti dropbear -F -E -p 130 -R -T /data/local/tmp/authorized_keys  -U 0 -G 0 -N root -A

On UE 2:
ip address add 192.168.42.131/24 dev rndis0
ip route add 192.168.42.0/24 dev rndis0 table local_network
dropbearmulti dropbear -F -E -p 131 -R -T /data/local/tmp/authorized_keys  -U 0 -G 0 -N root -A

On OGT slave unit:
sudo ip link add name ogt type bridge
sudo ip l set eth0 master ogt
sudo ip l set enp0s20f0u1 master ogt
sudo ip l set enp0s20f0u2 master ogt
sudo ip a a 192.168.42.1/24 dev ogt
sudo ip link set ogt up

Now we have to manually connect to every UE from OGT Master
to set up SSH keys and verify that the setup works.
Therefore, use:
ssh -p [UE-PORT] root@[OGT SLAVE UNIT's IP]

Finally, to finish the setup procedure create the
remote_run_dir for Android UEs on the slave unit like
following:
mkdir /osmo-gsm-tester-androidue
chown jenkins /osmo-gsm-tester-androidue

Example for modem in resource.conf:
- label: mi5g
  type: androidue
  imsi: '901700000034757'
  ki: '85E9E9A947B9ACBB966ED7113C7E1B8A'
  opc: '3E1C73A29B9C293DC5A763E42C061F15'
  ue_serial: '8d9d79a9'
  apn_name: 'srsapn'
  apn: 'srsapn'
  apn_mcc: '901'
  apn_mnc: '70'
  sel_apn: 'True'
  auth_algo: 'milenage'
  features: ['4g', 'dl_qam256', 'qc_diag']
  run_node:
    run_type: ssh
    run_addr: 100.113.1.170
    ssh_user: jenkins
    ssh_addr: 100.113.1.170
    ue_ssh_port: 130

Example for default-suites.conf:
- 4g:ms-label at mi5g+srsenb-rftype@uhd+mod-enb-nprb at 25+mod-enb-txmode@1

Change-Id: I79a5d803e869a868d4dac5e0d4c2feb38038dc5c
---
M src/osmo_gsm_tester/core/schema.py
A src/osmo_gsm_tester/obj/bitrate_monitor.py
M src/osmo_gsm_tester/obj/iperf3.py
M src/osmo_gsm_tester/obj/ms.py
A src/osmo_gsm_tester/obj/ms_android.py
A src/osmo_gsm_tester/obj/qc_diag.py
A sysmocom/scenarios/ms-label.conf
M sysmocom/suites/4g/iperf3_dl.py
M sysmocom/suites/4g/iperf3_ul.py
A utils/bin/osmo-gsm-tester_androidue_conn_chk.sh
A utils/bin/osmo-gsm-tester_androidue_diag_parser.sh
11 files changed, 801 insertions(+), 9 deletions(-)



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

diff --git a/src/osmo_gsm_tester/core/schema.py b/src/osmo_gsm_tester/core/schema.py
index 9d26b0f..96d85f5 100644
--- a/src/osmo_gsm_tester/core/schema.py
+++ b/src/osmo_gsm_tester/core/schema.py
@@ -111,7 +111,7 @@
     raise ValueError('Unknown Cipher value: %r' % val)
 
 def modem_feature(val):
-    if val in ('sms', 'gprs', 'voice', 'ussd', 'sim', '2g', '3g', '4g', 'dl_qam256'):
+    if val in ('sms', 'gprs', 'voice', 'ussd', 'sim', '2g', '3g', '4g', 'dl_qam256', 'qc_diag'):
         return True
     raise ValueError('Unknown Modem Feature: %r' % val)
 
diff --git a/src/osmo_gsm_tester/obj/bitrate_monitor.py b/src/osmo_gsm_tester/obj/bitrate_monitor.py
new file mode 100644
index 0000000..299a052
--- /dev/null
+++ b/src/osmo_gsm_tester/obj/bitrate_monitor.py
@@ -0,0 +1,145 @@
+# osmo_gsm_tester: specifics for running an AndroidUE modem
+#
+# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Nils Fürste <nils.fuerste at softwareradiosystems.com>
+# Author: Bedran Karakoc <bedran.karakoc at softwareradiosystems.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ..core import log, process
+
+
+class BitRateMonitor(log.Origin):
+
+##############
+# PROTECTED
+##############
+    def __init__(self, testenv, run_dir, run_node, rem_host, ue_serial, ue_data_intf, remote_port):
+        super().__init__(log.C_TST, 'BitRateMonitor_%s' % ue_serial)
+        self.testenv = testenv
+        self.rem_host = rem_host
+        self._run_node = run_node
+        self.run_dir = run_dir
+        self.ue_serial = ue_serial
+        self.ue_data_intf = ue_data_intf
+        self.remote_port = remote_port
+        self.rx_monitor_proc = None
+        self.tx_monitor_proc = None
+
+########################
+# PUBLIC - INTERNAL API
+########################
+    def run_androidue_cmd(self, name, popen_args, sync=True):
+        # This function executes the given command directly on the Android UE. Therefore,
+        # ADB is used to execute commands locally and ssh for remote execution. Make sure
+        # ADB is installed
+        if self._run_node.is_local():
+            # use adb locally instead of ssh
+            adb_cmd_pref = ['adb', '-s', self.ue_serial, 'exec-out', 'su', '-c']
+            popen_args = adb_cmd_pref + list(popen_args)
+            if sync:
+                proc = process.run_local_sync(self.run_dir, name, popen_args)
+            else:
+                proc = process.run_local(self.run_dir, name, popen_args)
+        else:
+            old_user = self.rem_host.remote_user
+            self.rem_host.remote_user = 'root'
+            proc = self.rem_host.RemoteProcess(name, popen_args, remote_env={}, remote_port=self._run_node.remote_port())
+            if sync:
+                proc.launch_sync()
+            else:
+                proc.launch()
+            self.rem_host.remote_user = old_user
+        return proc
+
+###################
+# PUBLIC (test API included)
+###################
+    def start(self):
+        # start bit rate monitoring on Android UE
+        popen_args_rx_mon = ['while', 'true;', 'do',
+                             'echo', '`cat', '/sys/class/net/' + self.ue_data_intf + '/statistics/rx_bytes`;',
+                             'sleep', '1;', 'done']
+        popen_args_tx_mon = ['while', 'true;', 'do',
+                             'echo', '`cat', '/sys/class/net/' + self.ue_data_intf + '/statistics/tx_bytes`;',
+                             'sleep', '1;', 'done']
+        self.rx_monitor_proc = self.run_androidue_cmd("rx-monitor", popen_args_rx_mon, sync=False)
+        self.testenv.remember_to_stop(self.rx_monitor_proc)
+        self.tx_monitor_proc = self.run_androidue_cmd("tx-monitor", popen_args_tx_mon, sync=False)
+        self.testenv.remember_to_stop(self.tx_monitor_proc)
+
+    def stop(self):
+        self.testenv.stop_process(self.rx_monitor_proc)
+        self.testenv.stop_process(self.tx_monitor_proc)
+
+    def save_metrics(self, metrics_file):
+        brate_rx_raw = self.rx_monitor_proc.get_stdout().split('\n')
+        brate_tx_raw = self.tx_monitor_proc.get_stdout().split('\n')
+        brate_rx_raw.remove('')
+        brate_tx_raw.remove('')
+        brate_rx_l = brate_rx_raw[1:]
+        brate_tx_l = brate_tx_raw[1:]
+
+        if len(brate_rx_l) < 2 or len(brate_tx_l) < 2:
+            raise log.Error("Insufficient data available to write metrics file")
+
+        # cut of elements if lists don't have the same length
+        if len(brate_rx_l) > len(brate_tx_l):
+            brate_rx_l = brate_rx_l[:len(brate_tx_l) - len(brate_rx_l)]
+        if len(brate_rx_l) < len(brate_tx_l):
+            brate_tx_l = brate_tx_l[:len(brate_rx_l) - len(brate_tx_l)]
+
+        # get start value
+        brate_rx_last = int(brate_rx_l[0])
+        brate_tx_last = int(brate_tx_l[0])
+
+        with open(metrics_file, "w") as ue_metrics_fh:
+            ue_metrics_fh.write("time;cc;earfcn;pci;rsrp;pl;cfo;pci_neigh;rsrp_neigh;cfo_neigh;"
+                                + "dl_mcs;dl_snr;dl_turbo;dl_brate;dl_bler;"
+                                + "ul_ta;ul_mcs;ul_buff;ul_brate;ul_bler;rf_o;rf_u;rf_l;"
+                                + "is_attached\n")
+            for i in range(1, len(brate_rx_l)):
+                time = "0"
+                cc = "0"
+                earfcn = "0"
+                pci = "0"
+                rsrp = "0"
+                pl = "0"
+                cfo = "0"
+                pci_neigh = "0"
+                rsrp_neigh = "0"
+                cfo_neigh = "0"
+                dl_mcs = "0"
+                dl_snr = "0"
+                dl_turbo = "0"
+                dl_brate = str((int(brate_rx_l[i]) - brate_rx_last) * 8)
+                brate_rx_last = int(brate_rx_l[i])
+                dl_bler = "0"
+                ul_ta = "0"
+                ul_mcs = "0"
+                ul_buff = "0"
+                ul_brate = str((int(brate_tx_l[i]) - brate_tx_last) * 8)
+                brate_tx_last = int(brate_tx_l[i])
+                ul_bler = "0"
+                rf_o = "0"
+                rf_u = "0"
+                rf_l = "0"
+                is_attached = "0"
+
+                line = time + ";" + cc + ";" + earfcn + ";" + pci + ";" + rsrp + ";" + pl + ";" + cfo + ";" \
+                       + pci_neigh + ";" + rsrp_neigh + ";" + cfo_neigh + ";" + dl_mcs + ";" + dl_snr + ";" \
+                       + dl_turbo + ";" + dl_brate + ";" + dl_bler + ";" + ul_ta + ";" + ul_mcs + ";" + ul_buff + ";" \
+                       + ul_brate + ";" + ul_bler + ";" + rf_o + ";" + rf_u + ";" + rf_l + ";" + is_attached
+                ue_metrics_fh.write(line + "\n")
diff --git a/src/osmo_gsm_tester/obj/iperf3.py b/src/osmo_gsm_tester/obj/iperf3.py
index 2039a9b..5fe51d1 100644
--- a/src/osmo_gsm_tester/obj/iperf3.py
+++ b/src/osmo_gsm_tester/obj/iperf3.py
@@ -232,7 +232,7 @@
         locally = not self._run_node or self._run_node.is_local()
         return locally
 
-    def prepare_test_proc(self, dir=None, netns=None, time_sec=None, proto=None, bitrate=0, tos=None):
+    def prepare_test_proc(self, dir=None, netns=None, time_sec=None, proto=None, bitrate=0, tos=None, ue=None):
         values = config.get_defaults('iperf3cli')
         config.overlay(values, self.testenv.suite().config().get('iperf3cli', {}))
 
@@ -264,6 +264,7 @@
         popen_args = ('iperf3', '-c',  self.server.addr(),
                       '-p', str(self.server.port()), '-J',
                       '-t', str(time_sec))
+
         if dir == IPerf3Client.DIR_DL:
             popen_args += ('-R',)
         elif dir == IPerf3Client.DIR_BI:
@@ -273,14 +274,20 @@
         if tos is not None:
             popen_args += ('-S', str(tos))
 
+        ue_serial = None
+        remote_port = None
+        if ue and ue.__class__.__name__ == 'AndroidUE':
+            ue_serial = ue.ue_serial
+            remote_port = ue.get_remote_port()
+
         if self.runs_locally():
-            proc = self.prepare_test_proc_locally(netns, popen_args)
+            proc = self.prepare_test_proc_locally(netns, popen_args, ue_serial)
         else:
-            proc = self.prepare_test_proc_remotely(netns, popen_args)
+            proc = self.prepare_test_proc_remotely(netns, popen_args, remote_port)
         proc.set_default_wait_timeout(time_sec + 120) # leave extra time for remote run, ctrl conn establishment, buffer draining, etc.
         return proc
 
-    def prepare_test_proc_remotely(self, netns, popen_args):
+    def prepare_test_proc_remotely(self, netns, popen_args, remote_port):
         self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
 
         remote_prefix_dir = util.Dir(IPerf3Client.REMOTE_DIR)
@@ -295,16 +302,21 @@
         if netns:
             self.process = self.rem_host.RemoteNetNSProcess(self.name(), netns, popen_args, env={})
         else:
-            self.process = self.rem_host.RemoteProcess(self.name(), popen_args, env={})
+            self.process = self.rem_host.RemoteProcess(self.name(), popen_args, remote_port=remote_port, env={})
         return self.process
 
-    def prepare_test_proc_locally(self, netns, popen_args):
+    def prepare_test_proc_locally(self, netns, popen_args, ue_serial):
         pcap_recorder.PcapRecorder(self.testenv, self.run_dir.new_dir('pcap'), None,
                                    'host %s and port not 22' % self.server.addr(), netns)
 
         if self.logfile_supported:
             popen_args += ('--logfile', os.path.abspath(self.log_file),)
 
+        # In case of an Android UE we need to extend the command to execute it with ADB
+        if ue_serial:
+            adb_cmd_pref = ['adb', '-s', ue_serial, 'exec-out', 'su', '-c']
+            popen_args = adb_cmd_pref + list(popen_args)
+
         if netns:
             self.process = process.NetNSProcess(self.name(), self.run_dir, netns, popen_args, env={})
         else:
diff --git a/src/osmo_gsm_tester/obj/ms.py b/src/osmo_gsm_tester/obj/ms.py
index 70ce558..bfd3f3b 100644
--- a/src/osmo_gsm_tester/obj/ms.py
+++ b/src/osmo_gsm_tester/obj/ms.py
@@ -72,6 +72,9 @@
         elif ms_type == 'srsue':
             from .ms_srs import srsUE
             ms_class = srsUE
+        elif ms_type == 'androidue':
+            from .ms_android import AndroidUE
+            ms_class = AndroidUE
         elif ms_type == 'amarisoftue':
             from .ms_amarisoft import AmarisoftUE
             ms_class = AmarisoftUE
diff --git a/src/osmo_gsm_tester/obj/ms_android.py b/src/osmo_gsm_tester/obj/ms_android.py
new file mode 100644
index 0000000..2ddc415
--- /dev/null
+++ b/src/osmo_gsm_tester/obj/ms_android.py
@@ -0,0 +1,433 @@
+# osmo_gsm_tester: specifics for running an AndroidUE modem
+#
+# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Nils Fürste <nils.fuerste at softwareradiosystems.com>
+# Author: Bedran Karakoc <bedran.karakoc at softwareradiosystems.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import pprint
+import re
+
+from ..core import log, util, config, remote, process
+from ..core import schema
+from .run_node import RunNode
+from .ms import MS
+from .srslte_common import srslte_common
+from ..core.event_loop import MainLoop
+from .ms_srs import srsUEMetrics
+from .bitrate_monitor import BitRateMonitor
+from .qc_diag import QcDiag
+
+
+def on_register_schemas():
+    resource_schema = {
+        'additional_args[]': schema.STR,
+        'apn': schema.STR,
+        'apn_name': schema.STR,
+        'apn_mcc': schema.STR,
+        'apn_mnc': schema.STR,
+        'sel_apn': schema.STR,
+        'ue_serial': schema.STR,
+        'enable_pcap': schema.BOOL_STR,
+        }
+    for key, val in RunNode.schema().items():
+        resource_schema['run_node.%s' % key] = val
+    schema.register_resource_schema('modem', resource_schema)
+
+    config_schema = {
+        'enable_pcap': schema.BOOL_STR,
+        'log_all_level': schema.STR,
+        }
+    schema.register_config_schema('modem', config_schema)
+
+
+class AndroidUE(MS, srslte_common):
+
+    REMOTEDIR = '/osmo-gsm-tester-androidue'
+    METRICSFILE = 'android_ue_metrics.csv'
+    PCAPFILE = 'android_ue.pcap'
+
+##############
+# PROTECTED
+##############
+    def __init__(self, testenv, conf):
+        self._run_node = RunNode.from_conf(conf.get('run_node', {}))
+        super().__init__('androidue_%s' % self.addr(), testenv, conf)
+        srslte_common.__init__(self)
+        self.testenv = testenv
+        self.run_dir = None
+        self.remote_run_dir = None
+        self.rem_host = None
+        self.emm_connected = False
+        self.rrc_connected = False
+        self.conn_reset_intvl = 20  # sec
+        self.connect_timeout = 300  # sec
+        self.ue_serial = None
+        self.enable_pcap = None
+        self.remote_pcap_file = None
+        self.pcap_file = None
+        self.ue_data_intf = None
+        self.remote_metrics_file = None
+        self.metrics_file = None
+        self.brate_mon = None
+        self.qc_diag_mon = None
+
+    def _clear_work_dirs(self):
+        # clear remote_run_dir
+        popen_args_clear_run_dir = \
+            ['sudo', 'find', str(self.remote_run_dir), '-type', 'f',
+             '\\(', '-iname', '\\*.qdb', '-o', '-iname', '\\*.qmdl', '-o', '-iname', '\\*.xml', '\\)',
+             '-delete']
+        clear_run_dir_proc = self.run_androidue_host_cmd('clear-remote_run_dir', popen_args_clear_run_dir)
+        self.dbg("Deleted the following files: %s" % clear_run_dir_proc.get_stdout())
+
+        # clear diag_logs
+        popen_args_clear_diag_logs = \
+            ['su', '-c', '\"', 'find', '/data/local/tmp/diag_logs/', '-type', 'f',
+             '\\(', '-iname', '\\*.qdb', '-o', '-iname', '\\*.qmdl', '-o', '-iname', '\\*.xml', '\\)',
+             '-delete', '\"']
+        clear_diag_logs_proc = self.run_androidue_cmd('clear-diag_logs', popen_args_clear_diag_logs)
+        self.dbg("Deleted the following files: %s" % clear_diag_logs_proc.get_stdout())
+
+    def _restart_adb_inst(self, as_root=False):
+        self.run_androidue_host_cmd("kill-adb",  ['sudo', 'adb', 'kill-server'])
+        if as_root:
+            self.run_androidue_host_cmd("start-adb", ['adb', 'start-server'])
+        else:
+            self.run_androidue_host_cmd("start-adb", ['sudo', 'adb', 'start-server'])
+
+########################
+# PUBLIC - INTERNAL API
+########################
+    def cleanup(self):
+        self.stop()
+
+    def check_device_availability(self):
+        serials_cmd = ['adb', 'devices']
+        proc = self.run_androidue_host_cmd("devices", serials_cmd)
+        return self.ue_serial in proc.get_stdout()
+
+    def get_assigned_addr(self, ipv6=False):
+        ip_prefix = "172.16.0"
+        proc = self.run_androidue_cmd("get-ip", ['ip', 'addr', 'show'])
+        out_l = proc.get_stdout().split('\n')
+        ip = None
+        for line in out_l:
+            if ip_prefix in line:
+                ip = line.split(' ')[5][:-3]  # UE's IP
+                self.ue_data_intf = line.split(' ')[-1]  # UE's data interface
+        return ip
+
+    def get_carrier_id(self, carrier_name):
+        qry_carrier_cmd = "content query --uri \"content://telephony/carriers\" "
+        proc = self.run_androidue_cmd("set-apn", [qry_carrier_cmd])
+        available_carriers = proc.get_stdout().split("\n")
+        carr_id = -1
+        for carr in available_carriers:
+            if 'name=' + carrier_name in carr:  # found carrier
+                carr_id = re.findall(r'_id=(\S+),', carr)[0]
+                break
+        return carr_id
+
+    def set_new_carrier(self, apn_parameter, carr_id):
+        # check if carrier was found, delete it if exists
+        if carr_id != -1:
+            self.delete_apn(apn_parameter["carrier"])
+
+        set_carrier_cmd = "content insert --uri content://telephony/carriers" \
+                          + " --bind name:s:\"" + apn_parameter["carrier"] + "\"" \
+                          + " --bind numeric:s:\"" + apn_parameter["mcc"] + apn_parameter["mnc"] + "\"" \
+                          + " --bind mcc:s:\"" + apn_parameter["mcc"] + "\"" \
+                          + " --bind mnc:s:\"" + apn_parameter["mnc"] + "\""\
+                          + " --bind apn:s:\"" + apn_parameter["apn"] + "\"" \
+                          + " --bind user:s:\"" + apn_parameter["user"] + "\"" \
+                          + " --bind password:s:\"" + apn_parameter["password"] + "\"" \
+                          + " --bind mmsc:s:\"" + apn_parameter["mmsc"] + "\"" \
+                          + " --bind mmsport:s:\"" + apn_parameter["mmsport"] + "\"" \
+                          + " --bind mmsproxy:s:\"" + apn_parameter["mmsproxy"] + "\"" \
+                          + " --bind authtype:s:\"" + apn_parameter["auth"] + "\"" \
+                          + " --bind type:s:\"" + apn_parameter["type"] + "\"" \
+                          + " --bind protocol:s:\"" + apn_parameter["protocol"] + "\"" \
+                          + " --bind mvno_type:s:\"" + apn_parameter["mvnotype"] + "\"" \
+                          + " --bind mvno_match_data:s:\"" + apn_parameter["mvnoval"] + "\"" \
+                          + " --bind sub_id:s:\"" + apn_parameter["groupid"] + "\""
+        self.run_androidue_cmd("set-apn", [set_carrier_cmd])
+        return self.get_carrier_id(apn_parameter["carrier"])
+
+    def set_preferred_apn(self, carr_id):
+        if carr_id != -1:
+            set_apn_cmd = "content insert --uri content://telephony/carriers/preferapn --bind apn_id:s:\"" + str(carr_id) + "\""
+            self.run_androidue_cmd("set-apn", [set_apn_cmd])
+
+    def select_apn(self, carr_name):
+        carr_id = self.get_carrier_id(carr_name)
+        if carr_id == 0:
+            return False
+
+        # select carrier by ID
+        sel_apn_cmd = "content update --uri content://telephony/carriers/preferapn --bind apn_id:s:\"" + str(carr_id) + "\""
+        self.run_androidue_cmd("set-apn", [sel_apn_cmd])
+        return True
+
+    def delete_apn(self, carr_name):
+        set_apn_cmd = "content delete --uri content://telephony/carriers --where \'name=\"" + str(carr_name) + "\" \'"
+        self.run_androidue_cmd("set-apn", [set_apn_cmd])
+
+###################
+# PUBLIC (test API included)
+###################
+    def run_androidue_cmd(self, name, popen_args, sync=True):
+        # This function executes the given command directly on the Android UE. Therefore,
+        # ADB is used to execute commands locally and ssh for remote execution. Make sure
+        # ADB is installed
+        if self._run_node.is_local():
+            # use adb locally instead of ssh
+            adb_cmd_pref = ['adb', '-s', self.ue_serial, 'exec-out', 'su', '-c']
+            popen_args = adb_cmd_pref + list(popen_args)
+            if sync:
+                proc = process.run_local_sync(self.run_dir, name, popen_args)
+            else:
+                proc = process.run_local(self.run_dir, name, popen_args)
+        else:
+            old_user = self.rem_host.remote_user
+            self.rem_host.remote_user = 'root'
+            proc = self.rem_host.RemoteProcess(name, popen_args, remote_env={}, remote_port=self._run_node.remote_port())
+            if sync:
+                proc.launch_sync()
+            else:
+                proc.launch()
+            self.rem_host.remote_user = old_user
+        return proc
+
+    def run_androidue_host_cmd(self, name, popen_args, sync=True):
+        # This function executes commands on the host connected to the Android UE.
+        # Make sure ADB is installed
+        if self._run_node.is_local():
+            if sync:
+                proc = process.run_local_sync(self.run_dir, name, popen_args)
+            else:
+                proc = process.run_local(self.run_dir, name, popen_args)
+        else:
+            proc = self.rem_host.RemoteProcess(name, popen_args, env={})
+            if sync:
+                proc.launch_sync()
+            else:
+                proc.launch()
+        return proc
+
+    def run_netns_wait(self, name, popen_args):
+        # This function guarantees the compatibility with the current ping test. Please
+        # note that this function cannot execute commands on the machine hosting the Android
+        # UE. Use run_androidue_host_cmd() for this purpose.
+        return self.run_androidue_cmd(name, popen_args, sync=True)
+
+    def start(self):
+        # Sometimes we need to restart adb in case a UE was not recognized
+        if not self.check_device_availability():
+            self.log("Can't find requested device. Restarting ADB and check again")
+            self._restart_adb_inst(True)
+            if not self.check_device_availability():
+                raise log.Error("Device with serial %s is not available" % self.ue_serial)
+        self.set_airplane_mode(True)
+
+    def stop(self):
+        if self.brate_mon:
+            self.brate_mon.stop()
+
+        if self.qc_diag_mon:
+            self.qc_diag_mon.stop()
+
+        self.set_airplane_mode(True)
+
+        if self.enable_pcap and self.qc_diag_mon:
+            self.qc_diag_mon.write_pcap()
+            if not self._run_node.is_local():
+                self.scp_back_pcap()
+
+    def configure(self):
+        values = dict(ue=config.get_defaults('androidue'))
+        config.overlay(values, dict(ue=self.testenv.suite().config().get('modem', {})))
+        config.overlay(values, dict(ue=self._conf))
+        self.dbg('AndroidUE CONFIG:\n' + pprint.pformat(values))
+        self.ue_serial = values['ue']['ue_serial']
+
+        if 'qc_diag' in self.features():
+            self.enable_pcap = util.str2bool(values['ue'].get('enable_pcap', 'false'))
+
+        self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
+        self.metrics_file = self.run_dir.child(AndroidUE.METRICSFILE)
+        if not self._run_node.is_local():
+            self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
+            self.remote_run_dir = util.Dir(AndroidUE.REMOTEDIR)
+            self.remote_metrics_file = self.remote_run_dir.child(AndroidUE.METRICSFILE)
+
+        self.pcap_file = self.run_dir.child(AndroidUE.PCAPFILE)
+        self.remote_pcap_file = self.remote_run_dir.child(AndroidUE.PCAPFILE)
+
+        apn_params = {
+            "carrier": str(values['ue']['apn_name'] or 'default'),
+            "apn": str(values['ue']['apn']),  # mandatory
+            "proxy": str(''),
+            "port": str(''),
+            "user": str(''),
+            "password": str(''),
+            "server": str(''),
+            "mmsc": str(''),
+            "mmsport": str(''),
+            "mmsproxy": str(''),
+            "mcc": str(values['ue']['apn_mcc']),  # mandatory
+            "mnc": str(values['ue']['apn_mnc']),  # mandatory
+            "auth": str('-1'),
+            "type": str(''),
+            "protocol": str(''),
+            "mvnotype": str(''),
+            "mvnoval": str(''),
+            "groupid": str('-1')
+        }
+
+        # On some UEs it is only possible to set a new APN if airplane mode is turned off
+        self.set_airplane_mode(False)
+        MainLoop.sleep(1)
+        self.dbg("APN parameters: " + str(apn_params))
+        # self.set_apn(apn_params, True)
+        MainLoop.sleep(1)
+        self.set_airplane_mode(False)
+
+        # clear working directories
+        self._clear_work_dirs()
+
+    def connect(self, enb):
+        self.log('Starting AndroidUE')
+        self.configure()
+        CONN_CHK = 'osmo-gsm-tester_androidue_conn_chk.sh'
+        popen_args_emm_conn_chk = [CONN_CHK, self.ue_serial]
+
+        if 'qc_diag' in self.features():
+            self.qc_diag_mon = QcDiag(self.testenv, self.run_dir, self.remote_run_dir, self._run_node,
+                                      self.rem_host, self.ue_serial, self._run_node.remote_port())
+            self.qc_diag_mon.start()
+
+        emm_conn_chk_proc = self.run_androidue_host_cmd('emm-conn-chk', popen_args_emm_conn_chk, sync=False)
+
+        # TODO
+        #   Adjust query 'LTE' to all other network types to make the AndroidUE class compatible with 2G network tests
+        #   maybe derive the network type from the enb-parameter
+
+        # check connection status
+        timer = self.connect_timeout
+        while timer > 0:
+            if 'LTE' in emm_conn_chk_proc.get_stdout():
+                if self.get_assigned_addr():
+                    self.emm_connected = True
+                    self.rrc_connected = True
+                    emm_conn_chk_proc.terminate()
+                    break
+
+            # reset connection
+            if timer % self.conn_reset_intvl == 0:
+                self.set_airplane_mode(True)
+                MainLoop.sleep(2)
+                timer -= 2
+                self.set_airplane_mode(False)
+            MainLoop.sleep(2)
+            timer -= 1
+
+        if timer == 0:
+            raise log.Error("Android UE %s connection timer expired" % self.ue_serial)
+
+        self.brate_mon = BitRateMonitor(self.testenv, self.run_dir, self._run_node, self.rem_host,
+                                        self.ue_serial, self.ue_data_intf, self._run_node.remote_port())
+        self.brate_mon.start()
+
+    def is_rrc_connected(self):
+        if not ('qc_diag' in self.features()):
+            raise log.Error("Monitoring RRC states not supported (missing qc_diag feature?)")
+
+        if not self.qc_diag_mon.running():
+            raise log.Error("Diag monitoring crashed or was not started")
+
+        rrc_state = self.qc_diag_mon.get_rrc_state()
+        if 'RRC_IDLE_CAMPED' in rrc_state:
+            self.rrc_connected = False
+        elif 'RRC_CONNECTED' in rrc_state:
+            self.rrc_connected = True
+        return self.rrc_connected
+
+    def is_registered(self, mcc_mnc=None):
+        return self.emm_connected
+
+    def get_counter(self, counter_name):
+        if counter_name == 'prach_sent':
+            # not implemented so far, return 2 to pass tests
+            return 2
+        elif counter_name == 'paging_received':
+            self.qc_diag_mon.get_paging_counter()
+        else:
+            raise log.Error("Counter %s not implemented" % counter_name)
+
+    def running(self):
+        # check if Android UE is available via ADB
+        if not (self.ue_serial is None):
+            serials_cmd = ['adb', 'devices']
+            proc = self.run_androidue_cmd("devices", serials_cmd)
+            if self.ue_serial in proc.get_stdout():
+                return True
+        self.dbg("Device with serial %s is currently not available" % self.ue_serial)
+        return False
+
+    def addr(self):
+        return self._run_node.run_addr()
+
+    def run_node(self):
+        return self._run_node
+
+    def features(self):
+        return self._conf.get('features', [])
+
+    def get_remote_port(self):
+        return self._run_node.remote_port()
+
+    def set_airplane_mode(self, apm_state):
+        self.log("Setting airplane mode: " + str(apm_state))
+        popen_args = ['settings', 'put', 'global', 'airplane_mode_on', str(int(apm_state)), ';',
+                      'wait $!;',
+                      'su', '-c', '\"', 'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '\"', ';']
+        self.run_androidue_cmd("set-airplane-mode", popen_args)
+
+    def set_apn(self, apn_param, sel_apn):
+        # search for carrier in APN database
+        carrier_id = self.get_carrier_id(apn_param["carrier"])
+
+        # add/update carrier
+        carrier_id = self.set_new_carrier(apn_param, carrier_id)
+
+        # select as preferred APN
+        if sel_apn:
+            self.set_preferred_apn(carrier_id)
+
+    def scp_back_pcap(self):
+        try:
+            self.rem_host.scpfrom('scp-back-pcap', self.remote_pcap_file, self.pcap_file)
+        except Exception as e:
+            self.log(repr(e))
+
+    def verify_metric(self, value, operation='avg', metric='dl_brate', criterion='gt', window=1):
+        self.brate_mon.save_metrics(self.metrics_file)
+        metrics = srsUEMetrics(self.metrics_file)
+        return metrics.verify(value, operation, metric, criterion, window)
+
+    def netns(self):
+        return None
diff --git a/src/osmo_gsm_tester/obj/qc_diag.py b/src/osmo_gsm_tester/obj/qc_diag.py
new file mode 100644
index 0000000..24486b7
--- /dev/null
+++ b/src/osmo_gsm_tester/obj/qc_diag.py
@@ -0,0 +1,172 @@
+# osmo_gsm_tester: specifics for running an AndroidUE modem
+#
+# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Nils Fürste <nils.fuerste at softwareradiosystems.com>
+# Author: Bedran Karakoc <bedran.karakoc at softwareradiosystems.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ..core import log, process
+from ..core.event_loop import MainLoop
+from .ms_android import AndroidUE
+
+
+class QcDiag(log.Origin):
+
+    DIAG_PARSER = 'osmo-gsm-tester_androidue_diag_parser.sh'
+
+##############
+# PROTECTED
+##############
+    def __init__(self, testenv, run_dir, remote_run_dir, run_node, rem_host, ue_serial, remote_port):
+        super().__init__(log.C_TST, 'QcDiag_%s' % ue_serial)
+        self.testenv = testenv
+        self.run_dir = run_dir
+        self.remote_run_dir = remote_run_dir
+        self._run_node = run_node
+        self.rem_host = rem_host
+        self.ue_serial = ue_serial
+        self.remote_port = remote_port
+        self.pcap_file = self.run_dir.child(AndroidUE.PCAPFILE)
+        self.remote_pcap_file = self.remote_run_dir.child(AndroidUE.PCAPFILE)
+        self.diag_parser_proc = None
+        self.diag_monitor_proc = None
+
+    def _clear_work_dirs(self):
+        # clear remote_run_dir
+        popen_args_clear_run_dir = \
+            ['sudo', 'find', str(self.remote_run_dir), '-type', 'f',
+             '\\(', '-iname', '\\*.qdb', '-o', '-iname', '\\*.qmdl', '-o', '-iname', '\\*.xml', '\\)',
+             '-delete']
+        clear_run_dir_proc = self.run_androidue_host_cmd('clear-remote_run_dir', popen_args_clear_run_dir)
+        self.dbg("Deleted the following files: %s" % clear_run_dir_proc.get_stdout())
+
+        # clear diag_logs
+        popen_args_clear_diag_logs = \
+            ['su', '-c', '\"', 'find', '/data/local/tmp/diag_logs/', '-type', 'f',
+             '\\(', '-iname', '\\*.qdb', '-o', '-iname', '\\*.qmdl', '-o', '-iname', '\\*.xml', '\\)',
+             '-delete', '\"']
+        clear_diag_logs_proc = self.run_androidue_cmd('clear-diag_logs', popen_args_clear_diag_logs)
+        self.dbg("Deleted the following files: %s" % clear_diag_logs_proc.get_stdout())
+
+########################
+# PUBLIC - INTERNAL API
+########################
+    def run_androidue_cmd(self, name, popen_args, sync=True):
+        # This function executes the given command directly on the Android UE. Therefore,
+        # ADB is used to execute commands locally and ssh for remote execution. Make sure
+        # ADB is installed
+        if self._run_node.is_local():
+            # use adb locally instead of ssh
+            adb_cmd_pref = ['adb', '-s', self.ue_serial, 'exec-out', 'su', '-c']
+            popen_args = adb_cmd_pref + list(popen_args)
+            if sync:
+                proc = process.run_local_sync(self.run_dir, name, popen_args)
+            else:
+                proc = process.run_local(self.run_dir, name, popen_args)
+        else:
+            old_user = self.rem_host.remote_user
+            self.rem_host.remote_user = 'root'
+            proc = self.rem_host.RemoteProcess(name, popen_args, remote_env={}, remote_port=self._run_node.remote_port())
+            if sync:
+                proc.launch_sync()
+            else:
+                proc.launch()
+            self.rem_host.remote_user = old_user
+        return proc
+
+    def run_androidue_host_cmd(self, name, popen_args, sync=True):
+        if self._run_node.is_local():
+            if sync:
+                proc = process.run_local_sync(self.run_dir, name, popen_args)
+            else:
+                proc = process.run_local(self.run_dir, name, popen_args)
+        else:
+            proc = self.rem_host.RemoteProcess(name, popen_args, env={})
+            if sync:
+                proc.launch_sync()
+            else:
+                proc.launch()
+        return proc
+
+###################
+# PUBLIC (test API included)
+###################
+    def start(self):
+        popen_args_diag = ['/vendor/bin/diag_mdlog', '-s', '90000', '-f', '/data/local/tmp/ogt_diag.cfg',
+                           '-o', '/data/local/tmp/diag_logs']
+        self.diag_monitor_proc = self.run_androidue_cmd("diag-monitor", popen_args_diag, sync=False)
+        self.testenv.remember_to_stop(self.diag_monitor_proc)
+
+        if self._run_node.is_local():
+            popen_args_diag_parser = [QcDiag.DIAG_PARSER, str(self.ue_serial), str(self.run_dir), str(self.pcap_file)]
+        else:
+            popen_args_diag_parser = [QcDiag.DIAG_PARSER, str(self.ue_serial), str(self.remote_run_dir), str(self.remote_pcap_file)]
+        self.diag_parser_proc = self.run_androidue_host_cmd("diag-parser", popen_args_diag_parser, sync=False)
+        self.testenv.remember_to_stop(self.diag_parser_proc)
+
+    def stop(self):
+        self.testenv.stop_process(self.diag_parser_proc)
+        self.testenv.stop_process(self.diag_monitor_proc)
+
+    def write_pcap(self):
+        # We need to stop the diag_parser to avoid pulling a new .qmdl during
+        # the parsing process. The process will be restarted if it was still running.
+        # The diag_monitor process can continue, as it does not hinder this process.
+        restart_diag_parser = False
+        if self.diag_parser_proc.is_running():
+            self.diag_parser_proc.terminate()
+            restart_diag_parser = True
+            self._clear_work_dirs()
+
+        if self._run_node.is_local():
+            popen_args_diag_parser = [QcDiag.DIAG_PARSER, str(self.ue_serial), str(self.run_dir), str(self.pcap_file)]
+        else:
+            popen_args_diag_parser = [QcDiag.DIAG_PARSER, str(self.ue_serial), str(self.remote_run_dir), str(self.remote_pcap_file)]
+        diag_parser_proc = self.run_androidue_host_cmd("diag-parser", popen_args_diag_parser, sync=False)
+
+        timer = 300  # sec
+        while timer > 0:
+            diag_parser_stdout = diag_parser_proc.get_stdout()
+            if diag_parser_stdout.count('Pulling new .qmdl file...') > 1:
+                # If the parsers pulls the .qmdl file for the second time we know that
+                # the parsing of the first one is done
+                break
+            MainLoop.sleep(2)
+            timer -= 2
+
+        if timer <= 0:
+            raise log.Error("Timed out writing PCAP file")
+
+        if restart_diag_parser:
+            self.diag_parser_proc = diag_parser_proc
+        else:
+            diag_parser_proc.terminate()
+
+    def get_rrc_state(self):
+        diag_parser_stdout_l = self.diag_parser_proc.get_stdout().split('\n')
+        for line in reversed(diag_parser_stdout_l):
+            if 'LTE_RRC_STATE_CHANGE' in line:
+                rrc_state = line.split(' ')[-1].replace('rrc_state=', '')
+                rrc_state.replace('\'', '')
+                return rrc_state
+        return ''
+
+    def get_paging_counter(self):
+        diag_parser_stdout_l = self.diag_parser_proc.get_stdout().split("\n")
+        return diag_parser_stdout_l.count('Paging received')
+
+    def running(self):
+        return self.diag_parser_proc.is_running() and self.diag_monitor_proc.is_running()
diff --git a/sysmocom/scenarios/ms-label.conf b/sysmocom/scenarios/ms-label.conf
new file mode 100644
index 0000000..a129c0e
--- /dev/null
+++ b/sysmocom/scenarios/ms-label.conf
@@ -0,0 +1,3 @@
+resources:
+  modem:
+  - label: ${param1}
diff --git a/sysmocom/suites/4g/iperf3_dl.py b/sysmocom/suites/4g/iperf3_dl.py
index bf5b1f0..ed137bd 100755
--- a/sysmocom/suites/4g/iperf3_dl.py
+++ b/sysmocom/suites/4g/iperf3_dl.py
@@ -23,7 +23,7 @@
 max_rate = enb.ue_max_rate(downlink=True, num_carriers=ue.num_carriers)
 
 iperf3srv.start()
-proc = iperf3cli.prepare_test_proc(iperf3cli.DIR_DL, ue.netns(), bitrate=max_rate)
+proc = iperf3cli.prepare_test_proc(iperf3cli.DIR_DL, ue.netns(), bitrate=max_rate, ue=ue)
 
 print('waiting for UE to attach...')
 wait(ue.is_registered)
diff --git a/sysmocom/suites/4g/iperf3_ul.py b/sysmocom/suites/4g/iperf3_ul.py
index 6c0d25d..6cc9f21 100755
--- a/sysmocom/suites/4g/iperf3_ul.py
+++ b/sysmocom/suites/4g/iperf3_ul.py
@@ -23,7 +23,7 @@
 max_rate = enb.ue_max_rate(downlink=False, num_carriers=ue.num_carriers)
 
 iperf3srv.start()
-proc = iperf3cli.prepare_test_proc(iperf3cli.DIR_UL, ue.netns(), bitrate=max_rate)
+proc = iperf3cli.prepare_test_proc(iperf3cli.DIR_UL, ue.netns(), bitrate=max_rate, ue=ue)
 
 print('waiting for UE to attach...')
 wait(ue.is_registered)
diff --git a/utils/bin/osmo-gsm-tester_androidue_conn_chk.sh b/utils/bin/osmo-gsm-tester_androidue_conn_chk.sh
new file mode 100644
index 0000000..7f0f79d
--- /dev/null
+++ b/utils/bin/osmo-gsm-tester_androidue_conn_chk.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# This script reads the network type of an Android phone via ADB
+# usage: osmo-gsm-tester_androidue_conn_chk.sh $serial
+serial=$1
+while true; do
+    sudo adb -s "${serial}" shell getprop "gsm.network.type";
+    sleep 1;
+done
diff --git a/utils/bin/osmo-gsm-tester_androidue_diag_parser.sh b/utils/bin/osmo-gsm-tester_androidue_diag_parser.sh
new file mode 100644
index 0000000..7d1ea3f
--- /dev/null
+++ b/utils/bin/osmo-gsm-tester_androidue_diag_parser.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# This script pulls the diag folder created by diag_mdlog and parses the
+# .qmdl file. Further, it writes all packets to a pcap file.
+# usage: osmo-gsm-tester_androidue_diag_parser.sh $serial $run_dir $pcap_path
+serial=$1
+run_dir=$2
+pcap_path=$3
+while true; do
+    echo "Pulling new .qmdl file...";
+    sudo adb -s "${serial}" pull /data/local/tmp/diag_logs "${run_dir}" > /dev/null;
+    wait $!;
+    QMDL_FN=$(find "${run_dir}" -maxdepth 2 -type f -name "*.qmdl");
+    wait $!;
+    sudo scat -t qc --event -d "$QMDL_FN" -F "${pcap_path}";
+    wait $!;
+done

-- 
To view, visit https://gerrit.osmocom.org/c/osmo-gsm-tester/+/21302
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: I79a5d803e869a868d4dac5e0d4c2feb38038dc5c
Gerrit-Change-Number: 21302
Gerrit-PatchSet: 1
Gerrit-Owner: ninjab3s <nils.fuerste at softwareradiosystems.com>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20201123/7eabbb7f/attachment.htm>


More information about the gerrit-log mailing list