<p>Pau Espin Pedrol <strong>merged</strong> this change.</p><p><a href="https://gerrit.osmocom.org/11462">View Change</a></p><div style="white-space:pre-wrap">Approvals:
  Jenkins Builder: Verified
  Pau Espin Pedrol: Looks good to me, approved
  Harald Welte: Looks good to me, but someone else must approve

</div><pre style="font-family: monospace,monospace; white-space: pre-wrap;">Add support to test gprs IPv4 data plane<br><br>Since the modem iface and the GGSN iface are on the same host/netns,<br>it's really difficult to conveniently test data plane without getting<br>routing loops. As a result, either GGSN or modem iface must be moved to<br>a different namespace. The decision after a few discussions was finally<br>to move modem interfaces to a different netns.<br><br>Expected setup:<br>* ofono is patched to avoid removing modem if it detects<br>through udev that its net iface was removed (due to for instance, net<br>iface being moved to another netns and thus not being reachable anymore<br>by systemd-udev process running in root netns).<br>* After ofono is started (and successfully configured all the modems and<br>detected its net ifaces through syfs/udev), script "modem-netns-setup.py<br>start" which creates a netns for each modem, naming it after its usb<br>path ID. net ifaces for that modem are moved into its netns.<br>* Modem is configured to use 802-3 data format, and as a result the net<br>iface is configured through DHCP (DHCP req only replied AFTER pdp ctx is<br>activated!).<br>* Since osmo-gsm-tester knowns the modem USB path ID (available in<br>resources.conf), it can run required steps (ifup, DHCP) to configure the<br>interface. The interface name is provided by ofono to osmo-gsm-tester.<br>* As a result, any process willing to transmit data through the modem<br>must be in the modem netns.<br><br>Related: OS#2308<br>Change-Id: Icb06bdfcdd37c797be95ab5addb28da2d9f6681c<br>---<br>M example/resources.conf.prod<br>M example/resources.conf.rnd<br>M src/osmo_gsm_tester/modem.py<br>M src/osmo_gsm_tester/process.py<br>M src/osmo_gsm_tester/suite.py<br>M suites/gprs/ping.py<br>A utils/osmo-gsm-tester_netns_exec.sh<br>7 files changed, 69 insertions(+), 15 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/example/resources.conf.prod b/example/resources.conf.prod</span><br><span>index 22134e7..b12a7bc 100644</span><br><span>--- a/example/resources.conf.prod</span><br><span>+++ b/example/resources.conf.prod</span><br><span>@@ -122,14 +122,14 @@</span><br><span>   ki: 'EBAB63D06C3F546A16C977CB40E57C68'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['sms', 'voice', 'ussd', 'gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sms', 'voice', 'ussd', 'sim']</span><br><span> </span><br><span> - label: sierra_2nd</span><br><span>   path: '/sys/devices/pci0000:00/0000:00:12.2/usb1/1-5/1-5.4/1-5.4.1/1-5.4.1.3'</span><br><span>   ki: 'EBD2B5F6CF3374106D0A66C11F922001'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['sms', 'voice', 'ussd', 'gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sms', 'voice', 'ussd', 'sim']</span><br><span> </span><br><span> - label: ec20</span><br><span>   path: '/sys/devices/pci0000:00/0000:00:12.2/usb1/1-5/1-5.4/1-5.4.1/1-5.4.1.6'</span><br><span>@@ -143,7 +143,7 @@</span><br><span>   ki: '5752B3F43277C35D2D1D957007DF74E2'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sim']</span><br><span> </span><br><span> osmocon_phone:</span><br><span>   - serial_device: '/dev/serial/by-id/usb-Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_00897B41-if00-port0'</span><br><span>diff --git a/example/resources.conf.rnd b/example/resources.conf.rnd</span><br><span>index dbdf3cc..63650e1 100644</span><br><span>--- a/example/resources.conf.rnd</span><br><span>+++ b/example/resources.conf.rnd</span><br><span>@@ -72,14 +72,14 @@</span><br><span>   ki: '80A37E6FDEA931EAC92FFA5F671EFEAD'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['sms', 'voice', 'ussd', 'gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sms', 'voice', 'ussd', 'sim']</span><br><span> </span><br><span> - label: sierra_2nd</span><br><span>   path: '/sys/devices/pci0000:00/0000:00:12.2/usb1/1-1/1-1.3'</span><br><span>   ki: '00969E283349D354A8239E877F2E0866'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['sms', 'voice', 'ussd', 'gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sms', 'voice', 'ussd', 'sim']</span><br><span> </span><br><span> - label: ec20</span><br><span>   path: '/sys/devices/pci0000:00/0000:00:12.2/usb1/1-1/1-1.6'</span><br><span>@@ -93,7 +93,7 @@</span><br><span>   ki: '2F70DCA43C45ACB97E947FDD0C7CA30A'</span><br><span>   auth_algo: 'comp128v1'</span><br><span>   ciphers: [a5_0, a5_1]</span><br><span style="color: hsl(0, 100%, 40%);">-  features: ['gprs', 'sim']</span><br><span style="color: hsl(120, 100%, 40%);">+  features: ['sim']</span><br><span> </span><br><span> osmocon_phone:</span><br><span> - serial_device: '/dev/serial/by-id/usb-Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_0089279D-if00-port0'</span><br><span>diff --git a/src/osmo_gsm_tester/modem.py b/src/osmo_gsm_tester/modem.py</span><br><span>index 21b208c..d35933a 100644</span><br><span>--- a/src/osmo_gsm_tester/modem.py</span><br><span>+++ b/src/osmo_gsm_tester/modem.py</span><br><span>@@ -17,10 +17,11 @@</span><br><span> # You should have received a copy of the GNU General Public License</span><br><span> # along with this program.  If not, see <http://www.gnu.org/licenses/>.</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-from . import log, util, sms</span><br><span style="color: hsl(120, 100%, 40%);">+from . import log, util, sms, process</span><br><span> from .event_loop import MainLoop</span><br><span> </span><br><span> from pydbus import SystemBus, Variant</span><br><span style="color: hsl(120, 100%, 40%);">+import os</span><br><span> </span><br><span> # Required for Gio.Cancellable.</span><br><span> # See https://lazka.github.io/pgi-docs/Gio-2.0/classes/Cancellable.html#Gio.Cancellable</span><br><span>@@ -323,15 +324,17 @@</span><br><span>     CTX_PROT_IPv6 = 'ipv6'</span><br><span>     CTX_PROT_IPv46 = 'dual'</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    def __init__(self, conf):</span><br><span style="color: hsl(120, 100%, 40%);">+    def __init__(self, suite_run, conf):</span><br><span style="color: hsl(120, 100%, 40%);">+        self.suite_run = suite_run</span><br><span>         self.conf = conf</span><br><span>         self.syspath = conf.get('path')</span><br><span>         self.dbuspath = get_dbuspath_from_syspath(self.syspath)</span><br><span>         super().__init__(log.C_TST, self.dbuspath)</span><br><span style="color: hsl(0, 100%, 40%);">-        self.dbg('creating from syspath %s', self.syspath)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.dbg('creating from syspath %s' % self.syspath)</span><br><span>         self.msisdn = None</span><br><span>         self._ki = None</span><br><span>         self._imsi = None</span><br><span style="color: hsl(120, 100%, 40%);">+        self.run_dir = util.Dir(self.suite_run.get_test_run_dir().new_dir(self.name().strip('/')))</span><br><span>         self.sms_received_list = []</span><br><span>         self.dbus = ModemDbusInteraction(self.dbuspath)</span><br><span>         self.register_attempts = 0</span><br><span>@@ -358,6 +361,9 @@</span><br><span>         self.dbus.cleanup()</span><br><span>         self.dbus = None</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+    def netns(self):</span><br><span style="color: hsl(120, 100%, 40%);">+        return os.path.basename(self.syspath.rstrip('/'))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span>     def properties(self, *args, **kwargs):</span><br><span>         '''Return a dict of properties on this modem. For the actual arguments,</span><br><span>         see ModemDbusInteraction.properties(), which this function calls.  The</span><br><span>@@ -627,6 +633,23 @@</span><br><span>         connmgr.RemoveContext(ctx_id)</span><br><span>         self.log('context deactivated', path=ctx_id)</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+    def run_netns_wait(self, name, popen_args):</span><br><span style="color: hsl(120, 100%, 40%);">+        proc = process.NetNSProcess(name, self.run_dir.new_dir(name), self.netns(), popen_args,</span><br><span style="color: hsl(120, 100%, 40%);">+                                       env={})</span><br><span style="color: hsl(120, 100%, 40%);">+        process.run_proc_sync(proc)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def setup_context_data_plane(self, ctx_id):</span><br><span style="color: hsl(120, 100%, 40%);">+        self.dbg('setup_context_data', path=ctx_id)</span><br><span style="color: hsl(120, 100%, 40%);">+        ctx = systembus_get(ctx_id)</span><br><span style="color: hsl(120, 100%, 40%);">+        ctx_settings = ctx.GetProperties().get('Settings', None)</span><br><span style="color: hsl(120, 100%, 40%);">+        if not ctx_settings:</span><br><span style="color: hsl(120, 100%, 40%);">+            raise log.Error('%s no Settings found! No way to get iface!' % ctx_id)</span><br><span style="color: hsl(120, 100%, 40%);">+        iface = ctx_settings.get('Interface', None)</span><br><span style="color: hsl(120, 100%, 40%);">+        if not iface:</span><br><span style="color: hsl(120, 100%, 40%);">+            raise log.Error('%s Settings contains no iface! %r' % (ctx_id, repr(ctx_settings)))</span><br><span style="color: hsl(120, 100%, 40%);">+        self.run_netns_wait('ifup', ('ip', 'link', 'set', 'dev', iface, 'up'))</span><br><span style="color: hsl(120, 100%, 40%);">+        self.run_netns_wait('dhcp', ('udhcpc', '-q', '-i', iface))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span>     def sms_send(self, to_msisdn_or_modem, *tokens):</span><br><span>         if isinstance(to_msisdn_or_modem, Modem):</span><br><span>             to_msisdn = to_msisdn_or_modem.msisdn</span><br><span>diff --git a/src/osmo_gsm_tester/process.py b/src/osmo_gsm_tester/process.py</span><br><span>index fb5c6f6..a845f7f 100644</span><br><span>--- a/src/osmo_gsm_tester/process.py</span><br><span>+++ b/src/osmo_gsm_tester/process.py</span><br><span>@@ -100,6 +100,9 @@</span><br><span>             time.sleep(wait_step)</span><br><span>         return False</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+    def send_signal(self, sig):</span><br><span style="color: hsl(120, 100%, 40%);">+        os.kill(self.process_obj.pid, sig)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span>     def terminate(self):</span><br><span>         if self.process_obj is None:</span><br><span>             return</span><br><span>@@ -109,21 +112,21 @@</span><br><span>         while True:</span><br><span>             # first try SIGINT to allow stdout+stderr flushing</span><br><span>             self.log('Terminating (SIGINT)')</span><br><span style="color: hsl(0, 100%, 40%);">-            os.kill(self.process_obj.pid, signal.SIGINT)</span><br><span style="color: hsl(120, 100%, 40%);">+            self.send_signal(signal.SIGINT)</span><br><span>             self.killed = signal.SIGINT</span><br><span>             if self._poll_termination():</span><br><span>                 break</span><br><span> </span><br><span>             # SIGTERM maybe?</span><br><span>             self.log('Terminating (SIGTERM)')</span><br><span style="color: hsl(0, 100%, 40%);">-            self.process_obj.terminate()</span><br><span style="color: hsl(120, 100%, 40%);">+            self.send_signal(signal.SIGTERM)</span><br><span>             self.killed = signal.SIGTERM</span><br><span>             if self._poll_termination():</span><br><span>                 break</span><br><span> </span><br><span>             # out of patience</span><br><span>             self.log('Terminating (SIGKILL)')</span><br><span style="color: hsl(0, 100%, 40%);">-            self.process_obj.kill()</span><br><span style="color: hsl(120, 100%, 40%);">+            self.send_signal(signal.SIGKILL)</span><br><span>             self.killed = signal.SIGKILL</span><br><span>             break;</span><br><span> </span><br><span>@@ -236,6 +239,22 @@</span><br><span>                                      ' '.join(self.popen_args))]</span><br><span>         self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+class NetNSProcess(Process):</span><br><span style="color: hsl(120, 100%, 40%);">+    NETNS_EXEC_BIN = 'osmo-gsm-tester_netns_exec.sh'</span><br><span style="color: hsl(120, 100%, 40%);">+    def __init__(self, name, run_dir, netns, popen_args, **popen_kwargs):</span><br><span style="color: hsl(120, 100%, 40%);">+        super().__init__(name, run_dir, popen_args, **popen_kwargs)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.netns = netns</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+        self.popen_args = ['sudo', self.NETNS_EXEC_BIN, self.netns] + list(popen_args)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    # HACK: Since we run under sudo, only way to kill root-owned process is to kill as root...</span><br><span style="color: hsl(120, 100%, 40%);">+    # This function is overwritten from Process.</span><br><span style="color: hsl(120, 100%, 40%);">+    def send_signal(self, sig):</span><br><span style="color: hsl(120, 100%, 40%);">+        kill_cmd = ('kill', '-%d' % int(sig), str(self.process_obj.pid))</span><br><span style="color: hsl(120, 100%, 40%);">+        run_local_netns_sync(self.run_dir, self.name()+"-kill", self.netns, kill_cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span> def run_proc_sync(proc):</span><br><span>     try:</span><br><span>         proc.launch()</span><br><span>@@ -252,6 +271,11 @@</span><br><span>     proc = Process(name, run_dir, popen_args)</span><br><span>     run_proc_sync(proc)</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+def run_local_netns_sync(run_dir, name, netns, popen_args):</span><br><span style="color: hsl(120, 100%, 40%);">+    run_dir =run_dir.new_dir(name)</span><br><span style="color: hsl(120, 100%, 40%);">+    proc = NetNSProcess(name, run_dir, netns, popen_args)</span><br><span style="color: hsl(120, 100%, 40%);">+    run_proc_sync(proc)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span> def run_remote_sync(run_dir, remote_user, remote_addr, name, popen_args, remote_cwd=None):</span><br><span>     run_dir = run_dir.new_dir(name)</span><br><span>     proc = RemoteProcess(name, run_dir, remote_user, remote_addr, remote_cwd, popen_args)</span><br><span>diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py</span><br><span>index 22a47a2..67ddefd 100644</span><br><span>--- a/src/osmo_gsm_tester/suite.py</span><br><span>+++ b/src/osmo_gsm_tester/suite.py</span><br><span>@@ -328,7 +328,7 @@</span><br><span>     def modem(self, specifics=None):</span><br><span>         conf = self.reserved_resources.get(resource.R_MODEM, specifics=specifics)</span><br><span>         self.dbg('create Modem object', conf=conf)</span><br><span style="color: hsl(0, 100%, 40%);">-        ms = modem.Modem(conf)</span><br><span style="color: hsl(120, 100%, 40%);">+        ms = modem.Modem(self, conf)</span><br><span>         self.register_for_cleanup(ms)</span><br><span>         return ms</span><br><span> </span><br><span>diff --git a/suites/gprs/ping.py b/suites/gprs/ping.py</span><br><span>index 1647445..9186fe6 100755</span><br><span>--- a/suites/gprs/ping.py</span><br><span>+++ b/suites/gprs/ping.py</span><br><span>@@ -48,8 +48,10 @@</span><br><span> </span><br><span> # We need to use inet46 since ofono qmi only uses ipv4v6 eua (OS#2713)</span><br><span> ctx_id_v4 = ms.activate_context(apn='inet46', protocol=ms.CTX_PROT_IPv4)</span><br><span style="color: hsl(0, 100%, 40%);">-sleep(5)</span><br><span style="color: hsl(0, 100%, 40%);">-# TODO: send ping to server or open TCP conn with a socket in python</span><br><span style="color: hsl(120, 100%, 40%);">+print("Setting up data plan for %r" % repr(ctx_id_v4))</span><br><span style="color: hsl(120, 100%, 40%);">+ms.setup_context_data_plane(ctx_id_v4)</span><br><span style="color: hsl(120, 100%, 40%);">+print("Running 10 ping requests for %r" % repr(ctx_id_v4))</span><br><span style="color: hsl(120, 100%, 40%);">+ms.run_netns_wait('ping', ('ping', '-c', '10', ggsn.addr()))</span><br><span> ms.deactivate_context(ctx_id_v4)</span><br><span> </span><br><span> # We need to use inet46 since ofono qmi only uses ipv4v6 eua (OS#2713)</span><br><span>diff --git a/utils/osmo-gsm-tester_netns_exec.sh b/utils/osmo-gsm-tester_netns_exec.sh</span><br><span>new file mode 100755</span><br><span>index 0000000..336b746</span><br><span>--- /dev/null</span><br><span>+++ b/utils/osmo-gsm-tester_netns_exec.sh</span><br><span>@@ -0,0 +1,5 @@</span><br><span style="color: hsl(120, 100%, 40%);">+#!/bin/bash</span><br><span style="color: hsl(120, 100%, 40%);">+netns="$1"</span><br><span style="color: hsl(120, 100%, 40%);">+shift</span><br><span style="color: hsl(120, 100%, 40%);">+#TODO: Later on I may want to call myself with specific ENV and calling sudo in order to run inside the netns but with dropped privileges</span><br><span style="color: hsl(120, 100%, 40%);">+ip netns exec $netns "$@"</span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.osmocom.org/11462">change 11462</a>. To unsubscribe, or for help writing mail filters, visit <a href="https://gerrit.osmocom.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.osmocom.org/11462"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: osmo-gsm-tester </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-MessageType: merged </div>
<div style="display:none"> Gerrit-Change-Id: Icb06bdfcdd37c797be95ab5addb28da2d9f6681c </div>
<div style="display:none"> Gerrit-Change-Number: 11462 </div>
<div style="display:none"> Gerrit-PatchSet: 4 </div>
<div style="display:none"> Gerrit-Owner: Pau Espin Pedrol <pespin@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: Harald Welte <laforge@gnumonks.org> </div>
<div style="display:none"> Gerrit-Reviewer: Jenkins Builder (1000002) </div>
<div style="display:none"> Gerrit-Reviewer: Max <msuraev@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: Pau Espin Pedrol <pespin@sysmocom.de> </div>