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
        - Android SDK Platform-Tools
	  (https://developer.android.com/studio/releases/platform-tools#downloads)
        - 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'
  apn:
    apn: 'srsapn'
    mcc: '901'
    mnc: '70'
    select: '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
    adb_serial_id: '8d3c79a7'
  scat_parser:
    run_type: local
    run_addr: 127.0.0.1
    adb_serial_id: '8d3c79a7'

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

Change-Id: I79a5d803e869a868d4dac5e0d4c2feb38038dc5c
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..9fddff3
--- /dev/null
+++ b/src/osmo_gsm_tester/obj/ms_android.py
@@ -0,0 +1,244 @@
+# osmo_gsm_tester: specifics for running an AndroidUE modem
+#
+# Copyright (C) 2020 by Software Radio Systems Limited
+#
+# Author: Nils Fürste <nils.fuerste@softwareradiosystems.com>
+# Author: Bedran Karakoc <bedran.karakoc@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
+
+from ..core import log, util, config, remote, schema, process
+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 .android_bitrate_monitor import BitRateMonitor
+from . import qc_diag
+from .android_apn import AndroidApn
+from .android_host import AndroidHost
+
+
+def on_register_schemas():
+    resource_schema = {
+        'additional_args[]': schema.STR,
+        'enable_pcap': schema.BOOL_STR,
+        }
+    for key, val in RunNode.schema().items():
+        resource_schema['run_node.%s' % key] = val
+    for key, val in AndroidApn.schema().items():
+        resource_schema['apn.%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, AndroidHost, 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', {}))
+        self.apn_worker = AndroidApn.from_conf(conf.get('apn', {})) if conf.get('apn', {}) != {} else None
+        self.qc_diag_mon = qc_diag.QcDiag(testenv, conf)
+        super().__init__('androidue_%s' % self.addr(), testenv, conf)
+        srslte_common.__init__(self)
+        self.rem_host = None
+        self.run_dir = None
+        self.remote_run_dir = None
+        self.emm_connected = False
+        self.rrc_connected = False
+        self.conn_reset_intvl = 20  # sec
+        self.connect_timeout = 300  # sec
+        self.enable_pcap = None
+        self.remote_pcap_file = None
+        self.pcap_file = None
+        self.data_interface = None
+        self.remote_metrics_file = None
+        self.metrics_file = None
+        self.brate_mon = None
+
+    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))
+
+        if 'qc_diag' in self.features():
+            self.enable_pcap = util.str2bool(values['ue'].get('enable_pcap', 'false'))
+
+        self.metrics_file = self.run_dir.child(AndroidUE.METRICSFILE)
+        self.pcap_file = self.run_dir.child(AndroidUE.PCAPFILE)
+        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(), None,
+                                              self._run_node.ssh_port())
+            self.remote_run_dir = util.Dir(AndroidUE.REMOTEDIR)
+            self.remote_metrics_file = self.remote_run_dir.child(AndroidUE.METRICSFILE)
+            self.remote_pcap_file = self.remote_run_dir.child(AndroidUE.PCAPFILE)
+
+        if self.apn_worker:
+            self.apn_worker.configure(self.testenv, self.run_dir, self._run_node, self.rem_host)
+            # some Android UEs only accept new APNs when airplane mode is turned off
+            self.set_airplane_mode(False)
+            self.apn_worker.set_apn()
+            MainLoop.sleep(1)
+            self.set_airplane_mode(True)
+
+        # clear old diag files
+        self._clear_diag_logs()
+
+    def _clear_diag_logs(self):
+        popen_args_clear_diag_logs = \
+            ['su', '-c', '\"rm -r /data/local/tmp/diag_logs/ || true\"']
+        clear_diag_logs_proc = self.run_androidue_cmd('clear-diag-logs', popen_args_clear_diag_logs)
+        clear_diag_logs_proc.launch_sync()
+
+    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 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\";']
+        proc = self.run_androidue_cmd('set-airplane-mode', popen_args)
+        proc.launch_sync()
+
+    def get_assigned_addr(self, ipv6=False):
+        ip_prefix = '172.16.0'
+        proc = self.run_androidue_cmd('get-assigned-addr', ['ip', 'addr', 'show'])
+        proc.launch_sync()
+        out_l = proc.get_stdout().split('\n')
+        ip = ''
+        for line in out_l:
+            if ip_prefix in line:
+                ip = line.split(' ')[5][:-3]
+                self.data_interface = line.split(' ')[-1]
+        return ip
+
+########################
+# PUBLIC - INTERNAL API
+########################
+    def cleanup(self):
+        self.set_airplane_mode(True)
+
+    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', [])
+
+###################
+# PUBLIC (test API included)
+###################
+    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 the Android UE
+        # is attached to.
+        proc = self.run_androidue_cmd(name, popen_args)
+        proc.launch_sync()
+        return proc
+
+    def connect(self, enb):
+        self.log('Starting AndroidUE')
+        self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
+        self.configure()
+        CONN_CHK = 'osmo-gsm-tester_androidue_conn_chk.sh'
+
+        if 'qc_diag' in self.features():
+            self.qc_diag_mon.start()
+
+        if self._run_node.is_local():
+            popen_args_emm_conn_chk = [CONN_CHK, self._run_node.adb_serial_id(), '0', '0']
+        else:
+            popen_args_emm_conn_chk = [CONN_CHK, '0', self.rem_host.host(), self.rem_host.get_remote_port()]
+
+        # make sure osmo-gsm-tester_androidue_conn_chk.sh is available on the OGT master unit
+        name = 'emm-conn-chk'
+        run_dir = self.run_dir.new_dir(name)
+        emm_conn_chk_proc = process.Process(name, run_dir, popen_args_emm_conn_chk)
+        self.testenv.remember_to_stop(emm_conn_chk_proc)
+        emm_conn_chk_proc.launch()
+
+        # check connection status
+        timer = self.connect_timeout
+        while timer > 0:
+            if timer % self.conn_reset_intvl == 0:
+                self.set_airplane_mode(True)
+                MainLoop.sleep(1)
+                timer -= 1
+                self.set_airplane_mode(False)
+
+            if 'LTE' in emm_conn_chk_proc.get_stdout():
+                if not(self.get_assigned_addr() is ''):
+                    self.emm_connected = True
+                    self.rrc_connected = True
+                    self.testenv.stop_process(emm_conn_chk_proc)
+                    break
+
+            MainLoop.sleep(2)
+            timer -= 2
+
+        if timer == 0:
+            raise log.Error('Connection timer of Android UE %s expired' % self._run_node.adb_serial_id())
+
+        self.brate_mon = BitRateMonitor(self.testenv, self.run_dir, self._run_node, self.rem_host, self.data_interface)
+        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):
+        if mcc_mnc:
+            raise log.Error('An AndroidUE cannot register to any predefined MCC/MNC')
+        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':
+            return self.qc_diag_mon.get_paging_counter()
+        else:
+            raise log.Error('Counter %s not implemented' % counter_name)
+
+    def netns(self):
+        return None