<p>Max <strong>merged</strong> this change.</p><p><a href="https://gerrit.osmocom.org/12476">View Change</a></p><div style="white-space:pre-wrap">Approvals:
  Jenkins Builder: Verified
  daniel: Looks good to me, but someone else must approve
  Neels Hofmeyr: Looks good to me, approved

</div><pre style="font-family: monospace,monospace; white-space: pre-wrap;">Add initial version of asyncio trap2cgi script<br><br>It uses the same config file format to simplify migration from ctrl2cgi.py<br><br>Change-Id: I7428cbfbc9f1b80ce42a70be555a38a3497d1cf9<br>Related: SYS#4399<br>---<br>M README<br>A contrib/systemd/osmo-trap2cgi.service<br>M debian/rules<br>A scripts/osmo_trap2cgi.py<br>M setup.py<br>5 files changed, 247 insertions(+), 1 deletion(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/README b/README</span><br><span>index e90e214..69eb764 100644</span><br><span>--- a/README</span><br><span>+++ b/README</span><br><span>@@ -9,7 +9,8 @@</span><br><span> There are currently following scripts in this package:</span><br><span> osmotestconfig.py - test that apps start/write with example configs</span><br><span> soap.py - implementation of SOAP <-> Ctrl proxy implemented on top of Twisted (deprecated, unmaintained)</span><br><span style="color: hsl(0, 100%, 40%);">-ctrl2cgi.py - implementation of CGI <-> Ctrl proxy implemented on top of Twisted</span><br><span style="color: hsl(120, 100%, 40%);">+ctrl2cgi.py - implementation of CGI <-> Ctrl proxy implemented on top of Twisted (deprecated, unmaintained)</span><br><span style="color: hsl(120, 100%, 40%);">+osmo_trap2cgi.py - implementation of CGI <-> Ctrl proxy implemented on top of asyncio and aiohttp</span><br><span> osmo_rate_ctr2csv.py - rate counter dumper on top of osmo_ipa</span><br><span> osmo_interact_vty.py - pipe stdin/stdout to a VTY session</span><br><span> osmo_interact_ctrl.py - pipe stdin/stdout to a CTRL port</span><br><span>diff --git a/contrib/systemd/osmo-trap2cgi.service b/contrib/systemd/osmo-trap2cgi.service</span><br><span>new file mode 100644</span><br><span>index 0000000..7a90813</span><br><span>--- /dev/null</span><br><span>+++ b/contrib/systemd/osmo-trap2cgi.service</span><br><span>@@ -0,0 +1,11 @@</span><br><span style="color: hsl(120, 100%, 40%);">+[Unit]</span><br><span style="color: hsl(120, 100%, 40%);">+Description=Proxy between given GCI service and Osmocom CTRL protocol</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+[Service]</span><br><span style="color: hsl(120, 100%, 40%);">+Type=simple</span><br><span style="color: hsl(120, 100%, 40%);">+Restart=always</span><br><span style="color: hsl(120, 100%, 40%);">+ExecStart=/usr/bin/osmo_trap2cgi.py -d -c %E/osmocom/%N.ini</span><br><span style="color: hsl(120, 100%, 40%);">+RestartSec=2</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+[Install]</span><br><span style="color: hsl(120, 100%, 40%);">+WantedBy=multi-user.target</span><br><span>diff --git a/debian/rules b/debian/rules</span><br><span>index b33b599..04b59f6 100755</span><br><span>--- a/debian/rules</span><br><span>+++ b/debian/rules</span><br><span>@@ -17,3 +17,4 @@</span><br><span>        # Install service file with different name than package name:</span><br><span>        # https://unix.stackexchange.com/questions/306234/is-it-possible-to-install-two-services-for-one-package-using-dh-installinit-how</span><br><span>    dh_installinit --name=osmo-ctrl2cgi</span><br><span style="color: hsl(120, 100%, 40%);">+   dh_installinit --name=osmo-trap2cgi</span><br><span>diff --git a/scripts/osmo_trap2cgi.py b/scripts/osmo_trap2cgi.py</span><br><span>new file mode 100755</span><br><span>index 0000000..ad66e7b</span><br><span>--- /dev/null</span><br><span>+++ b/scripts/osmo_trap2cgi.py</span><br><span>@@ -0,0 +1,232 @@</span><br><span style="color: hsl(120, 100%, 40%);">+#!/usr/bin/python3</span><br><span style="color: hsl(120, 100%, 40%);">+# -*- mode: python-mode; py-indent-tabs-mode: nil -*-</span><br><span style="color: hsl(120, 100%, 40%);">+"""</span><br><span style="color: hsl(120, 100%, 40%);">+/*</span><br><span style="color: hsl(120, 100%, 40%);">+ * Copyright (C) 2019 sysmocom s.f.m.c. GmbH</span><br><span style="color: hsl(120, 100%, 40%);">+ *</span><br><span style="color: hsl(120, 100%, 40%);">+ * All Rights Reserved</span><br><span style="color: hsl(120, 100%, 40%);">+ *</span><br><span style="color: hsl(120, 100%, 40%);">+ * This program is free software; you can redistribute it and/or modify</span><br><span style="color: hsl(120, 100%, 40%);">+ * it under the terms of the GNU General Public License as published by</span><br><span style="color: hsl(120, 100%, 40%);">+ * the Free Software Foundation; either version 3 of the License, or</span><br><span style="color: hsl(120, 100%, 40%);">+ * (at your option) any later version.</span><br><span style="color: hsl(120, 100%, 40%);">+ *</span><br><span style="color: hsl(120, 100%, 40%);">+ * This program is distributed in the hope that it will be useful,</span><br><span style="color: hsl(120, 100%, 40%);">+ * but WITHOUT ANY WARRANTY; without even the implied warranty of</span><br><span style="color: hsl(120, 100%, 40%);">+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</span><br><span style="color: hsl(120, 100%, 40%);">+ * GNU General Public License for more details.</span><br><span style="color: hsl(120, 100%, 40%);">+ *</span><br><span style="color: hsl(120, 100%, 40%);">+ * You should have received a copy of the GNU General Public License along</span><br><span style="color: hsl(120, 100%, 40%);">+ * with this program; if not, write to the Free Software Foundation, Inc.,</span><br><span style="color: hsl(120, 100%, 40%);">+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.</span><br><span style="color: hsl(120, 100%, 40%);">+ */</span><br><span style="color: hsl(120, 100%, 40%);">+"""</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+__version__ = "0.0.1" # bump this on every non-trivial change</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from functools import partial</span><br><span style="color: hsl(120, 100%, 40%);">+import configparser, argparse, time, os, asyncio, aiohttp</span><br><span style="color: hsl(120, 100%, 40%);">+from osmopy.trap_helper import make_params, gen_hash, log_init, comm_proc</span><br><span style="color: hsl(120, 100%, 40%);">+from osmopy.osmo_ipa import Ctrl</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def log_bsc_time(l, rq, task, ts, bsc, msg, *args, **kwargs):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Logging contextual wrapper.</span><br><span style="color: hsl(120, 100%, 40%);">+    FIXME: remove task parameter once we bump requirements to Python 3.7+</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    # FIXME: following function is deprecated and will be removed in Python 3.9</span><br><span style="color: hsl(120, 100%, 40%);">+    # Use the asyncio.all_tasks() function instead when available (Python 3.7+).</span><br><span style="color: hsl(120, 100%, 40%);">+    num_tasks = len(task.all_tasks())</span><br><span style="color: hsl(120, 100%, 40%);">+    num_req = len(rq)</span><br><span style="color: hsl(120, 100%, 40%);">+    delta = time.perf_counter() - ts</span><br><span style="color: hsl(120, 100%, 40%);">+    if delta < 1:</span><br><span style="color: hsl(120, 100%, 40%);">+        l('[%d/%d] BSC %s: ' + msg, num_req, num_tasks, bsc, *args, **kwargs)</span><br><span style="color: hsl(120, 100%, 40%);">+    else:</span><br><span style="color: hsl(120, 100%, 40%);">+        l('[%d/%d] BSC %s, %.2f sec: ' + msg, num_req, num_tasks, bsc, time.perf_counter() - ts, *args, **kwargs)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def check_h_val(ctrl, h, v, t, exp):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Check for header inconsistencies.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    if v != exp:</span><br><span style="color: hsl(120, 100%, 40%);">+        ctrl.log.error('Unexpected %s value %x (instead of %x) in |%s| header', t, v, exp, h.hex())</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def get_ctrl_len(ctrl, header):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Obtain expected message length.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    (dlen, p, e, _) = ctrl.del_header(header)</span><br><span style="color: hsl(120, 100%, 40%);">+    check_h_val(ctrl, header, p, "protocol", ctrl.PROTO['OSMO'])</span><br><span style="color: hsl(120, 100%, 40%);">+    check_h_val(ctrl, header, e, "extension", ctrl.EXT['CTRL'])</span><br><span style="color: hsl(120, 100%, 40%);">+    return dlen - 1</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+class Proxy(Ctrl):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Wrapper class to implement per-type message dispatch and keep BSC <-> http Task mapping.</span><br><span style="color: hsl(120, 100%, 40%);">+    N. B: keep async/await semantics out of it.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    def __init__(self, log):</span><br><span style="color: hsl(120, 100%, 40%);">+        super().__init__()</span><br><span style="color: hsl(120, 100%, 40%);">+        self.req = {}</span><br><span style="color: hsl(120, 100%, 40%);">+        self.log = log</span><br><span style="color: hsl(120, 100%, 40%);">+        self.conf = configparser.ConfigParser(interpolation = None)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.conf.read(self.config_file)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.timeout = self.conf['main'].getint('timeout', 30)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.location = self.conf['main'].get('location')</span><br><span style="color: hsl(120, 100%, 40%);">+        self.ctrl_addr = self.conf['main'].get('addr_ctrl', 'localhost')</span><br><span style="color: hsl(120, 100%, 40%);">+        self.ctrl_port = self.conf['main'].getint('port_ctrl', 4250)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.concurrency = self.conf['main'].getint('num_max_conn', 5)</span><br><span style="color: hsl(120, 100%, 40%);">+        # FIXME: use timeout parameter when available (aiohttp version 3.3) as follows</span><br><span style="color: hsl(120, 100%, 40%);">+        #self.http_client = aiohttp.ClientSession(connector = aiohttp.TCPConnector(limit = self.concurrency), timeout = self.timeout)</span><br><span style="color: hsl(120, 100%, 40%);">+        self.http_client = aiohttp.ClientSession(connector = aiohttp.TCPConnector(limit = self.concurrency))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def dispatch(self, w, data):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Basic dispatcher: the expected entry point for CTRL messages.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        (cmd, _, v) = data.decode('utf-8').split(' ', 2)</span><br><span style="color: hsl(120, 100%, 40%);">+        method = getattr(self, cmd, lambda *_: self.log.info('CTRL %s is unhandled by dispatch: ignored.', cmd))</span><br><span style="color: hsl(120, 100%, 40%);">+        method(w, v.split())</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def ERROR(self, _, k):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Handle CTRL ERROR messages.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        self.log_ignore('ERROR', k)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def SET_REPLY(self, _, k):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Handle CTRL SET_REPLY messages.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        self.log_ignore('SET_REPLY', k)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def TRAP(self, w, k):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Handle incoming TRAPs.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        p = k[0].split('.')</span><br><span style="color: hsl(120, 100%, 40%);">+        if p[-1] == 'location-state':</span><br><span style="color: hsl(120, 100%, 40%);">+            self.handle_locationstate(w, p[1], p[3], p[5], k[1])</span><br><span style="color: hsl(120, 100%, 40%);">+        else:</span><br><span style="color: hsl(120, 100%, 40%);">+            self.log_ignore('TRAP', k[0])</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def handle_locationstate(self, w, net, bsc, bts, data):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Handle location-state TRAP: parse trap content, build HTTP request and setup async handlers.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        ts = time.perf_counter()</span><br><span style="color: hsl(120, 100%, 40%);">+        self.cleanup_task(bsc)</span><br><span style="color: hsl(120, 100%, 40%);">+        params = make_params(bsc, data)</span><br><span style="color: hsl(120, 100%, 40%);">+        params['h'] = gen_hash(params, self.conf['main'].get('secret_key'))</span><br><span style="color: hsl(120, 100%, 40%);">+        # FIXME: use asyncio.create_task() when available (Python 3.7+).</span><br><span style="color: hsl(120, 100%, 40%);">+        t = asyncio.ensure_future(self.http_client.post(self.location, data = params))</span><br><span style="color: hsl(120, 100%, 40%);">+        log_bsc_time(self.log.info, self.req, t, ts, bsc, 'location-state@%s => %s', params['time_stamp'], data)</span><br><span style="color: hsl(120, 100%, 40%);">+        t.add_done_callback(partial(self.reply_callback, w, bsc, ts))</span><br><span style="color: hsl(120, 100%, 40%);">+        self.req[bsc] = (t, ts)</span><br><span style="color: hsl(120, 100%, 40%);">+        log_bsc_time(self.log.info, self.req, t, ts, bsc, 'request added (net %s, BTS %s)', net, bts)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def cleanup_task(self, bsc):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        It's ok to cancel() task which is done()</span><br><span style="color: hsl(120, 100%, 40%);">+        but if either of the checks above fires it means that Proxy() is in inconsistent state</span><br><span style="color: hsl(120, 100%, 40%);">+        which should never happen as long as we keep async/await semantics out of it.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        if bsc in self.req:</span><br><span style="color: hsl(120, 100%, 40%);">+            (task, ts) = self.req[bsc]</span><br><span style="color: hsl(120, 100%, 40%);">+            log_bsc = partial(log_bsc_time, self.log.error, self.req, task, ts, bsc)</span><br><span style="color: hsl(120, 100%, 40%);">+            if task.done():</span><br><span style="color: hsl(120, 100%, 40%);">+                log_bsc('task is done but not removed')</span><br><span style="color: hsl(120, 100%, 40%);">+            if task.cancelled():</span><br><span style="color: hsl(120, 100%, 40%);">+                log_bsc('task is cancelled without update')</span><br><span style="color: hsl(120, 100%, 40%);">+            task.cancel()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def log_ignore(self, kind, m):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Log ignored CTRL message.</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        self.log.error('Ignoring CTRL %s: %s', kind, ' '.join(m) if type(m) is list else m)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def reply_callback(self, w, bsc, ts, task):</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        Process per-BSC response status and prepare async handler if necessary.</span><br><span style="color: hsl(120, 100%, 40%);">+        We don't have to delete cancel()ed task from self.req explicitly because it will be replaced by new one in handle_locationstate()</span><br><span style="color: hsl(120, 100%, 40%);">+        """</span><br><span style="color: hsl(120, 100%, 40%);">+        log_bsc = partial(log_bsc_time, self.log.info, self.req, task, ts, bsc)</span><br><span style="color: hsl(120, 100%, 40%);">+        if task.cancelled():</span><br><span style="color: hsl(120, 100%, 40%);">+            log_bsc('request cancelled')</span><br><span style="color: hsl(120, 100%, 40%);">+        else:</span><br><span style="color: hsl(120, 100%, 40%);">+            exp = task.exception()</span><br><span style="color: hsl(120, 100%, 40%);">+            if exp:</span><br><span style="color: hsl(120, 100%, 40%);">+                log_bsc('exception %s triggered', repr(exp))</span><br><span style="color: hsl(120, 100%, 40%);">+            else:</span><br><span style="color: hsl(120, 100%, 40%);">+                resp = task.result()</span><br><span style="color: hsl(120, 100%, 40%);">+                if resp.status != 200:</span><br><span style="color: hsl(120, 100%, 40%);">+                    log_bsc('unexpected HTTP response %d', resp.status)</span><br><span style="color: hsl(120, 100%, 40%);">+                else:</span><br><span style="color: hsl(120, 100%, 40%);">+                    log_bsc('request completed')</span><br><span style="color: hsl(120, 100%, 40%);">+                    # FIXME: use asyncio.create_task() when available (Python 3.7+).</span><br><span style="color: hsl(120, 100%, 40%);">+                    asyncio.ensure_future(recv_response(self.log, w, bsc, resp.json()))</span><br><span style="color: hsl(120, 100%, 40%);">+            del self.req[bsc]</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+async def recv_response(log, w, bsc, resp):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Process json response asynchronously.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    js = await resp</span><br><span style="color: hsl(120, 100%, 40%);">+    if js.get('error'):</span><br><span style="color: hsl(120, 100%, 40%);">+        log.info('BSC %s response error: %s', bsc, repr(js.get('error')))</span><br><span style="color: hsl(120, 100%, 40%);">+    else:</span><br><span style="color: hsl(120, 100%, 40%);">+        comm_proc(js.get('commands'), bsc, w.write, log)</span><br><span style="color: hsl(120, 100%, 40%);">+        await w.drain() # Trigger Writer's flow control</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+async def recon_reader(proxy, reader, num_bytes):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Read requested amount of bytes, reconnect if necessary.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        return await reader.readexactly(num_bytes)</span><br><span style="color: hsl(120, 100%, 40%);">+    except asyncio.IncompleteReadError:</span><br><span style="color: hsl(120, 100%, 40%);">+        proxy.log.info('Failed to read %d bytes reconnecting to %s:%d...', num_bytes, proxy.ctrl_addr, proxy.ctrl_port)</span><br><span style="color: hsl(120, 100%, 40%);">+        await conn_client(proxy)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+async def ctrl_client(proxy, rd, wr):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    Recursively read CTRL stream and handle selected messages.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    header = await recon_reader(proxy, rd, 4)</span><br><span style="color: hsl(120, 100%, 40%);">+    data = await recon_reader(proxy, rd, get_ctrl_len(proxy, header))</span><br><span style="color: hsl(120, 100%, 40%);">+    proxy.dispatch(wr, data)</span><br><span style="color: hsl(120, 100%, 40%);">+    await ctrl_client(proxy, rd, wr)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+async def conn_client(proxy):</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    (Re)establish connection with CTRL server and pass Reader/Writer to CTRL handler.</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        reader, writer = await asyncio.open_connection(proxy.ctrl_addr, proxy.ctrl_port)</span><br><span style="color: hsl(120, 100%, 40%);">+        proxy.log.info('Connected to %s:%d', proxy.ctrl_addr, proxy.ctrl_port)</span><br><span style="color: hsl(120, 100%, 40%);">+        await ctrl_client(proxy, reader, writer)</span><br><span style="color: hsl(120, 100%, 40%);">+    except OSError as e:</span><br><span style="color: hsl(120, 100%, 40%);">+        proxy.log.info('%s: %d seconds delayed retrying...', e, proxy.timeout)</span><br><span style="color: hsl(120, 100%, 40%);">+        await asyncio.sleep(proxy.timeout)</span><br><span style="color: hsl(120, 100%, 40%);">+        await conn_client(proxy)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+if __name__ == '__main__':</span><br><span style="color: hsl(120, 100%, 40%);">+    a = argparse.ArgumentParser(description = 'Proxy between given GCI service and Osmocom CTRL protocol.')</span><br><span style="color: hsl(120, 100%, 40%);">+    a.add_argument('-v', '--version', action = 'version', version = ("%(prog)s v" + __version__))</span><br><span style="color: hsl(120, 100%, 40%);">+    a.add_argument('-d', '--debug', action = 'store_true', help = "Enable debug log")</span><br><span style="color: hsl(120, 100%, 40%);">+    a.add_argument('-c', '--config-file', required = True, help = "Path to mandatory config file (in INI format).")</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    P = Proxy(log_init('TRAP2CGI', a.parse_args(namespace=Proxy).debug))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    P.log.info('CGI proxy v%s starting with PID %d:', __version__, os.getpid())</span><br><span style="color: hsl(120, 100%, 40%);">+    P.log.info('Destination %s (concurrency %d)', P.location, P.concurrency)</span><br><span style="color: hsl(120, 100%, 40%);">+    P.log.info('Connecting to TRAP source %s:%d...', P.ctrl_addr, P.ctrl_port)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    loop = asyncio.get_event_loop()</span><br><span style="color: hsl(120, 100%, 40%);">+    loop.run_until_complete(conn_client(P))</span><br><span style="color: hsl(120, 100%, 40%);">+    # FIXME: use loop.run() function instead when available (Python 3.7+).</span><br><span>diff --git a/setup.py b/setup.py</span><br><span>index 061e5df..69e18b0 100755</span><br><span>--- a/setup.py</span><br><span>+++ b/setup.py</span><br><span>@@ -31,6 +31,7 @@</span><br><span>         "scripts/osmo_rate_ctr2csv.py",</span><br><span>         "scripts/soap.py",</span><br><span>         "scripts/ctrl2cgi.py",</span><br><span style="color: hsl(120, 100%, 40%);">+        "scripts/osmo_trap2cgi.py",</span><br><span>         "scripts/osmo_interact_vty.py",</span><br><span>         "scripts/osmo_interact_ctrl.py",</span><br><span>         "scripts/osmo_verify_transcript_vty.py",</span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.osmocom.org/12476">change 12476</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/12476"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: python/osmo-python-tests </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-MessageType: merged </div>
<div style="display:none"> Gerrit-Change-Id: I7428cbfbc9f1b80ce42a70be555a38a3497d1cf9 </div>
<div style="display:none"> Gerrit-Change-Number: 12476 </div>
<div style="display:none"> Gerrit-PatchSet: 4 </div>
<div style="display:none"> Gerrit-Owner: Max <msuraev@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: Neels Hofmeyr <nhofmeyr@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: Pau Espin Pedrol <pespin@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: Vadim Yanitskiy <axilirator@gmail.com> </div>
<div style="display:none"> Gerrit-Reviewer: daniel <dwillmann@sysmocom.de> </div>