On 7/4/24 16:09, [email protected] wrote:
> From: Jakob Meng <[email protected]>
>
> This patch introduces support for different output formats to Python
> Unixctl* classes and appctl.py, similar to what the previous commit did
> for ovs-appctl.
> In particular, tests/appctl.py gains a global option '-f,--format'
> which allows users to request JSON instead of plain-text for humans.
>
> Reported-at: https://bugzilla.redhat.com/1824861
> Signed-off-by: Jakob Meng <[email protected]>
> ---
> NEWS | 2 ++
> python/ovs/unixctl/__init__.py | 8 ++++++
> python/ovs/unixctl/client.py | 5 ++--
> python/ovs/unixctl/server.py | 52 +++++++++++++++++++++++++++++-----
> tests/appctl.py | 38 ++++++++++++++++++++-----
> tests/unixctl-py.at | 4 +++
> 6 files changed, 93 insertions(+), 16 deletions(-)
>
> diff --git a/NEWS b/NEWS
> index f182647c7..c750ebae2 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -19,6 +19,8 @@ Post-v3.3.0
> per interface 'options:dpdk-lsc-interrupt' to 'false'.
> - Python:
> * Added custom transaction support to the Idl via add_op().
> + * Added support for different output formats like 'json' to Python's
> + unixctl classes.
>
>
> v3.3.0 - 16 Feb 2024
> diff --git a/python/ovs/unixctl/__init__.py b/python/ovs/unixctl/__init__.py
> index 8ee312943..b05f3df72 100644
> --- a/python/ovs/unixctl/__init__.py
> +++ b/python/ovs/unixctl/__init__.py
> @@ -12,6 +12,7 @@
> # See the License for the specific language governing permissions and
> # limitations under the License.
>
> +import enum
> import sys
>
> import ovs.util
> @@ -19,6 +20,13 @@ import ovs.util
> commands = {}
>
>
> [email protected]
> +# FIXME: Use @enum.verify(enum.NAMED_FLAGS) from Python 3.11 when available.
> +class UnixctlOutputFormat(enum.IntFlag):
> + TEXT = 1 << 0
> + JSON = 1 << 1
> +
> +
> class _UnixctlCommand(object):
> def __init__(self, usage, min_args, max_args, callback, aux):
> self.usage = usage
> diff --git a/python/ovs/unixctl/client.py b/python/ovs/unixctl/client.py
> index 8283f99bb..8a6fcb1b9 100644
> --- a/python/ovs/unixctl/client.py
> +++ b/python/ovs/unixctl/client.py
> @@ -14,6 +14,7 @@
>
> import os
>
> +import ovs.json
> import ovs.jsonrpc
> import ovs.stream
> import ovs.util
> @@ -41,10 +42,10 @@ class UnixctlClient(object):
> return error, None, None
>
> if reply.error is not None:
> - return 0, str(reply.error), None
> + return 0, reply.error, None
> else:
> assert reply.result is not None
> - return 0, None, str(reply.result)
> + return 0, None, reply.result
>
> def close(self):
> self._conn.close()
> diff --git a/python/ovs/unixctl/server.py b/python/ovs/unixctl/server.py
> index d24a7092c..855f38d18 100644
> --- a/python/ovs/unixctl/server.py
> +++ b/python/ovs/unixctl/server.py
> @@ -12,6 +12,7 @@
> # See the License for the specific language governing permissions and
> # limitations under the License.
>
> +import argparse
> import copy
> import errno
> import os
> @@ -35,6 +36,7 @@ class UnixctlConnection(object):
> assert isinstance(rpc, ovs.jsonrpc.Connection)
> self._rpc = rpc
> self._request_id = None
> + self._fmt = ovs.unixctl.UnixctlOutputFormat.TEXT
>
> def run(self):
> self._rpc.run()
> @@ -63,10 +65,29 @@ class UnixctlConnection(object):
> return error
>
> def reply(self, body):
> - self._reply_impl(True, body)
> + assert body is None or isinstance(body, str)
> +
> + if body is None:
> + body = ""
> +
> + if self._fmt == ovs.unixctl.UnixctlOutputFormat.JSON:
> + body = {
> + "reply-format": "plain",
> + "reply": body
> + }
> +
> + return self._reply_impl_json(True, body)
> +
> + def reply_json(self, body):
> + self._reply_impl_json(True, body)
>
> def reply_error(self, body):
> - self._reply_impl(False, body)
> + assert body is None or isinstance(body, str)
> +
> + if body is None:
> + body = ""
> +
> + return self._reply_impl_json(False, body)
>
> # Called only by unixctl classes.
> def _close(self):
> @@ -78,15 +99,11 @@ class UnixctlConnection(object):
> if not self._rpc.get_backlog():
> self._rpc.recv_wait(poller)
>
> - def _reply_impl(self, success, body):
> + def _reply_impl_json(self, success, body):
> assert isinstance(success, bool)
> - assert body is None or isinstance(body, str)
>
> assert self._request_id is not None
>
> - if body is None:
> - body = ""
> -
> if success:
> reply = Message.create_reply(body, self._request_id)
> else:
> @@ -133,6 +150,24 @@ def _unixctl_version(conn, unused_argv, version):
> conn.reply(version)
>
>
> +def _unixctl_set_options(conn, argv, unused_aux):
> + assert isinstance(conn, UnixctlConnection)
> +
> + parser = argparse.ArgumentParser()
> + parser.add_argument("--format", default="text",
> + choices=[fmt.name.lower()
> + for fmt in ovs.unixctl.UnixctlOutputFormat])
Should add type=str.lower to have a case-insensitive check
as we have in C code.
> +
> + try:
> + args = parser.parse_args(args=argv)
> + except argparse.ArgumentError as e:
> + conn.reply_error(str(e))
> + return
> +
> + conn._fmt = ovs.unixctl.UnixctlOutputFormat[args.format.upper()]
> + conn.reply(None)
> +
> +
> class UnixctlServer(object):
> def __init__(self, listener):
> assert isinstance(listener, ovs.stream.PassiveStream)
> @@ -207,4 +242,7 @@ class UnixctlServer(object):
> ovs.unixctl.command_register("version", "", 0, 0, _unixctl_version,
> version)
>
> + ovs.unixctl.command_register("set-options", "[--format text|json]",
> 1,
> + 2, _unixctl_set_options, None)
> +
> return 0, UnixctlServer(listener)
> diff --git a/tests/appctl.py b/tests/appctl.py
> index e5cc28138..ce97a5dcf 100644
> --- a/tests/appctl.py
> +++ b/tests/appctl.py
> @@ -37,6 +37,18 @@ def connect_to_target(target):
> return client
>
>
> +def reply_to_string(reply, fmt=ovs.unixctl.UnixctlOutputFormat.TEXT):
> + if fmt == ovs.unixctl.UnixctlOutputFormat.TEXT:
> + body = str(reply)
> + else:
> + body = ovs.json.to_string(reply)
> +
> + if body and not body.endswith("\n"):
> + body += "\n"
> +
> + return body
> +
> +
> def main():
> parser = argparse.ArgumentParser(description="Python Implementation of"
> " ovs-appctl.")
> @@ -49,30 +61,42 @@ def main():
> help="Arguments to the command.")
> parser.add_argument("-T", "--timeout", metavar="SECS",
> help="wait at most SECS seconds for a response")
> + parser.add_argument("-f", "--format", metavar="FMT",
> + help="Output format.", default="text",
> + choices=[fmt.name.lower()
> + for fmt in ovs.unixctl.UnixctlOutputFormat])
Should be case-insensitive.
> args = parser.parse_args()
>
> signal_alarm(int(args.timeout) if args.timeout else None)
>
> ovs.vlog.Vlog.init()
> target = args.target
> + format = ovs.unixctl.UnixctlOutputFormat[args.format.upper()]
> client = connect_to_target(target)
> +
> + if format != ovs.unixctl.UnixctlOutputFormat.TEXT:
> + err_no, error, _ = client.transact(
> + "set-options", ["--format", args.format])
> +
> + if err_no:
> + ovs.util.ovs_fatal(err_no, "%s: transaction error" % target)
> + elif error is not None:
> + sys.stderr.write(reply_to_string(error))
> + ovs.util.ovs_error(0, "%s: server returned an error" % target)
> + sys.exit(2)
> +
> err_no, error, result = client.transact(args.command, args.argv)
> client.close()
>
> if err_no:
> ovs.util.ovs_fatal(err_no, "%s: transaction error" % target)
> elif error is not None:
> - sys.stderr.write(error)
> - if error and not error.endswith("\n"):
> - sys.stderr.write("\n")
> -
> + sys.stderr.write(reply_to_string(error))
> ovs.util.ovs_error(0, "%s: server returned an error" % target)
> sys.exit(2)
> else:
> assert result is not None
> - sys.stdout.write(result)
> - if result and not result.endswith("\n"):
> - sys.stdout.write("\n")
> + sys.stdout.write(reply_to_string(result, format))
>
>
> if __name__ == '__main__':
> diff --git a/tests/unixctl-py.at b/tests/unixctl-py.at
> index 724006118..4d59c5818 100644
> --- a/tests/unixctl-py.at
> +++ b/tests/unixctl-py.at
> @@ -100,6 +100,7 @@ The available commands are:
> exit
> help
> log [[arg ...]]
> + set-options [[--format text|json]]
> version
> vlog/close
> vlog/list
> @@ -112,6 +113,9 @@ AT_CHECK([PYAPPCTL_PY -t test-unixctl.py help], [0],
> [expout])
> AT_CHECK([ovs-vsctl --version | sed 's/ovs-vsctl/test-unixctl.py/' | head -1
> > expout])
> AT_CHECK([APPCTL -t test-unixctl.py version], [0], [expout])
> AT_CHECK([PYAPPCTL_PY -t test-unixctl.py version], [0], [expout])
> +AT_CHECK_UNQUOTED([PYAPPCTL_PY -t test-unixctl.py --format json version],
> [0], [dnl
> +{"reply":"$(cat expout)","reply-format":"plain"}
> +])
>
> AT_CHECK([APPCTL -t test-unixctl.py echo robot ninja], [0], [stdout])
> AT_CHECK([cat stdout | sed -e "s/u'/'/g"], [0], [dnl
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev