Harald Welte has submitted this change and it was merged. Change subject: fix osmo_interact_* and osmo_verify_transcript_* after dir split ......................................................................
fix osmo_interact_* and osmo_verify_transcript_* after dir split After I30cdf0f85b2a60a235960911c9827f4129da40db, * the osmo_interact_{vty,ctrl}.py can no longer import osmo_interact_common, since it was moved to scripts/ in error. * the osmo_verify_{vty,ctrl} scripts can no longer import osmo_interact_{vty,ctrl}, since it is also in scripts/. Notably, the osmo_interact_{vty,ctrl}.py also served as scripts while being modules at the same time, which is not good. Fix these issues by adding a new osmopy/osmo_interact/ submodule with osmopy/osmo_interact/common.py, /vty.py and /ctrl.py as modules, and add in scripts thin wrappers that invoke the modules' main(). Change-Id: I40a37b212274cb70ebb1e1d9d1b3743eb2d64d05 --- M osmopy/__init__.py A osmopy/osmo_interact/__init__.py R osmopy/osmo_interact/common.py A osmopy/osmo_interact/ctrl.py A osmopy/osmo_interact/vty.py M scripts/osmo_interact_ctrl.py M scripts/osmo_interact_vty.py M scripts/osmo_verify_transcript_ctrl.py M scripts/osmo_verify_transcript_vty.py 9 files changed, 321 insertions(+), 315 deletions(-) Approvals: Harald Welte: Looks good to me, approved Jenkins Builder: Verified diff --git a/osmopy/__init__.py b/osmopy/__init__.py index 6150ea4..b1b0651 100644 --- a/osmopy/__init__.py +++ b/osmopy/__init__.py @@ -1,4 +1,4 @@ #!/usr/bin/env python -__version__ = '0.0.5' +__version__ = '0.0.6' -__all__ = ['obscvty', 'osmoutil', 'osmo_ipa'] +__all__ = ['obscvty', 'osmoutil', 'osmo_ipa', 'osmo_interact'] diff --git a/osmopy/osmo_interact/__init__.py b/osmopy/osmo_interact/__init__.py new file mode 100644 index 0000000..4fc4fac --- /dev/null +++ b/osmopy/osmo_interact/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +__all__ = ['common', 'vty', 'ctrl'] diff --git a/scripts/osmo_interact_common.py b/osmopy/osmo_interact/common.py similarity index 97% rename from scripts/osmo_interact_common.py rename to osmopy/osmo_interact/common.py index 5efc22d..f7070ae 100644 --- a/scripts/osmo_interact_common.py +++ b/osmopy/osmo_interact/common.py @@ -24,6 +24,11 @@ osmo_interact_{vty,ctrl}.py plug VTY and CTRL interface specific bits. ''' +# Our setup.py currently wants everything to be parsable by both py2 and py3. +# IMHO that is not a good idea, but until that changes, let's just keep this +# py2 legacy shim in here so we can syntax-check this py3 module with py2. +from __future__ import print_function + import argparse import sys import os diff --git a/osmopy/osmo_interact/ctrl.py b/osmopy/osmo_interact/ctrl.py new file mode 100755 index 0000000..b752351 --- /dev/null +++ b/osmopy/osmo_interact/ctrl.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# (C) 2017 by sysmocom s.f.m.c. GmbH <i...@sysmocom.de> +# All rights reserved. +# +# Author: Neels Hofmeyr <nhofm...@sysmocom.de> +# +# 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/>. + +''' +Run CTRL commands or test transcripts against a given application. Commandline +invocation exposes only direct command piping, the transcript verification code +is exposed as commandline args by osmo_verify_transcript_ctrl.py. +''' + +import re + +from .common import * +from osmopy.osmo_ipa import Ctrl, IPA + +class InteractCtrl(Interact): + next_id = 1 + keep_ids = True + re_command = re.compile('^(SET|GET) ([^ ]*) (.*)$') + + class CtrlStep(Interact.StepBase): + + @staticmethod + def is_next_step(line, interact_instance): + m = InteractCtrl.re_command.match(line) + if not m: + return None + next_step = InteractCtrl.CtrlStep() + + set_get = m.group(1) + cmd_id = m.group(2) + var_val = m.group(3) + if not interact_instance.keep_ids: + cmd_id = interact_instance.next_id + interact_instance.next_id += 1 + next_step.command = '%s %s %s' % (set_get, cmd_id, var_val) + + return next_step + + def __init__(self, port, host, verbose=False, update=False, keep_ids=True): + if not update: + keep_ids = True + self.keep_ids = keep_ids + super().__init__(InteractCtrl.CtrlStep, port=port, host=host, verbose=verbose, update=update) + + def connect(self): + self.next_id = 1 + super().connect() + + def send(self, data): + data = Ctrl().add_header(data) + return self.socket.send(data) == len(data) + + def receive(self): + responses = [] + data = self.socket.recv(4096) + while (len(data)>0): + (response_with_header, data) = IPA().split_combined(data) + response = Ctrl().rem_header(response_with_header) + responses.append(response.decode('utf-8')) + return responses + + def command(self, command): + assert self.send(command) + res = self.receive() + split_responses = [] + for r in res: + split_responses.extend(r.splitlines()) + sys.stdout.flush() + sys.stderr.flush() + return split_responses + +def main_interact_ctrl(): + parser = common_parser() + parser_add_run_args(parser) + args = parser.parse_args() + + interact = InteractCtrl(args.port, args.host, verbose=False, update=False, + keep_ids=True) + + main_run_commands(args.run_app_str, args.output_path, args.cmd_str, + args.cmd_files, interact) + + +def main_verify_transcript_ctrl(): + parser = common_parser() + parser_add_verify_args(parser) + parser.add_argument('-i', '--keep-ids', dest='keep_ids', action='store_true', + help='With --update, default is to overwrite the command IDs' + ' so that they are consecutive numbers starting from 1.' + ' With --keep-ids, do not change these command IDs.') + args = parser.parse_args() + + interact = InteractCtrl(args.port, args.host, args.verbose, args.update, args.keep_ids) + + main_verify_transcripts(args.run_app_str, args.transcript_files, interact, args.verbose) + +# vim: tabstop=4 shiftwidth=4 expandtab nocin ai diff --git a/osmopy/osmo_interact/vty.py b/osmopy/osmo_interact/vty.py new file mode 100755 index 0000000..f34e87e --- /dev/null +++ b/osmopy/osmo_interact/vty.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# +# (C) 2017 by sysmocom s.f.m.c. GmbH <i...@sysmocom.de> +# All rights reserved. +# +# Author: Neels Hofmeyr <nhofm...@sysmocom.de> +# +# 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/>. + +''' +Run VTY commands or test transcripts against a given application. Commandline +invocation exposes only direct command piping, the transcript verification code +is exposed as commandline args by osmo_verify_transcript_vty.py. +''' + +import re + +from .common import * + +class InteractVty(Interact): + + class VtyStep(Interact.StepBase): + expect_node = None # e.g. '(config-net)' + expect_prompt_char = None # '>' or '#' + + def __init__(self, prompt): + super().__init__() + self.prompt = prompt + + def verify_interact_state(self, interact_instance): + if interact_instance.update: + return + if interact_instance.this_node != self.expect_node: + raise Exception('Mismatch: expected VTY node %r in the prompt, got %r' + % (self.expect_node, interact_instance.this_node)) + if interact_instance.this_prompt_char != self.expect_prompt_char: + raise Exception('Mismatch: expected VTY prompt character %r, got %r' + % (self.expect_prompt_char, interact_instance.this_prompt_char)) + + @staticmethod + def is_next_step(line, interact_instance): + m = interact_instance.re_prompt.match(line) + if not m: + return None + next_step = InteractVty.VtyStep(interact_instance.prompt) + next_step.expect_node = m.group(1) + next_step.expect_prompt_char = m.group(2) + next_step.command = m.group(3) + return next_step + + def command_str(self, interact_instance=None): + if interact_instance is None: + node = self.expect_node + prompt_char = self.expect_prompt_char + else: + node = interact_instance.last_node + prompt_char = interact_instance.last_prompt_char + if node: + node = '(%s)' % node + node = node or '' + return '%s%s%s %s' % (self.prompt, node, prompt_char, self.command) + + def __init__(self, prompt, port, host, verbose, update): + self.prompt = prompt + super().__init__(InteractVty.VtyStep, port, host, verbose, update) + + def connect(self): + self.this_node = None + self.this_prompt_char = '>' # slight cheat for initial prompt char + self.last_node = None + self.last_prompt_char = None + + super().connect() + # receive the first welcome message and discard + data = self.socket.recv(4096) + if not self.prompt: + b = data + b = b[b.rfind(b'\n') + 1:] + while b and (b[0] < ord('A') or b[0] > ord('z')): + b = b[1:] + prompt_str = b.decode('utf-8') + if '>' in prompt_str: + self.prompt = prompt_str[:prompt_str.find('>')] + if not self.prompt: + raise Exception('Could not find application name; needed to decode prompts.' + ' Initial data was: %r' % data) + self.re_prompt = re.compile('^%s(?:\(([\w-]*)\))?([#>]) (.*)$' % self.prompt) + + def _command(self, command_str, timeout=10): + self.socket.send(command_str.encode()) + + waited_since = time.time() + received_lines = [] + last_line = '' + + while True: + new_data = self.socket.recv(4096).decode('utf-8') + + last_line = "%s%s" % (last_line, new_data) + + if last_line: + lines = last_line.splitlines() + received_lines.extend(lines[:-1]) + last_line = lines[-1] + + match = self.re_prompt.match(last_line) + if not match: + if time.time() - waited_since > timeout: + raise IOError("Failed to read data (did the app crash?)") + time.sleep(.1) + continue + + self.last_node = self.this_node + self.last_prompt_char = self.this_prompt_char + self.this_node = match.group(1) or None + self.this_prompt_char = match.group(2) + break + + # expecting to have received the command we sent as echo, remove it + clean_command_str = command_str.strip() + if clean_command_str.endswith('?'): + clean_command_str = clean_command_str[:-1] + if received_lines and received_lines[0] == clean_command_str: + received_lines = received_lines[1:] + return received_lines + + def command(self, command_str, timeout=10): + command_str = command_str or '\r' + if command_str[-1] not in '?\r\t': + command_str = command_str + '\r' + + received_lines = self._command(command_str, timeout) + + # send escape to cancel the '?' command line + if command_str[-1] == '?': + self._command('\x03', timeout) + + return received_lines + +def parser_add_vty_args(parser): + parser.add_argument('-n', '--prompt-name', dest='prompt', + help="Name used in application's telnet VTY prompt." + " If omitted, will attempt to determine the name from" + " the initial VTY prompt.") + return parser + +def main_interact_vty(): + parser = common_parser() + parser_add_vty_args(parser) + parser_add_run_args(parser) + parser.add_argument('-X', '--gen-xml-ref', dest='gen_xml', action='store_true', + help="Equivalent to '-c \"show online-help\" -O -'," + " can be used to generate the VTY reference file as" + " required by osmo-gsm-manuals.git.") + args = parser.parse_args() + + if args.gen_xml: + if args.cmd_str: + raise Exception('It does not make sense to pass both --command and' + ' --gen-xml-ref.') + args.cmd_str = 'show online-help' + + interact = InteractVty(args.prompt, args.port, args.host, + verbose=False, update=False) + + main_run_commands(args.run_app_str, args.output_path, args.cmd_str, + args.cmd_files, interact) + +def main_verify_transcript_vty(): + parser = common_parser() + parser_add_vty_args(parser) + parser_add_verify_args(parser) + args = parser.parse_args() + + interact = InteractVty(args.prompt, args.port, args.host, args.verbose, args.update) + + main_verify_transcripts(args.run_app_str, args.transcript_files, interact, args.verbose) + +# vim: tabstop=4 shiftwidth=4 expandtab nocin ai diff --git a/scripts/osmo_interact_ctrl.py b/scripts/osmo_interact_ctrl.py index 9b1a20b..eb88800 100755 --- a/scripts/osmo_interact_ctrl.py +++ b/scripts/osmo_interact_ctrl.py @@ -18,83 +18,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -''' -Run CTRL commands or test transcripts against a given application. Commandline -invocation exposes only direct command piping, the transcript verification code -is exposed as commandline args by osmo_verify_transcript_ctrl.py. -''' +from osmopy.osmo_interact.ctrl import main_interact_ctrl -import re - -from osmopy.osmo_interact_common import * -from osmopy.osmo_ipa import Ctrl, IPA - -class InteractCtrl(Interact): - next_id = 1 - keep_ids = True - re_command = re.compile('^(SET|GET) ([^ ]*) (.*)$') - - class CtrlStep(Interact.StepBase): - - @staticmethod - def is_next_step(line, interact_instance): - m = InteractCtrl.re_command.match(line) - if not m: - return None - next_step = InteractCtrl.CtrlStep() - - set_get = m.group(1) - cmd_id = m.group(2) - var_val = m.group(3) - if not interact_instance.keep_ids: - cmd_id = interact_instance.next_id - interact_instance.next_id += 1 - next_step.command = '%s %s %s' % (set_get, cmd_id, var_val) - - return next_step - - def __init__(self, port, host, verbose=False, update=False, keep_ids=True): - if not update: - keep_ids = True - self.keep_ids = keep_ids - super().__init__(InteractCtrl.CtrlStep, port=port, host=host, verbose=verbose, update=update) - - def connect(self): - self.next_id = 1 - super().connect() - - def send(self, data): - data = Ctrl().add_header(data) - return self.socket.send(data) == len(data) - - def receive(self): - responses = [] - data = self.socket.recv(4096) - while (len(data)>0): - (response_with_header, data) = IPA().split_combined(data) - response = Ctrl().rem_header(response_with_header) - responses.append(response.decode('utf-8')) - return responses - - def command(self, command): - assert self.send(command) - res = self.receive() - split_responses = [] - for r in res: - split_responses.extend(r.splitlines()) - sys.stdout.flush() - sys.stderr.flush() - return split_responses - -if __name__ == '__main__': - parser = common_parser() - parser_add_run_args(parser) - args = parser.parse_args() - - interact = InteractCtrl(args.port, args.host, verbose=False, update=False, - keep_ids=True) - - main_run_commands(args.run_app_str, args.output_path, args.cmd_str, - args.cmd_files, interact) +main_interact_ctrl() # vim: tabstop=4 shiftwidth=4 expandtab nocin ai diff --git a/scripts/osmo_interact_vty.py b/scripts/osmo_interact_vty.py index b57cd8c..b47b46c 100755 --- a/scripts/osmo_interact_vty.py +++ b/scripts/osmo_interact_vty.py @@ -18,163 +18,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -''' -Run VTY commands or test transcripts against a given application. Commandline -invocation exposes only direct command piping, the transcript verification code -is exposed as commandline args by osmo_verify_transcript_vty.py. -''' +from osmopy.osmo_interact.vty import main_interact_vty -import re - -from osmopy.osmo_interact_common import * - -class InteractVty(Interact): - - class VtyStep(Interact.StepBase): - expect_node = None # e.g. '(config-net)' - expect_prompt_char = None # '>' or '#' - - def __init__(self, prompt): - super().__init__() - self.prompt = prompt - - def verify_interact_state(self, interact_instance): - if interact_instance.update: - return - if interact_instance.this_node != self.expect_node: - raise Exception('Mismatch: expected VTY node %r in the prompt, got %r' - % (self.expect_node, interact_instance.this_node)) - if interact_instance.this_prompt_char != self.expect_prompt_char: - raise Exception('Mismatch: expected VTY prompt character %r, got %r' - % (self.expect_prompt_char, interact_instance.this_prompt_char)) - - @staticmethod - def is_next_step(line, interact_instance): - m = interact_instance.re_prompt.match(line) - if not m: - return None - next_step = InteractVty.VtyStep(interact_instance.prompt) - next_step.expect_node = m.group(1) - next_step.expect_prompt_char = m.group(2) - next_step.command = m.group(3) - return next_step - - def command_str(self, interact_instance=None): - if interact_instance is None: - node = self.expect_node - prompt_char = self.expect_prompt_char - else: - node = interact_instance.last_node - prompt_char = interact_instance.last_prompt_char - if node: - node = '(%s)' % node - node = node or '' - return '%s%s%s %s' % (self.prompt, node, prompt_char, self.command) - - def __init__(self, prompt, port, host, verbose, update): - self.prompt = prompt - super().__init__(InteractVty.VtyStep, port, host, verbose, update) - - def connect(self): - self.this_node = None - self.this_prompt_char = '>' # slight cheat for initial prompt char - self.last_node = None - self.last_prompt_char = None - - super().connect() - # receive the first welcome message and discard - data = self.socket.recv(4096) - if not self.prompt: - b = data - b = b[b.rfind(b'\n') + 1:] - while b and (b[0] < ord('A') or b[0] > ord('z')): - b = b[1:] - prompt_str = b.decode('utf-8') - if '>' in prompt_str: - self.prompt = prompt_str[:prompt_str.find('>')] - if not self.prompt: - raise Exception('Could not find application name; needed to decode prompts.' - ' Initial data was: %r' % data) - self.re_prompt = re.compile('^%s(?:\(([\w-]*)\))?([#>]) (.*)$' % self.prompt) - - def _command(self, command_str, timeout=10): - self.socket.send(command_str.encode()) - - waited_since = time.time() - received_lines = [] - last_line = '' - - while True: - new_data = self.socket.recv(4096).decode('utf-8') - - last_line = "%s%s" % (last_line, new_data) - - if last_line: - lines = last_line.splitlines() - received_lines.extend(lines[:-1]) - last_line = lines[-1] - - match = self.re_prompt.match(last_line) - if not match: - if time.time() - waited_since > timeout: - raise IOError("Failed to read data (did the app crash?)") - time.sleep(.1) - continue - - self.last_node = self.this_node - self.last_prompt_char = self.this_prompt_char - self.this_node = match.group(1) or None - self.this_prompt_char = match.group(2) - break - - # expecting to have received the command we sent as echo, remove it - clean_command_str = command_str.strip() - if clean_command_str.endswith('?'): - clean_command_str = clean_command_str[:-1] - if received_lines and received_lines[0] == clean_command_str: - received_lines = received_lines[1:] - return received_lines - - def command(self, command_str, timeout=10): - command_str = command_str or '\r' - if command_str[-1] not in '?\r\t': - command_str = command_str + '\r' - - received_lines = self._command(command_str, timeout) - - # send escape to cancel the '?' command line - if command_str[-1] == '?': - self._command('\x03', timeout) - - return received_lines - -def parser_add_vty_args(parser): - parser.add_argument('-n', '--prompt-name', dest='prompt', - help="Name used in application's telnet VTY prompt." - " If omitted, will attempt to determine the name from" - " the initial VTY prompt.") - return parser - -if __name__ == '__main__': - parser = common_parser() - parser_add_vty_args(parser) - parser_add_run_args(parser) - parser.add_argument('-X', '--gen-xml-ref', dest='gen_xml', action='store_true', - help="Equivalent to '-c \"show online-help\" -O -'," - " can be used to generate the VTY reference file as" - " required by osmo-gsm-manuals.git.") - args = parser.parse_args() - - if args.gen_xml: - if args.cmd_str: - raise Exception('It does not make sense to pass both --command and' - ' --gen-xml-ref.') - args.cmd_str = 'show online-help' - - interact = InteractVty(args.prompt, args.port, args.host, - verbose=False, update=False) - - main_run_commands(args.run_app_str, args.output_path, args.cmd_str, - args.cmd_files, interact) +main_interact_vty() # vim: tabstop=4 shiftwidth=4 expandtab nocin ai diff --git a/scripts/osmo_verify_transcript_ctrl.py b/scripts/osmo_verify_transcript_ctrl.py index 3afbc62..b24fbfa 100755 --- a/scripts/osmo_verify_transcript_ctrl.py +++ b/scripts/osmo_verify_transcript_ctrl.py @@ -18,41 +18,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -''' -Run CTRL test transcripts against a given application. +from osmopy.osmo_interact.ctrl import main_verify_transcript_ctrl -A CTRL transcript contains CTRL commands and their expected results. -It looks like: - -" -SET 1 var val -SET_REPLY 1 var OK -GET 2 var -GET_REPLY 2 var val -" - -The application to be tested is described by -- a binary to run, -- command line arguments to pass to the binary, -- the CTRL port. - -This module can either be run directly to run or update a given CTRL transcript, -or it can be imported as a module to run more complex setups. -''' - -from osmopy.osmo_interact_ctrl import * - -if __name__ == '__main__': - parser = common_parser() - parser_add_verify_args(parser) - parser.add_argument('-i', '--keep-ids', dest='keep_ids', action='store_true', - help='With --update, default is to overwrite the command IDs' - ' so that they are consecutive numbers starting from 1.' - ' With --keep-ids, do not change these command IDs.') - args = parser.parse_args() - - interact = InteractCtrl(args.port, args.host, args.verbose, args.update, args.keep_ids) - - main_verify_transcripts(args.run_app_str, args.transcript_files, interact, args.verbose) +main_verify_transcript_ctrl() # vim: tabstop=4 shiftwidth=4 expandtab nocin ai diff --git a/scripts/osmo_verify_transcript_vty.py b/scripts/osmo_verify_transcript_vty.py index e70c36c..2695e86 100755 --- a/scripts/osmo_verify_transcript_vty.py +++ b/scripts/osmo_verify_transcript_vty.py @@ -18,50 +18,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -''' -Run VTY test transcripts against a given application. +from osmopy.osmo_interact.vty import main_verify_transcript_vty -A VTY transcript contains VTY commands and their expected results. -It looks like: - -" -OsmoHLR> enable - -OsmoHLR# subscriber show imsi 123456789023000 -% No subscriber for imsi = '123456789023000' -OsmoHLR# subscriber show msisdn 12345 -% No subscriber for msisdn = '12345' - -OsmoHLR# subscriber create imsi 123456789023000 -% Created subscriber 123456789023000 - ID: 1 - IMSI: 123456789023000 - MSISDN: none - No auth data -" - -The application to be tested is described by -- a binary to run, -- command line arguments to pass to the binary, -- the VTY telnet port, -- the application name as printed in the VTY prompt. - -This module can either be run directly to run or update a given VTY transcript, -or it can be imported as a module to run more complex setups. -''' - -import re - -from osmopy.osmo_interact_vty import * - -if __name__ == '__main__': - parser = common_parser() - parser_add_vty_args(parser) - parser_add_verify_args(parser) - args = parser.parse_args() - - interact = InteractVty(args.prompt, args.port, args.host, args.verbose, args.update) - - main_verify_transcripts(args.run_app_str, args.transcript_files, interact, args.verbose) +main_verify_transcript_vty() # vim: tabstop=4 shiftwidth=4 expandtab nocin ai -- To view, visit https://gerrit.osmocom.org/5492 To unsubscribe, visit https://gerrit.osmocom.org/settings Gerrit-MessageType: merged Gerrit-Change-Id: I40a37b212274cb70ebb1e1d9d1b3743eb2d64d05 Gerrit-PatchSet: 2 Gerrit-Project: python/osmo-python-tests Gerrit-Branch: master Gerrit-Owner: Neels Hofmeyr <nhofm...@sysmocom.de> Gerrit-Reviewer: Harald Welte <lafo...@gnumonks.org> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: Max <msur...@sysmocom.de> Gerrit-Reviewer: Neels Hofmeyr <nhofm...@sysmocom.de>