fixeria submitted this change.
[REST] osmo-s1gw-cli.py skeleton
This is an interactive shell based on Python's cmd2 library, providing
an alternative to the traditional VTY interface used in many Osmocom
projects. It communicates with the main process via the REST interface.
Currently there's only one command fetching the OpenAPI specification.
More commands will be introduced in follow-up commits.
Change-Id: I05600f2fa6d213b9cee28871761231722ff5b876
Related: SYS#7066
---
M Makefile
M README.md
A contrib/osmo-s1gw-cli.py
M debian/control
M debian/copyright
A debian/osmo-s1gw-cli.install
A doc/osmo-s1gw-cli.md
7 files changed, 230 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
index a68877f..a4492f1 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,8 @@
cp -r $(REBAR_BASE_DIR)/default/rel/osmo-s1gw $(DESTDIR)$(LIBDIR)/
install -Dm0755 contrib/osmo-s1gw.sh \
$(DESTDIR)$(BINDIR)/osmo-s1gw
+ install -Dm0755 contrib/osmo-s1gw-cli.py \
+ $(DESTDIR)$(BINDIR)/osmo-s1gw-cli
install -Dm0644 config/sys.config \
$(DESTDIR)$(CONFDIR)/osmo-s1gw.config
install -Dm0644 contrib/systemd/osmo-s1gw.service \
diff --git a/README.md b/README.md
index 3ff2091..6dc9fe3 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,7 @@
* `/usr/lib/osmo-s1gw` - complete OTP release package
* `/usr/bin/osmo-s1gw` - convenience script for running `osmo-s1gw`
+* `/usr/bin/osmo-s1gw-cli` - interactive CLI for `osmo-s1gw`
* `/lib/systemd/system/osmo-s1gw.service` - systemd unit file
* `/etc/osmocom/osmo-s1gw.config` - the configuration file
@@ -178,3 +179,12 @@
> **Note:** Both files must be kept in sync. When making changes to the YAML
document, make sure to update the JSON counterpart by invoking `make openapi`.
+
+
+Interactive CLI
+---------------
+
+OsmoS1GW comes with [`osmo-s1gw-cli.py`](contrib/osmo-s1gw-cli.py) - an interactive
+shell based on Python's **cmd2** library. This script serves as an alternative to the
+traditional telnet/VTY interface found in many Osmocom projects. For more details,
+see (doc/osmo-s1gw-cli.md).
diff --git a/contrib/osmo-s1gw-cli.py b/contrib/osmo-s1gw-cli.py
new file mode 100755
index 0000000..8007314
--- /dev/null
+++ b/contrib/osmo-s1gw-cli.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+# Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
+#
+# All Rights Reserved
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# 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 logging
+import argparse
+import cmd2
+import sys
+
+import tabulate
+import urllib.request
+import http.client
+import json
+
+# local logger for this module
+log = logging.getLogger(__name__)
+
+
+class RestIface:
+ ''' REST interface for OsmoS1GW '''
+
+ HTTPResponse = http.client.HTTPResponse
+ RESTResponse = dict | list[dict]
+
+ def __init__(self, host: str, port: int):
+ self.url = f'http://{host}:{port}'
+
+ def send_req(self, method: str,
+ path: str = '',
+ data: dict = {}) -> HTTPResponse:
+ ''' Send an HTTP request to the given endpoint (path) '''
+ req = urllib.request.Request(f'{self.url}/{path}', method=method)
+ req.add_header('Accept', 'application/json')
+ if data:
+ req.add_header('Content-Type', 'application/json')
+ req.data = json.dumps(data).encode('utf-8')
+ log.debug(f'HTTP {req.method} {req.full_url}')
+ return urllib.request.urlopen(req)
+
+ def send_get_req(self, path: str, query: dict = {}) -> HTTPResponse:
+ ''' Send an HTTP GET request to the given endpoint (path) '''
+ if query:
+ path += '?' + urllib.parse.urlencode(query)
+ return self.send_req('GET', path)
+
+ def send_post_req(self, path: str, data: dict = {}) -> HTTPResponse:
+ ''' Send an HTTP POST request to the given endpoint (path) '''
+ return self.send_req('POST', path, data)
+
+ def send_delete_req(self, path: str, data: dict = {}) -> HTTPResponse:
+ ''' Send an HTTP DELETE request to the given endpoint (path) '''
+ return self.send_req('DELETE', path, data)
+
+ def fetch_spec(self) -> RESTResponse:
+ ''' Fetch the OpenAPI specification (JSON) '''
+ with self.send_get_req('swagger/spec.json') as f:
+ return json.load(f)
+
+
+class OsmoS1GWCli(cmd2.Cmd):
+ DESC = 'Interactive CLI for OsmoS1GW'
+
+ def __init__(self, argv):
+ super().__init__(allow_cli_args=False, include_py=True)
+
+ if argv.verbose > 0:
+ logging.root.setLevel(logging.DEBUG)
+ self.debug = True
+
+ self.intro = cmd2.style('Welcome to %s!' % self.DESC, fg=cmd2.Fg.RED)
+ self.default_category = 'Built-in commands'
+ self.prompt = 'OsmoS1GW# '
+
+ self.tablefmt = 'github' # default table format for tabulate
+ self.add_settable(cmd2.Settable('tablefmt', str, 'Table format for tabulate', self,
+ choices=tabulate.tabulate_formats))
+
+ self.iface = RestIface(argv.HOST, argv.port)
+
+ def do_fetch_openapi_spec(self, opts):
+ ''' Fetch the OpenAPI specification (JSON), dump as text '''
+ spec = self.iface.fetch_spec()
+ self.poutput(json.dumps(spec, indent=4))
+
+
+ap = argparse.ArgumentParser(prog='osmo-s1gw-cli', description=OsmoS1GWCli.DESC)
+
+ap.add_argument('-v', '--verbose', action='count', default=0,
+ help='print debug logging')
+ap.add_argument('-p', '--port', metavar='PORT', type=int, default=8080,
+ help='OsmoS1GW REST port (default: %(default)s)')
+ap.add_argument('HOST', type=str, nargs='?', default='localhost',
+ help='OsmoS1GW REST host/address (default: %(default)s)')
+
+logging.basicConfig(
+ format='\r[%(levelname)s] %(filename)s:%(lineno)d %(message)s', level=logging.INFO)
+
+if __name__ == '__main__':
+ argv = ap.parse_args()
+ app = OsmoS1GWCli(argv)
+ sys.exit(app.cmdloop())
diff --git a/debian/control b/debian/control
index 45c3847..515e3b2 100644
--- a/debian/control
+++ b/debian/control
@@ -17,8 +17,22 @@
Depends: ${shlibs:Depends},
${misc:Depends},
${libsctp:Version}
+Suggests: osmo-s1gw-cli
Multi-Arch: foreign
Description: Osmocom S1 gateway
This can be used on the S1 interface between eNB and MME/CN, and
acts as separation between the eNB-facing IP network and the
CN-facing IP network, which may be separate without routing in between.
+
+Package: osmo-s1gw-cli
+Architecture: any
+Section: utils
+Depends: ${misc:Depends},
+ python3,
+ python3-cmd2,
+ python3-tabulate
+Suggests: osmo-s1gw
+Description: Interactive CLI for the Osmocom S1 gateway
+ This is an interactive shell for the Osmocom S1 gateway, providing
+ an alternative to the traditional VTY interface used in many Osmocom
+ projects. It communicates with OsmoS1GW via the REST interface.
diff --git a/debian/copyright b/debian/copyright
index 30d897e..997d2af 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -6,6 +6,7 @@
src/*.erl
include/*.hrl
contrib/yaml2json.py
+ contrib/osmo-s1gw-cli.py
Copyright: sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
License: AGPL-3.0-or-later
diff --git a/debian/osmo-s1gw-cli.install b/debian/osmo-s1gw-cli.install
new file mode 100644
index 0000000..037a640
--- /dev/null
+++ b/debian/osmo-s1gw-cli.install
@@ -0,0 +1 @@
+usr/bin/osmo-s1gw-cli
diff --git a/doc/osmo-s1gw-cli.md b/doc/osmo-s1gw-cli.md
new file mode 100644
index 0000000..9cbac7b
--- /dev/null
+++ b/doc/osmo-s1gw-cli.md
@@ -0,0 +1,83 @@
+Interactive CLI
+===============
+
+[`osmo-s1gw-cli.py`](contrib/osmo-s1gw-cli.py) is an interactive shell based on
+Python's **cmd2** library. This script serves as an alternative to the traditional
+telnet/VTY interface found in many Osmocom projects. It communicates with the main
+process via the **REST interface**, allowing users to inspect and interact with
+OsmoS1GW in a familiar CLI style.
+
+Installation
+------------
+
+`osmo-s1gw-cli.py` has the following dependencies:
+
+* [`cmd2`](https://pypi.org/project/cmd2/)
+* [`tabulate`](https://pypi.org/project/tabulate/)
+
+You can install them using **pip**:
+
+```bash
+pip install cmd2 tabulate
+```
+
+Or using your system's package manager (Debian/Ubuntu example):
+
+```bash
+sudo apt install python3-cmd2 python3-tabulate
+```
+
+Usage
+-----
+
+By default, `osmo-s1gw-cli.py` is sending HTTP requests to `http://localhost:8080`.
+The hostname/address and port can be specified using command-line arguments:
+
+```bash
+./contrib/osmo-s1gw-cli.py --help
+usage: osmo-s1gw-cli [-h] [-v] [-p PORT] [HOST]
+
+Interactive CLI for OsmoS1GW
+
+positional arguments:
+ HOST OsmoS1GW REST host/address (default: localhost)
+
+options:
+ -h, --help show this help message and exit
+ -v, --verbose print debug logging
+ -p, --port PORT OsmoS1GW REST port (default: 8080)
+```
+
+Available commands can be listed by entering `help -v`:
+
+```
+Welcome to Interactive CLI for OsmoS1GW!
+OsmoS1GW# help -v
+...
+```
+
+Each command has its own detailed help, accessible with `help CMD`:
+
+```
+OsmoS1GW# help quit
+Usage: quit [-h]
+
+Exit this application
+
+optional arguments:
+ -h, --help show this help message and exit
+```
+
+In addition to tab-completion, you can filter (`CMD | grep ...`) and/or
+redirect (`CMD > FILE`) output of a command to a file. For more details
+on the available features and usage patterns, please refer to the
+[cmd2 documentation](https://cmd2.readthedocs.io/en/stable/features/).
+
+Commands
+--------
+
+Below is a list of currently supported commands and some examples.
+
+### `fetch_openapi_spec`
+
+Fetch the OpenAPI specification (JSON), dump as text.
To view, visit change 41097. To unsubscribe, or for help writing mail filters, visit settings.